From 189f8c65255a1ebe274baf290167e9424134b4ed Mon Sep 17 00:00:00 2001 From: David Hohn Date: Tue, 17 Mar 2026 15:13:06 +0100 Subject: [PATCH 1/4] Add a test for unclosed groups that are not the last group. up to now only the last group is required to be closed correctly. If any previous group omits the closing '/' it is silently skipped. --- tests/first_grp_no_end.nml | 6 ++++++ tests/test_f90nml.py | 3 +++ 2 files changed, 9 insertions(+) create mode 100644 tests/first_grp_no_end.nml diff --git a/tests/first_grp_no_end.nml b/tests/first_grp_no_end.nml new file mode 100644 index 0000000..55c9adf --- /dev/null +++ b/tests/first_grp_no_end.nml @@ -0,0 +1,6 @@ +&open_group +a = 1 + +&closed_group +b = 2 +/ diff --git a/tests/test_f90nml.py b/tests/test_f90nml.py index 96cc991..ed18a2b 100644 --- a/tests/test_f90nml.py +++ b/tests/test_f90nml.py @@ -1612,6 +1612,9 @@ def test_string_grp_no_end(self): def test_file_grp_no_end(self): self.assertRaises(ValueError, f90nml.read, 'grp_no_end.nml') + def test_file_first_grp_no_end(self): + self.assertRaises(ValueError, f90nml.read, 'first_grp_no_end.nml') + if __name__ == '__main__': if os.path.isfile('tmp.nml'): From 22b3d95a57990a96ca54d5756d4602d3d59e2ba3 Mon Sep 17 00:00:00 2001 From: David Hohn Date: Tue, 17 Mar 2026 15:15:02 +0100 Subject: [PATCH 2/4] check groups are closed correctly This adds a check whether a group is closed correctly. old behaviour would just skip group that follows the misformated group silently. Also added a few more test cases for f77 formatting because it apparently allows bare '&' to close a group. --- f90nml/parser.py | 16 ++++++++++++++++ tests/f77.nml | 8 ++++++++ tests/f77_target.nml | 8 ++++++++ tests/test_f90nml.py | 2 ++ 4 files changed, 34 insertions(+) diff --git a/f90nml/parser.py b/f90nml/parser.py index 29caf56..04ab57a 100644 --- a/f90nml/parser.py +++ b/f90nml/parser.py @@ -384,6 +384,22 @@ def _readstream(self, nml_file, nml_patch_in=None): # Finalise namelist group if self.token in ('/', '&', '$'): + if self.token == '&': + # Peek at the next non-whitespace token without + # consuming it, to distinguish F77-style terminators + # (&end or standalone &) from an unclosed group. + self.tokens, peek_iter = itertools.tee(self.tokens) + skip = self.comment_tokens + whitespace + next_tok = next( + (t for t in peek_iter if t[0] not in skip), None + ) + if next_tok not in ('end', '&', '$', None): + raise ValueError( + "f90nml: error: Namelist group '&{}' was not " + "closed before the start of a new group." + .format(g_name) + ) + # Append any remaining patched variables for v_name, v_val in grp_patch.items(): g_vars[v_name] = v_val diff --git a/tests/f77.nml b/tests/f77.nml index d795759..af4f9e5 100644 --- a/tests/f77.nml +++ b/tests/f77.nml @@ -1,3 +1,7 @@ +$f77_nml_dollar + x = 123 +$end + &f77_nml x = 123 &end @@ -5,3 +9,7 @@ &next_f77_nml y = 'abc' &end + +&another_f77_nml + z = 99 +& diff --git a/tests/f77_target.nml b/tests/f77_target.nml index b7e90d7..ad946f6 100644 --- a/tests/f77_target.nml +++ b/tests/f77_target.nml @@ -1,3 +1,7 @@ +&f77_nml_dollar + x = 123 +/ + &f77_nml x = 123 / @@ -5,3 +9,7 @@ &next_f77_nml y = 'abc' / + +&another_f77_nml + z = 99 +/ diff --git a/tests/test_f90nml.py b/tests/test_f90nml.py index ed18a2b..4bf9fe3 100644 --- a/tests/test_f90nml.py +++ b/tests/test_f90nml.py @@ -427,8 +427,10 @@ def setUp(self): } self.f77_nml = { + 'f77_nml_dollar': {'x': 123}, 'f77_nml': {'x': 123}, 'next_f77_nml': {'y': 'abc'}, + 'another_f77_nml': {'z': 99}, } self.dollar_nml = {'dollar_nml': {'v': 1.}} From b21336706e3beaa339c354ace56c08d736076383 Mon Sep 17 00:00:00 2001 From: David Hohn Date: Thu, 19 Mar 2026 09:12:59 +0100 Subject: [PATCH 3/4] add tests for capitel &END token and unclosed $group --- tests/end_uppercase.nml | 7 +++++++ tests/first_grp_no_end_dollar.nml | 7 +++++++ tests/test_f90nml.py | 8 ++++++++ 3 files changed, 22 insertions(+) create mode 100644 tests/end_uppercase.nml create mode 100644 tests/first_grp_no_end_dollar.nml diff --git a/tests/end_uppercase.nml b/tests/end_uppercase.nml new file mode 100644 index 0000000..572e841 --- /dev/null +++ b/tests/end_uppercase.nml @@ -0,0 +1,7 @@ +&END_TOKEN_UPPERCASE + a = 1 +&END + +&last_grp + a = 1 +& \ No newline at end of file diff --git a/tests/first_grp_no_end_dollar.nml b/tests/first_grp_no_end_dollar.nml new file mode 100644 index 0000000..32939d0 --- /dev/null +++ b/tests/first_grp_no_end_dollar.nml @@ -0,0 +1,7 @@ +$open_group +a = 1 + + +$closed_group +b = 2 +$end diff --git a/tests/test_f90nml.py b/tests/test_f90nml.py index 4bf9fe3..6835971 100644 --- a/tests/test_f90nml.py +++ b/tests/test_f90nml.py @@ -1597,6 +1597,11 @@ def test_repeat_repeating_logical(self): line2 = out.readline() self.assertEqual(line2.lstrip(), "b = 2*.true., .false.\n") + def test_end_uppercase(self): + test_nml = f90nml.read('end_uppercase.nml') + self.assertEqual(test_nml, {'end_token_uppercase': {'a': 1}, + 'last_grp': {'a': 1}}) + # Failed namelist parsing # NOTE: This is a very weak test, since '& x=1' / will pass def test_grp_token_end(self): @@ -1617,6 +1622,9 @@ def test_file_grp_no_end(self): def test_file_first_grp_no_end(self): self.assertRaises(ValueError, f90nml.read, 'first_grp_no_end.nml') + def test_file_first_grp_no_end_dollar(self): + self.assertRaises(ValueError, f90nml.read, 'first_grp_no_end_dollar.nml') + if __name__ == '__main__': if os.path.isfile('tmp.nml'): From c16e87c8669aa3a6bc2db9ff9d6e98ad0dcebb17 Mon Sep 17 00:00:00 2001 From: David Hohn Date: Thu, 19 Mar 2026 09:34:25 +0100 Subject: [PATCH 4/4] allow caps END token, check for & and $ in next group The parser now checks whether the f77 style &end token case insensitively. It now also allows both opening tokens & and $ to follow another group. --- f90nml/parser.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/f90nml/parser.py b/f90nml/parser.py index 04ab57a..0523e4f 100644 --- a/f90nml/parser.py +++ b/f90nml/parser.py @@ -329,6 +329,9 @@ def _readstream(self, nml_file, nml_patch_in=None): except StopIteration: break + # save whether & or $ is opening this group + opening_token = self.token + # Create the next namelist try: self._update_tokens() @@ -384,20 +387,22 @@ def _readstream(self, nml_file, nml_patch_in=None): # Finalise namelist group if self.token in ('/', '&', '$'): - if self.token == '&': + if self.token in ('&', '$'): # Peek at the next non-whitespace token without # consuming it, to distinguish F77-style terminators - # (&end or standalone &) from an unclosed group. + # (&end or standalone &, or same with $) from + # an unclosed group. self.tokens, peek_iter = itertools.tee(self.tokens) skip = self.comment_tokens + whitespace next_tok = next( - (t for t in peek_iter if t[0] not in skip), None + (t for t in peek_iter if t[0] not in skip), + None ) - if next_tok not in ('end', '&', '$', None): + if next_tok and next_tok.lower() not in ('end', '&', '$'): raise ValueError( - "f90nml: error: Namelist group '&{}' was not " - "closed before the start of a new group." - .format(g_name) + "f90nml: error: Namelist group '{}{}' was not " + "closed before the start of a new group or EOF." + .format(opening_token, g_name) ) # Append any remaining patched variables