diff --git a/CHANGELOG.md b/CHANGELOG.md index 4409a769..66071843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Modifications by (in alphabetical order): * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +30/03/2023 PR #396 for #395. Fix trailing whitespace bug in CallBase. + 13/03/2023 PR #391 for #324. Add GH workfow to automate a pypi upload during GH releases. diff --git a/src/fparser/.pylintrc b/src/fparser/.pylintrc new file mode 100644 index 00000000..7a937382 --- /dev/null +++ b/src/fparser/.pylintrc @@ -0,0 +1,17 @@ +[FORMAT] + +# Maximum number of characters on a single line. Black's default is 88. +max-line-length=88 + +# fparser dynamically generates *_List classes so pylint can't +# find them. +generated-members=Fortran2003.*_List,Fortran2008.*_List + +[DESIGN] +# Maximum number of parents for a class (see R0901) +max-parents=9 + +# Make sure private functions (_my_private_function) are also +# documented, but standard double-underscore functions do not +# need to have a docstring. +no-docstring-rgx=__.*__ diff --git a/src/fparser/two/tests/.pylintrc b/src/fparser/two/tests/.pylintrc new file mode 100644 index 00000000..7a937382 --- /dev/null +++ b/src/fparser/two/tests/.pylintrc @@ -0,0 +1,17 @@ +[FORMAT] + +# Maximum number of characters on a single line. Black's default is 88. +max-line-length=88 + +# fparser dynamically generates *_List classes so pylint can't +# find them. +generated-members=Fortran2003.*_List,Fortran2008.*_List + +[DESIGN] +# Maximum number of parents for a class (see R0901) +max-parents=9 + +# Make sure private functions (_my_private_function) are also +# documented, but standard double-underscore functions do not +# need to have a docstring. +no-docstring-rgx=__.*__ diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py index cc68a6a4..ca0d89f6 100644 --- a/src/fparser/two/tests/test_fortran2003.py +++ b/src/fparser/two/tests/test_fortran2003.py @@ -1,4 +1,4 @@ -# Modified work Copyright (c) 2017-2022 Science and Technology +# Modified work Copyright (c) 2017-2023 Science and Technology # Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson # @@ -1902,6 +1902,20 @@ def test_assignment_stmt(): assert isinstance(obj, tcls), repr(obj) assert str(obj) == "b = a + 1D-8 + 1.1E+3" + # Extra white space around a part-ref + obj = tcls("zdepth(:) = ((gdept_1d(:) ))") + assert isinstance(obj, tcls), repr(obj) + assert str(obj) == "zdepth(:) = ((gdept_1d(:)))" + obj = tcls("zdepth(:) = (( gdept_1d(:) ))") + assert isinstance(obj, tcls), repr(obj) + assert str(obj) == "zdepth(:) = ((gdept_1d(:)))" + obj = tcls("zdepth(:) = ( ( gdept_1d(:) ) )") + assert isinstance(obj, tcls), repr(obj) + assert str(obj) == "zdepth(:) = ((gdept_1d(:)))" + obj = tcls("zdepth(:) = ( gdept_1d(:) ) ") + assert isinstance(obj, tcls), repr(obj) + assert str(obj) == "zdepth(:) = (gdept_1d(:))" + @pytest.mark.usefixtures("fake_symbol_table") def test_pointer_assignment_stmt(): # R735 diff --git a/src/fparser/two/tests/test_utils.py b/src/fparser/two/tests/test_utils.py index c91efb0b..18f7217a 100644 --- a/src/fparser/two/tests/test_utils.py +++ b/src/fparser/two/tests/test_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2020 Science and Technology Facilities Council +# Copyright (c) 2018-2023 Science and Technology Facilities Council. # All rights reserved. @@ -32,14 +32,15 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Test utils.py which contain base classes to support fparser, +"""Test utils.py which contains base classes to support fparser, exception handling and ast traversal. """ import pytest -from fparser.two.utils import FortranSyntaxError from fparser.api import get_reader +from fparser.two import Fortran2003, utils + # test BlockBase @@ -50,29 +51,27 @@ def test_blockbase_match_names(f2003_create): as it sets match_names to True. """ - from fparser.two.Fortran2003 import Derived_Type_Def, Case_Construct - # working named example reader = get_reader("type abc\nend type abc") - ast = Derived_Type_Def(reader) + ast = Fortran2003.Derived_Type_Def(reader) assert "TYPE :: abc\nEND TYPE abc" in str(ast) # case insensitive reader = get_reader("type abc\nend type ABC") - ast = Derived_Type_Def(reader) + ast = Fortran2003.Derived_Type_Def(reader) assert "TYPE :: abc\nEND TYPE ABC" in str(ast) # incorrect name exception reader = get_reader("type abc\nend type cde") - with pytest.raises(FortranSyntaxError) as excinfo: - ast = Derived_Type_Def(reader) + with pytest.raises(utils.FortranSyntaxError) as excinfo: + ast = Fortran2003.Derived_Type_Def(reader) assert "at line 2\n>>>end type cde\nExpecting name 'abc'" in str(excinfo.value) # first name required if second name supplied # switch to using select case as it can trip the exception reader = get_reader("select case (i)\nend select label") - with pytest.raises(FortranSyntaxError) as excinfo: - ast = Case_Construct(reader) + with pytest.raises(utils.FortranSyntaxError) as excinfo: + ast = Fortran2003.Case_Construct(reader) assert ( "at line 2\n>>>end select label\nName 'label' has no " "corresponding starting name" @@ -86,28 +85,26 @@ def test_blockbase_match_name_classes(f2003_create): used when names can appear in multiple places. """ - from fparser.two.Fortran2003 import If_Construct - # working named example reader = get_reader("label:if (.true.) then\nendif label") - ast = If_Construct(reader) + ast = Fortran2003.If_Construct(reader) assert "label:IF (.TRUE.) THEN\nEND IF label" in str(ast) # case insensitive reader = get_reader("label:if (.true.) then\nendif LABEL") - ast = If_Construct(reader) + ast = Fortran2003.If_Construct(reader) assert "label:IF (.TRUE.) THEN\nEND IF LABEL" in str(ast) # incorrect name exception reader = get_reader("label:if (.true.) then\nendif bella") - with pytest.raises(FortranSyntaxError) as excinfo: - ast = If_Construct(reader) + with pytest.raises(utils.FortranSyntaxError) as excinfo: + ast = Fortran2003.If_Construct(reader) assert "at line 2\n>>>endif bella\nExpecting name 'label'" in str(excinfo.value) # first name required if subsequent name supplied reader = get_reader("if (.true.) then\nendif label") - with pytest.raises(FortranSyntaxError) as excinfo: - ast = If_Construct(reader) + with pytest.raises(utils.FortranSyntaxError) as excinfo: + ast = Fortran2003.If_Construct(reader) assert ( "at line 2\n>>>endif label\nName 'label' has no corresponding " "starting name" ) in str(excinfo.value) diff --git a/src/fparser/two/tests/utils/test_call_base.py b/src/fparser/two/tests/utils/test_call_base.py new file mode 100644 index 00000000..4cb066ef --- /dev/null +++ b/src/fparser/two/tests/utils/test_call_base.py @@ -0,0 +1,115 @@ +# Copyright (c) 2023 Science and Technology Facilities Council. + +# All rights reserved. + +# Modifications made as part of the fparser project are distributed +# under the following license: + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: + +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" Module containing pytest tests for the fparser2 CallBase class. """ + +import pytest +from fparser.two import Fortran2003 +from fparser.two import utils + + +@pytest.mark.usefixtures("f2003_create") +def test_call_base_match(): + """Check that parent information is correctly set-up in the + parse tree.""" + # Basic match is OK. + assert utils.CallBase.match( + Fortran2003.Procedure_Designator, Fortran2003.Actual_Arg_Spec_List, "ogg()" + ) + # Trailing white space is ignored. + assert utils.CallBase.match( + Fortran2003.Procedure_Designator, Fortran2003.Actual_Arg_Spec_List, "ogg() " + ) + # String doesn't end with ')' + assert not utils.CallBase.match( + Fortran2003.Procedure_Designator, Fortran2003.Actual_Arg_Spec_List, "ogg" + ) + # Missing opening '(' + assert not utils.CallBase.match( + Fortran2003.Procedure_Designator, Fortran2003.Actual_Arg_Spec_List, "ogg)" + ) + # Missing lhs. + assert not utils.CallBase.match( + Fortran2003.Procedure_Designator, Fortran2003.Actual_Arg_Spec_List, "(ogg)" + ) + # Additional, matching parentheses are OK. + assert utils.CallBase.match( + Fortran2003.Procedure_Designator, + Fortran2003.Actual_Arg_Spec_List, + "ogg((nanny) )", + ) + # upper_lhs makes no difference when lhs is matched with a class. + assert utils.CallBase.match( + Fortran2003.Procedure_Designator, + Fortran2003.Actual_Arg_Spec_List, + "ogg()", + upper_lhs=True, + ) + # upper_lhs is respected when lhs is matched with a str + assert not utils.CallBase.match( + "ogg", Fortran2003.Actual_Arg_Spec_List, "ogg() ", upper_lhs=True + ) + assert utils.CallBase.match( + "OGG", Fortran2003.Actual_Arg_Spec_List, "ogg() ", upper_lhs=True + ) == ("OGG", None) + # rhs can be matched using a str + assert utils.CallBase.match( + "ogg", + "nanny", + "ogg(nanny)", + ) + assert not utils.CallBase.match( + "ogg", + "nanny", + "ogg(granny)", + ) + # require_rhs is respected + assert utils.CallBase.match("ogg", "nanny", "ogg(nanny)", require_rhs=False) == ( + "ogg", + "nanny", + ) + assert utils.CallBase.match("ogg", "nanny", "ogg(nanny)", require_rhs=True) == ( + "ogg", + "nanny", + ) + assert not utils.CallBase.match("ogg", "nanny", "ogg()", require_rhs=True) + + +@pytest.mark.usefixtures("f2003_create") +def test_call_base_tostr(): + """Test the tostr() method of CallBase.""" + fref = Fortran2003.Function_Reference("gytha(ogg)") + assert fref.tostr() == "gytha(ogg)" + fref = Fortran2003.Function_Reference("weatherwax( )") + assert fref.tostr() == "weatherwax()" diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index fc99024a..2fa2d09f 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -1,4 +1,4 @@ -# Modified work Copyright (c) 2017-2022 Science and Technology +# Modified work Copyright (c) 2017-2023 Science and Technology # Facilities Council # Original work Copyright (c) 1999-2008 Pearu Peterson @@ -109,6 +109,10 @@ # 'dollar-descriptor' is specified in the EXTENSIONS list. EXTENSIONS += ["dollar-descriptor"] +# Set this to True to get verbose output (on stdout) detailing the matches made +# while parsing. +_SHOW_MATCH_RESULTS = False + class FparserException(Exception): """Base class exception for fparser. This allows an external tool to @@ -177,15 +181,47 @@ class InternalSyntaxError(FparserException): def show_result(func): - return func + """ + A decorator that enables the matching sequence to be debugged by outputting + the result (to stdout) whenever a new node in the parse tree is successfully + constructed. + + :param function func: the functor that is being called. + + :returns: the supplied functor. + :rtype: function + + """ + if not _SHOW_MATCH_RESULTS: + # Just return the supplied functor unchanged. + return func + + # It's not possible to monkeypatch decorators since the functions they are + # wrapping get modified at module-import time. Therefore, we can't get + # coverage of the rest of this routine. + def new_func(cls, string, **kws): # pragma: no cover + """ + New functor to replace the one supplied. Simply wraps the supplied + functor with some code that prints the match if it was successful. + + :param type cls: the Class that is being matched. + :param str string: the string we are attempting to match. + :param *kws: additional keyword arguments. + :type *kws: Dict[str, Any] + + :returns: new functor object. + :rtype: function - def new_func(cls, string, **kws): - r = func(cls, string, **kws) - if r is not None and isinstance(r, StmtBase): - print("%s(%r) -> %r" % (cls.__name__, string, str(r))) - return r + """ + result = func(cls, string, **kws) + if isinstance(result, StmtBase): + if result: + print(f"{cls.__name__}({string}) -> {result}") + else: + print(f"{cls.__name__}({string}) did NOT match") + return result - return new_func + return new_func # pragma: no cover # @@ -360,7 +396,7 @@ def __new__(cls, string, parent_cls=None): parent_cls.append(cls) # Get the class' match method if it has one - match = getattr(cls, "match") if hasattr(cls, "match") else None + match = getattr(cls, "match", None) if ( isinstance(string, FortranReaderBase) @@ -1341,37 +1377,52 @@ class CallBase(Base): @staticmethod def match(lhs_cls, rhs_cls, string, upper_lhs=False, require_rhs=False): - if not string.endswith(")"): - return + """ + :param lhs_cls: the class to match with the lhs. + :type lhs_cls: str | class + :param rhs_cls: the class to match with the rhs. + :type rhs_cls: str | class + :param str string: the string to attempt to match. + :param bool upper_lhs: whether or not to convert the lhs to uppercase \ + before attempting the match. + :param bool require_rhs: whether the rhs (the part within parentheses) \ + must be present. + + :returns: a tuple containing the lhs and rhs matches or None if there is \ + no match. + :rtype: Optional[Tuple[:py:class:`fparser.two.utils.Base`, \ + Optional[:py:class:`fparser.two.utils.Base`]]] + + """ + if not string.rstrip().endswith(")"): + return None line, repmap = string_replace_map(string) - i = line.rfind("(") - if i == -1: - return - lhs = line[:i].rstrip() + open_idx = line.rfind("(") + if open_idx == -1: + return None + lhs = line[:open_idx].rstrip() if not lhs: return - j = line.rfind(")") - rhs = line[i + 1 : j].strip() - if line[j + 1 :].lstrip(): - return + close_idx = line.rfind(")") + rhs = line[open_idx + 1 : close_idx].strip() lhs = repmap(lhs) if upper_lhs: lhs = lhs.upper() rhs = repmap(rhs) if isinstance(lhs_cls, str): if lhs_cls != lhs: - return + return None else: lhs = lhs_cls(lhs) if rhs: if isinstance(rhs_cls, str): if rhs_cls != rhs: - return + return None else: rhs = rhs_cls(rhs) return lhs, rhs if require_rhs: - return + return None return lhs, None def tostr(self):