From f79a0a3e0c4df7da9458bcc70d8b3a69da72abb5 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 4 Apr 2024 13:17:32 +1100 Subject: [PATCH 01/16] #443 Support flag that triggers fparser to read code in OpenMP conditional sentinels. --- src/fparser/common/readfortran.py | 116 ++++++++++++++++--- src/fparser/common/tests/test_readfortran.py | 97 +++++++++++++++- 2 files changed, 194 insertions(+), 19 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index a5f886d4..4627ca48 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Modified work Copyright (c) 2017-2023 Science and Technology +# Modified work Copyright (c) 2017-2024 Science and Technology # Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson @@ -69,6 +69,7 @@ # Created: May 2006 # Modified by R. W. Ford and A. R. Porter, STFC Daresbury Lab # Modified by P. Elson, Met Office +# Modified by J. Henrichs, Bureau of Meteorology """Provides Fortran reader classes. @@ -555,16 +556,19 @@ class FortranReaderBase: :type mode: :py:class:`fparser.common.sourceinfo.Format` :param bool isstrict: whether we are strictly enforcing fixed format. :param bool ignore_comments: whether or not to discard comments. + :param bool omp_sentinel: whether or not the content of a line + with an OMP sentinel is parsed or not The Fortran source is iterated by `get_single_line`, `get_next_line`, `put_single_line` methods. """ - def __init__(self, source, mode, ignore_comments): + def __init__(self, source, mode, ignore_comments, + omp_sentinel=False): self.source = source - self._format = mode - + self._omp_sentinel = omp_sentinel + self.set_format(mode) self.linecount = 0 # the current number of consumed lines self.isclosed = False # This value for ignore_comments can be overridden by using the @@ -614,12 +618,48 @@ def find_module_source_file(self, mod_name): def set_format(self, mode): """ - Set Fortran code mode (fixed/free format etc). + Set Fortran code mode (fixed/free format etc). If handling of + OMP sentinels is also enabled, this function will also create + the required regular expressions to handle conditional sentinels + depending on the (new) format :param mode: Object describing the desired mode for the reader :type mode: :py:class:`fparser.common.sourceinfo.FortranFormat` """ self._format = mode + if not self._omp_sentinel: + return + + if self._format.is_fixed or self._format.is_f77: + sentinel = r"^([\!\*c]\$)" + # Initial lines fixed format sentinels: !$, c$, *! in first + # column then only spaces and digits up to column 5, and a + # space or 0 at column 6 + init_line = r"[ 0-9]{3}[ 0]" + # Continued lines fixed format sentinels: !$, c$, *! in first + # columns, then three spaces, and a non-space non-0 character + # in column 6: + cont_line = r" [^ 0]" + # Combine these two regular expressions + self._re_omp_sentinel = re.compile( + f"{sentinel}({init_line}|{cont_line})", re.IGNORECASE) + else: + # Initial free format sentinels: !$ as the first non-space + # character followed by a space. + self._re_omp_sentinel = re.compile(r"^ *(\!\$) ", re.IGNORECASE) + # Continued lines free format sentinels: !$ as the first non-space + # character with an optional & that can have spaces or not. The + # important implication of the latter is that a continuation line + # can only be properly detected if the previous line had a + # sentinel (since it does not require a space after the sentinel + # anymore. Without the requirement of a space, the regular + # expression for continuation lines will also match !$omp + # directives). So we need to have two different regular + # expressions for free format, and the detection of continuation + # lines need to be done in a later stage, when multiple lines + # are concatenated. + self._re_omp_sentinel_cont = re.compile(r"^ *(\!\$) *&?", + re.IGNORECASE) @property def format(self): @@ -696,6 +736,17 @@ def get_single_line(self, ignore_empty=False, ignore_comments=None): # expand tabs, replace special symbols, get rid of nl characters line = line.expandtabs().replace("\xa0", " ").rstrip() + if self._omp_sentinel and self._format.is_fixed: + # Fixed line sentinels can be handled here, since a continuation + # line does not depend on the previous line. The regular + # expression checks for both an initial or a continuation line, + # and if it is found, the sentinel is replaced with two spaces: + grp = self._re_omp_sentinel.match(line) + if grp: + # Remove the OMP sentinel. There are two groups which might + # be matched, depending if the line is the first line + line = line[:grp.start(1)] + " " + line[grp.end(1):] + self.source_lines.append(line) @@ -1250,7 +1301,7 @@ def get_source_item(self): """ Return the next source item. - A source item is\: + A source item is: - a fortran line - a list of continued fortran lines - a multiline - lines inside triple-quotes, only when in ispyf mode @@ -1266,6 +1317,8 @@ def get_source_item(self): :py:class:`fparser.common.readfortran.SyntaxErrorMultiLine` """ + # pylint: disable=too-many-return-statements, too-many-branches + # pylint: disable=too-many-statements get_single_line = self.get_single_line line = get_single_line() if line is None: @@ -1284,6 +1337,17 @@ def get_source_item(self): return self.cpp_directive_item("".join(lines), startlineno, endlineno) line = self.handle_cf2py_start(line) + had_omp_sentinels = False + # Free format omp sentinels need to be handled here, since a + # continuation line can only be properly detected if there was a + # previous non-continued conditional sentinel: + if self._format.is_free and self._omp_sentinel: + grp = self._re_omp_sentinel.match(line) + if grp: + # Replace the sentinel with spaces + line = line[:grp.start(1)] + " " + line[grp.end(1):] + had_omp_sentinels = True + is_f2py_directive = ( self._format.f2py_enabled and startlineno in self.f2py_comment_lines ) @@ -1449,6 +1513,14 @@ def get_source_item(self): put_item = self.fifo_item.append qchar = None while line is not None: + if had_omp_sentinels: + # In free-format we can only have a continuation line + # if we had a omp line previously: + grp = self._re_omp_sentinel_cont.match(line) + if grp: + # Replace the OMP sentinel with two spaces + line = line[:grp.start(1)] + " " + line[grp.end(1):] + if start_index: # fix format code line, qchar, had_comment = handle_inline_comment( line[start_index:], self.linecount, qchar @@ -1543,12 +1615,14 @@ class FortranFileReader(FortranReaderBase): :param file_candidate: A filename or file-like object. :param list include_dirs: Directories in which to look for inclusions. - :param list source_only: Fortran source files to search for modules \ - required by "use" statements. + :param list source_only: Fortran source files to search for modules + required by "use" statements. :param bool ignore_comments: Whether or not to ignore comments - :param Optional[bool] ignore_encoding: whether or not to ignore Python-style \ - encoding information (e.g. "-*- fortran -*-") when attempting to determine \ - the format of the file. Default is True. + :param Optional[bool] ignore_encoding: whether or not to ignore + Python-style encoding information (e.g. "-*- fortran -*-") when + attempting to determine the format of the file. Default is True. + :param Optional[bool] omp_sentinel: whether or not the content of a line + with an OMP sentinel is parsed or not. Default is False. For example:: @@ -1565,6 +1639,7 @@ def __init__( source_only=None, ignore_comments=True, ignore_encoding=True, + omp_sentinel=False ): # The filename is used as a unique ID. This is then used to cache the # contents of the file. Obviously if the file changes content but not @@ -1592,7 +1667,8 @@ def __init__( file_candidate, ignore_encoding ) - super().__init__(self.file, mode, ignore_comments) + super().__init__(self.file, mode, ignore_comments, + omp_sentinel=omp_sentinel) if include_dirs is None: self.include_dirs.insert(0, os.path.dirname(self.id)) @@ -1615,12 +1691,14 @@ class FortranStringReader(FortranReaderBase): :param str string: string to read :param list include_dirs: List of dirs to search for include files - :param list source_only: Fortran source files to search for modules \ - required by "use" statements. + :param list source_only: Fortran source files to search for modules + required by "use" statements. :param bool ignore_comments: Whether or not to ignore comments - :param Optional[bool] ignore_encoding: whether or not to ignore Python-style \ - encoding information (e.g. "-*- fortran -*-") when attempting to determine \ - the format of the source. Default is True. + :param Optional[bool] ignore_encoding: whether or not to ignore + Python-style encoding information (e.g. "-*- fortran -*-") when + attempting to determine the format of the source. Default is True. + :param Optional[bool] omp_sentinel: whether or not the content of a line + with an OMP sentinel is parsed or not. Default is False. For example: @@ -1642,6 +1720,7 @@ def __init__( source_only=None, ignore_comments=True, ignore_encoding=True, + omp_sentinel=False, ): # The Python ID of the string was used to uniquely identify it for # caching purposes. Unfortunately this ID is only unique for the @@ -1657,7 +1736,8 @@ def __init__( mode = fparser.common.sourceinfo.get_source_info_str( string, ignore_encoding=ignore_encoding ) - super().__init__(source, mode, ignore_comments) + super().__init__(source, mode, ignore_comments, + omp_sentinel=omp_sentinel) if include_dirs is not None: self.include_dirs = include_dirs[:] if source_only is not None: diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index 05d48bb9..b2df0e16 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# Copyright (c) 2017-2023 Science and Technology Facilities Council. +# Copyright (c) 2017-2024 Science and Technology Facilities Council. # # All rights reserved. # @@ -36,6 +36,7 @@ ############################################################################## # Modified M. Hambley and P. Elson, Met Office # Modified R. W. Ford and A. R. Porter, STFC Daresbury Lab +# Modified by J. Henrichs, Bureau of Meteorology ############################################################################## """ Test battery associated with fparser.common.readfortran package. @@ -1461,3 +1462,97 @@ def test_blank_lines_within_continuation(): assert isinstance(lines[1], Line) assert lines[1].span == (7, 7) assert lines[1].line == "real :: c" + + +def test_omp_sentinels_single_line(): + """Test OMP sentinels for conditional directives in a single + line.""" + + # Test fixed lines: + # ----------------- + for sentinel in ["!$", "c$", "C$", "*$"]: + input_text = f"{sentinel} bla" + reader = FortranStringReader(input_text, ignore_comments=False) + comment = reader.next() + assert isinstance(comment, Comment) + assert comment.comment == input_text + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + line = reader.next() + assert isinstance(line, Line) + assert line.line == "bla" + + input_text = f"{sentinel}omp something" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + line = reader.next() + # This is not a conditional sentinel, so it must be returned + # as a comment line: + assert isinstance(line, Comment) + assert line.line == input_text + + # Free format: + # ------------ + input_text = " !$ bla" + reader = FortranStringReader(input_text, ignore_comments=False) + comment = reader.next() + assert isinstance(comment, Comment) + assert comment.comment == input_text.strip() + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + line = reader.next() + assert isinstance(line, Line) + assert line.line == "bla" + + input_text = " !$omp something" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + line = reader.next() + # This is not a conditional sentinel, so it must be returned + # as a comment line: + assert isinstance(line, Comment) + assert line.line == input_text.strip() + + +def test_omp_sentinels_multiple_line(): + """Test OMP sentinels for conditional directives with continuation + lines.""" + + # Fixed format + # ------------ + input_text = "!$ bla\n!$ &bla" + reader = FortranStringReader(input_text, ignore_comments=False) + # Without handling of sentinels, this should return + # two comment lines: + comment = reader.next() + assert isinstance(comment, Comment) + assert comment.comment == "!$ bla" + comment = reader.next() + assert isinstance(comment, Comment) + assert comment.comment == "!$ &bla" + + # Now enable handling of sentinels, which will result + # in returning only one line with both concatenated. + input_text = "!$ bla\n!$ &bla" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + line = reader.next() + assert isinstance(line, Line) + assert line.line == "blabla" + + # Free format + # ----------- + input_text = "!$ bla &\n!$& bla" + reader = FortranStringReader(input_text, ignore_comments=False) + # Make sure to enforce free format + reader.set_format(FortranFormat(True, True)) + line = reader.next() + assert line.line == "bla bla" + + input_text = "!$ bla &\n!$& bla" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + # Make sure to enforce free format + reader.set_format(FortranFormat(True, True)) + line = reader.next() + assert line.line == "bla bla" From fec93c3079028aa37e72db51679bf80fd5a77399 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 4 Apr 2024 13:57:46 +1100 Subject: [PATCH 02/16] #443 Fixed failing tests. --- src/fparser/common/tests/test_readfortran.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index b2df0e16..86ea56ca 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1546,8 +1546,11 @@ def test_omp_sentinels_multiple_line(): reader = FortranStringReader(input_text, ignore_comments=False) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) - line = reader.next() - assert line.line == "bla bla" + comment = reader.next() + + assert comment.comment == "!$ bla &" + comment = reader.next() + assert comment.comment == "!$& bla" input_text = "!$ bla &\n!$& bla" reader = FortranStringReader(input_text, ignore_comments=False, From 9b1c0f1efa2a003816f7001f14decbdb62de4a7e Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 4 Apr 2024 15:04:39 +1100 Subject: [PATCH 03/16] #443 Fixed black formatting. --- src/fparser/common/readfortran.py | 24 ++++++++------------ src/fparser/common/tests/test_readfortran.py | 22 ++++++++---------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 4627ca48..361b81c3 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -564,8 +564,7 @@ class FortranReaderBase: """ - def __init__(self, source, mode, ignore_comments, - omp_sentinel=False): + def __init__(self, source, mode, ignore_comments, omp_sentinel=False): self.source = source self._omp_sentinel = omp_sentinel self.set_format(mode) @@ -642,7 +641,8 @@ def set_format(self, mode): cont_line = r" [^ 0]" # Combine these two regular expressions self._re_omp_sentinel = re.compile( - f"{sentinel}({init_line}|{cont_line})", re.IGNORECASE) + f"{sentinel}({init_line}|{cont_line})", re.IGNORECASE + ) else: # Initial free format sentinels: !$ as the first non-space # character followed by a space. @@ -658,8 +658,7 @@ def set_format(self, mode): # expressions for free format, and the detection of continuation # lines need to be done in a later stage, when multiple lines # are concatenated. - self._re_omp_sentinel_cont = re.compile(r"^ *(\!\$) *&?", - re.IGNORECASE) + self._re_omp_sentinel_cont = re.compile(r"^ *(\!\$) *&?", re.IGNORECASE) @property def format(self): @@ -745,8 +744,7 @@ def get_single_line(self, ignore_empty=False, ignore_comments=None): if grp: # Remove the OMP sentinel. There are two groups which might # be matched, depending if the line is the first line - line = line[:grp.start(1)] + " " + line[grp.end(1):] - + line = line[: grp.start(1)] + " " + line[grp.end(1) :] self.source_lines.append(line) @@ -1345,7 +1343,7 @@ def get_source_item(self): grp = self._re_omp_sentinel.match(line) if grp: # Replace the sentinel with spaces - line = line[:grp.start(1)] + " " + line[grp.end(1):] + line = line[: grp.start(1)] + " " + line[grp.end(1) :] had_omp_sentinels = True is_f2py_directive = ( @@ -1519,7 +1517,7 @@ def get_source_item(self): grp = self._re_omp_sentinel_cont.match(line) if grp: # Replace the OMP sentinel with two spaces - line = line[:grp.start(1)] + " " + line[grp.end(1):] + line = line[: grp.start(1)] + " " + line[grp.end(1) :] if start_index: # fix format code line, qchar, had_comment = handle_inline_comment( @@ -1639,7 +1637,7 @@ def __init__( source_only=None, ignore_comments=True, ignore_encoding=True, - omp_sentinel=False + omp_sentinel=False, ): # The filename is used as a unique ID. This is then used to cache the # contents of the file. Obviously if the file changes content but not @@ -1667,8 +1665,7 @@ def __init__( file_candidate, ignore_encoding ) - super().__init__(self.file, mode, ignore_comments, - omp_sentinel=omp_sentinel) + super().__init__(self.file, mode, ignore_comments, omp_sentinel=omp_sentinel) if include_dirs is None: self.include_dirs.insert(0, os.path.dirname(self.id)) @@ -1736,8 +1733,7 @@ def __init__( mode = fparser.common.sourceinfo.get_source_info_str( string, ignore_encoding=ignore_encoding ) - super().__init__(source, mode, ignore_comments, - omp_sentinel=omp_sentinel) + super().__init__(source, mode, ignore_comments, omp_sentinel=omp_sentinel) if include_dirs is not None: self.include_dirs = include_dirs[:] if source_only is not None: diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index 86ea56ca..bab3661f 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1476,15 +1476,17 @@ def test_omp_sentinels_single_line(): comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == input_text - reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + reader = FortranStringReader( + input_text, ignore_comments=False, omp_sentinel=True + ) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" input_text = f"{sentinel}omp something" - reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + reader = FortranStringReader( + input_text, ignore_comments=False, omp_sentinel=True + ) line = reader.next() # This is not a conditional sentinel, so it must be returned # as a comment line: @@ -1498,15 +1500,13 @@ def test_omp_sentinels_single_line(): comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == input_text.strip() - reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" input_text = " !$omp something" - reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) line = reader.next() # This is not a conditional sentinel, so it must be returned # as a comment line: @@ -1534,8 +1534,7 @@ def test_omp_sentinels_multiple_line(): # Now enable handling of sentinels, which will result # in returning only one line with both concatenated. input_text = "!$ bla\n!$ &bla" - reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) line = reader.next() assert isinstance(line, Line) assert line.line == "blabla" @@ -1553,8 +1552,7 @@ def test_omp_sentinels_multiple_line(): assert comment.comment == "!$& bla" input_text = "!$ bla &\n!$& bla" - reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() From fda2a3866abc7155d7f704d83de4e8a114d0aaa6 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 18 Apr 2024 11:11:00 +1000 Subject: [PATCH 04/16] #443 Split sentinel tests into free and fixed format, added tests without optional &. --- src/fparser/common/tests/test_readfortran.py | 65 ++++++++++++++------ 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index bab3661f..0df164ea 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1464,12 +1464,10 @@ def test_blank_lines_within_continuation(): assert lines[1].line == "real :: c" -def test_omp_sentinels_single_line(): - """Test OMP sentinels for conditional directives in a single - line.""" +def test_conditional_omp_sentinels_fixed_format_single_line(): + """Test handling of conditional OMP sentinels in a single line + with source code in fixed format.""" - # Test fixed lines: - # ----------------- for sentinel in ["!$", "c$", "C$", "*$"]: input_text = f"{sentinel} bla" reader = FortranStringReader(input_text, ignore_comments=False) @@ -1493,20 +1491,25 @@ def test_omp_sentinels_single_line(): assert isinstance(line, Comment) assert line.line == input_text - # Free format: - # ------------ + +def test_conditional_omp_sentinels_free_format_single_line(): + """Test handling of conditional OMP sentinels in a single line + with source code in free format.""" + input_text = " !$ bla" reader = FortranStringReader(input_text, ignore_comments=False) comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == input_text.strip() - reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" input_text = " !$omp something" - reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) line = reader.next() # This is not a conditional sentinel, so it must be returned # as a comment line: @@ -1514,12 +1517,10 @@ def test_omp_sentinels_single_line(): assert line.line == input_text.strip() -def test_omp_sentinels_multiple_line(): - """Test OMP sentinels for conditional directives with continuation - lines.""" +def test_conditional_omp_sentinels_fixed_format_multiple_line(): + """Test handling of conditional OMP sentinels with continuation lines + with source code in fixed format.""" - # Fixed format - # ------------ input_text = "!$ bla\n!$ &bla" reader = FortranStringReader(input_text, ignore_comments=False) # Without handling of sentinels, this should return @@ -1534,13 +1535,19 @@ def test_omp_sentinels_multiple_line(): # Now enable handling of sentinels, which will result # in returning only one line with both concatenated. input_text = "!$ bla\n!$ &bla" - reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) line = reader.next() assert isinstance(line, Line) assert line.line == "blabla" - # Free format - # ----------- + +def test_conditional_omp_sentinels_free_format_multiple_line(): + """Test handling of conditional OMP sentinels with continuation lines + with source code in free format.""" + + # Test with the optional & in the continuation line: + # --------------------------------------------------- input_text = "!$ bla &\n!$& bla" reader = FortranStringReader(input_text, ignore_comments=False) # Make sure to enforce free format @@ -1552,8 +1559,30 @@ def test_omp_sentinels_multiple_line(): assert comment.comment == "!$& bla" input_text = "!$ bla &\n!$& bla" - reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() assert line.line == "bla bla" + + # Test without the optional & in the continuation line: + # ----------------------------------------------------- + input_text = "!$ bla &\n!$ bla" + reader = FortranStringReader(input_text, ignore_comments=False) + # Make sure to enforce free format + reader.set_format(FortranFormat(True, True)) + comment = reader.next() + + assert comment.comment == "!$ bla &" + comment = reader.next() + assert comment.comment == "!$ bla" + + input_text = "!$ bla &\n!$ bla" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + # Make sure to enforce free format + reader.set_format(FortranFormat(True, True)) + line = reader.next() + # 8 spaces in input_text, plus two for replacing the !$ + assert line.line == "bla bla" From 6d7b6ef04f038823b7a4af11925d33d0c22b966a Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 18 Apr 2024 16:45:26 +1000 Subject: [PATCH 05/16] #443 Added more tests as requested in the review. --- src/fparser/common/tests/test_readfortran.py | 77 ++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index 0df164ea..3dfad99d 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1470,10 +1470,21 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): for sentinel in ["!$", "c$", "C$", "*$"]: input_text = f"{sentinel} bla" + + # 1. By default (not ignoring comments), the line is just a comment: reader = FortranStringReader(input_text, ignore_comments=False) comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == input_text + + # 2. And if comments are ignored, nothing should be returned and a + # StopIteration exception will be raised. + reader = FortranStringReader(input_text, ignore_comments=True) + with pytest.raises(StopIteration): + comment = reader.next() + + # 3. When omp-sentinels are accepted, we should get a line, + # not a comment: reader = FortranStringReader( input_text, ignore_comments=False, omp_sentinel=True ) @@ -1481,6 +1492,16 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): assert isinstance(line, Line) assert line.line == "bla" + # 4. If omp-sentinels are accepted, and comments ignored, + # we should still get the line (with the sentinel removed): + reader = FortranStringReader( + input_text, ignore_comments=True, omp_sentinel=True + ) + line = reader.next() + assert isinstance(line, Line) + assert line.line == "bla" + + # 5. Make sure that a real omp directive stays a comment: input_text = f"{sentinel}omp something" reader = FortranStringReader( input_text, ignore_comments=False, omp_sentinel=True @@ -1491,31 +1512,78 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): assert isinstance(line, Comment) assert line.line == input_text + # 6. Test some corner cases (all of which are not valid sentinels): + for sentinel in ["!!$", "! $", " !$", " ! $"]: + input_text = f"{sentinel} bla" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + # Enforce fixed format, otherwise fparser will silently switch + # to free format and suddenly interpret comments differently + reader.set_format(FortranFormat(False, False)) + comment = reader.next() + assert isinstance(comment, Comment) + assert comment.comment == input_text + def test_conditional_omp_sentinels_free_format_single_line(): """Test handling of conditional OMP sentinels in a single line with source code in free format.""" + # 1. By default, a omp sentinel will be returned as a comment input_text = " !$ bla" reader = FortranStringReader(input_text, ignore_comments=False) comment = reader.next() + reader.set_format(FortranFormat(True, True)) assert isinstance(comment, Comment) assert comment.comment == input_text.strip() + + # 2. And if comments are ignored, nothing should be returned and a + # StopIteration exception will be raised. + reader = FortranStringReader(input_text, ignore_comments=True) + reader.set_format(FortranFormat(True, True)) + with pytest.raises(StopIteration): + comment = reader.next() + + # 3. When omp-sentinels are accepted, we should get a line, + # not a comment:4 reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) + reader.set_format(FortranFormat(True, True)) + line = reader.next() + assert isinstance(line, Line) + assert line.line == "bla" + + # 4. If omp-sentinels are accepted, and comments ignored, + # we should still get the line (with the sentinel removed): + reader = FortranStringReader(input_text, ignore_comments=True, + omp_sentinel=True) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" + # 5. Make sure that a real omp directive stays a comment: input_text = " !$omp something" reader = FortranStringReader(input_text, ignore_comments=False, omp_sentinel=True) + reader.set_format(FortranFormat(True, True)) line = reader.next() # This is not a conditional sentinel, so it must be returned # as a comment line: assert isinstance(line, Comment) assert line.line == input_text.strip() + # 6. Test some corner cases (all of which are not valid sentinels): + for sentinel in ["!!$", "! $", " ! $"]: + input_text = f"{sentinel} bla" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + reader.set_format(FortranFormat(True, True)) + comment = reader.next() + assert isinstance(comment, Comment) + # Since fparser will remove leading white spaces, we need to + # compare with the input text after removing its white spaces: + assert comment.comment == input_text.strip() + def test_conditional_omp_sentinels_fixed_format_multiple_line(): """Test handling of conditional OMP sentinels with continuation lines @@ -1541,6 +1609,15 @@ def test_conditional_omp_sentinels_fixed_format_multiple_line(): assert isinstance(line, Line) assert line.line == "blabla" + # Add invalid sentinels in continuation lines: + input_text = "!$ bla\n! $ &bla" + reader = FortranStringReader(input_text, ignore_comments=False, + omp_sentinel=True) + line = reader.next() + assert line.line == "bla" + line = reader.next() + assert line.line == "! $ &bla" + def test_conditional_omp_sentinels_free_format_multiple_line(): """Test handling of conditional OMP sentinels with continuation lines From 350e46ed7cc509ca15b23dfb7269774d4914921d Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 18 Apr 2024 16:54:28 +1000 Subject: [PATCH 06/16] #443 Updated comments. --- src/fparser/common/readfortran.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 361b81c3..0c60d443 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -556,8 +556,9 @@ class FortranReaderBase: :type mode: :py:class:`fparser.common.sourceinfo.Format` :param bool isstrict: whether we are strictly enforcing fixed format. :param bool ignore_comments: whether or not to discard comments. - :param bool omp_sentinel: whether or not the content of a line - with an OMP sentinel is parsed or not + :param Optional[bool] omp_sentinel: whether or not the content of a line + with an OMP sentinel is parsed or not. Default is False (in which + case it is treated as a Comment). The Fortran source is iterated by `get_single_line`, `get_next_line`, `put_single_line` methods. @@ -736,7 +737,7 @@ def get_single_line(self, ignore_empty=False, ignore_comments=None): # expand tabs, replace special symbols, get rid of nl characters line = line.expandtabs().replace("\xa0", " ").rstrip() if self._omp_sentinel and self._format.is_fixed: - # Fixed line sentinels can be handled here, since a continuation + # Fixed-format line sentinels can be handled here, since a continuation # line does not depend on the previous line. The regular # expression checks for both an initial or a continuation line, # and if it is found, the sentinel is replaced with two spaces: @@ -1620,7 +1621,8 @@ class FortranFileReader(FortranReaderBase): Python-style encoding information (e.g. "-*- fortran -*-") when attempting to determine the format of the file. Default is True. :param Optional[bool] omp_sentinel: whether or not the content of a line - with an OMP sentinel is parsed or not. Default is False. + with an OMP sentinel is parsed or not. Default is False (in which + case it is treated as a Comment). For example:: @@ -1695,7 +1697,8 @@ class FortranStringReader(FortranReaderBase): Python-style encoding information (e.g. "-*- fortran -*-") when attempting to determine the format of the source. Default is True. :param Optional[bool] omp_sentinel: whether or not the content of a line - with an OMP sentinel is parsed or not. Default is False. + with an OMP sentinel is parsed or not. Default is False (in which + case it is treated as a Comment). For example: From 56bdde8002ead3b6c30907bf23c5ce71f4d92c43 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 18 Apr 2024 21:41:33 +1000 Subject: [PATCH 07/16] #443 Used better names for parameter, cleaned up many pycodestyle errors. --- src/fparser/common/readfortran.py | 180 ++++++++++++++++++------------ 1 file changed, 108 insertions(+), 72 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 0c60d443..43ef8c6c 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -198,10 +198,8 @@ def _is_fix_comment(line, isstrict, f2py_enabled): # line continuation return False return True - else: - # inline comment or ! is used in character context - # inline comments are handled elsewhere - pass + # inline comment or ! is used in character context + # inline comments are handled elsewhere elif line == "": return True return False @@ -231,7 +229,7 @@ def extract_label(line): match = _LABEL_RE.match(line) if match: label = int(match.group("label")) - line = line[match.end() :].lstrip() + line = line[match.end():].lstrip() return label, line @@ -253,7 +251,7 @@ def extract_construct_name(line): match = _CONSTRUCT_NAME_RE.match(line) if match: construct_name = match.group("name") - line = line[match.end() :].lstrip() + line = line[match.end():].lstrip() return construct_name, line @@ -355,7 +353,8 @@ def __str__(self): return s + repr(self.line) def isempty(self, ignore_comments=False): - return not (self.line or self.label is not None or self.name is not None) + return not (self.line or self.label is not None or + self.name is not None) def get_line(self, apply_map=False): if apply_map: @@ -379,20 +378,22 @@ def get_line(self, apply_map=False): if i != -1 and l2[-1] == ")": substrings = ["call " + l2[: i + 1]] start_search = _HOLLERITH_START_SEARCH - l2 = l2[i + 1 : -1].strip() + l2 = l2[i + 1:-1].strip() m = start_search(l2) while m: substrings.append(l2[: m.start()]) substrings.append(m.group("pre")) num = int(m.group("num")) - substrings.append("'" + l2[m.end() : m.end() + num] + "'") - l2 = l2[m.end() + num :] + substrings.append("'" + l2[m.end():m.end() + num] + + "'") + l2 = l2[m.end() + num:] m = start_search(l2) substrings.append(l2) substrings.append(")") line = "".join(substrings) - line, str_map = string_replace_map(line, lower=not self.reader.format.is_pyf) + line, str_map = string_replace_map(line, + lower=not self.reader.format.is_pyf) self.strline = line self.strlinemap = str_map return line @@ -533,7 +534,8 @@ class CppDirective(Line): """ def __init__(self, line, linenospan, reader): - super(CppDirective, self).__init__(line, linenospan, None, None, reader) + super(CppDirective, self).__init__(line, linenospan, None, None, + reader) ############################################################################## @@ -556,18 +558,19 @@ class FortranReaderBase: :type mode: :py:class:`fparser.common.sourceinfo.Format` :param bool isstrict: whether we are strictly enforcing fixed format. :param bool ignore_comments: whether or not to discard comments. - :param Optional[bool] omp_sentinel: whether or not the content of a line - with an OMP sentinel is parsed or not. Default is False (in which - case it is treated as a Comment). + :param Optional[bool] include_omp_conditional_lines: whether or not the + content of a line with an OMP sentinel is parsed or not. Default is + False (in which case it is treated as a Comment). The Fortran source is iterated by `get_single_line`, `get_next_line`, `put_single_line` methods. """ - def __init__(self, source, mode, ignore_comments, omp_sentinel=False): + def __init__(self, source, mode, ignore_comments, + include_omp_conditional_lines=False): self.source = source - self._omp_sentinel = omp_sentinel + self._include_omp_conditional_lines = include_omp_conditional_lines self.set_format(mode) self.linecount = 0 # the current number of consumed lines self.isclosed = False @@ -627,7 +630,7 @@ def set_format(self, mode): :type mode: :py:class:`fparser.common.sourceinfo.FortranFormat` """ self._format = mode - if not self._omp_sentinel: + if not self._include_omp_conditional_lines: return if self._format.is_fixed or self._format.is_f77: @@ -659,7 +662,8 @@ def set_format(self, mode): # expressions for free format, and the detection of continuation # lines need to be done in a later stage, when multiple lines # are concatenated. - self._re_omp_sentinel_cont = re.compile(r"^ *(\!\$) *&?", re.IGNORECASE) + self._re_omp_sentinel_cont = re.compile(r"^ *(\!\$) *&?", + re.IGNORECASE) @property def format(self): @@ -675,7 +679,8 @@ def name(self): :returns: the name of this reader. :rtype: str """ - return "{source} mode={mode}".format(source=self.source, mode=self._format.mode) + return "{source} mode={mode}".format(source=self.source, + mode=self._format.mode) def close_source(self): """Called when self.source.next() raises StopIteration.""" @@ -736,16 +741,17 @@ def get_single_line(self, ignore_empty=False, ignore_comments=None): # expand tabs, replace special symbols, get rid of nl characters line = line.expandtabs().replace("\xa0", " ").rstrip() - if self._omp_sentinel and self._format.is_fixed: - # Fixed-format line sentinels can be handled here, since a continuation - # line does not depend on the previous line. The regular - # expression checks for both an initial or a continuation line, - # and if it is found, the sentinel is replaced with two spaces: + if self._include_omp_conditional_lines and self._format.is_fixed: + # Fixed-format line sentinels can be handled here, since a + # continuation line does not depend on the previous line. The + # regular expression checks for both an initial or a continuation + # line, and if it is found, the sentinel is replaced with two + # spaces: grp = self._re_omp_sentinel.match(line) if grp: # Remove the OMP sentinel. There are two groups which might # be matched, depending if the line is the first line - line = line[: grp.start(1)] + " " + line[grp.end(1) :] + line = line[:grp.start(1)] + " " + line[grp.end(1):] self.source_lines.append(line) @@ -872,7 +878,8 @@ def next(self, ignore_comments=None): return item reader.info("including file %r" % (path), item) self.reader = FortranFileReader( - path, include_dirs=include_dirs, ignore_comments=ignore_comments + path, include_dirs=include_dirs, + ignore_comments=ignore_comments ) result = self.reader.next(ignore_comments=ignore_comments) return result @@ -883,7 +890,8 @@ def next(self, ignore_comments=None): # rather than catching *every* exception. except Exception as err: message = self.format_message( - "FATAL ERROR", "while processing line", self.linecount, self.linecount + "FATAL ERROR", "while processing line", + self.linecount, self.linecount ) logging.getLogger(__name__).critical(message) message = "Traceback\n" + "".join(traceback.format_stack()) @@ -963,7 +971,8 @@ def _next(self, ignore_comments=None): # using the existing span (line numbers) and # reader. new_line = Line( - item.apply_map(line), item.span, label, name, item.reader + item.apply_map(line), item.span, label, name, + item.reader ) items.append(new_line) items.reverse() @@ -974,7 +983,8 @@ def _next(self, ignore_comments=None): # Interface to returned items: - def line_item(self, line, startlineno, endlineno, label, name, errmessage=None): + def line_item(self, line, startlineno, endlineno, label, + name, errmessage=None): """Construct Line item.""" if errmessage is None: return Line(line, (startlineno, endlineno), label, name, self) @@ -987,7 +997,8 @@ def multiline_item( ): """Construct MultiLine item.""" if errmessage is None: - return MultiLine(prefix, lines, suffix, (startlineno, endlineno), self) + return MultiLine(prefix, lines, suffix, (startlineno, endlineno), + self) return SyntaxErrorMultiLine( prefix, lines, suffix, (startlineno, endlineno), self, errmessage ) @@ -1023,7 +1034,8 @@ def format_message( for i in range(max(1, startlineno - back_index), startlineno): r.append("%5d:%s" % (i, self.source_lines[i - 1])) for i in range( - startlineno, min(endlineno + back_index, len(self.source_lines)) + 1 + startlineno, min(endlineno + back_index, + len(self.source_lines)) + 1 ): if i == 0 and not self.source_lines: break @@ -1070,7 +1082,8 @@ def info(self, message, item=None): len(self.source_lines), ) else: - m = self.format_message("INFORMATION", message, item.span[0], item.span[1]) + m = self.format_message("INFORMATION", message, item.span[0], + item.span[1]) logging.getLogger(__name__).info(m) def error(self, message, item=None): @@ -1096,7 +1109,8 @@ def warning(self, message, item=None): message, len(self.source_lines) - 2, len(self.source_lines) ) else: - m = self.format_warning_message(message, item.span[0], item.span[1]) + m = self.format_warning_message(message, item.span[0], + item.span[1]) logging.getLogger(__name__).warning(m) # Auxiliary methods for processing raw source lines: @@ -1259,12 +1273,12 @@ def handle_multilines(self, line, startlineno, mlstr): suffix = None multilines = [] - line = line[i + 3 :] + line = line[i + 3:] while line is not None: j = line.find(mlstr) if j != -1 and "!" not in line[:j]: multilines.append(line[:j]) - suffix = line[j + 3 :] + suffix = line[j + 3:] break multilines.append(line) line = self.get_single_line() @@ -1274,12 +1288,16 @@ def handle_multilines(self, line, startlineno, mlstr): message, startlineno, startlineno, i ) return self.multiline_item( - prefix, multilines, suffix, startlineno, self.linecount, message + prefix, multilines, suffix, startlineno, self.linecount, + message ) - suffix, qc, had_comment = self.handle_inline_comment(suffix, self.linecount) + suffix, qc, had_comment = self.handle_inline_comment( + suffix, self.linecount + ) # no line continuation allowed in multiline suffix if qc is not None: - message = "following character continuation: {!r}," + " expected None." + message = ("following character continuation: {!r}," + " expected None.") message = self.format_message( "ASSERTION FAILURE(pyf)", message.format(qc), @@ -1333,22 +1351,24 @@ def get_source_item(self): line = get_single_line() lines.append(line) endlineno = self.linecount - return self.cpp_directive_item("".join(lines), startlineno, endlineno) + return self.cpp_directive_item("".join(lines), startlineno, + endlineno) line = self.handle_cf2py_start(line) - had_omp_sentinels = False + had_include_omp_conditional_liness = False # Free format omp sentinels need to be handled here, since a # continuation line can only be properly detected if there was a # previous non-continued conditional sentinel: - if self._format.is_free and self._omp_sentinel: + if self._format.is_free and self._include_omp_conditional_lines: grp = self._re_omp_sentinel.match(line) if grp: # Replace the sentinel with spaces - line = line[: grp.start(1)] + " " + line[grp.end(1) :] - had_omp_sentinels = True + line = line[: grp.start(1)] + " " + line[grp.end(1):] + had_include_omp_conditional_liness = True is_f2py_directive = ( - self._format.f2py_enabled and startlineno in self.f2py_comment_lines + self._format.f2py_enabled and + startlineno in self.f2py_comment_lines ) isstrict = self._format.is_strict have_comment = False @@ -1385,8 +1405,10 @@ def get_source_item(self): logging.getLogger(__name__).warning(message) if i == 0: # non standard comment line: - return self.comment_item(line, startlineno, startlineno) - mode = fparser.common.sourceinfo.FortranFormat(True, False) + return self.comment_item(line, startlineno, + startlineno) + mode = fparser.common.sourceinfo.FortranFormat(True, + False) self.set_format(mode) else: message = self.format_warning_message( @@ -1395,14 +1417,16 @@ def get_source_item(self): logging.getLogger(__name__).warning(message) if i == 0: # non standard comment line: - return self.comment_item(line, startlineno, startlineno) + return self.comment_item(line, startlineno, + startlineno) # return line item with error message # TODO: handle cases with line[6:]=='' message = self.format_error_message( message, startlineno, self.linecount ) return self.line_item( - line[6:], startlineno, self.linecount, label, name, message + line[6:], startlineno, self.linecount, label, + name, message ) if self._format.is_fixed: # Check for switched to free format # check for label @@ -1413,14 +1437,15 @@ def get_source_item(self): m = _CONSTRUCT_NAME_RE.match(line[6:]) if m: name = m.group("name") - line = line[:6] + line[6:][m.end() :].lstrip() + line = line[:6] + line[6:][m.end():].lstrip() if not line[6:].strip(): # check for a blank line if name is not None: self.error("No construct following construct-name.") elif label is not None: self.warning( - "Label must follow nonblank character" + " (F2008:3.2.5_2)" + "Label must follow nonblank character" + " (F2008:3.2.5_2)" ) return self.comment_item("", startlineno, self.linecount) # line is not a comment and the start of the line is valid @@ -1449,7 +1474,8 @@ def get_source_item(self): endlineno = self.linecount if self._format.is_fix and not is_f2py_directive: # handle inline comment - newline, qc, had_comment = handle_inline_comment(line[6:], startlineno) + newline, qc, had_comment = handle_inline_comment(line[6:], + startlineno) have_comment |= had_comment lines = [newline] next_line = self.get_next_line() @@ -1465,7 +1491,8 @@ def get_source_item(self): if _is_fix_comment(line2, isstrict, self._format.f2py_enabled): # handle fix format comments inside line continuations # after the line construction - citem = self.comment_item(line2, self.linecount, self.linecount) + citem = self.comment_item(line2, self.linecount, + self.linecount) self.fifo_item.append(citem) else: # line continuation @@ -1478,7 +1505,8 @@ def get_source_item(self): next_line = self.get_next_line() # no character continuation should follows now if qc is not None: - message = "following character continuation: " + "{!r}, expected None." + message = ("following character continuation: " + "{!r}, expected None.") message = self.format_message( "ASSERTION FAILURE(fix)", message.format(qc), @@ -1499,7 +1527,8 @@ def get_source_item(self): message, startlineno + i, startlineno + i, location ) logging.getLogger(__name__).warning(message) - return self.line_item("".join(lines), startlineno, endlineno, label, name) + return self.line_item("".join(lines), startlineno, endlineno, + label, name) # line is free format or fixed format with f2py directive (that # will be interpreted as free format line). @@ -1512,13 +1541,13 @@ def get_source_item(self): put_item = self.fifo_item.append qchar = None while line is not None: - if had_omp_sentinels: + if had_include_omp_conditional_liness: # In free-format we can only have a continuation line # if we had a omp line previously: grp = self._re_omp_sentinel_cont.match(line) if grp: # Replace the OMP sentinel with two spaces - line = line[: grp.start(1)] + " " + line[grp.end(1) :] + line = line[:grp.start(1)] + " " + line[grp.end(1):] if start_index: # fix format code line, qchar, had_comment = handle_inline_comment( @@ -1558,7 +1587,7 @@ def get_source_item(self): i = line.rfind("&") if i != -1: - line_i1_rstrip = line[i + 1 :].rstrip() + line_i1_rstrip = line[i + 1:].rstrip() if not lines: # first line if i == -1 or line_i1_rstrip: @@ -1578,20 +1607,23 @@ def get_source_item(self): if k != 1 and line[:k].lstrip(): k = -1 endlineno = self.linecount - lines_append(line[k + 1 : i]) + lines_append(line[k + 1:i]) if i == len(line): break line = get_single_line() if qchar is not None: - message = "following character continuation: {!r}, " + "expected None." + message = ("following character continuation: {!r}, " + "expected None.") message = self.format_message( - "ASSERTION FAILURE(free)", message.format(qchar), startlineno, endlineno + "ASSERTION FAILURE(free)", message.format(qchar), + startlineno, endlineno ) logging.getLogger(__name__).error(message) line_content = "".join(lines).strip() if line_content: - return self.line_item(line_content, startlineno, endlineno, label, name) + return self.line_item(line_content, startlineno, endlineno, + label, name) if label is not None: message = "Label must follow nonblank character (F2008:3.2.5_2)" self.warning(message) @@ -1620,9 +1652,9 @@ class FortranFileReader(FortranReaderBase): :param Optional[bool] ignore_encoding: whether or not to ignore Python-style encoding information (e.g. "-*- fortran -*-") when attempting to determine the format of the file. Default is True. - :param Optional[bool] omp_sentinel: whether or not the content of a line - with an OMP sentinel is parsed or not. Default is False (in which - case it is treated as a Comment). + :param Optional[bool] include_omp_conditional_lines: whether or not the + content of a line with an OMP sentinel is parsed or not. Default is + False (in which case it is treated as a Comment). For example:: @@ -1639,7 +1671,7 @@ def __init__( source_only=None, ignore_comments=True, ignore_encoding=True, - omp_sentinel=False, + include_omp_conditional_lines=False, ): # The filename is used as a unique ID. This is then used to cache the # contents of the file. Obviously if the file changes content but not @@ -1667,7 +1699,9 @@ def __init__( file_candidate, ignore_encoding ) - super().__init__(self.file, mode, ignore_comments, omp_sentinel=omp_sentinel) + super().__init__( + self.file, mode, ignore_comments, + include_omp_conditional_lines=include_omp_conditional_lines) if include_dirs is None: self.include_dirs.insert(0, os.path.dirname(self.id)) @@ -1696,9 +1730,9 @@ class FortranStringReader(FortranReaderBase): :param Optional[bool] ignore_encoding: whether or not to ignore Python-style encoding information (e.g. "-*- fortran -*-") when attempting to determine the format of the source. Default is True. - :param Optional[bool] omp_sentinel: whether or not the content of a line - with an OMP sentinel is parsed or not. Default is False (in which - case it is treated as a Comment). + :param Optional[bool] include_omp_conditional_lines: whether or not + the content of a line with an OMP sentinel is parsed or not. Default + is False (in which case it is treated as a Comment). For example: @@ -1720,7 +1754,7 @@ def __init__( source_only=None, ignore_comments=True, ignore_encoding=True, - omp_sentinel=False, + include_omp_conditional_lines=False, ): # The Python ID of the string was used to uniquely identify it for # caching purposes. Unfortunately this ID is only unique for the @@ -1736,7 +1770,9 @@ def __init__( mode = fparser.common.sourceinfo.get_source_info_str( string, ignore_encoding=ignore_encoding ) - super().__init__(source, mode, ignore_comments, omp_sentinel=omp_sentinel) + super().__init__( + source, mode, ignore_comments, + include_omp_conditional_lines=include_omp_conditional_lines) if include_dirs is not None: self.include_dirs = include_dirs[:] if source_only is not None: From 5c897d58636aa70b9491a419664acbc26296a107 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 18 Apr 2024 23:16:21 +1000 Subject: [PATCH 08/16] #443 Updated manual. --- doc/source/fparser2.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/fparser2.rst b/doc/source/fparser2.rst index 661c756f..3adf1d33 100644 --- a/doc/source/fparser2.rst +++ b/doc/source/fparser2.rst @@ -162,6 +162,14 @@ Preprocessing directives are retained as `CppDirective` objects by the readers and are represented by matching nodes in the parse tree created by fparser2. See section `Preprocessing Directives`_ for more details. +If the optional parameter `include_omp_conditional_lines` is set to `True`, +then any source code line that contains a conditional OpenMP sentinel +(e.g. `!$` at the beginning of a line) will be handled as if OpenMP is +enabled - i.e. the sentinel will be replaced by spaces, and the remainder +of the line is parsed. In this case, the lines will not be returned +as comment lines, nor would they be ignored even if +`ignore_comments` is set to `True`. + Note that empty input, or input that consists of purely white space and/or newlines, is not treated as invalid Fortran and an empty parse tree is returned. Whilst this is not strictly valid, most compilers From cda6ded0cf0d6a15a8fe9b853c5223d77967e1ee Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 18 Apr 2024 23:22:35 +1000 Subject: [PATCH 09/16] #443 Updated comments, and some refactoring. --- src/fparser/common/readfortran.py | 50 +++++++++------ src/fparser/common/tests/test_readfortran.py | 66 ++++++++++++-------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 43ef8c6c..258682e3 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -634,13 +634,16 @@ def set_format(self, mode): return if self._format.is_fixed or self._format.is_f77: - sentinel = r"^([\!\*c]\$)" # Initial lines fixed format sentinels: !$, c$, *! in first - # column then only spaces and digits up to column 5, and a + # column: + sentinel = r"^([\!\*c]\$)" + + # Then only spaces and digits up to column 5, and a # space or 0 at column 6 init_line = r"[ 0-9]{3}[ 0]" - # Continued lines fixed format sentinels: !$, c$, *! in first - # columns, then three spaces, and a non-space non-0 character + + # Continued lines fixed format sentinels: the sentinel as + # above followed by three spaces, and a non-space, non-0 character # in column 6: cont_line = r" [^ 0]" # Combine these two regular expressions @@ -747,11 +750,7 @@ def get_single_line(self, ignore_empty=False, ignore_comments=None): # regular expression checks for both an initial or a continuation # line, and if it is found, the sentinel is replaced with two # spaces: - grp = self._re_omp_sentinel.match(line) - if grp: - # Remove the OMP sentinel. There are two groups which might - # be matched, depending if the line is the first line - line = line[:grp.start(1)] + " " + line[grp.end(1):] + line, _ = self.replace_omp_sentinels(line, self._re_omp_sentinel) self.source_lines.append(line) @@ -1115,6 +1114,25 @@ def warning(self, message, item=None): # Auxiliary methods for processing raw source lines: + @staticmethod + def replace_omp_sentinels(line, regex): + '''Checks if the specified line matches the regex, which represents + a conditional OpenMP sentinel. If it is a match, the sentinel (which + must be the first group in the regex) is replaced with two spaces. + :param line: the line to check if it contains an OpenMP sentinel + :type line: str + :param regex: the compiled regular expression to use for detecting a + conditional sentinel. + :type regex: :py:class:`re.Pattern` + + ''' + grp = regex.match(line) + if grp: + # Replace the OMP sentinel with two spaces + line = line[:grp.start(1)] + " " + line[grp.end(1):] + return (line, True) + return (line, False) + def handle_cpp_directive(self, line): """ Determine whether the current line is likely to hold @@ -1360,11 +1378,8 @@ def get_source_item(self): # continuation line can only be properly detected if there was a # previous non-continued conditional sentinel: if self._format.is_free and self._include_omp_conditional_lines: - grp = self._re_omp_sentinel.match(line) - if grp: - # Replace the sentinel with spaces - line = line[: grp.start(1)] + " " + line[grp.end(1):] - had_include_omp_conditional_liness = True + line, had_include_omp_conditional_liness = \ + self.replace_omp_sentinels(line, self._re_omp_sentinel) is_f2py_directive = ( self._format.f2py_enabled and @@ -1544,11 +1559,8 @@ def get_source_item(self): if had_include_omp_conditional_liness: # In free-format we can only have a continuation line # if we had a omp line previously: - grp = self._re_omp_sentinel_cont.match(line) - if grp: - # Replace the OMP sentinel with two spaces - line = line[:grp.start(1)] + " " + line[grp.end(1):] - + line, _ = self.replace_omp_sentinels( + line, self._re_omp_sentinel_cont) if start_index: # fix format code line, qchar, had_comment = handle_inline_comment( line[start_index:], self.linecount, qchar diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index 3dfad99d..bf18574c 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -885,11 +885,12 @@ def test_string_reader(): assert unit_under_test.get_single_line(ignore_empty=True) == expected -@pytest.mark.parametrize("reader_cls", [FortranStringReader, FortranFileReader]) +@pytest.mark.parametrize("reader_cls", [FortranStringReader, + FortranFileReader]) def test_reader_ignore_encoding(reader_cls, tmp_path): """ - Tests that the Fortran{String,File}Reader can be configured to take notice of - Python-style encoding information. + Tests that the Fortran{String,File}Reader can be configured to take + notice of Python-style encoding information. """ source = "! -*- f77 -*-\n" + FULL_FREE_SOURCE if reader_cls is FortranFileReader: @@ -903,7 +904,8 @@ def test_reader_ignore_encoding(reader_cls, tmp_path): # By default the encoding information is ignored so the format should be # free format, not strict. assert reader.format == FortranFormat(True, False) - # Check that explicitly setting ignore_encoding=True gives the same behaviour. + # Check that explicitly setting ignore_encoding=True gives + # the same behaviour. reader1 = reader_cls(rinput, ignore_encoding=True) assert reader1.format == FortranFormat(True, False) # Now test when the reader takes notice of the encoding information. @@ -952,7 +954,8 @@ def test_inherited_f77(): with open(filename, "w") as fortran_file: print(string_f77, file=fortran_file) - reader = FortranFileReader(filename, ignore_comments=False, ignore_encoding=False) + reader = FortranFileReader(filename, ignore_comments=False, + ignore_encoding=False) stack = expected[:] for item in reader: assert str(item) == stack.pop(0) @@ -1093,7 +1096,8 @@ def test_f2py_directive_fixf90(f2py_enabled): expected = ["Comment('c -*- fix -*-',(1, 1))", "line #2'subroutine foo'"] if f2py_enabled: expected.extend( - ["line #3'a = 3.14'", "Comment('! pi!',(3, 3))", "line #4'a = 0.0'"] + ["line #3'a = 3.14'", "Comment('! pi!',(3, 3))", + "line #4'a = 0.0'"] ) else: expected.extend( @@ -1122,7 +1126,8 @@ def test_f2py_freef90(f2py_enabled): expected = ["line #1'subroutine foo'"] if f2py_enabled: expected.extend( - ["line #2'a = 3.14'", "Comment('! pi!',(2, 2))", "line #3'a = 0.0'"] + ["line #2'a = 3.14'", "Comment('! pi!',(2, 2))", + "line #3'a = 0.0'"] ) else: expected.extend( @@ -1137,7 +1142,8 @@ def test_f2py_freef90(f2py_enabled): assert str(item) == expected.pop(0) -@pytest.mark.xfail(reason="Issue #270: f2py directives not working in F77 " "code.") +@pytest.mark.xfail(reason="Issue #270: f2py directives not working in F77 " + "code.") def test_f2py_directive_f77(f2py_enabled): """Test the handling of the f2py directive in fixed-format f77.""" string_f77 = """c -*- f77 -*- @@ -1322,7 +1328,8 @@ def test_many_comments(): @pytest.mark.parametrize("inline_comment", [' "', " '", " 'andy' '"]) def test_quotes_in_inline_comments(inline_comment): - """Test that an in-line comment containing a quotation mark is read successfully.""" + """Test that an in-line comment containing a quotation mark is + read successfully.""" input_text = f"""\ character(*) :: a='hello' &!{inline_comment} & b @@ -1400,7 +1407,8 @@ def test_multiple_blank_lines(): output as an empty Comment objects. """ - input_text = " \n\n" "program test\n" " \n\n" "end program test\n" " \n\n" + input_text = (" \n\n" "program test\n" " \n\n" "end program test\n" + " \n\n") reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 8 @@ -1433,7 +1441,8 @@ def test_blank_lines_within_continuation(): """ input_text = ( - " \n" " real :: a &\n" " \n\n" " ,b\n" " \n" " real :: c\n" + " \n" " real :: a &\n" " \n\n" " ,b\n" " \n" + " real :: c\n" ) reader = FortranStringReader(input_text, ignore_comments=False) @@ -1464,7 +1473,7 @@ def test_blank_lines_within_continuation(): assert lines[1].line == "real :: c" -def test_conditional_omp_sentinels_fixed_format_single_line(): +def test_conditional_include_omp_conditional_liness_fixed_format_single_line(): """Test handling of conditional OMP sentinels in a single line with source code in fixed format.""" @@ -1486,7 +1495,8 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): # 3. When omp-sentinels are accepted, we should get a line, # not a comment: reader = FortranStringReader( - input_text, ignore_comments=False, omp_sentinel=True + input_text, ignore_comments=False, + include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) @@ -1495,7 +1505,8 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): # 4. If omp-sentinels are accepted, and comments ignored, # we should still get the line (with the sentinel removed): reader = FortranStringReader( - input_text, ignore_comments=True, omp_sentinel=True + input_text, ignore_comments=True, + include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) @@ -1504,7 +1515,8 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): # 5. Make sure that a real omp directive stays a comment: input_text = f"{sentinel}omp something" reader = FortranStringReader( - input_text, ignore_comments=False, omp_sentinel=True + input_text, ignore_comments=False, + include_omp_conditional_lines=True ) line = reader.next() # This is not a conditional sentinel, so it must be returned @@ -1516,7 +1528,7 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): for sentinel in ["!!$", "! $", " !$", " ! $"]: input_text = f"{sentinel} bla" reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) # Enforce fixed format, otherwise fparser will silently switch # to free format and suddenly interpret comments differently reader.set_format(FortranFormat(False, False)) @@ -1525,7 +1537,7 @@ def test_conditional_omp_sentinels_fixed_format_single_line(): assert comment.comment == input_text -def test_conditional_omp_sentinels_free_format_single_line(): +def test_conditional_include_omp_conditional_liness_free_format_single_line(): """Test handling of conditional OMP sentinels in a single line with source code in free format.""" @@ -1547,7 +1559,7 @@ def test_conditional_omp_sentinels_free_format_single_line(): # 3. When omp-sentinels are accepted, we should get a line, # not a comment:4 reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) reader.set_format(FortranFormat(True, True)) line = reader.next() assert isinstance(line, Line) @@ -1556,7 +1568,7 @@ def test_conditional_omp_sentinels_free_format_single_line(): # 4. If omp-sentinels are accepted, and comments ignored, # we should still get the line (with the sentinel removed): reader = FortranStringReader(input_text, ignore_comments=True, - omp_sentinel=True) + include_omp_conditional_lines=True) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" @@ -1564,7 +1576,7 @@ def test_conditional_omp_sentinels_free_format_single_line(): # 5. Make sure that a real omp directive stays a comment: input_text = " !$omp something" reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) reader.set_format(FortranFormat(True, True)) line = reader.next() # This is not a conditional sentinel, so it must be returned @@ -1576,7 +1588,7 @@ def test_conditional_omp_sentinels_free_format_single_line(): for sentinel in ["!!$", "! $", " ! $"]: input_text = f"{sentinel} bla" reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) reader.set_format(FortranFormat(True, True)) comment = reader.next() assert isinstance(comment, Comment) @@ -1585,7 +1597,7 @@ def test_conditional_omp_sentinels_free_format_single_line(): assert comment.comment == input_text.strip() -def test_conditional_omp_sentinels_fixed_format_multiple_line(): +def test_conditional_include_omp_conditional_liness_fixed_format_multiple(): """Test handling of conditional OMP sentinels with continuation lines with source code in fixed format.""" @@ -1604,7 +1616,7 @@ def test_conditional_omp_sentinels_fixed_format_multiple_line(): # in returning only one line with both concatenated. input_text = "!$ bla\n!$ &bla" reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) line = reader.next() assert isinstance(line, Line) assert line.line == "blabla" @@ -1612,14 +1624,14 @@ def test_conditional_omp_sentinels_fixed_format_multiple_line(): # Add invalid sentinels in continuation lines: input_text = "!$ bla\n! $ &bla" reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) line = reader.next() assert line.line == "bla" line = reader.next() assert line.line == "! $ &bla" -def test_conditional_omp_sentinels_free_format_multiple_line(): +def test_conditional_include_omp_conditional_liness_free_format_multiple(): """Test handling of conditional OMP sentinels with continuation lines with source code in free format.""" @@ -1637,7 +1649,7 @@ def test_conditional_omp_sentinels_free_format_multiple_line(): input_text = "!$ bla &\n!$& bla" reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() @@ -1657,7 +1669,7 @@ def test_conditional_omp_sentinels_free_format_multiple_line(): input_text = "!$ bla &\n!$ bla" reader = FortranStringReader(input_text, ignore_comments=False, - omp_sentinel=True) + include_omp_conditional_lines=True) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() From 14406fe240c4cfb43100e2c22056bca907d66207 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 23 Apr 2024 09:02:08 +1000 Subject: [PATCH 10/16] #443 Applied black, since CI is complaining. --- src/fparser/common/readfortran.py | 145 ++++++++----------- src/fparser/common/tests/test_readfortran.py | 75 +++++----- 2 files changed, 97 insertions(+), 123 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 258682e3..2989fe90 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -229,7 +229,7 @@ def extract_label(line): match = _LABEL_RE.match(line) if match: label = int(match.group("label")) - line = line[match.end():].lstrip() + line = line[match.end() :].lstrip() return label, line @@ -251,7 +251,7 @@ def extract_construct_name(line): match = _CONSTRUCT_NAME_RE.match(line) if match: construct_name = match.group("name") - line = line[match.end():].lstrip() + line = line[match.end() :].lstrip() return construct_name, line @@ -353,8 +353,7 @@ def __str__(self): return s + repr(self.line) def isempty(self, ignore_comments=False): - return not (self.line or self.label is not None or - self.name is not None) + return not (self.line or self.label is not None or self.name is not None) def get_line(self, apply_map=False): if apply_map: @@ -378,22 +377,20 @@ def get_line(self, apply_map=False): if i != -1 and l2[-1] == ")": substrings = ["call " + l2[: i + 1]] start_search = _HOLLERITH_START_SEARCH - l2 = l2[i + 1:-1].strip() + l2 = l2[i + 1 : -1].strip() m = start_search(l2) while m: substrings.append(l2[: m.start()]) substrings.append(m.group("pre")) num = int(m.group("num")) - substrings.append("'" + l2[m.end():m.end() + num] + - "'") - l2 = l2[m.end() + num:] + substrings.append("'" + l2[m.end() : m.end() + num] + "'") + l2 = l2[m.end() + num :] m = start_search(l2) substrings.append(l2) substrings.append(")") line = "".join(substrings) - line, str_map = string_replace_map(line, - lower=not self.reader.format.is_pyf) + line, str_map = string_replace_map(line, lower=not self.reader.format.is_pyf) self.strline = line self.strlinemap = str_map return line @@ -534,8 +531,7 @@ class CppDirective(Line): """ def __init__(self, line, linenospan, reader): - super(CppDirective, self).__init__(line, linenospan, None, None, - reader) + super(CppDirective, self).__init__(line, linenospan, None, None, reader) ############################################################################## @@ -567,8 +563,9 @@ class FortranReaderBase: """ - def __init__(self, source, mode, ignore_comments, - include_omp_conditional_lines=False): + def __init__( + self, source, mode, ignore_comments, include_omp_conditional_lines=False + ): self.source = source self._include_omp_conditional_lines = include_omp_conditional_lines self.set_format(mode) @@ -665,8 +662,7 @@ def set_format(self, mode): # expressions for free format, and the detection of continuation # lines need to be done in a later stage, when multiple lines # are concatenated. - self._re_omp_sentinel_cont = re.compile(r"^ *(\!\$) *&?", - re.IGNORECASE) + self._re_omp_sentinel_cont = re.compile(r"^ *(\!\$) *&?", re.IGNORECASE) @property def format(self): @@ -682,8 +678,7 @@ def name(self): :returns: the name of this reader. :rtype: str """ - return "{source} mode={mode}".format(source=self.source, - mode=self._format.mode) + return "{source} mode={mode}".format(source=self.source, mode=self._format.mode) def close_source(self): """Called when self.source.next() raises StopIteration.""" @@ -877,8 +872,7 @@ def next(self, ignore_comments=None): return item reader.info("including file %r" % (path), item) self.reader = FortranFileReader( - path, include_dirs=include_dirs, - ignore_comments=ignore_comments + path, include_dirs=include_dirs, ignore_comments=ignore_comments ) result = self.reader.next(ignore_comments=ignore_comments) return result @@ -889,8 +883,7 @@ def next(self, ignore_comments=None): # rather than catching *every* exception. except Exception as err: message = self.format_message( - "FATAL ERROR", "while processing line", - self.linecount, self.linecount + "FATAL ERROR", "while processing line", self.linecount, self.linecount ) logging.getLogger(__name__).critical(message) message = "Traceback\n" + "".join(traceback.format_stack()) @@ -970,8 +963,7 @@ def _next(self, ignore_comments=None): # using the existing span (line numbers) and # reader. new_line = Line( - item.apply_map(line), item.span, label, name, - item.reader + item.apply_map(line), item.span, label, name, item.reader ) items.append(new_line) items.reverse() @@ -982,8 +974,7 @@ def _next(self, ignore_comments=None): # Interface to returned items: - def line_item(self, line, startlineno, endlineno, label, - name, errmessage=None): + def line_item(self, line, startlineno, endlineno, label, name, errmessage=None): """Construct Line item.""" if errmessage is None: return Line(line, (startlineno, endlineno), label, name, self) @@ -996,8 +987,7 @@ def multiline_item( ): """Construct MultiLine item.""" if errmessage is None: - return MultiLine(prefix, lines, suffix, (startlineno, endlineno), - self) + return MultiLine(prefix, lines, suffix, (startlineno, endlineno), self) return SyntaxErrorMultiLine( prefix, lines, suffix, (startlineno, endlineno), self, errmessage ) @@ -1033,8 +1023,7 @@ def format_message( for i in range(max(1, startlineno - back_index), startlineno): r.append("%5d:%s" % (i, self.source_lines[i - 1])) for i in range( - startlineno, min(endlineno + back_index, - len(self.source_lines)) + 1 + startlineno, min(endlineno + back_index, len(self.source_lines)) + 1 ): if i == 0 and not self.source_lines: break @@ -1081,8 +1070,7 @@ def info(self, message, item=None): len(self.source_lines), ) else: - m = self.format_message("INFORMATION", message, item.span[0], - item.span[1]) + m = self.format_message("INFORMATION", message, item.span[0], item.span[1]) logging.getLogger(__name__).info(m) def error(self, message, item=None): @@ -1108,15 +1096,14 @@ def warning(self, message, item=None): message, len(self.source_lines) - 2, len(self.source_lines) ) else: - m = self.format_warning_message(message, item.span[0], - item.span[1]) + m = self.format_warning_message(message, item.span[0], item.span[1]) logging.getLogger(__name__).warning(m) # Auxiliary methods for processing raw source lines: @staticmethod def replace_omp_sentinels(line, regex): - '''Checks if the specified line matches the regex, which represents + """Checks if the specified line matches the regex, which represents a conditional OpenMP sentinel. If it is a match, the sentinel (which must be the first group in the regex) is replaced with two spaces. :param line: the line to check if it contains an OpenMP sentinel @@ -1125,11 +1112,11 @@ def replace_omp_sentinels(line, regex): conditional sentinel. :type regex: :py:class:`re.Pattern` - ''' + """ grp = regex.match(line) if grp: # Replace the OMP sentinel with two spaces - line = line[:grp.start(1)] + " " + line[grp.end(1):] + line = line[: grp.start(1)] + " " + line[grp.end(1) :] return (line, True) return (line, False) @@ -1291,12 +1278,12 @@ def handle_multilines(self, line, startlineno, mlstr): suffix = None multilines = [] - line = line[i + 3:] + line = line[i + 3 :] while line is not None: j = line.find(mlstr) if j != -1 and "!" not in line[:j]: multilines.append(line[:j]) - suffix = line[j + 3:] + suffix = line[j + 3 :] break multilines.append(line) line = self.get_single_line() @@ -1306,16 +1293,12 @@ def handle_multilines(self, line, startlineno, mlstr): message, startlineno, startlineno, i ) return self.multiline_item( - prefix, multilines, suffix, startlineno, self.linecount, - message + prefix, multilines, suffix, startlineno, self.linecount, message ) - suffix, qc, had_comment = self.handle_inline_comment( - suffix, self.linecount - ) + suffix, qc, had_comment = self.handle_inline_comment(suffix, self.linecount) # no line continuation allowed in multiline suffix if qc is not None: - message = ("following character continuation: {!r}," - " expected None.") + message = "following character continuation: {!r}," " expected None." message = self.format_message( "ASSERTION FAILURE(pyf)", message.format(qc), @@ -1369,8 +1352,7 @@ def get_source_item(self): line = get_single_line() lines.append(line) endlineno = self.linecount - return self.cpp_directive_item("".join(lines), startlineno, - endlineno) + return self.cpp_directive_item("".join(lines), startlineno, endlineno) line = self.handle_cf2py_start(line) had_include_omp_conditional_liness = False @@ -1378,12 +1360,12 @@ def get_source_item(self): # continuation line can only be properly detected if there was a # previous non-continued conditional sentinel: if self._format.is_free and self._include_omp_conditional_lines: - line, had_include_omp_conditional_liness = \ - self.replace_omp_sentinels(line, self._re_omp_sentinel) + line, had_include_omp_conditional_liness = self.replace_omp_sentinels( + line, self._re_omp_sentinel + ) is_f2py_directive = ( - self._format.f2py_enabled and - startlineno in self.f2py_comment_lines + self._format.f2py_enabled and startlineno in self.f2py_comment_lines ) isstrict = self._format.is_strict have_comment = False @@ -1420,10 +1402,8 @@ def get_source_item(self): logging.getLogger(__name__).warning(message) if i == 0: # non standard comment line: - return self.comment_item(line, startlineno, - startlineno) - mode = fparser.common.sourceinfo.FortranFormat(True, - False) + return self.comment_item(line, startlineno, startlineno) + mode = fparser.common.sourceinfo.FortranFormat(True, False) self.set_format(mode) else: message = self.format_warning_message( @@ -1432,16 +1412,14 @@ def get_source_item(self): logging.getLogger(__name__).warning(message) if i == 0: # non standard comment line: - return self.comment_item(line, startlineno, - startlineno) + return self.comment_item(line, startlineno, startlineno) # return line item with error message # TODO: handle cases with line[6:]=='' message = self.format_error_message( message, startlineno, self.linecount ) return self.line_item( - line[6:], startlineno, self.linecount, label, - name, message + line[6:], startlineno, self.linecount, label, name, message ) if self._format.is_fixed: # Check for switched to free format # check for label @@ -1452,15 +1430,14 @@ def get_source_item(self): m = _CONSTRUCT_NAME_RE.match(line[6:]) if m: name = m.group("name") - line = line[:6] + line[6:][m.end():].lstrip() + line = line[:6] + line[6:][m.end() :].lstrip() if not line[6:].strip(): # check for a blank line if name is not None: self.error("No construct following construct-name.") elif label is not None: self.warning( - "Label must follow nonblank character" - " (F2008:3.2.5_2)" + "Label must follow nonblank character" " (F2008:3.2.5_2)" ) return self.comment_item("", startlineno, self.linecount) # line is not a comment and the start of the line is valid @@ -1489,8 +1466,7 @@ def get_source_item(self): endlineno = self.linecount if self._format.is_fix and not is_f2py_directive: # handle inline comment - newline, qc, had_comment = handle_inline_comment(line[6:], - startlineno) + newline, qc, had_comment = handle_inline_comment(line[6:], startlineno) have_comment |= had_comment lines = [newline] next_line = self.get_next_line() @@ -1506,8 +1482,7 @@ def get_source_item(self): if _is_fix_comment(line2, isstrict, self._format.f2py_enabled): # handle fix format comments inside line continuations # after the line construction - citem = self.comment_item(line2, self.linecount, - self.linecount) + citem = self.comment_item(line2, self.linecount, self.linecount) self.fifo_item.append(citem) else: # line continuation @@ -1520,8 +1495,7 @@ def get_source_item(self): next_line = self.get_next_line() # no character continuation should follows now if qc is not None: - message = ("following character continuation: " - "{!r}, expected None.") + message = "following character continuation: " "{!r}, expected None." message = self.format_message( "ASSERTION FAILURE(fix)", message.format(qc), @@ -1542,8 +1516,7 @@ def get_source_item(self): message, startlineno + i, startlineno + i, location ) logging.getLogger(__name__).warning(message) - return self.line_item("".join(lines), startlineno, endlineno, - label, name) + return self.line_item("".join(lines), startlineno, endlineno, label, name) # line is free format or fixed format with f2py directive (that # will be interpreted as free format line). @@ -1559,8 +1532,7 @@ def get_source_item(self): if had_include_omp_conditional_liness: # In free-format we can only have a continuation line # if we had a omp line previously: - line, _ = self.replace_omp_sentinels( - line, self._re_omp_sentinel_cont) + line, _ = self.replace_omp_sentinels(line, self._re_omp_sentinel_cont) if start_index: # fix format code line, qchar, had_comment = handle_inline_comment( line[start_index:], self.linecount, qchar @@ -1599,7 +1571,7 @@ def get_source_item(self): i = line.rfind("&") if i != -1: - line_i1_rstrip = line[i + 1:].rstrip() + line_i1_rstrip = line[i + 1 :].rstrip() if not lines: # first line if i == -1 or line_i1_rstrip: @@ -1619,23 +1591,20 @@ def get_source_item(self): if k != 1 and line[:k].lstrip(): k = -1 endlineno = self.linecount - lines_append(line[k + 1:i]) + lines_append(line[k + 1 : i]) if i == len(line): break line = get_single_line() if qchar is not None: - message = ("following character continuation: {!r}, " - "expected None.") + message = "following character continuation: {!r}, " "expected None." message = self.format_message( - "ASSERTION FAILURE(free)", message.format(qchar), - startlineno, endlineno + "ASSERTION FAILURE(free)", message.format(qchar), startlineno, endlineno ) logging.getLogger(__name__).error(message) line_content = "".join(lines).strip() if line_content: - return self.line_item(line_content, startlineno, endlineno, - label, name) + return self.line_item(line_content, startlineno, endlineno, label, name) if label is not None: message = "Label must follow nonblank character (F2008:3.2.5_2)" self.warning(message) @@ -1712,8 +1681,11 @@ def __init__( ) super().__init__( - self.file, mode, ignore_comments, - include_omp_conditional_lines=include_omp_conditional_lines) + self.file, + mode, + ignore_comments, + include_omp_conditional_lines=include_omp_conditional_lines, + ) if include_dirs is None: self.include_dirs.insert(0, os.path.dirname(self.id)) @@ -1783,8 +1755,11 @@ def __init__( string, ignore_encoding=ignore_encoding ) super().__init__( - source, mode, ignore_comments, - include_omp_conditional_lines=include_omp_conditional_lines) + source, + mode, + ignore_comments, + include_omp_conditional_lines=include_omp_conditional_lines, + ) if include_dirs is not None: self.include_dirs = include_dirs[:] if source_only is not None: diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index bf18574c..ee2a396b 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -885,8 +885,7 @@ def test_string_reader(): assert unit_under_test.get_single_line(ignore_empty=True) == expected -@pytest.mark.parametrize("reader_cls", [FortranStringReader, - FortranFileReader]) +@pytest.mark.parametrize("reader_cls", [FortranStringReader, FortranFileReader]) def test_reader_ignore_encoding(reader_cls, tmp_path): """ Tests that the Fortran{String,File}Reader can be configured to take @@ -954,8 +953,7 @@ def test_inherited_f77(): with open(filename, "w") as fortran_file: print(string_f77, file=fortran_file) - reader = FortranFileReader(filename, ignore_comments=False, - ignore_encoding=False) + reader = FortranFileReader(filename, ignore_comments=False, ignore_encoding=False) stack = expected[:] for item in reader: assert str(item) == stack.pop(0) @@ -1096,8 +1094,7 @@ def test_f2py_directive_fixf90(f2py_enabled): expected = ["Comment('c -*- fix -*-',(1, 1))", "line #2'subroutine foo'"] if f2py_enabled: expected.extend( - ["line #3'a = 3.14'", "Comment('! pi!',(3, 3))", - "line #4'a = 0.0'"] + ["line #3'a = 3.14'", "Comment('! pi!',(3, 3))", "line #4'a = 0.0'"] ) else: expected.extend( @@ -1126,8 +1123,7 @@ def test_f2py_freef90(f2py_enabled): expected = ["line #1'subroutine foo'"] if f2py_enabled: expected.extend( - ["line #2'a = 3.14'", "Comment('! pi!',(2, 2))", - "line #3'a = 0.0'"] + ["line #2'a = 3.14'", "Comment('! pi!',(2, 2))", "line #3'a = 0.0'"] ) else: expected.extend( @@ -1142,8 +1138,7 @@ def test_f2py_freef90(f2py_enabled): assert str(item) == expected.pop(0) -@pytest.mark.xfail(reason="Issue #270: f2py directives not working in F77 " - "code.") +@pytest.mark.xfail(reason="Issue #270: f2py directives not working in F77 " "code.") def test_f2py_directive_f77(f2py_enabled): """Test the handling of the f2py directive in fixed-format f77.""" string_f77 = """c -*- f77 -*- @@ -1407,8 +1402,7 @@ def test_multiple_blank_lines(): output as an empty Comment objects. """ - input_text = (" \n\n" "program test\n" " \n\n" "end program test\n" - " \n\n") + input_text = " \n\n" "program test\n" " \n\n" "end program test\n" " \n\n" reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 8 @@ -1441,8 +1435,7 @@ def test_blank_lines_within_continuation(): """ input_text = ( - " \n" " real :: a &\n" " \n\n" " ,b\n" " \n" - " real :: c\n" + " \n" " real :: a &\n" " \n\n" " ,b\n" " \n" " real :: c\n" ) reader = FortranStringReader(input_text, ignore_comments=False) @@ -1495,8 +1488,7 @@ def test_conditional_include_omp_conditional_liness_fixed_format_single_line(): # 3. When omp-sentinels are accepted, we should get a line, # not a comment: reader = FortranStringReader( - input_text, ignore_comments=False, - include_omp_conditional_lines=True + input_text, ignore_comments=False, include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) @@ -1505,8 +1497,7 @@ def test_conditional_include_omp_conditional_liness_fixed_format_single_line(): # 4. If omp-sentinels are accepted, and comments ignored, # we should still get the line (with the sentinel removed): reader = FortranStringReader( - input_text, ignore_comments=True, - include_omp_conditional_lines=True + input_text, ignore_comments=True, include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) @@ -1515,8 +1506,7 @@ def test_conditional_include_omp_conditional_liness_fixed_format_single_line(): # 5. Make sure that a real omp directive stays a comment: input_text = f"{sentinel}omp something" reader = FortranStringReader( - input_text, ignore_comments=False, - include_omp_conditional_lines=True + input_text, ignore_comments=False, include_omp_conditional_lines=True ) line = reader.next() # This is not a conditional sentinel, so it must be returned @@ -1527,8 +1517,9 @@ def test_conditional_include_omp_conditional_liness_fixed_format_single_line(): # 6. Test some corner cases (all of which are not valid sentinels): for sentinel in ["!!$", "! $", " !$", " ! $"]: input_text = f"{sentinel} bla" - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) # Enforce fixed format, otherwise fparser will silently switch # to free format and suddenly interpret comments differently reader.set_format(FortranFormat(False, False)) @@ -1558,8 +1549,9 @@ def test_conditional_include_omp_conditional_liness_free_format_single_line(): # 3. When omp-sentinels are accepted, we should get a line, # not a comment:4 - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) reader.set_format(FortranFormat(True, True)) line = reader.next() assert isinstance(line, Line) @@ -1567,16 +1559,18 @@ def test_conditional_include_omp_conditional_liness_free_format_single_line(): # 4. If omp-sentinels are accepted, and comments ignored, # we should still get the line (with the sentinel removed): - reader = FortranStringReader(input_text, ignore_comments=True, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=True, include_omp_conditional_lines=True + ) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" # 5. Make sure that a real omp directive stays a comment: input_text = " !$omp something" - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) reader.set_format(FortranFormat(True, True)) line = reader.next() # This is not a conditional sentinel, so it must be returned @@ -1587,8 +1581,9 @@ def test_conditional_include_omp_conditional_liness_free_format_single_line(): # 6. Test some corner cases (all of which are not valid sentinels): for sentinel in ["!!$", "! $", " ! $"]: input_text = f"{sentinel} bla" - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) reader.set_format(FortranFormat(True, True)) comment = reader.next() assert isinstance(comment, Comment) @@ -1615,16 +1610,18 @@ def test_conditional_include_omp_conditional_liness_fixed_format_multiple(): # Now enable handling of sentinels, which will result # in returning only one line with both concatenated. input_text = "!$ bla\n!$ &bla" - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) line = reader.next() assert isinstance(line, Line) assert line.line == "blabla" # Add invalid sentinels in continuation lines: input_text = "!$ bla\n! $ &bla" - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) line = reader.next() assert line.line == "bla" line = reader.next() @@ -1648,8 +1645,9 @@ def test_conditional_include_omp_conditional_liness_free_format_multiple(): assert comment.comment == "!$& bla" input_text = "!$ bla &\n!$& bla" - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() @@ -1668,8 +1666,9 @@ def test_conditional_include_omp_conditional_liness_free_format_multiple(): assert comment.comment == "!$ bla" input_text = "!$ bla &\n!$ bla" - reader = FortranStringReader(input_text, ignore_comments=False, - include_omp_conditional_lines=True) + reader = FortranStringReader( + input_text, ignore_comments=False, include_omp_conditional_lines=True + ) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() From b3226b5c67ce0757d2180c47c86b50a03ec1cc49 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 25 Apr 2024 01:21:07 +1000 Subject: [PATCH 11/16] #443 Fixed issues from review. --- src/fparser/common/readfortran.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 2989fe90..6903a6f1 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -631,7 +631,7 @@ def set_format(self, mode): return if self._format.is_fixed or self._format.is_f77: - # Initial lines fixed format sentinels: !$, c$, *! in first + # Initial lines fixed format sentinels: !$, c$, *$ in first # column: sentinel = r"^([\!\*c]\$)" @@ -1106,12 +1106,16 @@ def replace_omp_sentinels(line, regex): """Checks if the specified line matches the regex, which represents a conditional OpenMP sentinel. If it is a match, the sentinel (which must be the first group in the regex) is replaced with two spaces. - :param line: the line to check if it contains an OpenMP sentinel - :type line: str + + :param str line: the line to check if it contains an OpenMP sentinel :param regex: the compiled regular expression to use for detecting a conditional sentinel. :type regex: :py:class:`re.Pattern` + :returns: 2-tuple consisting of the (potentially modified) line, + and whether a sentinel was found or not. + :type: Tuple[str, bool] + """ grp = regex.match(line) if grp: @@ -1298,7 +1302,7 @@ def handle_multilines(self, line, startlineno, mlstr): suffix, qc, had_comment = self.handle_inline_comment(suffix, self.linecount) # no line continuation allowed in multiline suffix if qc is not None: - message = "following character continuation: {!r}," " expected None." + message = "following character continuation: {!r}, expected None." message = self.format_message( "ASSERTION FAILURE(pyf)", message.format(qc), @@ -1437,7 +1441,7 @@ def get_source_item(self): self.error("No construct following construct-name.") elif label is not None: self.warning( - "Label must follow nonblank character" " (F2008:3.2.5_2)" + "Label must follow nonblank character (F2008:3.2.5_2)" ) return self.comment_item("", startlineno, self.linecount) # line is not a comment and the start of the line is valid @@ -1495,7 +1499,7 @@ def get_source_item(self): next_line = self.get_next_line() # no character continuation should follows now if qc is not None: - message = "following character continuation: " "{!r}, expected None." + message = "following character continuation: {!r}, expected None." message = self.format_message( "ASSERTION FAILURE(fix)", message.format(qc), @@ -1597,7 +1601,7 @@ def get_source_item(self): line = get_single_line() if qchar is not None: - message = "following character continuation: {!r}, " "expected None." + message = "following character continuation: {!r}, expected None." message = self.format_message( "ASSERTION FAILURE(free)", message.format(qchar), startlineno, endlineno ) From 42bb0d53b01da5a57209a23691c3f26127e72b64 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 25 Apr 2024 01:27:35 +1000 Subject: [PATCH 12/16] #443 Renamed variable. --- src/fparser/common/readfortran.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 6903a6f1..5db41031 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -1359,12 +1359,12 @@ def get_source_item(self): return self.cpp_directive_item("".join(lines), startlineno, endlineno) line = self.handle_cf2py_start(line) - had_include_omp_conditional_liness = False + had_omp_sentinels = False # Free format omp sentinels need to be handled here, since a # continuation line can only be properly detected if there was a # previous non-continued conditional sentinel: if self._format.is_free and self._include_omp_conditional_lines: - line, had_include_omp_conditional_liness = self.replace_omp_sentinels( + line, had_omp_sentinels = self.replace_omp_sentinels( line, self._re_omp_sentinel ) @@ -1533,7 +1533,7 @@ def get_source_item(self): put_item = self.fifo_item.append qchar = None while line is not None: - if had_include_omp_conditional_liness: + if had_omp_sentinels: # In free-format we can only have a continuation line # if we had a omp line previously: line, _ = self.replace_omp_sentinels(line, self._re_omp_sentinel_cont) From cb6ed8ac8b49a05c09f3fd9d4e1554a85050007e Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 25 Apr 2024 01:36:47 +1000 Subject: [PATCH 13/16] #443 Fixed typo in comment. --- src/fparser/common/tests/test_readfortran.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index ee2a396b..7eabe08d 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1548,7 +1548,7 @@ def test_conditional_include_omp_conditional_liness_free_format_single_line(): comment = reader.next() # 3. When omp-sentinels are accepted, we should get a line, - # not a comment:4 + # not a comment: reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) From 1a202aca02c96acd6fbd9a9530b321c0b666b82a Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 25 Apr 2024 01:49:01 +1000 Subject: [PATCH 14/16] #443 Added more tests for ignore_comments=True, fixed a few more string concatenations warnings. --- src/fparser/common/tests/test_readfortran.py | 30 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index 7eabe08d..38900b11 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1402,7 +1402,7 @@ def test_multiple_blank_lines(): output as an empty Comment objects. """ - input_text = " \n\n" "program test\n" " \n\n" "end program test\n" " \n\n" + input_text = " \n\nprogram test\n \n\nend program test\n \n\n" reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 8 @@ -1435,7 +1435,7 @@ def test_blank_lines_within_continuation(): """ input_text = ( - " \n" " real :: a &\n" " \n\n" " ,b\n" " \n" " real :: c\n" + " \n real :: a &\n \n\n ,b\n \n real :: c\n" ) reader = FortranStringReader(input_text, ignore_comments=False) @@ -1597,6 +1597,9 @@ def test_conditional_include_omp_conditional_liness_fixed_format_multiple(): with source code in fixed format.""" input_text = "!$ bla\n!$ &bla" + reader = FortranStringReader(input_text, ignore_comments=True) + with pytest.raises(StopIteration): + reader.next() reader = FortranStringReader(input_text, ignore_comments=False) # Without handling of sentinels, this should return # two comment lines: @@ -1617,8 +1620,25 @@ def test_conditional_include_omp_conditional_liness_fixed_format_multiple(): assert isinstance(line, Line) assert line.line == "blabla" + # Ignoring comments must not change the behaviour: + reader = FortranStringReader( + input_text, ignore_comments=True, include_omp_conditional_lines=True + ) + line = reader.next() + assert isinstance(line, Line) + assert line.line == "blabla" + # Add invalid sentinels in continuation lines: input_text = "!$ bla\n! $ &bla" + reader = FortranStringReader( + input_text, ignore_comments=True, include_omp_conditional_lines=True + ) + line = reader.next() + assert line.line == "bla" + # The second line is just a comment line, so it must be ignored: + with pytest.raises(StopIteration): + reader.next() + reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) @@ -1665,6 +1685,12 @@ def test_conditional_include_omp_conditional_liness_free_format_multiple(): comment = reader.next() assert comment.comment == "!$ bla" + reader = FortranStringReader(input_text, ignore_comments=True) + # Make sure to enforce free format + reader.set_format(FortranFormat(True, True)) + with pytest.raises(StopIteration): + comment = reader.next() + input_text = "!$ bla &\n!$ bla" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True From f14e68fd6a3bd675cbd6aef2ccd3423384fa7e52 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 25 Apr 2024 01:50:51 +1000 Subject: [PATCH 15/16] #443 Black again, just a single line which looks fine. --- src/fparser/common/tests/test_readfortran.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index 38900b11..8cc4baec 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -1434,9 +1434,7 @@ def test_blank_lines_within_continuation(): removed. """ - input_text = ( - " \n real :: a &\n \n\n ,b\n \n real :: c\n" - ) + input_text = " \n real :: a &\n \n\n ,b\n \n real :: c\n" reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) From 338bd07418243a537241109f49ee2153865581b9 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 24 Apr 2024 21:29:45 +0100 Subject: [PATCH 16/16] #444 update changelog and fix typo in doc string --- CHANGELOG.md | 3 +++ src/fparser/common/readfortran.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb9145c..5f24e1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ Modifications by (in alphabetical order): * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +24/04/2024 PR #444 for #443. Adds an option to the reader to handle code + behind OpenMP sentinels (e.g. '!$ write(*,*) "hello"'). + 23/04/2024 PR #446 for #445. Updates Codecov action to v4. 09/04/2024 PR #442 for #440. Adds a new 'split file' example that splits a single source diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 5db41031..344c9db8 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -1114,7 +1114,7 @@ def replace_omp_sentinels(line, regex): :returns: 2-tuple consisting of the (potentially modified) line, and whether a sentinel was found or not. - :type: Tuple[str, bool] + :rtype: tuple[str, bool] """ grp = regex.match(line)