From aff4f34e23c023f48a97a50b69e5b13df02a0d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Tue, 18 Aug 2026 01:07:44 -0400 Subject: [PATCH 1/7] Add 'format-missing-comma' extension (#484) Many compilers (gfortran, ifort, ifx) accept a missing comma between a character-string edit descriptor and a neighbouring format item, e.g. FORMAT('a' 1x,'b'), FORMAT(15x'a') or FORMAT('a' 'b'). Following the extension convention, 'format-missing-comma' is added to the EXTENSIONS list in utils.py and the relaxation is implemented in Format_Item_C1002, whose match logic is split into the standard C1002 pass and an extension fallback. Found while parsing MODFLOW-2005 (gwf2swr7.f, gwf2swi27.fpp, gwf2lak7.f, gwf2sfr7.f); also covers the example in issue #484. Co-Authored-By: Claude Fable 5 --- doc/source/fparser2.rst | 17 +++ src/fparser/two/Fortran2003.py | 111 ++++++++++++++++++ .../fortran2003/test_format_item_c1002.py | 56 +++++++++ .../test_format_specification_r1002.py | 36 +++++- src/fparser/two/utils.py | 7 ++ 5 files changed, 224 insertions(+), 3 deletions(-) diff --git a/doc/source/fparser2.rst b/doc/source/fparser2.rst index 60aaf85fb..573c3b055 100644 --- a/doc/source/fparser2.rst +++ b/doc/source/fparser2.rst @@ -319,6 +319,23 @@ omitted, the value is implicitly assumed to be one. For example:: For more information see https://gcc.gnu.org/onlinedocs/gfortran/X-format-descriptor-without-count-field.html +Missing Comma in Format Specifications +++++++++++++++++++++++++++++++++++++++ + +Standard Fortran only permits the comma separating format items to be +omitted in a small number of situations (constraint C1002). However, +many compilers (e.g. gfortran, ifort, ifx) additionally accept a +missing comma between a character-string edit descriptor and a +neighbouring format item. For example:: + + 100 format('a' 1x,'b') + 200 format(15x'a') + 300 format('a' 'b') + +The 'format-missing-comma' extension adds support in fparser for this +relaxation. Note that when such a format specification is re-generated +from the parse tree the omitted commas are re-introduced. + Hollerith Constant ++++++++++++++++++ diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 5ed465a5d..02e46b56a 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -10054,6 +10054,35 @@ def skip_digits(string): return found, index +def split_leading_char_literal(string): + """Splits a string that starts with a character literal into the + literal and the remainder, honouring doubled (escaped) quotes + inside the literal. + + :param str string: the string to split. + + :returns: a 2-tuple containing the character literal and the \ + remainder of the string, or None if the string does not start \ + with a complete character literal. + :rtype: Optional[Tuple[str, str]] + + """ + if not string or string[0] not in "'\"": + return None + quote = string[0] + index = 1 + while index < len(string): + if string[index] == quote: + if index + 1 < len(string) and string[index + 1] == quote: + # A doubled quote is an escaped quote, still inside the + # literal. + index += 2 + continue + return string[: index + 1], string[index + 1 :] + index += 1 + return None + + class Format_Item_C1002(Base): # pylint: disable=invalid-name """ Fortran 2003 constraint C1002:: @@ -10078,6 +10107,12 @@ class Format_Item_C1002(Base): # pylint: disable=invalid-name (4) Before or after a colon edit descriptor. + If 'format-missing-comma' is specified in the EXTENSIONS list then + the comma may additionally be omitted between a character-string + edit descriptor and any neighbouring format item, e.g. + FORMAT('a' 1x,'b'), FORMAT(15x'a') or FORMAT('a' 'b'), as accepted + by many compilers (e.g. gfortran, ifort, ifx). + """ subclass_names = [] @@ -10085,6 +10120,36 @@ class Format_Item_C1002(Base): # pylint: disable=invalid-name @staticmethod def match(string): + """Implements the matching for the C1002 Format Item constraint, + optionally relaxed by the 'format-missing-comma' extension. + + :param str string: The string to check for conformance with a \ + C1002 format item constraint. + :return: `None` if there is no match, otherwise a tuple of \ + size 2 containing a mixture of Control_Edit_Descriptor and \ + Format_Item classes depending on what has been matched. + + :rtype: `NoneType` or ( \ + :py:class:`fparser.two.Control_Edit_Desc`, \ + :py:class:`fparser.two.Format_Item` ) or \ + (:py:class:`fparser.two.Format_Item`, \ + :py:class:`fparser.two.Control_Edit_Desc`) or \ + (:py:class:`fparser.two.Format_Item`, \ + :py:class:`fparser.two.Format_Item`) + + """ + try: + result = Format_Item_C1002._standard_match(string) + except NoMatchError: + result = None + if result: + return result + if "format-missing-comma" not in EXTENSIONS(): + return None + return Format_Item_C1002._extension_match(string) + + @staticmethod + def _standard_match(string): """Implements the matching for the C1002 Format Item constraint. The constraints specify certain combinations of format items that do not need a comma to separate them. Rather than sorting this @@ -10186,6 +10251,52 @@ def match(string): Format_Item(option + repmap(right.lstrip())), ) + return None + + @staticmethod + def _extension_match(string): + """Implements the matching for the 'format-missing-comma' + extension. Various compilers (e.g. gfortran, ifort, ifx) accept + a missing comma between a character-string edit descriptor and + the neighbouring format item, e.g. FORMAT('a' 1x,'b'), + FORMAT(15x'a') or FORMAT('a' 'b'). The item is split at the + boundary of the first character literal and both sides are + matched separately. + + :param str string: The string to check for conformance with the \ + 'format-missing-comma' extension. + :return: `None` if there is no match, otherwise a tuple of \ + size 2 containing two Format_Item classes. + :rtype: `NoneType` or \ + (:py:class:`fparser.two.Format_Item`, \ + :py:class:`fparser.two.Format_Item`) + + """ + if not string: + return None + strip_string = string.strip() + split = split_leading_char_literal(strip_string) + if split: + # The item starts with a character literal, e.g. "'a' 1x". + literal, rest = split + rest = rest.lstrip() + if not rest or rest.startswith(","): + # Nothing follows the literal, or standard syntax. + return None + return (Format_Item(literal), Format_Item(rest)) + indices = [ + strip_string.find(quote) for quote in "'\"" if strip_string.find(quote) > 0 + ] + if indices: + # The item contains a character literal preceded by another + # format item, e.g. "15x'a'". Split at the earliest quote. + index = min(indices) + return ( + Format_Item(strip_string[:index].rstrip()), + Format_Item(strip_string[index:]), + ) + return None + def tostr(self): """ :return: Parsed representation of two format items diff --git a/src/fparser/two/tests/fortran2003/test_format_item_c1002.py b/src/fparser/two/tests/fortran2003/test_format_item_c1002.py index 8d00209f6..a49c5a251 100644 --- a/src/fparser/two/tests/fortran2003/test_format_item_c1002.py +++ b/src/fparser/two/tests/fortran2003/test_format_item_c1002.py @@ -42,6 +42,7 @@ import pytest from fparser.two.Fortran2003 import Format_Item_C1002 +from fparser.two import utils from fparser.two.utils import InternalError, NoMatchError @@ -106,3 +107,58 @@ def test_internal_error3(f2003_create, monkeypatch): assert ( "items entry 1 should contain a format items object but it " "is empty or None" ) in str(excinfo.value) + + +@pytest.mark.parametrize( + "my_input,expected", + [ + ("'a' 1x", "'a', 1X"), + ("'a' 'b'", "'a', 'b'"), + ("'a''b' 'c'", "'a''b', 'c'"), + ("15x'a'", "15X, 'a'"), + ("21x ' if it is the case '", "21X, ' if it is the case '"), + ('1x"don\'t"', '1X, "don\'t"'), + ("'a' 1x 'b'", "'a', 1X, 'b'"), + ], +) +def test_format_missing_comma_extension(f2003_create, my_input, expected): + """Check that a missing comma between a character-string edit + descriptor and a neighbouring format item is matched when the + 'format-missing-comma' extension is enabled (which it is by + default). + + """ + ast = Format_Item_C1002(my_input) + assert str(ast) == expected + + +@pytest.mark.parametrize("my_input", ["'a' 1x", "15x'a'", "'a' 'b'"]) +def test_format_missing_comma_disabled(f2003_create, monkeypatch, my_input): + """Check that a missing comma between a character-string edit + descriptor and a neighbouring format item is not matched when the + 'format-missing-comma' extension is not enabled. + + """ + monkeypatch.setattr( + utils, + "_EXTENSIONS", + [ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"], + ) + with pytest.raises(NoMatchError): + _ = Format_Item_C1002(my_input) + + +def test_format_missing_comma_format_statement(f2003_create): + """Check that format statements making use of the + 'format-missing-comma' extension are parsed (see issue #484 and + the MODFLOW-2005 codebase). + + """ + from fparser.two.Fortran2003 import Format_Stmt + + ast = Format_Stmt("FORMAT(//1X,'SWR PROCESS REQUIRES LAYCON'1X,'FOR',//)") + assert ( + str(ast) == "FORMAT(/, /, 1X, 'SWR PROCESS REQUIRES LAYCON', 1X, 'FOR', /, /)" + ) + ast = Format_Stmt("FORMAT(/1x,'unable to find:',1x,A,/15x' in file:')") + assert str(ast) == "FORMAT(/, 1X, 'unable to find:', 1X, A, /, 15X, ' in file:')" diff --git a/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py b/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py index 53e06e3a5..4a2dd0e56 100644 --- a/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py +++ b/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py @@ -38,6 +38,7 @@ """ import pytest +from fparser.two import utils from fparser.two.Fortran2003 import Format_Specification from fparser.two.utils import NoMatchError @@ -247,7 +248,7 @@ def test_syntaxerror(f2003_create): _ = Format_Specification(my_input) -def test_syntaxerror_c1002(f2003_create): +def test_syntaxerror_c1002(f2003_create, monkeypatch): """Test that we get an exception in situations where no comma is supplied and the C1002 constraints for optional commas do not apply. @@ -259,9 +260,25 @@ def test_syntaxerror_c1002(f2003_create): for my_input in ["(2P, 2/)", "('hello', 2/)", "(2E2.2, 3/)"]: ast = Format_Specification(my_input) # invalid syntax - for my_input in ["(2P 2/)", "(2P2/)", "('hello' 2/)", "('hello'2/)"]: + for my_input in ["(2P 2/)", "(2P2/)"]: with pytest.raises(NoMatchError): _ = Format_Specification(my_input) + # A missing comma after a character-string edit descriptor is + # accepted by the 'format-missing-comma' extension (enabled by + # default) ... + for my_input in ["('hello' 2/)", "('hello'2/)"]: + ast = Format_Specification(my_input) + assert str(ast) == "('hello', 2/)" + # ... and is invalid syntax without the extension. + with monkeypatch.context() as mpatch: + mpatch.setattr( + utils, + "_EXTENSIONS", + [ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"], + ) + for my_input in ["('hello' 2/)", "('hello'2/)"]: + with pytest.raises(NoMatchError): + _ = Format_Specification(my_input) # Comma is mandatory after a P descriptor if not one of ['F', 'E', # 'EN', 'ES', 'D', 'G'] or not a '/' or a ':' # Test valid syntax. @@ -285,7 +302,6 @@ def test_syntaxerror_c1002(f2003_create): _ = Format_Specification(my_input) # Comma is mandatory if C1002 is not relevant for my_input in [ - "('hello' 'hello')", "(2P 2P)", "(2P2P)", "(F2.2 F2.2)", @@ -299,3 +315,17 @@ def test_syntaxerror_c1002(f2003_create): ]: with pytest.raises(NoMatchError): _ = Format_Specification(my_input) + # A missing comma between two character-string edit descriptors is + # accepted by the 'format-missing-comma' extension (enabled by + # default) ... + ast = Format_Specification("('hello' 'hello')") + assert str(ast) == "('hello', 'hello')" + # ... and is invalid syntax without the extension. + with monkeypatch.context() as mpatch: + mpatch.setattr( + utils, + "_EXTENSIONS", + [ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"], + ) + with pytest.raises(NoMatchError): + _ = Format_Specification("('hello' 'hello')") diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 0fdefa2a3..79f0cd67f 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -122,6 +122,13 @@ # with a directory, using e.g. INQUIRE (DIRECTORY=".", DIRSPEC=C_DIRSPEC, ...) _EXTENSIONS += ["inquire-directory"] +# Many compilers (e.g. gfortran, ifort, ifx) accept a missing comma +# between a character-string edit descriptor and a neighbouring format +# item in a format specification, e.g. FORMAT('a' 1x,'b'), FORMAT(15x'a') +# or FORMAT('a' 'b'). This is supported by fparser if +# 'format-missing-comma' is specified in the EXTENSIONS list. +_EXTENSIONS += ["format-missing-comma"] + def EXTENSIONS(): """ From e149a3b47d5eef692a8ee8cfa62ce9947ec7515d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Tue, 25 Aug 2026 21:48:00 -0400 Subject: [PATCH 2/7] Reuse string_replace_map() for format-missing-comma literal splitting arporter (review on #526): reimplementing quote/escape handling for character literals is error-prone and duplicates logic that already exists elsewhere in this file. _standard_match() already masks literals via string_replace_map() before splitting on '/' and ':'; _extension_match() now uses the same masking to find literal boundaries instead of the bespoke split_leading_char_literal() helper, which is removed. Also modernises the docstrings touched by this PR (type hints instead of line-continuation backslashes), per the same review. --- src/fparser/two/Fortran2003.py | 116 +++++++++++++-------------------- 1 file changed, 44 insertions(+), 72 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 02e46b56a..6d1fcd5bc 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -10054,35 +10054,6 @@ def skip_digits(string): return found, index -def split_leading_char_literal(string): - """Splits a string that starts with a character literal into the - literal and the remainder, honouring doubled (escaped) quotes - inside the literal. - - :param str string: the string to split. - - :returns: a 2-tuple containing the character literal and the \ - remainder of the string, or None if the string does not start \ - with a complete character literal. - :rtype: Optional[Tuple[str, str]] - - """ - if not string or string[0] not in "'\"": - return None - quote = string[0] - index = 1 - while index < len(string): - if string[index] == quote: - if index + 1 < len(string) and string[index + 1] == quote: - # A doubled quote is an escaped quote, still inside the - # literal. - index += 2 - continue - return string[: index + 1], string[index + 1 :] - index += 1 - return None - - class Format_Item_C1002(Base): # pylint: disable=invalid-name """ Fortran 2003 constraint C1002:: @@ -10119,23 +10090,19 @@ class Format_Item_C1002(Base): # pylint: disable=invalid-name use_names = ["K", "W", "D", "E", "Format_Item", "R"] @staticmethod - def match(string): + def match(string: str): """Implements the matching for the C1002 Format Item constraint, optionally relaxed by the 'format-missing-comma' extension. - :param str string: The string to check for conformance with a \ - C1002 format item constraint. - :return: `None` if there is no match, otherwise a tuple of \ - size 2 containing a mixture of Control_Edit_Descriptor and \ - Format_Item classes depending on what has been matched. + :param string: the string to check for conformance with a + C1002 format item constraint. - :rtype: `NoneType` or ( \ - :py:class:`fparser.two.Control_Edit_Desc`, \ - :py:class:`fparser.two.Format_Item` ) or \ - (:py:class:`fparser.two.Format_Item`, \ - :py:class:`fparser.two.Control_Edit_Desc`) or \ - (:py:class:`fparser.two.Format_Item`, \ - :py:class:`fparser.two.Format_Item`) + :returns: `None` if there is no match, otherwise a tuple of + size 2 containing a mixture of Control_Edit_Descriptor and + Format_Item classes depending on what has been matched. + :rtype: Optional[Tuple[ + Union[Control_Edit_Desc, Format_Item], + Union[Control_Edit_Desc, Format_Item]]] """ try: @@ -10149,7 +10116,7 @@ def match(string): return Format_Item_C1002._extension_match(string) @staticmethod - def _standard_match(string): + def _standard_match(string: str): """Implements the matching for the C1002 Format Item constraint. The constraints specify certain combinations of format items that do not need a comma to separate them. Rather than sorting this @@ -10157,19 +10124,15 @@ def _standard_match(string): separately and match them in this class. As a result the generated class hierarchy is a little more complicated. - :param str string: The string to check for conformance with a \ - C1002 format item constraint. - :return: `None` if there is no match, otherwise a tuple of \ - size 2 containing a mixture of Control_Edit_Descriptor and \ - Format_Item classes depending on what has been matched. + :param string: the string to check for conformance with a + C1002 format item constraint. - :rtype: `NoneType` or ( \ - :py:class:`fparser.two.Control_Edit_Desc`, \ - :py:class:`fparser.two.Format_Item` ) or \ - (:py:class:`fparser.two.Format_Item`, \ - :py:class:`fparser.two.Control_Edit_Desc`) or \ - (:py:class:`fparser.two.Format_Item`, \ - :py:class:`fparser.two.Format_Item`) + :returns: `None` if there is no match, otherwise a tuple of + size 2 containing a mixture of Control_Edit_Descriptor and + Format_Item classes depending on what has been matched. + :rtype: Optional[Tuple[ + Union[Control_Edit_Desc, Format_Item], + Union[Control_Edit_Desc, Format_Item]]] """ if not string: @@ -10254,7 +10217,7 @@ def _standard_match(string): return None @staticmethod - def _extension_match(string): + def _extension_match(string: str): """Implements the matching for the 'format-missing-comma' extension. Various compilers (e.g. gfortran, ifort, ifx) accept a missing comma between a character-string edit descriptor and @@ -10263,37 +10226,46 @@ def _extension_match(string): boundary of the first character literal and both sides are matched separately. - :param str string: The string to check for conformance with the \ - 'format-missing-comma' extension. - :return: `None` if there is no match, otherwise a tuple of \ - size 2 containing two Format_Item classes. - :rtype: `NoneType` or \ - (:py:class:`fparser.two.Format_Item`, \ - :py:class:`fparser.two.Format_Item`) + Quote/escape handling is delegated to string_replace_map() + (the same helper _standard_match() uses to locate the '/' and + ':' edit descriptors), rather than re-implementing it here: it + masks character literals - simple ones are left untouched and + complex ones (e.g. containing doubled quotes) are replaced + with a placeholder - so the masked line can never contain the + literal's own quote character before its closing quote. + + :param string: the string to check for conformance with the + 'format-missing-comma' extension. + + :returns: `None` if there is no match, otherwise a tuple of + size 2 containing two Format_Item classes. + :rtype: Optional[Tuple[Format_Item, Format_Item]] """ if not string: return None strip_string = string.strip() - split = split_leading_char_literal(strip_string) - if split: + line, repmap = string_replace_map(strip_string) + if line and line[0] in "'\"": # The item starts with a character literal, e.g. "'a' 1x". - literal, rest = split - rest = rest.lstrip() + quote = line[0] + end = line.find(quote, 1) + if end == -1: + return None + literal = repmap(line[: end + 1]) + rest = repmap(line[end + 1 :]).lstrip() if not rest or rest.startswith(","): # Nothing follows the literal, or standard syntax. return None return (Format_Item(literal), Format_Item(rest)) - indices = [ - strip_string.find(quote) for quote in "'\"" if strip_string.find(quote) > 0 - ] + indices = [line.find(quote) for quote in "'\"" if line.find(quote) > 0] if indices: # The item contains a character literal preceded by another # format item, e.g. "15x'a'". Split at the earliest quote. index = min(indices) return ( - Format_Item(strip_string[:index].rstrip()), - Format_Item(strip_string[index:]), + Format_Item(repmap(line[:index]).rstrip()), + Format_Item(repmap(line[index:])), ) return None From 8d7c04a7521d3eabcd8c72811859318e578171f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Tue, 25 Aug 2026 21:48:00 -0400 Subject: [PATCH 3/7] Simplify format-missing-comma test monkeypatching arporter/hiker (review on #526): monkeypatch.setattr(utils, "_EXTENSIONS", []) is simpler than filtering EXTENSIONS() by name, and is safe here since these tests only exercise narrow snippets. --- .../two/tests/fortran2003/test_format_item_c1002.py | 6 +----- .../fortran2003/test_format_specification_r1002.py | 12 ++---------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/fparser/two/tests/fortran2003/test_format_item_c1002.py b/src/fparser/two/tests/fortran2003/test_format_item_c1002.py index a49c5a251..83eb08adc 100644 --- a/src/fparser/two/tests/fortran2003/test_format_item_c1002.py +++ b/src/fparser/two/tests/fortran2003/test_format_item_c1002.py @@ -139,11 +139,7 @@ def test_format_missing_comma_disabled(f2003_create, monkeypatch, my_input): 'format-missing-comma' extension is not enabled. """ - monkeypatch.setattr( - utils, - "_EXTENSIONS", - [ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"], - ) + monkeypatch.setattr(utils, "_EXTENSIONS", []) with pytest.raises(NoMatchError): _ = Format_Item_C1002(my_input) diff --git a/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py b/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py index 4a2dd0e56..2ba42cd3a 100644 --- a/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py +++ b/src/fparser/two/tests/fortran2003/test_format_specification_r1002.py @@ -271,11 +271,7 @@ def test_syntaxerror_c1002(f2003_create, monkeypatch): assert str(ast) == "('hello', 2/)" # ... and is invalid syntax without the extension. with monkeypatch.context() as mpatch: - mpatch.setattr( - utils, - "_EXTENSIONS", - [ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"], - ) + mpatch.setattr(utils, "_EXTENSIONS", []) for my_input in ["('hello' 2/)", "('hello'2/)"]: with pytest.raises(NoMatchError): _ = Format_Specification(my_input) @@ -322,10 +318,6 @@ def test_syntaxerror_c1002(f2003_create, monkeypatch): assert str(ast) == "('hello', 'hello')" # ... and is invalid syntax without the extension. with monkeypatch.context() as mpatch: - mpatch.setattr( - utils, - "_EXTENSIONS", - [ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"], - ) + mpatch.setattr(utils, "_EXTENSIONS", []) with pytest.raises(NoMatchError): _ = Format_Specification("('hello' 'hello')") From ff99a52abb22246527c5be80d10ec66de435ca62 Mon Sep 17 00:00:00 2001 From: Andy Porter Date: Wed, 26 Aug 2026 11:28:16 +0100 Subject: [PATCH 4/7] Update docstring in test_format_item_c1002.py --- src/fparser/two/tests/fortran2003/test_format_item_c1002.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fparser/two/tests/fortran2003/test_format_item_c1002.py b/src/fparser/two/tests/fortran2003/test_format_item_c1002.py index 83eb08adc..37e789311 100644 --- a/src/fparser/two/tests/fortran2003/test_format_item_c1002.py +++ b/src/fparser/two/tests/fortran2003/test_format_item_c1002.py @@ -146,8 +146,7 @@ def test_format_missing_comma_disabled(f2003_create, monkeypatch, my_input): def test_format_missing_comma_format_statement(f2003_create): """Check that format statements making use of the - 'format-missing-comma' extension are parsed (see issue #484 and - the MODFLOW-2005 codebase). + 'format-missing-comma' extension are parsed. """ from fparser.two.Fortran2003 import Format_Stmt From a34eaaba56515e9056cbf8789a773cdfa8de286e Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 26 Aug 2026 11:39:23 +0100 Subject: [PATCH 5/7] #526 add rtype type hints --- src/fparser/two/Fortran2003.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 6d1fcd5bc..4ebc1addf 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -74,7 +74,7 @@ import re import sys -from typing import Union +from typing import Optional, Union from fparser.common.splitline import string_replace_map from fparser.two import pattern_tools as pattern @@ -10090,7 +10090,10 @@ class Format_Item_C1002(Base): # pylint: disable=invalid-name use_names = ["K", "W", "D", "E", "Format_Item", "R"] @staticmethod - def match(string: str): + def match(string: str) -> Optional[tuple[Union[Control_Edit_Desc, + Format_Item], + Union[Control_Edit_Desc, + Format_Item]]]: """Implements the matching for the C1002 Format Item constraint, optionally relaxed by the 'format-missing-comma' extension. @@ -10100,9 +10103,6 @@ def match(string: str): :returns: `None` if there is no match, otherwise a tuple of size 2 containing a mixture of Control_Edit_Descriptor and Format_Item classes depending on what has been matched. - :rtype: Optional[Tuple[ - Union[Control_Edit_Desc, Format_Item], - Union[Control_Edit_Desc, Format_Item]]] """ try: @@ -10116,7 +10116,10 @@ def match(string: str): return Format_Item_C1002._extension_match(string) @staticmethod - def _standard_match(string: str): + def _standard_match(string: str) -> Optional[tuple[Union[Control_Edit_Desc, + Format_Item], + Union[Control_Edit_Desc, + Format_Item]]]: """Implements the matching for the C1002 Format Item constraint. The constraints specify certain combinations of format items that do not need a comma to separate them. Rather than sorting this @@ -10130,9 +10133,6 @@ def _standard_match(string: str): :returns: `None` if there is no match, otherwise a tuple of size 2 containing a mixture of Control_Edit_Descriptor and Format_Item classes depending on what has been matched. - :rtype: Optional[Tuple[ - Union[Control_Edit_Desc, Format_Item], - Union[Control_Edit_Desc, Format_Item]]] """ if not string: @@ -10217,7 +10217,8 @@ def _standard_match(string: str): return None @staticmethod - def _extension_match(string: str): + def _extension_match(string: str) -> Optional[tuple[Format_Item, + Format_Item]]: """Implements the matching for the 'format-missing-comma' extension. Various compilers (e.g. gfortran, ifort, ifx) accept a missing comma between a character-string edit descriptor and @@ -10239,7 +10240,6 @@ def _extension_match(string: str): :returns: `None` if there is no match, otherwise a tuple of size 2 containing two Format_Item classes. - :rtype: Optional[Tuple[Format_Item, Format_Item]] """ if not string: From 631a3401ece42b731aa51389a41b619c4196059a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 26 Aug 2026 11:41:53 +0100 Subject: [PATCH 6/7] #526 update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8932671a..dbcfce802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ Modifications by (in alphabetical order): * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +26/08/2026 PR #526 for #484. Adds a 'format-missing-comma' extension which + permits commas to be omitted between character-string edit + descriptors and neighbouring format items. + 24/08/2026 PR #525 for #524. Bug fix for Io_implied_Do rejecting a minimal- length io-implied-do. From 5a657f0b02ba0e678a43b7b5aaf475e523d97a90 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 26 Aug 2026 11:52:29 +0100 Subject: [PATCH 7/7] #526 fix black and add annotations import for older python --- src/fparser/two/Fortran2003.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 4ebc1addf..e703b06bd 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -70,6 +70,7 @@ # Original author: Pearu Peterson # First version created: Oct 2006 +from __future__ import annotations import inspect import re import sys @@ -10090,10 +10091,13 @@ class Format_Item_C1002(Base): # pylint: disable=invalid-name use_names = ["K", "W", "D", "E", "Format_Item", "R"] @staticmethod - def match(string: str) -> Optional[tuple[Union[Control_Edit_Desc, - Format_Item], - Union[Control_Edit_Desc, - Format_Item]]]: + def match( + string: str, + ) -> Optional[ + tuple[ + Union[Control_Edit_Desc, Format_Item], Union[Control_Edit_Desc, Format_Item] + ] + ]: """Implements the matching for the C1002 Format Item constraint, optionally relaxed by the 'format-missing-comma' extension. @@ -10116,10 +10120,13 @@ def match(string: str) -> Optional[tuple[Union[Control_Edit_Desc, return Format_Item_C1002._extension_match(string) @staticmethod - def _standard_match(string: str) -> Optional[tuple[Union[Control_Edit_Desc, - Format_Item], - Union[Control_Edit_Desc, - Format_Item]]]: + def _standard_match( + string: str, + ) -> Optional[ + tuple[ + Union[Control_Edit_Desc, Format_Item], Union[Control_Edit_Desc, Format_Item] + ] + ]: """Implements the matching for the C1002 Format Item constraint. The constraints specify certain combinations of format items that do not need a comma to separate them. Rather than sorting this @@ -10217,8 +10224,7 @@ def _standard_match(string: str) -> Optional[tuple[Union[Control_Edit_Desc, return None @staticmethod - def _extension_match(string: str) -> Optional[tuple[Format_Item, - Format_Item]]: + def _extension_match(string: str) -> Optional[tuple[Format_Item, Format_Item]]: """Implements the matching for the 'format-missing-comma' extension. Various compilers (e.g. gfortran, ifort, ifx) accept a missing comma between a character-string edit descriptor and