From 3c5fc7240669c4d824d97d3e2512d97c446d895b Mon Sep 17 00:00:00 2001 From: SCHREIBER Martin Date: Mon, 18 Nov 2024 15:58:35 +0100 Subject: [PATCH 1/9] Allow deepcopy --- src/fparser/common/utils.py | 3 +++ src/fparser/two/Fortran2003.py | 8 ++++++-- src/fparser/two/utils.py | 10 +++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/fparser/common/utils.py b/src/fparser/common/utils.py index f6eed64e..e3a6ed9e 100644 --- a/src/fparser/common/utils.py +++ b/src/fparser/common/utils.py @@ -380,3 +380,6 @@ def __new__(metacls, name, bases, dict): cls = type.__new__(metacls, name, bases, dict) _classes_cache[name] = cls return cls + + def __getnewargs__(self): + return (self.name, self.bases, self.dict) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 3068efbd..764663e0 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -252,7 +252,7 @@ class Program(BlockBase): # R201 use_names = ["Program_Unit"] @show_result - def __new__(cls, string): + def __new__(cls, string, _deepcopy=False): """Wrapper around base class __new__ to catch an internal NoMatchError exception and raise it as an external FortranSyntaxError exception. @@ -264,7 +264,7 @@ def __new__(cls, string): """ # pylint: disable=unused-argument try: - return Base.__new__(cls, string) + return Base.__new__(cls, string, _deepcopy=_deepcopy) except NoMatchError: # At the moment there is no useful information provided by # NoMatchError so we pass on an empty string. @@ -277,6 +277,10 @@ def __new__(cls, string): # provides line number information). raise FortranSyntaxError(string, excinfo) + def __getnewargs__(self): + # For deep copy + return (self.string, True) + @staticmethod def match(reader): """Implements the matching for a Program. Whilst the rule looks like diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 3745697c..271d2f22 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -409,7 +409,7 @@ def __init__(self, string, parent_cls=None): self.parent = None @show_result - def __new__(cls, string, parent_cls=None): + def __new__(cls, string, parent_cls=None, _deepcopy=False): if parent_cls is None: parent_cls = [cls] elif cls not in parent_cls: @@ -418,6 +418,11 @@ def __new__(cls, string, parent_cls=None): # Get the class' match method if it has one match = getattr(cls, "match", None) + if _deepcopy: + # If this is deep copied (and string is None), simply call + # the super method without string + return super().__new__(cls) + if ( isinstance(string, FortranReaderBase) and match @@ -505,6 +510,9 @@ def __new__(cls, string, parent_cls=None): errmsg = f"{cls.__name__}: '{string}'" raise NoMatchError(errmsg) + def __getnewargs__(self): + return (self.string, None, True) + def get_root(self): """ Gets the node at the root of the parse tree to which this node belongs. From 3f8c43c42b0c2b93caa39e37e33451d634b55d98 Mon Sep 17 00:00:00 2001 From: Martin SCHREIBER Date: Mon, 18 Nov 2024 17:22:05 +0100 Subject: [PATCH 2/9] updates for Andy --- .gitignore | 1 + src/fparser/common/utils.py | 9 ++++++ src/fparser/two/Fortran2003.py | 13 +++++++- src/fparser/two/tests/test_parser.py | 47 ++++++++++++++++++++++++++++ src/fparser/two/utils.py | 11 ++++++- 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 61cac378..9e94c00f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *.pyc *~ +src/**/*.mod *.log _build/ htmlcov/ diff --git a/src/fparser/common/utils.py b/src/fparser/common/utils.py index e3a6ed9e..36f436c2 100644 --- a/src/fparser/common/utils.py +++ b/src/fparser/common/utils.py @@ -382,4 +382,13 @@ def __new__(metacls, name, bases, dict): return cls def __getnewargs__(self): + """Method to dictate the values passed to the __new__() method upon + unpickling. The method must return a pair (args, kwargs) where + args is a tuple of positional arguments and kwargs a dictionary + of named arguments for constructing the object. Those will be + passed to the __new__() method upon unpickling. + + :return: set of arguments for __new__ + :rtype: set + """ return (self.name, self.bases, self.dict) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 764663e0..f7b1138a 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -259,6 +259,9 @@ def __new__(cls, string, _deepcopy=False): :param type cls: the class of object to create :param string: (source of) Fortran string to parse :type string: :py:class:`FortranReaderBase` + :param _deepcopy: Flag to signal whether this class is + created by a deep copy + :type _deepcopy: bool :raises FortranSyntaxError: if the code is not valid Fortran """ @@ -278,7 +281,15 @@ def __new__(cls, string, _deepcopy=False): raise FortranSyntaxError(string, excinfo) def __getnewargs__(self): - # For deep copy + """Method to dictate the values passed to the __new__() method upon + unpickling. The method must return a pair (args, kwargs) where + args is a tuple of positional arguments and kwargs a dictionary + of named arguments for constructing the object. Those will be + passed to the __new__() method upon unpickling. + + :return: set of arguments for __new__ + :rtype: set + """ return (self.string, True) @staticmethod diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 5d4a6e19..16b53b86 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -84,3 +84,50 @@ class do not affect current calls. with pytest.raises(ValueError) as excinfo: parser = ParserFactory().create(std="invalid") assert "is an invalid standard" in str(excinfo.value) + + + + +def test_deepcopy(): + """ + Test that we can deepcopy a parsed fparser tree. + """ + + f90_source = """ +program main + + implicit none + +end +""" + parser = ParserFactory().create(std="f2008") + reader = FortranStringReader(f90_source) + ast = parser(reader) + + print("<>"*80) + print(ast) + + import copy + _ = copy.deepcopy(ast) + + +def test_pickle(): + """ + Test that we can pickle and unpickle a parsed fparser tree. + """ + + f90_source = """ +program main + + implicit none + +end +""" + + parser = ParserFactory().create(std="f2008") + reader = FortranStringReader(f90_source) + ast = parser(reader) + + import pickle + s = pickle.dumps(ast) + _ = pickle.loads(s) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 271d2f22..08daade1 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -419,7 +419,7 @@ def __new__(cls, string, parent_cls=None, _deepcopy=False): match = getattr(cls, "match", None) if _deepcopy: - # If this is deep copied (and string is None), simply call + # If this is part of a deep-copy operation (and string is None), simply call # the super method without string return super().__new__(cls) @@ -511,6 +511,15 @@ def __new__(cls, string, parent_cls=None, _deepcopy=False): raise NoMatchError(errmsg) def __getnewargs__(self): + """Method to dictate the values passed to the __new__() method upon + unpickling. The method must return a pair (args, kwargs) where + args is a tuple of positional arguments and kwargs a dictionary + of named arguments for constructing the object. Those will be + passed to the __new__() method upon unpickling. + + :return: set of arguments for __new__ + :rtype: set + """ return (self.string, None, True) def get_root(self): From eefd14e4f7feb782aacd09a3ddbd22ec012d015a Mon Sep 17 00:00:00 2001 From: SCHREIBER Martin Date: Tue, 19 Nov 2024 11:19:19 +0100 Subject: [PATCH 3/9] applied black --- src/fparser/two/tests/test_parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 16b53b86..3a499a66 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -86,8 +86,6 @@ class do not affect current calls. assert "is an invalid standard" in str(excinfo.value) - - def test_deepcopy(): """ Test that we can deepcopy a parsed fparser tree. @@ -104,10 +102,11 @@ def test_deepcopy(): reader = FortranStringReader(f90_source) ast = parser(reader) - print("<>"*80) + print("<>" * 80) print(ast) import copy + _ = copy.deepcopy(ast) @@ -129,5 +128,6 @@ def test_pickle(): ast = parser(reader) import pickle + s = pickle.dumps(ast) _ = pickle.loads(s) From 156d7a5c62a1525849a07410d91381fffc940269 Mon Sep 17 00:00:00 2001 From: SCHREIBER Martin Date: Tue, 19 Nov 2024 11:50:49 +0100 Subject: [PATCH 4/9] fix of coverage test --- src/fparser/common/utils.py | 12 ------------ src/fparser/two/tests/test_parser.py | 3 --- 2 files changed, 15 deletions(-) diff --git a/src/fparser/common/utils.py b/src/fparser/common/utils.py index 36f436c2..f6eed64e 100644 --- a/src/fparser/common/utils.py +++ b/src/fparser/common/utils.py @@ -380,15 +380,3 @@ def __new__(metacls, name, bases, dict): cls = type.__new__(metacls, name, bases, dict) _classes_cache[name] = cls return cls - - def __getnewargs__(self): - """Method to dictate the values passed to the __new__() method upon - unpickling. The method must return a pair (args, kwargs) where - args is a tuple of positional arguments and kwargs a dictionary - of named arguments for constructing the object. Those will be - passed to the __new__() method upon unpickling. - - :return: set of arguments for __new__ - :rtype: set - """ - return (self.name, self.bases, self.dict) diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 3a499a66..1c22b465 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -102,9 +102,6 @@ def test_deepcopy(): reader = FortranStringReader(f90_source) ast = parser(reader) - print("<>" * 80) - print(ast) - import copy _ = copy.deepcopy(ast) From 78ebaad8113c0a814de5be5a4d9678a9af82f8fb Mon Sep 17 00:00:00 2001 From: SCHREIBER Martin Date: Wed, 20 Nov 2024 16:08:01 +0100 Subject: [PATCH 5/9] For Andy & Sergi --- src/fparser/two/Fortran2003.py | 3 +- src/fparser/two/tests/test_parser.py | 49 ++++++++++++++++++++++++++-- src/fparser/two/utils.py | 2 +- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index f7b1138a..9919900d 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -262,6 +262,7 @@ def __new__(cls, string, _deepcopy=False): :param _deepcopy: Flag to signal whether this class is created by a deep copy :type _deepcopy: bool + :raises FortranSyntaxError: if the code is not valid Fortran """ @@ -288,7 +289,7 @@ def __getnewargs__(self): passed to the __new__() method upon unpickling. :return: set of arguments for __new__ - :rtype: set + :rtype: tuple[str, bool] """ return (self.string, True) diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 1c22b465..bb093f14 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -86,6 +86,47 @@ class do not affect current calls. assert "is an invalid standard" in str(excinfo.value) +def _cmp_tree_types_rec( + node1: Fortran2003.Program, node2: Fortran2003.Program, depth: int = 0 +): + """Helper function to recursively check for deepcopied programs + + :param node1: First AST tree to check + :type node1: Fortran2003.Program + :param node2: Second AST tree to check + :type node2: Fortran2003.Program + :param depth: Depth useful later on for debugging reasons, + defaults to 0 + :type depth: int, optional + """ + # Make sure that both trees are the same + assert type(node1) is type( + node2 + ), f"Nodes have different types: '{type(node1)}' and '{type(node2)}" + + if type(node1) is str: + # Strings could refer to the same object, e.g., the string + # of the implicit statement "NONE". + # Therefore, we only check that their string is matching + assert node1 == node2, "String values should be the same" + if node1 not in ["NONE", "PROGRAM"]: + assert ( + node1 is not node2 + ), "Nodes should refer to different objects" + return + + if node1 is None: + # Just return for None objects + return + + # Make sure that we're working on a copy rather than the same object + assert node1 is not node2, "Nodes refer to the same object" + + # Continue recursive traversal of ast + for child1, child2 in zip(node1.children, node2.children): + _cmp_tree_types_rec(child1, child2, depth + 1) + + def test_deepcopy(): """ Test that we can deepcopy a parsed fparser tree. @@ -104,7 +145,9 @@ def test_deepcopy(): import copy - _ = copy.deepcopy(ast) + new_ast = copy.deepcopy(ast) + + _cmp_tree_types_rec(new_ast, ast) def test_pickle(): @@ -127,4 +170,6 @@ def test_pickle(): import pickle s = pickle.dumps(ast) - _ = pickle.loads(s) + new_ast = pickle.loads(s) + + _cmp_tree_types_rec(new_ast, ast) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 08daade1..de2731df 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -518,7 +518,7 @@ def __getnewargs__(self): passed to the __new__() method upon unpickling. :return: set of arguments for __new__ - :rtype: set + :rtype: tuple[str, NoneType, bool] """ return (self.string, None, True) From 512f865bc651dde2a053c7f1c5932758fbcac7d5 Mon Sep 17 00:00:00 2001 From: SCHREIBER Martin Date: Wed, 20 Nov 2024 22:49:49 +0100 Subject: [PATCH 6/9] black... --- src/fparser/two/tests/test_parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index bb093f14..36f36c5d 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -110,9 +110,7 @@ def _cmp_tree_types_rec( # Therefore, we only check that their string is matching assert node1 == node2, "String values should be the same" if node1 not in ["NONE", "PROGRAM"]: - assert ( - node1 is not node2 - ), "Nodes should refer to different objects" + assert node1 is not node2, "Nodes should refer to different objects" return if node1 is None: From 538f370ddea08113cde9245035f37051d8df1fdb Mon Sep 17 00:00:00 2001 From: SCHREIBER Martin Date: Fri, 22 Nov 2024 13:10:35 +0100 Subject: [PATCH 7/9] update for andy --- src/fparser/two/tests/test_parser.py | 79 ++++++++++++++++++---------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 36f36c5d..3907675b 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -36,7 +36,7 @@ import pytest from fparser.two.parser import ParserFactory from fparser.common.readfortran import FortranStringReader -from fparser.two.utils import FortranSyntaxError +from fparser.two.utils import FortranSyntaxError, StmtBase from fparser.two.symbol_table import SYMBOL_TABLES from fparser.two import Fortran2003, Fortran2008 @@ -99,46 +99,75 @@ def _cmp_tree_types_rec( defaults to 0 :type depth: int, optional """ + # Make sure that both trees are the same assert type(node1) is type( node2 ), f"Nodes have different types: '{type(node1)}' and '{type(node2)}" - if type(node1) is str: - # Strings could refer to the same object, e.g., the string - # of the implicit statement "NONE". - # Therefore, we only check that their string is matching - assert node1 == node2, "String values should be the same" - if node1 not in ["NONE", "PROGRAM"]: - assert node1 is not node2, "Nodes should refer to different objects" - return - if node1 is None: # Just return for None objects return - # Make sure that we're working on a copy rather than the same object - assert node1 is not node2, "Nodes refer to the same object" + if type(node1) is str: + # WARNING: Different string objects with the same can have the same id. + # Therefore, we can't compare with 'is' or with 'id(.) == id(.)'. + # We can just compare the both strings have the same content. + # See https://stackoverflow.com/questions/20753364/why-does-creating-multiple-objects-without-naming-them-result-in-them-having-the + assert node1 == node2 + return + + else: + # Make sure that we're working on a copy rather than the same object + assert node1 is not node2, "Nodes refer to the same object" # Continue recursive traversal of ast for child1, child2 in zip(node1.children, node2.children): _cmp_tree_types_rec(child1, child2, depth + 1) +_f90_source_test = """ +module andy +implicit none + + real :: apple = 1.0 + real, parameter :: pi = 3.14 + +contains + subroutine sergi() + print *, "Pi = ", pi + print *, "apple = ", apple + end subroutine + +end module andy + + +program awesome + use andy + implicit none + + real :: x + integer :: i + + x = 2.2 + i = 7 + + call sergi() + + print *, "apple pie: ", apple, pi + print *, "i: ", i + +end program awesome + +""" + def test_deepcopy(): """ Test that we can deepcopy a parsed fparser tree. """ - f90_source = """ -program main - - implicit none - -end -""" parser = ParserFactory().create(std="f2008") - reader = FortranStringReader(f90_source) + reader = FortranStringReader(_f90_source_test) ast = parser(reader) import copy @@ -153,16 +182,8 @@ def test_pickle(): Test that we can pickle and unpickle a parsed fparser tree. """ - f90_source = """ -program main - - implicit none - -end -""" - parser = ParserFactory().create(std="f2008") - reader = FortranStringReader(f90_source) + reader = FortranStringReader(_f90_source_test) ast = parser(reader) import pickle From ed6decbd6aa4f8255c8282542d77fb911a966e4a Mon Sep 17 00:00:00 2001 From: SCHREIBER Martin Date: Fri, 22 Nov 2024 13:10:51 +0100 Subject: [PATCH 8/9] black update --- src/fparser/two/tests/test_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 3907675b..9552eaa9 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -161,6 +161,7 @@ def _cmp_tree_types_rec( """ + def test_deepcopy(): """ Test that we can deepcopy a parsed fparser tree. From 4b7e555fe18af15a94b62cfb5739ebde987ac55a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 25 Nov 2024 09:47:56 +0000 Subject: [PATCH 9/9] #453 update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9407774a..9704829a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,14 @@ Modifications by (in alphabetical order): * A. R. Porter, Science & Technology Facilities Council, UK * B. Reuter, ECMWF, UK * S. Siso, Science & Technology Facilities Council, UK +* M. Schreiber, Universite Grenoble Alpes, France * J. Tiira, University of Helsinki, Finland * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +25/11/2024 PR #453 extension of base node types to allow the parse tree to be + deepcopied and pickled. + 14/10/2024 PR #451 for #320. Adds an extension to Fortran2003 to support non-standard STOP expressions and adds support for them in 2008.