From 85d800bc4d0038d6139bc77c0e3990e0d82d7ede Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 27 Mar 2023 20:37:00 +0100 Subject: [PATCH 1/7] #395 fix bug in CallBase.match() --- src/fparser/two/tests/test_fortran2003.py | 7 +++- src/fparser/two/utils.py | 51 ++++++++++++++++++----- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py index cc68a6a4..3c5905c4 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,11 @@ def test_assignment_stmt(): assert isinstance(obj, tcls), repr(obj) assert str(obj) == "b = a + 1D-8 + 1.1E+3" + # Trailing space after a part-ref + 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/utils.py b/src/fparser/two/utils.py index fc99024a..ed9e13a1 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -177,12 +177,27 @@ class InternalSyntaxError(FparserException): def show_result(func): + """ + A xxxxx that enables the matching sequence to be debugged by outputting + the result whenever a new node in the parse tree is successfully + constructed. + + :param func: + + :returns: the supplied function. + :rtype: + """ + # Comment-out the line below to see debug output (on stdout). return func 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))) + if isinstance(r, StmtBase): + if r: + print("%s(%r) -> %r" % (cls.__name__, string, str(r))) + else: + print(f"{cls.__name__}({string}) did NOT match") return r return new_func @@ -360,7 +375,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 hasattr(cls, "match") else None if ( isinstance(string, FortranReaderBase) @@ -1246,6 +1261,9 @@ def match(brackets, cls, string, require_cls=True): `cls`, `str` ) """ + # import pdb + + # pdb.set_trace() if not cls and require_cls: return None if not string: @@ -1341,37 +1359,50 @@ 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: + :type lhs_cls: + :param rhs_cls: + :type rhs_cls: + :param str string: the string to attempt to match. + :param bool upper_lhs: + :param bool require_rhs: + + :returns: + :rtype: Optional[Tuple[, Optional[]]] + + """ + if not string.rstrip().endswith(")"): + return None line, repmap = string_replace_map(string) i = line.rfind("(") if i == -1: - return + return None lhs = line[:i].rstrip() if not lhs: return j = line.rfind(")") rhs = line[i + 1 : j].strip() if line[j + 1 :].lstrip(): - return + return None 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): From a1b221d0f21e2c692425ee0ca32dd6a1e8f0586c Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 28 Mar 2023 09:46:08 +0100 Subject: [PATCH 2/7] #395 add testing of CallBase --- src/fparser/two/tests/.pylintrc | 17 +++ src/fparser/two/tests/utils/test_call_base.py | 115 ++++++++++++++++++ src/fparser/two/utils.py | 62 ++++++---- 3 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 src/fparser/two/tests/.pylintrc create mode 100644 src/fparser/two/tests/utils/test_call_base.py 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/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 ed9e13a1..5a36f4d7 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 @@ -178,20 +178,34 @@ class InternalSyntaxError(FparserException): def show_result(func): """ - A xxxxx that enables the matching sequence to be debugged by outputting - the result whenever a new node in the parse tree is successfully + 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 func: + :param function func: the functor that is being called. + + :returns: the supplied functor. + :rtype: function - :returns: the supplied function. - :rtype: """ - # Comment-out the line below to see debug output (on stdout). + # Just return the supplied functor unchanged. Comment-out this line to see + # debug output (on stdout). return func def new_func(cls, string, **kws): - """ """ + """ + 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 + + """ r = func(cls, string, **kws) if isinstance(r, StmtBase): if r: @@ -1360,31 +1374,33 @@ class CallBase(Base): @staticmethod def match(lhs_cls, rhs_cls, string, upper_lhs=False, require_rhs=False): """ - :param lhs_cls: - :type lhs_cls: - :param rhs_cls: - :type rhs_cls: + :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: - :param bool require_rhs: + :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: - :rtype: Optional[Tuple[, Optional[]]] + :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: + open_idx = line.rfind("(") + if open_idx == -1: return None - lhs = line[:i].rstrip() + lhs = line[:open_idx].rstrip() if not lhs: return - j = line.rfind(")") - rhs = line[i + 1 : j].strip() - if line[j + 1 :].lstrip(): - return None + close_idx = line.rfind(")") + rhs = line[open_idx + 1 : close_idx].strip() lhs = repmap(lhs) if upper_lhs: lhs = lhs.upper() From e3f87421841afa340b2209c51bb8f1e5c40ddaf0 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 28 Mar 2023 12:29:49 +0100 Subject: [PATCH 3/7] #395 give up trying to get decorator coverage --- src/fparser/two/tests/test_utils.py | 35 +++++++++++++---------------- src/fparser/two/utils.py | 26 +++++++++++++-------- 2 files changed, 33 insertions(+), 28 deletions(-) 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/utils.py b/src/fparser/two/utils.py index 5a36f4d7..d1242ecf 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -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 @@ -188,10 +192,14 @@ def show_result(func): :rtype: function """ - # Just return the supplied functor unchanged. Comment-out this line to see - # debug output (on stdout). - return func - + 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. + # pragma: no cover def new_func(cls, string, **kws): """ New functor to replace the one supplied. Simply wraps the supplied @@ -206,13 +214,13 @@ def new_func(cls, string, **kws): :rtype: function """ - r = func(cls, string, **kws) - if isinstance(r, StmtBase): - if r: - print("%s(%r) -> %r" % (cls.__name__, string, str(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 r + return result return new_func From 0215b22ae6dd4ffa26243d2c82e2495bbf97411a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 28 Mar 2023 13:44:58 +0100 Subject: [PATCH 4/7] #396 tidy code and add coverage-ignore pragmas --- src/fparser/two/utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index d1242ecf..2fa2d09f 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -199,8 +199,7 @@ def show_result(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. - # pragma: no cover - def new_func(cls, string, **kws): + 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. @@ -222,7 +221,7 @@ def new_func(cls, string, **kws): print(f"{cls.__name__}({string}) did NOT match") return result - return new_func + return new_func # pragma: no cover # @@ -397,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", None) # if hasattr(cls, "match") else None + match = getattr(cls, "match", None) if ( isinstance(string, FortranReaderBase) @@ -1283,9 +1282,6 @@ def match(brackets, cls, string, require_cls=True): `cls`, `str` ) """ - # import pdb - - # pdb.set_trace() if not cls and require_cls: return None if not string: From 5c28f9c48cee92e2643d0a55f7493a73c6aae926 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 30 Mar 2023 09:44:55 +0100 Subject: [PATCH 5/7] #396 extend test with extra examples of whitespace --- src/fparser/two/tests/test_fortran2003.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py index 3c5905c4..ca0d89f6 100644 --- a/src/fparser/two/tests/test_fortran2003.py +++ b/src/fparser/two/tests/test_fortran2003.py @@ -1902,10 +1902,19 @@ def test_assignment_stmt(): assert isinstance(obj, tcls), repr(obj) assert str(obj) == "b = a + 1D-8 + 1.1E+3" - # Trailing space after a part-ref + # 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") From fa8fbd9ed5d18a14dcf1f3619434ce000599cb42 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 30 Mar 2023 09:51:14 +0100 Subject: [PATCH 6/7] #396 add .pylintrc to top-level directory --- src/fparser/.pylintrc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/fparser/.pylintrc 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=__.*__ From 017750af0efdee03e92c4701f2e80e1db1bc9596 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 30 Mar 2023 10:50:20 +0100 Subject: [PATCH 7/7] #395 Updage changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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.