Skip to content

Commit

Permalink
Merge pull request #453 from stfc/martin_deepcopy_issue
Browse files Browse the repository at this point in the history
Allow deepcopy of fparser tree
  • Loading branch information
arporter authored Nov 25, 2024
2 parents 84a4b1d + 4b7e555 commit 8c870f8
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*.pyc
*~
src/**/*.mod
*.log
_build/
htmlcov/
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
20 changes: 18 additions & 2 deletions src/fparser/two/Fortran2003.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,19 +252,23 @@ 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.
: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
"""
# 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.
Expand All @@ -277,6 +281,18 @@ def __new__(cls, string):
# provides line number information).
raise FortranSyntaxError(string, excinfo)

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: tuple[str, bool]
"""
return (self.string, True)

@staticmethod
def match(reader):
"""Implements the matching for a Program. Whilst the rule looks like
Expand Down
111 changes: 110 additions & 1 deletion src/fparser/two/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -84,3 +84,112 @@ 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 _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 node1 is None:
# Just return for None objects
return

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.
"""

parser = ParserFactory().create(std="f2008")
reader = FortranStringReader(_f90_source_test)
ast = parser(reader)

import copy

new_ast = copy.deepcopy(ast)

_cmp_tree_types_rec(new_ast, ast)


def test_pickle():
"""
Test that we can pickle and unpickle a parsed fparser tree.
"""

parser = ParserFactory().create(std="f2008")
reader = FortranStringReader(_f90_source_test)
ast = parser(reader)

import pickle

s = pickle.dumps(ast)
new_ast = pickle.loads(s)

_cmp_tree_types_rec(new_ast, ast)
19 changes: 18 additions & 1 deletion src/fparser/two/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 part of a deep-copy operation (and string is None), simply call
# the super method without string
return super().__new__(cls)

if (
isinstance(string, FortranReaderBase)
and match
Expand Down Expand Up @@ -505,6 +510,18 @@ def __new__(cls, string, parent_cls=None):
errmsg = f"{cls.__name__}: '{string}'"
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: tuple[str, NoneType, bool]
"""
return (self.string, None, True)

def get_root(self):
"""
Gets the node at the root of the parse tree to which this node belongs.
Expand Down

0 comments on commit 8c870f8

Please sign in to comment.