From b92d3224db3db058775d91ac6930a950f1faf3df Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 5 Apr 2022 15:05:45 +0100 Subject: [PATCH 01/23] Add support for `block` and `critical` constructs Fixes #320 Restrictions C806, C810, C811 not implemented yet --- src/fparser/two/Fortran2008.py | 143 +++++++++++++++++- src/fparser/two/parser.py | 3 +- .../two/tests/fortran2008/test_block.py | 126 +++++++++++++++ .../two/tests/fortran2008/test_critical.py | 104 +++++++++++++ 4 files changed, 367 insertions(+), 9 deletions(-) create mode 100644 src/fparser/two/tests/fortran2008/test_block.py create mode 100644 src/fparser/two/tests/fortran2008/test_critical.py diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index 2aeb8cbd..187675e4 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -75,14 +75,14 @@ # pylint: disable=unused-import from fparser.common.splitline import string_replace_map from fparser.two import pattern_tools as pattern - +from fparser.two.symbol_table import SYMBOL_TABLES from fparser.two.utils import STRINGBase, BracketBase, WORDClsBase, \ SeparatorBase, Type_Declaration_StmtBase, StmtBase from fparser.two.Fortran2003 import ( EndStmtBase, BlockBase, SequenceBase, Base, Specification_Part, Module_Subprogram_Part, Implicit_Part, Implicit_Part_Stmt, Declaration_Construct, Use_Stmt, Import_Stmt, Declaration_Type_Spec, - Entity_Decl_List, Component_Decl_List, Stop_Code) + Entity_Decl_List, Component_Decl_List, Stop_Code, Execution_Part_Construct) # Import of F2003 classes that are updated in this standard. from fparser.two.Fortran2003 import ( Program_Unit as Program_Unit_2003, Attr_Spec as Attr_Spec_2003, @@ -116,7 +116,6 @@ class Program_Unit(Program_Unit_2003): # R202 subclass_names.append("Submodule") - class Executable_Construct(Executable_Construct_2003): # R213 # pylint: disable=invalid-name ''' @@ -137,13 +136,10 @@ class Executable_Construct(Executable_Construct_2003): # R213 "C201 (R208) An execution-part shall not contain an end-function-stmt, end-mp-subprogram-stmt, end-program-stmt, or end-subroutine-stmt." - NB: The new block-construct and critical-construct are not yet implemented. - TODO: Implement missing F2008 executable-construct (#320) - ''' subclass_names = [ - 'Action_Stmt', 'Associate_Construct', 'Case_Construct', - 'Do_Construct', 'Forall_Construct', 'If_Construct', + 'Action_Stmt', 'Associate_Construct', 'Block_Construct', 'Case_Construct', + 'Critical_Construct', 'Do_Construct', 'Forall_Construct', 'If_Construct', 'Select_Type_Construct', 'Where_Construct'] @@ -1022,6 +1018,137 @@ def tostr(self): return "{0}:{1}".format(self.items[0], self.items[1]) return str(self.items[0]) + +class Block_Construct(BlockBase): # R807 + """ + = + [ ] + == [ ]... + + + TODO: Should disallow COMMON, EQUIVALENCE, IMPLICIT, INTENT, + NAMELIST, OPTIONAL, VALUE, and statement functions (C806) + """ + subclass_names = [] + use_names = ["Block_Stmt", "Specification_Part", "Execution_Part_Construct", + "End_Block_Stmt"] + + @staticmethod + def match(reader): + return BlockBase.match( + Block_Stmt, [Specification_Part, Execution_Part_Construct], + End_Block_Stmt, reader, + match_names=True, # C810 + strict_match_names=True, # C810 + ) + + +class Block_Stmt(StmtBase, WORDClsBase): # R808 + """ + = [ : ] BLOCK + """ + subclass_names = [] + use_names = ['Block_Construct_Name'] + + class Counter: + """Global counter so that each block-stmt introduces a new scope + """ + counter = 0 + + def __init__(self): + self.counter = Block_Stmt.Counter.counter + Block_Stmt.Counter.counter += 1 + + def __repr__(self): + return "block_{0}".format(self.counter) + + @staticmethod + def match(string): + found = WORDClsBase.match("BLOCK", None, string) + if not found: + return None + block, _ = found + return block, Block_Stmt.Counter() + + def get_start_name(self): + return self.item.name + + def tostr(self): + return "BLOCK" + + +class End_Block_Stmt(EndStmtBase): # R809 + """ = END BLOCK [ ]""" + subclass_names = [] + use_names = ["Block_Construct_Name"] + + @staticmethod + def match(string): + ''' + :param str string: Fortran code to check for a match + :return: code line matching the "END DO" statement + :rtype: string + ''' + return EndStmtBase.match('BLOCK', Block_Construct_Name, string, + require_stmt_type=True) + + +class Critical_Construct(BlockBase): # R807 + """ + = + == [ ]... + + + TODO: Should disallow RETURN (C809) and CYCLE or EXIT to outside block (C811) + """ + subclass_names = [] + use_names = ["Critical_Stmt", "Execution_Part_Construct", + "End_Critical_Stmt"] + + @staticmethod + def match(reader): + return BlockBase.match( + Critical_Stmt, [Execution_Part_Construct], + End_Critical_Stmt, reader, + match_names=True, # C810 + strict_match_names=True, # C810 + ) + + +class Critical_Stmt(StmtBase, WORDClsBase): # R808 + """ + = [ : ] CRITICAL + """ + subclass_names = [] + use_names = ['Critical_Construct_Name'] + + @staticmethod + def match(string): + return WORDClsBase.match("CRITICAL", None, string) + + def get_start_name(self): + return self.item.name + + def tostr(self): + return "CRITICAL" + + +class End_Critical_Stmt(EndStmtBase): # R809 + """ = END CRITICAL [ ]""" + subclass_names = [] + use_names = ["Critical_Construct_Name"] + + @staticmethod + def match(string): + ''' + :param str string: Fortran code to check for a match + :return: code line matching the "END DO" statement + :rtype: string + ''' + return EndStmtBase.match('CRITICAL', Critical_Construct_Name, string, + require_stmt_type=True) + + # # GENERATE Scalar_, _List, _Name CLASSES # diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index e3afafdf..66ef99f6 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -166,7 +166,8 @@ def create(self, std=None): Fortran2003.Subroutine_Stmt, Fortran2003.Program_Stmt, Fortran2003.Function_Stmt, - Fortran2008.Submodule_Stmt] + Fortran2008.Submodule_Stmt, + Fortran2008.Block_Stmt] # the class hierarchy has been set up so return the top # level class that we start from when parsing Fortran # code. Fortran2008 does not extend the top level class so diff --git a/src/fparser/two/tests/fortran2008/test_block.py b/src/fparser/two/tests/fortran2008/test_block.py new file mode 100644 index 00000000..75a20aee --- /dev/null +++ b/src/fparser/two/tests/fortran2008/test_block.py @@ -0,0 +1,126 @@ +# Copyright (c) 2018-2021 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. + + +from fparser.api import get_reader +from fparser.two.utils import FortranSyntaxError +from fparser.two.Fortran2008 import Block_Construct, Program_Unit + +import pytest + + +def test_block(f2008_create): + block = Block_Construct( + get_reader( + """\ + block + integer :: b = 4 + a = 1 + b + end block + """ + ) + ) + + assert "BLOCK\n INTEGER :: b = 4\n a = 1 + b\nEND BLOCK" in str(block) + + +def test_block_new_scope(f2008_create): + block = Program_Unit( + get_reader( + """\ + program foo + integer :: b = 3 + block + integer :: b = 4 + a = 1 + b + end block + end program foo + """ + ) + ) + + assert "BLOCK\nINTEGER :: b = 4\na = 1 + b\nEND BLOCK" in str(block).replace( + " ", "" + ) + + +def test_named_block(f2008_create): + block = Block_Construct( + get_reader( + """\ + foo: block + integer :: b = 4 + a = 1 + b + end block foo + """ + ) + ) + + assert "foo:BLOCK\n INTEGER :: b = 4\n a = 1 + b\nEND BLOCK foo" in str(block) + + +def test_end_block_missing_start_name(f2008_create): # C808 + with pytest.raises(FortranSyntaxError): + Block_Construct( + get_reader( + """\ + block + end block foo + """ + ) + ) + + +def test_end_block_missing_end_name(f2008_create): # C808 + with pytest.raises(FortranSyntaxError): + Block_Construct( + get_reader( + """\ + foo: block + end block + """ + ) + ) + + +def test_end_block_wrong_name(f2008_create): # C808 + with pytest.raises(FortranSyntaxError): + Block_Construct( + get_reader( + """\ + foo: block + end block bar + """ + ) + ) diff --git a/src/fparser/two/tests/fortran2008/test_critical.py b/src/fparser/two/tests/fortran2008/test_critical.py new file mode 100644 index 00000000..7038bb17 --- /dev/null +++ b/src/fparser/two/tests/fortran2008/test_critical.py @@ -0,0 +1,104 @@ +# Copyright (c) 2018-2021 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. + + +from fparser.api import get_reader +from fparser.two.utils import FortranSyntaxError +from fparser.two.Fortran2008 import Critical_Construct + +import pytest + + +def test_critical(f2008_create): + critical = Critical_Construct( + get_reader( + """\ + critical + a = 1 + b + end critical + """ + ) + ) + + assert "CRITICAL\n a = 1 + b\nEND CRITICAL" in str(critical) + + +def test_named_critical(f2008_create): + critical = Critical_Construct( + get_reader( + """\ + foo: critical + a = 1 + b + end critical foo + """ + ) + ) + + assert "foo:CRITICAL\n a = 1 + b\nEND CRITICAL foo" in str(critical) + + +def test_end_critical_missing_start_name(f2008_create): # C809 + with pytest.raises(FortranSyntaxError): + Critical_Construct( + get_reader( + """\ + critical + end critical foo + """ + ) + ) + + +def test_end_critical_missing_end_name(f2008_create): # C809 + with pytest.raises(FortranSyntaxError): + Critical_Construct( + get_reader( + """\ + foo: critical + end critical + """ + ) + ) + + +def test_end_critical_wrong_name(f2008_create): # C809 + with pytest.raises(FortranSyntaxError): + Critical_Construct( + get_reader( + """\ + foo: critical + end critical bar + """ + ) + ) From 3dc7fc6f3ebe252e8d02e369e92d2e6b48d23f83 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 18 Oct 2022 13:23:28 +0100 Subject: [PATCH 02/23] Apply black formatting --- src/fparser/two/Fortran2008.py | 69 +++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index 5501ed73..18249b02 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -114,7 +114,7 @@ Entity_Decl_List, Component_Decl_List, Stop_Code, - Execution_Part_Construct + Execution_Part_Construct, ) # Import of F2003 classes that are updated in this standard. @@ -1304,6 +1304,7 @@ def match(string): return obj return None + class Block_Construct(BlockBase): # R807 """ = @@ -1314,30 +1315,38 @@ class Block_Construct(BlockBase): # R807 TODO: Should disallow COMMON, EQUIVALENCE, IMPLICIT, INTENT, NAMELIST, OPTIONAL, VALUE, and statement functions (C806) """ + subclass_names = [] - use_names = ["Block_Stmt", "Specification_Part", "Execution_Part_Construct", - "End_Block_Stmt"] + use_names = [ + "Block_Stmt", + "Specification_Part", + "Execution_Part_Construct", + "End_Block_Stmt", + ] @staticmethod def match(reader): return BlockBase.match( - Block_Stmt, [Specification_Part, Execution_Part_Construct], - End_Block_Stmt, reader, + Block_Stmt, + [Specification_Part, Execution_Part_Construct], + End_Block_Stmt, + reader, match_names=True, # C810 strict_match_names=True, # C810 - ) + ) class Block_Stmt(StmtBase, WORDClsBase): # R808 """ = [ : ] BLOCK """ + subclass_names = [] - use_names = ['Block_Construct_Name'] + use_names = ["Block_Construct_Name"] class Counter: - """Global counter so that each block-stmt introduces a new scope - """ + """Global counter so that each block-stmt introduces a new scope""" + counter = 0 def __init__(self): @@ -1362,21 +1371,23 @@ def tostr(self): return "BLOCK" -class End_Block_Stmt(EndStmtBase): # R809 +class End_Block_Stmt(EndStmtBase): # R809 """ = END BLOCK [ ]""" + subclass_names = [] use_names = ["Block_Construct_Name"] @staticmethod def match(string): - ''' + """ :param str string: Fortran code to check for a match :return: code line matching the "END DO" statement :rtype: string - ''' - return EndStmtBase.match('BLOCK', Block_Construct_Name, string, - require_stmt_type=True) - + """ + return EndStmtBase.match( + "BLOCK", Block_Construct_Name, string, require_stmt_type=True + ) + class Critical_Construct(BlockBase): # R807 """ @@ -1386,26 +1397,29 @@ class Critical_Construct(BlockBase): # R807 TODO: Should disallow RETURN (C809) and CYCLE or EXIT to outside block (C811) """ + subclass_names = [] - use_names = ["Critical_Stmt", "Execution_Part_Construct", - "End_Critical_Stmt"] + use_names = ["Critical_Stmt", "Execution_Part_Construct", "End_Critical_Stmt"] @staticmethod def match(reader): return BlockBase.match( - Critical_Stmt, [Execution_Part_Construct], - End_Critical_Stmt, reader, + Critical_Stmt, + [Execution_Part_Construct], + End_Critical_Stmt, + reader, match_names=True, # C810 strict_match_names=True, # C810 - ) + ) class Critical_Stmt(StmtBase, WORDClsBase): # R808 """ = [ : ] CRITICAL """ + subclass_names = [] - use_names = ['Critical_Construct_Name'] + use_names = ["Critical_Construct_Name"] @staticmethod def match(string): @@ -1418,20 +1432,23 @@ def tostr(self): return "CRITICAL" -class End_Critical_Stmt(EndStmtBase): # R809 +class End_Critical_Stmt(EndStmtBase): # R809 """ = END CRITICAL [ ]""" + subclass_names = [] use_names = ["Critical_Construct_Name"] @staticmethod def match(string): - ''' + """ :param str string: Fortran code to check for a match :return: code line matching the "END DO" statement :rtype: string - ''' - return EndStmtBase.match('CRITICAL', Critical_Construct_Name, string, - require_stmt_type=True) + """ + return EndStmtBase.match( + "CRITICAL", Critical_Construct_Name, string, require_stmt_type=True + ) + # # GENERATE Scalar_, _List, _Name CLASSES From a6546100c39b04f1c24280c5ca13a684c2c4945f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 1 Mar 2023 15:20:00 +0000 Subject: [PATCH 03/23] #392 WIP adding new ScopingRegionMixin [skip ci] --- src/fparser/two/Fortran2003.py | 8 +- src/fparser/two/Fortran2008.py | 92 +++++++++++++------ src/fparser/two/tests/conftest.py | 11 +++ .../two/tests/fortran2008/test_block.py | 75 +++++++++++++-- src/fparser/two/utils.py | 14 ++- 5 files changed, 161 insertions(+), 39 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 6c913a23..06f4a117 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -93,6 +93,7 @@ CALLBase, CallBase, KeywordValueBase, + ScopingRegionMixin, SeparatorBase, SequenceBase, UnaryOpBase, @@ -10920,6 +10921,9 @@ def get_name(self): """ return self.items[1] + def get_scope_name(self): + return self.get_start_name() + def get_start_name(self): """Provides the program name as a string. This is used for matching with the equivalent `end program` name if there is one. @@ -10973,7 +10977,7 @@ def match(reader): ) -class Module_Stmt(StmtBase, WORDClsBase): # R1105 +class Module_Stmt(StmtBase, WORDClsBase, ScopingRegionMixin): # R1105 """ = MODULE """ @@ -12472,7 +12476,7 @@ def match(reader): ) -class Function_Stmt(StmtBase): # R1224 +class Function_Stmt(StmtBase, ScopingRegionMixin): # R1224 """ :: diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index e92909c0..f2297aa0 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -1,5 +1,5 @@ -# Modified work Copyright (c) 2018-2022 Science and Technology -# Facilities Council +# Modified work Copyright (c) 2018-2023 Science and Technology +# Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson # All rights reserved. @@ -86,6 +86,7 @@ CALLBase, KeywordValueBase, NoMatchError, + ScopingRegionMixin, SeparatorBase, StmtBase, STRINGBase, @@ -93,29 +94,30 @@ WORDClsBase, ) from fparser.two.Fortran2003 import ( - EndStmtBase, - BlockBase, - SequenceBase, Base, - Specification_Part, - Stat_Variable, - Errmsg_Variable, - Source_Expr, - Module_Subprogram_Part, - Implicit_Part, - Implicit_Part_Stmt, + BlockBase, + Component_Decl_List, Declaration_Construct, - Use_Stmt, + Declaration_Type_Spec, + EndStmtBase, + Entity_Decl_List, + Errmsg_Variable, + Execution_Part_Construct, File_Name_Expr, File_Unit_Number, + Implicit_Part, + Implicit_Part_Stmt, Import_Stmt, Iomsg_Variable, Label, - Declaration_Type_Spec, - Entity_Decl_List, - Component_Decl_List, + Module_Subprogram_Part, + Name, + SequenceBase, + Source_Expr, + Specification_Part, + Stat_Variable, Stop_Code, - Execution_Part_Construct, + Use_Stmt, ) # Import of F2003 classes that are updated in this standard. @@ -1030,7 +1032,7 @@ def match(reader): return result -class Submodule_Stmt(Base): # R1117 +class Submodule_Stmt(Base, ScopingRegionMixin): # R1117 """ Fortran 2008 rule R1117:: @@ -1342,12 +1344,14 @@ def match(string): return None -class Block_Construct(BlockBase): # R807 +class Block_Construct(BlockBase): """ - = - [ ] - == [ ]... - + Fortran 2008 Rule 807. + + block-construct is block-stmt + [ specification-part ] + block + end-block-stmt TODO: Should disallow COMMON, EQUIVALENCE, IMPLICIT, INTENT, NAMELIST, OPTIONAL, VALUE, and statement functions (C806) @@ -1373,38 +1377,68 @@ def match(reader): ) -class Block_Stmt(StmtBase, WORDClsBase): # R808 +class Block_Stmt(StmtBase, WORDClsBase): """ - = [ : ] BLOCK + Fortran 2008 Rule 808. + + block-stmt is [ block-construct-name : ] BLOCK + """ subclass_names = [] use_names = ["Block_Construct_Name"] + counter = 0 class Counter: - """Global counter so that each block-stmt introduces a new scope""" + """Global counter so that each block-stmt introduces a new scope.""" counter = 0 def __init__(self): - self.counter = Block_Stmt.Counter.counter + self._counter = Block_Stmt.Counter.counter Block_Stmt.Counter.counter += 1 def __repr__(self): - return "block_{0}".format(self.counter) + return "_block_{0}".format(self._counter) @staticmethod def match(string): + """ + Attempts to match the supplied text with this rule. + + :param str string: the text to match. + + :returns: a tuple of the matched node and instance of Counter or \ + None if there is no match. + :rtype: Tuple["BLOCK", \ + :py:class:`fparser.two.Fortran2008.Block_Stmt.Counter`] \ + | NoneType + """ found = WORDClsBase.match("BLOCK", None, string) if not found: return None block, _ = found - return block, Block_Stmt.Counter() + internal_name = f"_block_{Block_Stmt.counter}" + Block_Stmt.counter += 1 + return block, Block_Stmt.Counter() # internal_name + + def get_scope_name(self): + if self.item.name: + return self.item.name + return f"_block_{self.items[1]}" def get_start_name(self): + """ + :returns: the name associated with this Block construct or None. + :rtype: str | NoneType + """ return self.item.name def tostr(self): + """ + :returns: the string representation of this node. + :rtype: str + """ return "BLOCK" diff --git a/src/fparser/two/tests/conftest.py b/src/fparser/two/tests/conftest.py index 67bd6bad..cf0ad977 100644 --- a/src/fparser/two/tests/conftest.py +++ b/src/fparser/two/tests/conftest.py @@ -58,6 +58,17 @@ def f2003_parser(): return ParserFactory().create(std="f2003") +@pytest.fixture +def f2008_parser(): + """Create a Fortran 2008 parser class hierarchy and return the parser + for usage in tests. + + :return: a Program class (not object) for use with the Fortran reader. + :rtype: :py:class:`fparser.two.Fortran2008.Program` + """ + return ParserFactory().create(std="f2008") + + @pytest.fixture(name="clear_symbol_table", autouse=True) def clear_symbol_tables_fixture(): """Clear-up any existing symbol-table hierarchy.""" diff --git a/src/fparser/two/tests/fortran2008/test_block.py b/src/fparser/two/tests/fortran2008/test_block.py index 75a20aee..c03f86bb 100644 --- a/src/fparser/two/tests/fortran2008/test_block.py +++ b/src/fparser/two/tests/fortran2008/test_block.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2021 Science and Technology Facilities Council. +# Copyright (c) 2022-2023 Science and Technology Facilities Council. # All rights reserved. @@ -33,14 +33,16 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import pytest + from fparser.api import get_reader -from fparser.two.utils import FortranSyntaxError from fparser.two.Fortran2008 import Block_Construct, Program_Unit - -import pytest +from fparser.two.symbol_table import SymbolTable, SYMBOL_TABLES +from fparser.two.utils import FortranSyntaxError, walk def test_block(f2008_create): + """Test that the Block_Construct matches as expected.""" block = Block_Construct( get_reader( """\ @@ -55,16 +57,22 @@ def test_block(f2008_create): assert "BLOCK\n INTEGER :: b = 4\n a = 1 + b\nEND BLOCK" in str(block) -def test_block_new_scope(f2008_create): - block = Program_Unit( +@pytest.mark.parametrize( + "before, after", [("", ""), ("b = 2.0 * b", ""), ("", "b = 2.0 * b")] +) +def test_block_new_scope(f2008_parser, before, after): + """Test that a Block_Construct creates a new scoping region.""" + block = f2008_parser( get_reader( - """\ + f"""\ program foo integer :: b = 3 + {before} block integer :: b = 4 a = 1 + b end block + {after} end program foo """ ) @@ -75,7 +83,33 @@ def test_block_new_scope(f2008_create): ) +def test_block_in_if(f2008_parser): + """Test that a Block may appear inside an IF.""" + ptree = f2008_parser( + get_reader( + """\ + program foo + integer :: b = 3 + if (b == 2) then + block + real :: tmp + tmp = ATAN(0.5) + b = NINT(tmp) + end block + end if + end program foo + """ + ) + ) + blocks = walk([ptree], Block_Construct) + assert len(blocks) == 1 + + def test_named_block(f2008_create): + """ + Test that a named block construct is correctly captured and also + reproduced. + """ block = Block_Construct( get_reader( """\ @@ -124,3 +158,30 @@ def test_end_block_wrong_name(f2008_create): # C808 """ ) ) + + +def test_block_in_subroutine(f2008_parser): + """Check that we get two, nested symbol tables when a subroutine contains + a Block construct.""" + code = """\ + program my_prog + real :: a + a = -1.0 + if (a < 0.0) then + rocking: block + real :: b + b = 42.0 + a = b + end block rocking + else + a = 10.0 + end if + end program my_prog + """ + print(code) + _ = f2008_parser(get_reader(code)) + tables = SYMBOL_TABLES + assert list(tables._symbol_tables.keys()) == ["my_prog"] + table = SYMBOL_TABLES.lookup("my_prog") + assert len(table.children) == 1 + assert table.children[0].name == "rocking" diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index fc99024a..df25931e 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -526,6 +526,18 @@ def restore_reader(self, reader): reader.put_item(self.item) +class ScopingRegionMixin: + """ + """ + + def get_scope_name(self): + """ + :returns: the name of this scoping region. + :rtype: str + """ + return self.get_name().string + + class BlockBase(Base): """ Base class for matching all block constructs:: @@ -608,7 +620,7 @@ def match( # symbol table. # NOTE: if the match subsequently fails then we must # delete this symbol table. - table_name = str(obj.children[1]) + table_name = obj.get_scope_name() #str(obj.children[1]) SYMBOL_TABLES.enter_scope(table_name) # Store the index of the start of this block proper (i.e. # excluding any comments) From 490de0826b58fdaaae097dc7a94ecab603b3edd8 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 2 Mar 2023 13:24:51 +0000 Subject: [PATCH 04/23] #392 WIP tidying subclass code --- src/fparser/two/Fortran2003.py | 2 +- src/fparser/two/parser.py | 114 +++++++++++++----- src/fparser/two/tests/conftest.py | 11 -- .../two/tests/fortran2008/test_block.py | 1 - 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 06f4a117..e77e8415 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -12794,7 +12794,7 @@ def c1242_valid(prefix, binding_spec): return True -class Subroutine_Stmt(StmtBase): # R1232 +class Subroutine_Stmt(StmtBase, ScopingRegionMixin): # R1232 """ Fortran2003 rule R1232:: diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index 832cae42..1f10833b 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -68,6 +68,7 @@ # pylint: disable=eval-used import inspect +import logging import sys from fparser.two.symbol_table import SYMBOL_TABLES @@ -202,7 +203,6 @@ class name and a class. __autodoc__ = [] base_classes = {} - import logging import fparser.two.Fortran2003 class_type = type(fparser.two.Fortran2003.Base) @@ -214,6 +214,10 @@ class name and a class. for clsinfo in input_classes: clsname = "{0}.{1}".format(clsinfo[1].__module__, clsinfo[0]) + # if "Executable_Construct" in clsname: + # import pdb + + # pdb.set_trace() cls = eval(clsname) # ?? classtype is set to Base so why have issubclass? if ( @@ -228,37 +232,85 @@ class name and a class. # # OPTIMIZE subclass_names tree. # + def _rpl_list(clsname): + """ + Starting at the named class, searches down the tree defined by the + classes named in the `subclass_names` list to find the closest that + have `match` methods. If the current class does not have a + `match` method then this method is called again for each of + the classes in its `subclass_names` list. + + :param str clsname: The name of the class from which to search. + + :returns: names of subclasses with `match` methods. + :rtype: List[str | NoneType] + + """ + if clsname not in base_classes: + error_string = "Not implemented: {0}".format(clsname) + logging.getLogger(__name__).debug(error_string) + return [] + # remove this code when all classes are implemented. + cls = base_classes[clsname] + if hasattr(cls, "match"): + # This class has a `match` method so no need to search further + # down the tree. + return [clsname] + # clsname doesn't have a `match` method so we look at each of its + # subclasses and find the nearest class in each that does have a + # `match` method. + bits = [] + for names in getattr(cls, "subclass_names", []): + list1 = _rpl_list(names) + for names1 in list1: + if names1 not in bits: + bits.append(names1) + return bits + + if "Block_Construct" in base_classes: + cls = base_classes["Executable_Construct"] + print(sorted(cls.subclass_names)) + + cls = base_classes["Execution_Part_Construct"] + print(sorted(cls.subclass_names)) + # exit(1) + # import pdb + + # pdb.set_trace() + + # Ensure we keep a copy of the original subclass_names list for each + # class. + for cls in list(base_classes.values()): + if not hasattr(cls, "subclass_names"): + continue + if not hasattr(cls, "_original_subclass_names"): + setattr(cls, "_original_subclass_names", cls.subclass_names[:]) + # else: + # cls.subclass_names = cls._original_subclass_names[:] - if 1: # Optimize subclass tree: - - def _rpl_list(clsname): - if clsname not in base_classes: - error_string = "Not implemented: {0}".format(clsname) - logging.getLogger(__name__).debug(error_string) - return [] - # remove this code when all classes are implemented. - cls = base_classes[clsname] - if hasattr(cls, "match"): - return [clsname] - bits = [] - for names in getattr(cls, "subclass_names", []): - list1 = _rpl_list(names) - for names1 in list1: - if names1 not in bits: - bits.append(names1) - return bits - - for cls in list(base_classes.values()): - if not hasattr(cls, "subclass_names"): - continue - opt_subclass_names = [] - for names in cls.subclass_names: - for names1 in _rpl_list(names): - if names1 not in opt_subclass_names: - opt_subclass_names.append(names1) - if not opt_subclass_names == cls.subclass_names: - cls.subclass_names[:] = opt_subclass_names - + for cls in list(base_classes.keys()): + if not hasattr(cls, "subclass_names"): + continue + # The optimised list of subclass names will only include subclasses + # that have `match` methods. + opt_subclass_names = [] + for names in cls.subclass_names: + for names1 in _rpl_list(names): + if names1 not in opt_subclass_names: + opt_subclass_names.append(names1) + if not opt_subclass_names == cls.subclass_names: + cls.subclass_names[:] = opt_subclass_names + + if "Block_Construct" in base_classes: + cls = base_classes["Executable_Construct"] + print(sorted(cls.subclass_names)) + + cls = base_classes["Execution_Part_Construct"] + print(sorted(cls.subclass_names)) + # exit(1) + # import pdb + + # pdb.set_trace() # Initialize Base.subclasses dictionary: for clsname, cls in list(base_classes.items()): subclass_names = getattr(cls, "subclass_names", None) diff --git a/src/fparser/two/tests/conftest.py b/src/fparser/two/tests/conftest.py index cf0ad977..67bd6bad 100644 --- a/src/fparser/two/tests/conftest.py +++ b/src/fparser/two/tests/conftest.py @@ -58,17 +58,6 @@ def f2003_parser(): return ParserFactory().create(std="f2003") -@pytest.fixture -def f2008_parser(): - """Create a Fortran 2008 parser class hierarchy and return the parser - for usage in tests. - - :return: a Program class (not object) for use with the Fortran reader. - :rtype: :py:class:`fparser.two.Fortran2008.Program` - """ - return ParserFactory().create(std="f2008") - - @pytest.fixture(name="clear_symbol_table", autouse=True) def clear_symbol_tables_fixture(): """Clear-up any existing symbol-table hierarchy.""" diff --git a/src/fparser/two/tests/fortran2008/test_block.py b/src/fparser/two/tests/fortran2008/test_block.py index c03f86bb..f99eb8b0 100644 --- a/src/fparser/two/tests/fortran2008/test_block.py +++ b/src/fparser/two/tests/fortran2008/test_block.py @@ -178,7 +178,6 @@ def test_block_in_subroutine(f2008_parser): end if end program my_prog """ - print(code) _ = f2008_parser(get_reader(code)) tables = SYMBOL_TABLES assert list(tables._symbol_tables.keys()) == ["my_prog"] From 0c1159e3181faf847df859fa93a799d9000d45a2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 2 Mar 2023 13:35:18 +0000 Subject: [PATCH 05/23] #392 remove debug code --- src/fparser/two/Fortran2003.py | 5 +---- src/fparser/two/Fortran2008.py | 2 +- src/fparser/two/parser.py | 28 ++-------------------------- 3 files changed, 4 insertions(+), 31 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index e77e8415..fe5fae74 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -10883,7 +10883,7 @@ def match(reader): return result -class Program_Stmt(StmtBase, WORDClsBase): # R1102 +class Program_Stmt(StmtBase, WORDClsBase, ScopingRegionMixin): # R1102 """ Fortran 2003 rule R1102:: @@ -10921,9 +10921,6 @@ def get_name(self): """ return self.items[1] - def get_scope_name(self): - return self.get_start_name() - def get_start_name(self): """Provides the program name as a string. This is used for matching with the equivalent `end program` name if there is one. diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index f2297aa0..72757e41 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -1377,7 +1377,7 @@ def match(reader): ) -class Block_Stmt(StmtBase, WORDClsBase): +class Block_Stmt(StmtBase, WORDClsBase, ScopingRegionMixin): """ Fortran 2008 Rule 808. diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index 1f10833b..b080477a 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -1,4 +1,4 @@ -# Modified work Copyright (c) 2018-2022 Science and Technology +# Modified work Copyright (c) 2018-2023 Science and Technology # Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson @@ -214,10 +214,7 @@ class name and a class. for clsinfo in input_classes: clsname = "{0}.{1}".format(clsinfo[1].__module__, clsinfo[0]) - # if "Executable_Construct" in clsname: - # import pdb - - # pdb.set_trace() + # Why not just clsinfo[1] instead of eval()? cls = eval(clsname) # ?? classtype is set to Base so why have issubclass? if ( @@ -267,17 +264,6 @@ def _rpl_list(clsname): bits.append(names1) return bits - if "Block_Construct" in base_classes: - cls = base_classes["Executable_Construct"] - print(sorted(cls.subclass_names)) - - cls = base_classes["Execution_Part_Construct"] - print(sorted(cls.subclass_names)) - # exit(1) - # import pdb - - # pdb.set_trace() - # Ensure we keep a copy of the original subclass_names list for each # class. for cls in list(base_classes.values()): @@ -301,16 +287,6 @@ def _rpl_list(clsname): if not opt_subclass_names == cls.subclass_names: cls.subclass_names[:] = opt_subclass_names - if "Block_Construct" in base_classes: - cls = base_classes["Executable_Construct"] - print(sorted(cls.subclass_names)) - - cls = base_classes["Execution_Part_Construct"] - print(sorted(cls.subclass_names)) - # exit(1) - # import pdb - - # pdb.set_trace() # Initialize Base.subclasses dictionary: for clsname, cls in list(base_classes.items()): subclass_names = getattr(cls, "subclass_names", None) From 5e54723bdd538ca477946a677eaa1d1b57290c26 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 2 Mar 2023 13:39:46 +0000 Subject: [PATCH 06/23] #392 fix error in list in parser.py --- src/fparser/two/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index b080477a..27f58971 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -274,7 +274,7 @@ def _rpl_list(clsname): # else: # cls.subclass_names = cls._original_subclass_names[:] - for cls in list(base_classes.keys()): + for cls in list(base_classes.values()): if not hasattr(cls, "subclass_names"): continue # The optimised list of subclass names will only include subclasses From 44c0025137c7c91967e5f3523a3b2b35cf26f1fa Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 2 Mar 2023 16:03:14 +0000 Subject: [PATCH 07/23] #392 WIP getting to grips with modified class types --- src/fparser/two/Fortran2003.py | 2 +- src/fparser/two/parser.py | 67 +++++++++++++++++++--------------- src/fparser/two/utils.py | 6 ++- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index fe5fae74..c9992350 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -7037,7 +7037,7 @@ class If_Construct(BlockBase): # R802 """ subclass_names = [] - use_names = ["If_Then_Stmt", "Block", "Else_If_Stmt", "Else_Stmt", "End_If_Stmt"] + use_names = ["If_Then_Stmt", "Execution_Part_Construct", "Else_If_Stmt", "Else_Stmt", "End_If_Stmt"] @staticmethod def match(string): diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index 27f58971..5cbe09b2 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -87,9 +87,9 @@ def get_module_classes(input_module): # classes. all_cls_members = inspect.getmembers(sys.modules[module_name], inspect.isclass) # next only keep classes that are specified in the module. - for cls_member in all_cls_members: - if cls_member[1].__module__ == module_name: - module_cls_members.append(cls_member) + for name, cls in all_cls_members: + if cls.__module__ == module_name: + module_cls_members.append((name, cls)) return module_cls_members @@ -160,6 +160,10 @@ def create(self, std=None): from fparser.two import Fortran2008 f2008_cls_members = get_module_classes(Fortran2008) + for _, cls in f2008_cls_members: + if hasattr(cls, "_original_subclass_names"): + delattr(cls, "_original_subclass_names") + # next add in Fortran2003 classes if they do not already # exist as a Fortran2008 class. f2008_class_names = [i[0] for i in f2008_cls_members] @@ -199,33 +203,28 @@ def _setup(self, input_classes): class name and a class. """ + # pylint: disable=import-outside-toplevel + from fparser.two import Fortran2003 - __autodoc__ = [] - base_classes = {} - - import fparser.two.Fortran2003 - - class_type = type(fparser.two.Fortran2003.Base) + class_type = type(Fortran2003.Base) # Reset subclasses dictionary in case this function has been # called before. If this is not done then multiple calls to # the ParserFactory create method may not work correctly. - fparser.two.Fortran2003.Base.subclasses = {} + Fortran2003.Base.subclasses = {} + base_classes = {} - for clsinfo in input_classes: - clsname = "{0}.{1}".format(clsinfo[1].__module__, clsinfo[0]) - # Why not just clsinfo[1] instead of eval()? - cls = eval(clsname) + for _, cls in input_classes: # ?? classtype is set to Base so why have issubclass? if ( isinstance(cls, class_type) - and issubclass(cls, fparser.two.Fortran2003.Base) + and issubclass(cls, Fortran2003.Base) and not cls.__name__.endswith("Base") ): base_classes[cls.__name__] = cls - if len(__autodoc__) < 10: - __autodoc__.append(cls.__name__) - + #if cls.__name__ == "Executable_Construct": + # import pdb; pdb.set_trace() + # dir(cls) # # OPTIMIZE subclass_names tree. # @@ -265,18 +264,20 @@ def _rpl_list(clsname): return bits # Ensure we keep a copy of the original subclass_names list for each - # class. - for cls in list(base_classes.values()): + # class (because this gets altered below). + for cls in base_classes.values(): if not hasattr(cls, "subclass_names"): continue if not hasattr(cls, "_original_subclass_names"): setattr(cls, "_original_subclass_names", cls.subclass_names[:]) - # else: - # cls.subclass_names = cls._original_subclass_names[:] + else: + cls.subclass_names = cls._original_subclass_names[:] - for cls in list(base_classes.values()): + for cls in base_classes.values(): if not hasattr(cls, "subclass_names"): continue + #if cls.__name__ == "Executable_Construct": + # import pdb; pdb.set_trace() # The optimised list of subclass names will only include subclasses # that have `match` methods. opt_subclass_names = [] @@ -285,19 +286,20 @@ def _rpl_list(clsname): if names1 not in opt_subclass_names: opt_subclass_names.append(names1) if not opt_subclass_names == cls.subclass_names: - cls.subclass_names[:] = opt_subclass_names + cls.subclass_names = opt_subclass_names[:] - # Initialize Base.subclasses dictionary: - for clsname, cls in list(base_classes.items()): + # Now that we've optimised the list of subclass names for each class, + # use this information to initialise the Base.subclasses dictionary: + for clsname, cls in base_classes.items(): subclass_names = getattr(cls, "subclass_names", None) if subclass_names is None: message = "%s class is missing subclass_names list" % (clsname) logging.getLogger(__name__).debug(message) continue try: - bits = fparser.two.Fortran2003.Base.subclasses[clsname] + bits = Fortran2003.Base.subclasses[clsname] except KeyError: - fparser.two.Fortran2003.Base.subclasses[clsname] = bits = [] + Fortran2003.Base.subclasses[clsname] = bits = [] for name in subclass_names: if name in base_classes: bits.append(base_classes[name]) @@ -305,9 +307,14 @@ def _rpl_list(clsname): message = "{0} not implemented needed by {1}".format(name, clsname) logging.getLogger(__name__).debug(message) + #import pdb; pdb.set_trace() + names = [cls.__name__ for cls in Fortran2003.Base.subclasses["Executable_Construct"]] + print(sorted(names)) + #print(fparser.two.Fortran2003.Base.subclasses["Executable_Construct"]) + if 1: - for cls in list(base_classes.values()): - # subclasses = fparser.two.Fortran2003.Base.subclasses.get( + for cls in base_classes.values(): + # subclasses = Fortran2003.Base.subclasses.get( # cls.__name__, []) # subclasses_names = [c.__name__ for c in subclasses] subclass_names = getattr(cls, "subclass_names", []) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index df25931e..46c3d90b 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -344,8 +344,9 @@ class Base(ComparableMixin): """ # This dict of subclasses is populated dynamically by code at the end - # of this module. That code uses the entries in the + # of the fparser.two.parser module. That code uses the entries in the # 'subclass_names' list belonging to each class defined in this module. + # See Issue #191 for a discussion of a way of getting rid of this state. subclasses = {} def __init__(self, string, parent_cls=None): @@ -411,7 +412,8 @@ def __new__(cls, string, parent_cls=None): return result if result is None: # Loop over the possible sub-classes of this class and - # check for matches + # check for matches. This uses the list of subclasses calculated + # at runtime in fparser.two.parser. for subcls in Base.subclasses.get(cls.__name__, []): if subcls in parent_cls: # avoid recursion 2. continue From 55c61977bc40176acae70a64b441ce3fcf79d8e5 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 3 Mar 2023 09:23:47 +0000 Subject: [PATCH 08/23] #392 finally fix subclass_names lists and tidy following ScopingRegionMixin introduction --- src/fparser/two/parser.py | 83 ++++++++------------- src/fparser/two/symbol_table.py | 32 -------- src/fparser/two/tests/test_bases.py | 26 ------- src/fparser/two/tests/test_parser.py | 10 --- src/fparser/two/tests/test_symbol_tables.py | 17 ----- src/fparser/two/utils.py | 17 +++-- 6 files changed, 41 insertions(+), 144 deletions(-) diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index 5cbe09b2..a885d51c 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -65,7 +65,6 @@ """This file provides utilities to create a Fortran parser suitable for a particular standard.""" -# pylint: disable=eval-used import inspect import logging @@ -74,17 +73,18 @@ def get_module_classes(input_module): - """Return all classes local to a module. + """ + Return all classes local to a module. :param module input_module: the module containing the classes. - :return: a `list` of tuples each containing a class name and a \ - class. + + :returns: list of class names and types. + :rtype: List[Tuple[str, type]] """ module_cls_members = [] module_name = input_module.__name__ - # first find all classes in the module. This includes imported - # classes. + # First find all classes in the module. This includes imported classes. all_cls_members = inspect.getmembers(sys.modules[module_name], inspect.isclass) # next only keep classes that are specified in the module. for name, cls in all_cls_members: @@ -138,16 +138,7 @@ def create(self, std=None): # we already have our required list of classes so call _setup # to setup our class hierarchy. self._setup(f2003_cls_members) - # We can now specify which classes are taken as defining new - # scoping regions. Programs without the optional program-stmt - # are handled separately in the Fortran2003.Main_Program0 class. - SYMBOL_TABLES.scoping_unit_classes = [ - Fortran2003.Module_Stmt, - Fortran2003.Subroutine_Stmt, - Fortran2003.Program_Stmt, - Fortran2003.Function_Stmt, - ] - # the class hierarchy has been set up so return the top + # The class hierarchy has been set up so return the top # level class that we start from when parsing Fortran code. return Fortran2003.Program if std == "f2008": @@ -162,7 +153,10 @@ def create(self, std=None): f2008_cls_members = get_module_classes(Fortran2008) for _, cls in f2008_cls_members: if hasattr(cls, "_original_subclass_names"): - delattr(cls, "_original_subclass_names") + # Reset the list of original subclass names as it will have + # been inherited from the corresponding F2003 class. + # pylint: disable=protected-access + cls._original_subclass_names = [] # next add in Fortran2003 classes if they do not already # exist as a Fortran2008 class. @@ -173,18 +167,7 @@ def create(self, std=None): # we now have our required list of classes so call _setup # to setup our class hierarchy. self._setup(f2008_cls_members) - # We can now specify which classes are taken as defining new - # scoping regions. Programs without the optional program-stmt - # are handled separately in the Fortran2003.Main_Program0 class. - SYMBOL_TABLES.scoping_unit_classes = [ - Fortran2003.Module_Stmt, - Fortran2003.Subroutine_Stmt, - Fortran2003.Program_Stmt, - Fortran2003.Function_Stmt, - Fortran2008.Submodule_Stmt, - Fortran2008.Block_Stmt, - ] - # the class hierarchy has been set up so return the top + # The class hierarchy has been set up so return the top # level class that we start from when parsing Fortran # code. Fortran2008 does not extend the top level class so # we return the Fortran2003 one. @@ -222,10 +205,21 @@ class name and a class. and not cls.__name__.endswith("Base") ): base_classes[cls.__name__] = cls - #if cls.__name__ == "Executable_Construct": - # import pdb; pdb.set_trace() - # dir(cls) - # + + # Ensure we keep a copy of the original subclass_names list for each + # class (because this gets altered below). + for cls in base_classes.values(): + if not hasattr(cls, "subclass_names"): + continue + # pylint: disable=protected-access + if ( + not hasattr(cls, "_original_subclass_names") + or not cls._original_subclass_names + ): + setattr(cls, "_original_subclass_names", cls.subclass_names[:]) + else: + cls.subclass_names = cls._original_subclass_names[:] + # OPTIMIZE subclass_names tree. # def _rpl_list(clsname): @@ -238,12 +232,12 @@ def _rpl_list(clsname): :param str clsname: The name of the class from which to search. - :returns: names of subclasses with `match` methods. + :returns: names of 'nearest' subclasses with `match` methods. :rtype: List[str | NoneType] """ if clsname not in base_classes: - error_string = "Not implemented: {0}".format(clsname) + error_string = f"Not implemented: {clsname}" logging.getLogger(__name__).debug(error_string) return [] # remove this code when all classes are implemented. @@ -263,21 +257,9 @@ def _rpl_list(clsname): bits.append(names1) return bits - # Ensure we keep a copy of the original subclass_names list for each - # class (because this gets altered below). - for cls in base_classes.values(): - if not hasattr(cls, "subclass_names"): - continue - if not hasattr(cls, "_original_subclass_names"): - setattr(cls, "_original_subclass_names", cls.subclass_names[:]) - else: - cls.subclass_names = cls._original_subclass_names[:] - for cls in base_classes.values(): if not hasattr(cls, "subclass_names"): continue - #if cls.__name__ == "Executable_Construct": - # import pdb; pdb.set_trace() # The optimised list of subclass names will only include subclasses # that have `match` methods. opt_subclass_names = [] @@ -304,14 +286,9 @@ def _rpl_list(clsname): if name in base_classes: bits.append(base_classes[name]) else: - message = "{0} not implemented needed by {1}".format(name, clsname) + message = f"{name} not implemented needed by {clsname}" logging.getLogger(__name__).debug(message) - #import pdb; pdb.set_trace() - names = [cls.__name__ for cls in Fortran2003.Base.subclasses["Executable_Construct"]] - print(sorted(names)) - #print(fparser.two.Fortran2003.Base.subclasses["Executable_Construct"]) - if 1: for cls in base_classes.values(): # subclasses = Fortran2003.Base.subclasses.get( diff --git a/src/fparser/two/symbol_table.py b/src/fparser/two/symbol_table.py index 6b258932..d765e5c2 100644 --- a/src/fparser/two/symbol_table.py +++ b/src/fparser/two/symbol_table.py @@ -56,8 +56,6 @@ class SymbolTables: def __init__(self): self._symbol_tables = {} - # Those classes that correspond to a new scoping unit - self._scoping_unit_classes = [] # The symbol table of the current scope self._current_scope = None # Whether or not we enable consistency checks in the symbol tables @@ -126,36 +124,6 @@ def lookup(self, name): """ return self._symbol_tables[name.lower()] - @property - def scoping_unit_classes(self): - """ - :returns: the fparser2 classes that are taken to mark the start of \ - a new scoping region. - :rtype: list of types - - """ - return self._scoping_unit_classes - - @scoping_unit_classes.setter - def scoping_unit_classes(self, value): - """ - Set the list of fparser2 classes that are taken to mark the start of \ - a new scoping region. - - :param value: the list of fparser2 classes. - :type value: list of types - - :raises TypeError: if the supplied value is not a list of types. - - """ - if not isinstance(value, list): - raise TypeError( - f"Supplied value must be a list but got '{type(value).__name__}'" - ) - if not all(isinstance(item, type) for item in value): - raise TypeError(f"Supplied list must contain only classes but got: {value}") - self._scoping_unit_classes = value - @property def current_scope(self): """ diff --git a/src/fparser/two/tests/test_bases.py b/src/fparser/two/tests/test_bases.py index ae9e9fdf..309dbeca 100644 --- a/src/fparser/two/tests/test_bases.py +++ b/src/fparser/two/tests/test_bases.py @@ -156,29 +156,3 @@ def test_blockbase_tofortran_non_ascii(): # Explicitly call tofortran() on the BlockBase class. out_str = BlockBase.tofortran(bbase) assert "for e1=1" in out_str - - -@pytest.mark.usefixtures("f2003_create") -def test_blockbase_symbol_table(monkeypatch): - """Check that the BlockBase.match method creates symbol-tables - for those classes that correspond to a scoping unit and not - otherwise.""" - # Monkeypatch the list of classes that are recognised as - # defining scoping regions. - monkeypatch.setattr( - SYMBOL_TABLES, "_scoping_unit_classes", [Fortran2003.Program_Stmt] - ) - code = "program my_test\n" "end program\n" - reader = FortranStringReader(code, ignore_comments=False) - obj = BlockBase.match( - Fortran2003.Program_Stmt, [], Fortran2003.End_Program_Stmt, reader - ) - # We should have a new symbol table named "my_test" - assert SYMBOL_TABLES.lookup("my_test") - code = "subroutine my_sub\n" "end subroutine\n" - reader = FortranStringReader(code, ignore_comments=False) - obj = BlockBase.match( - Fortran2003.Subroutine_Stmt, [], Fortran2003.End_Subroutine_Stmt, reader - ) - # There should be no new symbol table - assert "my_sub" not in SYMBOL_TABLES._symbol_tables diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 9a8cc033..5d4a6e19 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -54,14 +54,6 @@ class do not affect current calls. with pytest.raises(FortranSyntaxError) as excinfo: _ = parser(reader) assert "at line 1\n>>>submodule (x) y\n" in str(excinfo.value) - # Check that the list of classes used to define scoping regions is - # correctly set. - assert SYMBOL_TABLES.scoping_unit_classes == [ - Fortran2003.Module_Stmt, - Fortran2003.Subroutine_Stmt, - Fortran2003.Program_Stmt, - Fortran2003.Function_Stmt, - ] parser = ParserFactory().create(std="f2003") reader = FortranStringReader(fstring) @@ -76,7 +68,6 @@ class do not affect current calls. assert "SUBMODULE (x) y\nEND" in code # Submodule_Stmt should now be included in the list of classes that define # scoping regions. - assert Fortran2008.Submodule_Stmt in SYMBOL_TABLES.scoping_unit_classes assert "y" in SYMBOL_TABLES._symbol_tables # Repeat f2003 example to make sure that a previously valid (f2008) @@ -86,7 +77,6 @@ class do not affect current calls. with pytest.raises(FortranSyntaxError) as excinfo: _ = parser(reader) assert "at line 1\n>>>submodule (x) y\n" in str(excinfo.value) - assert Fortran2008.Submodule_Stmt not in SYMBOL_TABLES.scoping_unit_classes # The previous symbol table entries should have been removed when # creating the new parser. assert "y" not in SYMBOL_TABLES._symbol_tables diff --git a/src/fparser/two/tests/test_symbol_tables.py b/src/fparser/two/tests/test_symbol_tables.py index 719e33d2..9fa34d17 100644 --- a/src/fparser/two/tests/test_symbol_tables.py +++ b/src/fparser/two/tests/test_symbol_tables.py @@ -80,23 +80,6 @@ def test_construction_addition_removal(): assert tables._symbol_tables == {} -def test_scoping_unit_classes_setter(): - """Check that the setter for the list of classes used to define scoping - regions works as expected.""" - tables = SymbolTables() - assert tables.scoping_unit_classes == [] - tables.scoping_unit_classes = [Fortran2003.Block_Data] - assert tables.scoping_unit_classes == [Fortran2003.Block_Data] - with pytest.raises(TypeError) as err: - tables.scoping_unit_classes = "hello" - assert "Supplied value must be a list but got 'str'" in str(err.value) - with pytest.raises(TypeError) as err: - tables.scoping_unit_classes = ["hello"] - assert "Supplied list must contain only classes but got: ['hello']" in str( - err.value - ) - - def test_str_method(): """Tests for the str() method.""" tables = SymbolTables() diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 46c3d90b..dfc7ad2e 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -530,6 +530,8 @@ def restore_reader(self, reader): class ScopingRegionMixin: """ + Mixin class for use in all classes that represent a scoping region. + """ def get_scope_name(self): @@ -538,7 +540,7 @@ def get_scope_name(self): :rtype: str """ return self.get_name().string - + class BlockBase(Base): """ @@ -601,6 +603,9 @@ def match( # top-level due to circular dependencies). assert isinstance(reader, FortranReaderBase), repr(reader) content = [] + # This will store the name of the new SymbolTable if we match a + # scoping region. + table_name = None if startcls is not None: # Deal with any preceding comments, includes, and/or directives @@ -617,12 +622,12 @@ def match( for obj in reversed(content): obj.restore_reader(reader) return - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if isinstance(obj, ScopingRegionMixin): # We are entering a new scoping unit so create a new # symbol table. # NOTE: if the match subsequently fails then we must # delete this symbol table. - table_name = obj.get_scope_name() #str(obj.children[1]) + table_name = obj.get_scope_name() SYMBOL_TABLES.enter_scope(table_name) # Store the index of the start of this block proper (i.e. # excluding any comments) @@ -749,20 +754,20 @@ def match( except FortranSyntaxError as err: # We hit trouble so clean up the symbol table - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if table_name: SYMBOL_TABLES.exit_scope() # Remove any symbol table that we created SYMBOL_TABLES.remove(table_name) raise err - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if table_name: SYMBOL_TABLES.exit_scope() if not had_match or endcls and not found_end: # We did not get a match from any of the subclasses or # failed to find the endcls if endcls is not None: - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if table_name: # Remove any symbol table that we created SYMBOL_TABLES.remove(table_name) for obj in reversed(content): From dce6d40d50d8d827ad80cd90808cd5e3af173347 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 3 Mar 2023 10:35:06 +0000 Subject: [PATCH 09/23] #392 fix black error --- src/fparser/two/Fortran2003.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index c9992350..d314d1e4 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -7037,7 +7037,13 @@ class If_Construct(BlockBase): # R802 """ subclass_names = [] - use_names = ["If_Then_Stmt", "Execution_Part_Construct", "Else_If_Stmt", "Else_Stmt", "End_If_Stmt"] + use_names = [ + "If_Then_Stmt", + "Execution_Part_Construct", + "Else_If_Stmt", + "Else_Stmt", + "End_If_Stmt", + ] @staticmethod def match(string): From f79d50d6818e42f0c971e70ab0bd1d9f9a5e0f32 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 3 Mar 2023 15:51:04 +0000 Subject: [PATCH 10/23] #392 replace modification of class subclass_names with local dict --- src/fparser/two/parser.py | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index a885d51c..56560041 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -151,12 +151,6 @@ def create(self, std=None): from fparser.two import Fortran2008 f2008_cls_members = get_module_classes(Fortran2008) - for _, cls in f2008_cls_members: - if hasattr(cls, "_original_subclass_names"): - # Reset the list of original subclass names as it will have - # been inherited from the corresponding F2003 class. - # pylint: disable=protected-access - cls._original_subclass_names = [] # next add in Fortran2003 classes if they do not already # exist as a Fortran2008 class. @@ -206,20 +200,6 @@ class name and a class. ): base_classes[cls.__name__] = cls - # Ensure we keep a copy of the original subclass_names list for each - # class (because this gets altered below). - for cls in base_classes.values(): - if not hasattr(cls, "subclass_names"): - continue - # pylint: disable=protected-access - if ( - not hasattr(cls, "_original_subclass_names") - or not cls._original_subclass_names - ): - setattr(cls, "_original_subclass_names", cls.subclass_names[:]) - else: - cls.subclass_names = cls._original_subclass_names[:] - # OPTIMIZE subclass_names tree. # def _rpl_list(clsname): @@ -257,6 +237,9 @@ def _rpl_list(clsname): bits.append(names1) return bits + # Dict in which to store optimised list of subclass names for each cls. + local_subclass_names = {} + for cls in base_classes.values(): if not hasattr(cls, "subclass_names"): continue @@ -267,17 +250,16 @@ def _rpl_list(clsname): for names1 in _rpl_list(names): if names1 not in opt_subclass_names: opt_subclass_names.append(names1) - if not opt_subclass_names == cls.subclass_names: - cls.subclass_names = opt_subclass_names[:] + local_subclass_names[cls] = opt_subclass_names[:] # Now that we've optimised the list of subclass names for each class, # use this information to initialise the Base.subclasses dictionary: for clsname, cls in base_classes.items(): - subclass_names = getattr(cls, "subclass_names", None) - if subclass_names is None: + if not hasattr(cls, "subclass_names"): message = "%s class is missing subclass_names list" % (clsname) logging.getLogger(__name__).debug(message) continue + subclass_names = local_subclass_names.get(cls, []) try: bits = Fortran2003.Base.subclasses[clsname] except KeyError: @@ -294,7 +276,7 @@ def _rpl_list(clsname): # subclasses = Fortran2003.Base.subclasses.get( # cls.__name__, []) # subclasses_names = [c.__name__ for c in subclasses] - subclass_names = getattr(cls, "subclass_names", []) + subclass_names = local_subclass_names.get(cls, []) use_names = getattr(cls, "use_names", []) # for name in subclasses_names: # break From 6a1097deb346c5574074311f7e592d88b2c0c43d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 3 Mar 2023 16:27:50 +0000 Subject: [PATCH 11/23] #392 tidying and adding docstrings --- src/fparser/two/Fortran2008.py | 67 ++++++++++++------- .../two/tests/fortran2008/test_block.py | 33 +++++++-- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index 72757e41..a00d701f 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -1389,18 +1389,6 @@ class Block_Stmt(StmtBase, WORDClsBase, ScopingRegionMixin): use_names = ["Block_Construct_Name"] counter = 0 - class Counter: - """Global counter so that each block-stmt introduces a new scope.""" - - counter = 0 - - def __init__(self): - self._counter = Block_Stmt.Counter.counter - Block_Stmt.Counter.counter += 1 - - def __repr__(self): - return "_block_{0}".format(self._counter) - @staticmethod def match(string): """ @@ -1418,14 +1406,21 @@ def match(string): if not found: return None block, _ = found - internal_name = f"_block_{Block_Stmt.counter}" + # Construct a unique name for this BLOCK (in case it isn't named). We + # ensure the name is not a valid Fortran name so that it can't clash + # with any regions named in the code. + scope_name = f"block:{Block_Stmt.counter}" Block_Stmt.counter += 1 - return block, Block_Stmt.Counter() # internal_name + return block, scope_name def get_scope_name(self): + """ + :returns: the name of this scoping region. + :rtype: str + """ if self.item.name: return self.item.name - return f"_block_{self.items[1]}" + return self.items[1] def get_start_name(self): """ @@ -1452,19 +1447,23 @@ class End_Block_Stmt(EndStmtBase): # R809 def match(string): """ :param str string: Fortran code to check for a match - :return: code line matching the "END DO" statement - :rtype: string + + :return: code line matching the "END BLOCK" statement + :rtype: str + """ return EndStmtBase.match( "BLOCK", Block_Construct_Name, string, require_stmt_type=True ) -class Critical_Construct(BlockBase): # R807 +class Critical_Construct(BlockBase): """ - = - == [ ]... - + Fortran 2008 Rule 810. + + critical-construct is critical-stmt + block + end-critical-stmt TODO: Should disallow RETURN (C809) and CYCLE or EXIT to outside block (C811) """ @@ -1474,6 +1473,16 @@ class Critical_Construct(BlockBase): # R807 @staticmethod def match(reader): + """ + Attempt to match the supplied content with this Rule. + + :param reader: + :type reader: + + :returns: + :rtype: + + """ return BlockBase.match( Critical_Stmt, [Execution_Part_Construct], @@ -1484,9 +1493,12 @@ def match(reader): ) -class Critical_Stmt(StmtBase, WORDClsBase): # R808 +class Critical_Stmt(StmtBase, WORDClsBase): """ - = [ : ] CRITICAL + Fortran 2008 Rule R811. + + critical-stmt is [ critical-construct-name : ] CRITICAL + """ subclass_names = [] @@ -1503,8 +1515,13 @@ def tostr(self): return "CRITICAL" -class End_Critical_Stmt(EndStmtBase): # R809 - """ = END CRITICAL [ ]""" +class End_Critical_Stmt(EndStmtBase): + """ + Fortran 2008 Rule 812. + + end-critical-stmt is END CRITICAL [ critical-construct-name ] + + """ subclass_names = [] use_names = ["Critical_Construct_Name"] diff --git a/src/fparser/two/tests/fortran2008/test_block.py b/src/fparser/two/tests/fortran2008/test_block.py index f99eb8b0..5d927252 100644 --- a/src/fparser/two/tests/fortran2008/test_block.py +++ b/src/fparser/two/tests/fortran2008/test_block.py @@ -34,10 +34,11 @@ import pytest +import re from fparser.api import get_reader -from fparser.two.Fortran2008 import Block_Construct, Program_Unit -from fparser.two.symbol_table import SymbolTable, SYMBOL_TABLES +from fparser.two.Fortran2008 import Block_Construct +from fparser.two.symbol_table import SYMBOL_TABLES from fparser.two.utils import FortranSyntaxError, walk @@ -81,6 +82,11 @@ def test_block_new_scope(f2008_parser, before, after): assert "BLOCK\nINTEGER :: b = 4\na = 1 + b\nEND BLOCK" in str(block).replace( " ", "" ) + tables = SYMBOL_TABLES + assert list(tables._symbol_tables.keys()) == ["foo"] + table = SYMBOL_TABLES.lookup("foo") + assert len(table.children) == 1 + assert re.match(r"block:[\d+]", table.children[0].name) def test_block_in_if(f2008_parser): @@ -109,6 +115,7 @@ def test_named_block(f2008_create): """ Test that a named block construct is correctly captured and also reproduced. + """ block = Block_Construct( get_reader( @@ -125,6 +132,11 @@ def test_named_block(f2008_create): def test_end_block_missing_start_name(f2008_create): # C808 + """ + Test Constraint 808 - that a name on the 'end block' must correspond + with the same name on the 'block'. + + """ with pytest.raises(FortranSyntaxError): Block_Construct( get_reader( @@ -137,6 +149,11 @@ def test_end_block_missing_start_name(f2008_create): # C808 def test_end_block_missing_end_name(f2008_create): # C808 + """ + Test that a named block that is missing a name on its 'end block' statement + results in a syntax error. + + """ with pytest.raises(FortranSyntaxError): Block_Construct( get_reader( @@ -149,6 +166,11 @@ def test_end_block_missing_end_name(f2008_create): # C808 def test_end_block_wrong_name(f2008_create): # C808 + """ + Test that an incorrect name on the end block statement results in a + syntax error. + + """ with pytest.raises(FortranSyntaxError): Block_Construct( get_reader( @@ -161,8 +183,11 @@ def test_end_block_wrong_name(f2008_create): # C808 def test_block_in_subroutine(f2008_parser): - """Check that we get two, nested symbol tables when a subroutine contains - a Block construct.""" + """ + Check that we get two, nested symbol tables when a subroutine contains + a Block construct. + + """ code = """\ program my_prog real :: a From d724e77f7ae7b7ca7c1ce27c7df399b3192600ba Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 16 Mar 2023 16:44:55 +0000 Subject: [PATCH 12/23] #392 update CRITICAL doc strings --- src/fparser/two/Fortran2008.py | 52 +++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index a00d701f..d47173fc 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -80,7 +80,6 @@ from fparser.common.splitline import string_replace_map, splitparen from fparser.two import pattern_tools as pattern -from fparser.two.symbol_table import SYMBOL_TABLES from fparser.two.utils import ( BracketBase, CALLBase, @@ -897,13 +896,13 @@ def match(reader): param reader: the fortran file reader containing the line(s) of code that we are trying to match - :type reader: :py:class:`fparser.common.readfortran.FortranFileReader` - or - :py:class:`fparser.common.readfortran.FortranStringReader` - :return: `tuple` containing a single `list` which contains + :type reader: :py:class:`fparser.common.readfortran.FortranFileReader` \ + | :py:class:`fparser.common.readfortran.FortranStringReader` + + :returns: `tuple` containing a single `list` which contains instance of the classes that have matched if there is a match or `None` if there is no match - + :rtype: Tuple[List[:py:class:`fparser.two.utils.Base`]] | NoneType """ return BlockBase.match( None, @@ -1438,7 +1437,12 @@ def tostr(self): class End_Block_Stmt(EndStmtBase): # R809 - """ = END BLOCK [ ]""" + """ + Fortran 2008 Rule 809. + + end-block-stmt is END BLOCK [ block-construct-name ] + + """ subclass_names = [] use_names = ["Block_Construct_Name"] @@ -1476,11 +1480,13 @@ def match(reader): """ Attempt to match the supplied content with this Rule. - :param reader: - :type reader: + :param reader: the fortran file reader containing the line(s) + of code that we are trying to match + :type reader: :py:class:`fparser.common.readfortran.FortranFileReader` \ + | :py:class:`fparser.common.readfortran.FortranStringReader` - :returns: - :rtype: + :returns: instance of class that has matched or `None` if no match. + :rtype: :py:class:`fparser.two.utils.BlockBase` | NoneType """ return BlockBase.match( @@ -1506,12 +1512,30 @@ class Critical_Stmt(StmtBase, WORDClsBase): @staticmethod def match(string): + """ + Attempts to match the supplied string as a CRITICAL statement. + + :param str string: the string to attempt to match. + + :returns: 2-tuple containing the matched word "CRITICAL" and None or \ + None if no match. + :rtype: Tuple[str, NoneType] or NoneType + + """ return WORDClsBase.match("CRITICAL", None, string) def get_start_name(self): + """ + :returns: the name associated with the start of this CRITICAL region (if any) + :rtype: str | NoneType + """ return self.item.name def tostr(self): + """ + :returns: the string representation of this node. + :rtype: str + """ return "CRITICAL" @@ -1530,8 +1554,10 @@ class End_Critical_Stmt(EndStmtBase): def match(string): """ :param str string: Fortran code to check for a match - :return: code line matching the "END DO" statement - :rtype: string + + :returns: code line matching the "END CRITICAL" statement + :rtype: str + """ return EndStmtBase.match( "CRITICAL", Critical_Construct_Name, string, require_stmt_type=True From ec6d7fbecb69bc5402a747219f8be59004a034f6 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 17 Mar 2023 08:53:33 +0000 Subject: [PATCH 13/23] #392 update tests of critical --- .../two/tests/fortran2008/test_critical.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/fparser/two/tests/fortran2008/test_critical.py b/src/fparser/two/tests/fortran2008/test_critical.py index 7038bb17..7ffdff82 100644 --- a/src/fparser/two/tests/fortran2008/test_critical.py +++ b/src/fparser/two/tests/fortran2008/test_critical.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2021 Science and Technology Facilities Council. +# Copyright (c) 2022-2023 Science and Technology Facilities Council. # All rights reserved. @@ -34,13 +34,15 @@ from fparser.api import get_reader +from fparser.two.Fortran2003 import Assignment_Stmt +from fparser.two.Fortran2008 import Critical_Construct, Critical_Stmt, End_Critical_Stmt from fparser.two.utils import FortranSyntaxError -from fparser.two.Fortran2008 import Critical_Construct import pytest def test_critical(f2008_create): + """Test that a basic critical construct is correctly constructed.""" critical = Critical_Construct( get_reader( """\ @@ -50,11 +52,16 @@ def test_critical(f2008_create): """ ) ) - + assert isinstance(critical.children[0], Critical_Stmt) + assert isinstance(critical.children[1], Assignment_Stmt) + assert isinstance(critical.children[2], End_Critical_Stmt) + assert critical.children[0].get_start_name() is None assert "CRITICAL\n a = 1 + b\nEND CRITICAL" in str(critical) def test_named_critical(f2008_create): + """Test that a named critical construct is matched correctly and that + its name can be queried.""" critical = Critical_Construct( get_reader( """\ @@ -64,12 +71,14 @@ def test_named_critical(f2008_create): """ ) ) - + assert critical.children[0].get_start_name() == "foo" assert "foo:CRITICAL\n a = 1 + b\nEND CRITICAL foo" in str(critical) def test_end_critical_missing_start_name(f2008_create): # C809 - with pytest.raises(FortranSyntaxError): + """Check that a critical construct with an end name but no start name + results in a syntax error (C809).""" + with pytest.raises(FortranSyntaxError) as err: Critical_Construct( get_reader( """\ @@ -78,10 +87,13 @@ def test_end_critical_missing_start_name(f2008_create): # C809 """ ) ) + assert "Name 'foo' has no corresponding starting name" in str(err) def test_end_critical_missing_end_name(f2008_create): # C809 - with pytest.raises(FortranSyntaxError): + """Test that a named critical construct with the name omitted from + the end critical results in a syntax error (C809).""" + with pytest.raises(FortranSyntaxError) as err: Critical_Construct( get_reader( """\ @@ -90,10 +102,12 @@ def test_end_critical_missing_end_name(f2008_create): # C809 """ ) ) + assert "Expecting name 'foo' but none given" in str(err) def test_end_critical_wrong_name(f2008_create): # C809 - with pytest.raises(FortranSyntaxError): + """Test that mismatched start and end names result in a syntax error (C809)""" + with pytest.raises(FortranSyntaxError) as err: Critical_Construct( get_reader( """\ @@ -102,3 +116,4 @@ def test_end_critical_wrong_name(f2008_create): # C809 """ ) ) + assert "Expecting name 'foo', got 'bar'" in str(err) From 9ee12d5ef04a82f34069e53681ab4d7000cfc716 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 17 Mar 2023 09:24:10 +0000 Subject: [PATCH 14/23] #392 more tidying of tests --- .../two/tests/fortran2008/test_block.py | 29 +++++++++++++------ src/fparser/two/utils.py | 9 +++--- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/fparser/two/tests/fortran2008/test_block.py b/src/fparser/two/tests/fortran2008/test_block.py index 5d927252..537d41ca 100644 --- a/src/fparser/two/tests/fortran2008/test_block.py +++ b/src/fparser/two/tests/fortran2008/test_block.py @@ -37,9 +37,9 @@ import re from fparser.api import get_reader -from fparser.two.Fortran2008 import Block_Construct +from fparser.two.Fortran2008 import Block_Construct, Block_Stmt from fparser.two.symbol_table import SYMBOL_TABLES -from fparser.two.utils import FortranSyntaxError, walk +from fparser.two.utils import FortranSyntaxError, ScopingRegionMixin, walk def test_block(f2008_create): @@ -54,7 +54,10 @@ def test_block(f2008_create): """ ) ) - + assert isinstance(block.children[0], Block_Stmt) + assert isinstance(block.children[0], ScopingRegionMixin) + name = block.children[0].get_scope_name() + assert re.match(r"block:[\d+]", name) assert "BLOCK\n INTEGER :: b = 4\n a = 1 + b\nEND BLOCK" in str(block) @@ -137,7 +140,7 @@ def test_end_block_missing_start_name(f2008_create): # C808 with the same name on the 'block'. """ - with pytest.raises(FortranSyntaxError): + with pytest.raises(FortranSyntaxError) as err: Block_Construct( get_reader( """\ @@ -146,6 +149,7 @@ def test_end_block_missing_start_name(f2008_create): # C808 """ ) ) + assert "Name 'foo' has no corresponding starting name" in str(err) def test_end_block_missing_end_name(f2008_create): # C808 @@ -154,7 +158,7 @@ def test_end_block_missing_end_name(f2008_create): # C808 results in a syntax error. """ - with pytest.raises(FortranSyntaxError): + with pytest.raises(FortranSyntaxError) as err: Block_Construct( get_reader( """\ @@ -163,6 +167,7 @@ def test_end_block_missing_end_name(f2008_create): # C808 """ ) ) + assert "Expecting name 'foo' but none given" in str(err) def test_end_block_wrong_name(f2008_create): # C808 @@ -171,7 +176,7 @@ def test_end_block_wrong_name(f2008_create): # C808 syntax error. """ - with pytest.raises(FortranSyntaxError): + with pytest.raises(FortranSyntaxError) as err: Block_Construct( get_reader( """\ @@ -180,11 +185,12 @@ def test_end_block_wrong_name(f2008_create): # C808 """ ) ) + assert "Expecting name 'foo', got 'bar'" in str(err) def test_block_in_subroutine(f2008_parser): """ - Check that we get two, nested symbol tables when a subroutine contains + Check that we get two, nested symbol tables when a routine contains a Block construct. """ @@ -199,7 +205,11 @@ def test_block_in_subroutine(f2008_parser): a = b end block rocking else - a = 10.0 + block + real :: c + c = 42.0 / 5.0 + a = 10.0 * c + end block end if end program my_prog """ @@ -207,5 +217,6 @@ def test_block_in_subroutine(f2008_parser): tables = SYMBOL_TABLES assert list(tables._symbol_tables.keys()) == ["my_prog"] table = SYMBOL_TABLES.lookup("my_prog") - assert len(table.children) == 1 + assert len(table.children) == 2 assert table.children[0].name == "rocking" + assert re.match(r"block:[\d+]", table.children[1].name) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index dfc7ad2e..ca4f2ef8 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -1,5 +1,5 @@ -# Modified work Copyright (c) 2017-2022 Science and Technology -# Facilities Council +# Modified work Copyright (c) 2017-2023 Science and Technology +# Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson # All rights reserved. @@ -337,7 +337,7 @@ class Base(ComparableMixin): :param type cls: the class of object to create. :param string: (source of) Fortran string to parse. - :type string: [Str | :py:class:`fparser.common.readfortran.FortranReaderBase`] + :type string: [str | :py:class:`fparser.common.readfortran.FortranReaderBase`] :param parent_cls: the parent class of this object. :type parent_cls: `type` @@ -530,7 +530,8 @@ def restore_reader(self, reader): class ScopingRegionMixin: """ - Mixin class for use in all classes that represent a scoping region. + Mixin class for use in all classes that represent a scoping region and + thus have an associated symbol table. """ From 700c4665929d7a78068261b6d341871468f27d41 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 17 Mar 2023 09:34:15 +0000 Subject: [PATCH 15/23] #392 update dev guide with new way of defining scoping regions --- doc/source/developers_guide.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/source/developers_guide.rst b/doc/source/developers_guide.rst index 76c0cd2b..5b6b2c80 100644 --- a/doc/source/developers_guide.rst +++ b/doc/source/developers_guide.rst @@ -294,10 +294,11 @@ there is no name associated with such a program, the corresponding symbol table is given the name "fparser2:main_program", chosen so as to prevent any clashes with other Fortran names. -Those classes taken to define scoping regions are stored as -a list within the `SymbolTables` instance. This list is populated -after the class hierarchy has been constructed for the parser (since -this depends on which Fortran standard has been chosen). +Those classes which define scoping regions must subclass the +`ScopingRegionMixin` class: + +.. autoclass:: fparser.two.utils.ScopingRegionMixin + Class Generation ++++++++++++++++ From dd841064c93cb4f21ba3ee0931ac5feb533e4ee0 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 21 Mar 2023 17:09:33 +0000 Subject: [PATCH 16/23] #392 update dev guide and tidy for pylint --- doc/source/developers_guide.rst | 10 +++++--- src/fparser/two/Fortran2008.py | 45 ++++++++++++++++++--------------- src/fparser/two/parser.py | 34 +++++++------------------ src/fparser/two/symbol_table.py | 3 +-- 4 files changed, 42 insertions(+), 50 deletions(-) diff --git a/doc/source/developers_guide.rst b/doc/source/developers_guide.rst index 5b6b2c80..f1cf2308 100644 --- a/doc/source/developers_guide.rst +++ b/doc/source/developers_guide.rst @@ -170,9 +170,11 @@ returned. An example of a simple choice rule is `R202`. See the :ref:`program-unit-class` section for a description of its implementation. -.. note:: - - A `use_names` description, explanation and example needs to be added. +The `use_names` list should contain any classes that are referenced by the +implementation of the current class. These lists of names are aggregated +(along with `subclass_names`) and used to ensure that all necessary `Scalar_`, +`_List` and `_Name` classes are generated (in code at the end of the +`Fortran2003` and `Fortran2008` modules - see :ref:`class-generation`). When the rule is not a simple choice the developer needs to supply a static `match` method. An example of this is rule `R201`. See the @@ -300,6 +302,8 @@ Those classes which define scoping regions must subclass the .. autoclass:: fparser.two.utils.ScopingRegionMixin +.. _class-generation: + Class Generation ++++++++++++++++ diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index d47173fc..e62397d6 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -92,6 +92,11 @@ Type_Declaration_StmtBase, WORDClsBase, ) + +# The _List classes imported here are auto-generated which confuses pylint +# (because it doesn't actually import modules and therefore the classes are +# not generated). +# pylint: disable=no-name-in-module from fparser.two.Fortran2003 import ( Base, BlockBase, @@ -119,6 +124,8 @@ Use_Stmt, ) +# pylint: enable=no-name-in-module + # Import of F2003 classes that are updated in this standard. from fparser.two.Fortran2003 import ( Action_Stmt as Action_Stmt_2003, @@ -902,7 +909,7 @@ def match(reader): :returns: `tuple` containing a single `list` which contains instance of the classes that have matched if there is a match or `None` if there is no match - :rtype: Tuple[List[:py:class:`fparser.two.utils.Base`]] | NoneType + :rtype: Optional[Tuple[List[:py:class:`fparser.two.utils.Base`]]] """ return BlockBase.match( None, @@ -1352,8 +1359,9 @@ class Block_Construct(BlockBase): block end-block-stmt - TODO: Should disallow COMMON, EQUIVALENCE, IMPLICIT, INTENT, - NAMELIST, OPTIONAL, VALUE, and statement functions (C806) + TODO #394: Should disallow COMMON, EQUIVALENCE, IMPLICIT, INTENT, + NAMELIST, OPTIONAL, VALUE, and statement functions (C806) (which are all + valid members of Specification_Part). """ subclass_names = [] @@ -1572,15 +1580,15 @@ def match(string): ClassType = type(Base) _names = dir() for clsname in _names: - cls = eval(clsname) + new_cls = eval(clsname) if not ( - isinstance(cls, ClassType) - and issubclass(cls, Base) - and not cls.__name__.endswith("Base") + isinstance(new_cls, ClassType) + and issubclass(new_cls, Base) + and not new_cls.__name__.endswith("Base") ): continue - names = getattr(cls, "subclass_names", []) + getattr(cls, "use_names", []) + names = getattr(new_cls, "subclass_names", []) + getattr(new_cls, "use_names", []) for n in names: if n in _names: continue @@ -1589,34 +1597,31 @@ def match(string): n = n[:-5] # Generate 'list' class exec( - """\ -class %s_List(SequenceBase): - subclass_names = [\'%s\'] + f"""\ +class {n}_List(SequenceBase): + subclass_names = [\'{n}\'] use_names = [] @staticmethod - def match(string): return SequenceBase.match(r\',\', %s, string) + def match(string): return SequenceBase.match(r\',\', {n}, string) """ - % (n, n, n) ) elif n.endswith("_Name"): _names.append(n) n = n[:-5] exec( - """\ -class %s_Name(Base): + f"""\ +class {n}_Name(Base): subclass_names = [\'Name\'] """ - % (n) ) elif n.startswith("Scalar_"): _names.append(n) n = n[7:] exec( - """\ -class Scalar_%s(Base): - subclass_names = [\'%s\'] + f"""\ +class Scalar_{n}(Base): + subclass_names = [\'{n}\'] """ - % (n, n) ) diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index 56560041..3f74704d 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -256,7 +256,7 @@ def _rpl_list(clsname): # use this information to initialise the Base.subclasses dictionary: for clsname, cls in base_classes.items(): if not hasattr(cls, "subclass_names"): - message = "%s class is missing subclass_names list" % (clsname) + message = f"{clsname} class is missing subclass_names list" logging.getLogger(__name__).debug(message) continue subclass_names = local_subclass_names.get(cls, []) @@ -271,27 +271,11 @@ def _rpl_list(clsname): message = f"{name} not implemented needed by {clsname}" logging.getLogger(__name__).debug(message) - if 1: - for cls in base_classes.values(): - # subclasses = Fortran2003.Base.subclasses.get( - # cls.__name__, []) - # subclasses_names = [c.__name__ for c in subclasses] - subclass_names = local_subclass_names.get(cls, []) - use_names = getattr(cls, "use_names", []) - # for name in subclasses_names: - # break - # if name not in subclass_names: - # message = ('%s needs to be added to %s ' - # 'subclasses_name list' - # % (name, cls.__name__)) - # logging.getLogger(__name__).debug(message) - # for name in subclass_names: - # break - # if name not in subclasses_names: - # message = '%s needs to be added to %s ' - # 'subclass_name list' % (name, cls.__name__) - # logging.getLogger(__name__).debug(message) - for name in use_names + subclass_names: - if name not in base_classes: - message = "%s not defined used " "by %s" % (name, cls.__name__) - logging.getLogger(__name__).debug(message) + # Double-check that all required classes have been constructed. + for cls in base_classes.values(): + subclass_names = local_subclass_names.get(cls, []) + use_names = getattr(cls, "use_names", []) + for name in use_names + subclass_names: + if name not in base_classes: + message = f"{name} not defined, used by {cls.__name__}" + logging.getLogger(__name__).debug(message) diff --git a/src/fparser/two/symbol_table.py b/src/fparser/two/symbol_table.py index d765e5c2..fc2c32a4 100644 --- a/src/fparser/two/symbol_table.py +++ b/src/fparser/two/symbol_table.py @@ -81,8 +81,7 @@ def enable_checks(self, value): def clear(self): """ - Deletes any stored SymbolTables but retains the stored list of - classes that define scoping units. + Deletes any stored SymbolTables. """ self._symbol_tables = {} From 839e815659dc0df9758f227b6a5b2eb7446270ae Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 22 Mar 2023 09:53:25 +0000 Subject: [PATCH 17/23] #392 add pylintrc for tests, fix pylint errors and rename _rpl_list --- src/fparser/two/parser.py | 6 +++--- src/fparser/two/tests/.pylintrc | 19 +++++++++++++++++++ .../two/tests/fortran2008/test_block.py | 14 ++++++++------ 3 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 src/fparser/two/tests/.pylintrc diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index 3f74704d..6cf599be 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -202,7 +202,7 @@ class name and a class. # OPTIMIZE subclass_names tree. # - def _rpl_list(clsname): + def _closest_descendants_with_match(clsname): """ Starting at the named class, searches down the tree defined by the classes named in the `subclass_names` list to find the closest that @@ -231,7 +231,7 @@ def _rpl_list(clsname): # `match` method. bits = [] for names in getattr(cls, "subclass_names", []): - list1 = _rpl_list(names) + list1 = _closest_descendants_with_match(names) for names1 in list1: if names1 not in bits: bits.append(names1) @@ -247,7 +247,7 @@ def _rpl_list(clsname): # that have `match` methods. opt_subclass_names = [] for names in cls.subclass_names: - for names1 in _rpl_list(names): + for names1 in _closest_descendants_with_match(names): if names1 not in opt_subclass_names: opt_subclass_names.append(names1) local_subclass_names[cls] = opt_subclass_names[:] diff --git a/src/fparser/two/tests/.pylintrc b/src/fparser/two/tests/.pylintrc new file mode 100644 index 00000000..380f1971 --- /dev/null +++ b/src/fparser/two/tests/.pylintrc @@ -0,0 +1,19 @@ +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=useless-suppression + + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=suppressed-message,duplicate-code,too-many-locals,too-many-lines,protected-access,locally-disabled,too-few-public-methods,too-many-arguments,use-implicit-booleaness-not-comparison diff --git a/src/fparser/two/tests/fortran2008/test_block.py b/src/fparser/two/tests/fortran2008/test_block.py index 537d41ca..ba63e4a3 100644 --- a/src/fparser/two/tests/fortran2008/test_block.py +++ b/src/fparser/two/tests/fortran2008/test_block.py @@ -32,9 +32,11 @@ # (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 support of the Fortran2008 +Block construct.""" -import pytest import re +import pytest from fparser.api import get_reader from fparser.two.Fortran2008 import Block_Construct, Block_Stmt @@ -42,7 +44,7 @@ from fparser.two.utils import FortranSyntaxError, ScopingRegionMixin, walk -def test_block(f2008_create): +def test_block(): """Test that the Block_Construct matches as expected.""" block = Block_Construct( get_reader( @@ -114,7 +116,7 @@ def test_block_in_if(f2008_parser): assert len(blocks) == 1 -def test_named_block(f2008_create): +def test_named_block(): """ Test that a named block construct is correctly captured and also reproduced. @@ -134,7 +136,7 @@ def test_named_block(f2008_create): assert "foo:BLOCK\n INTEGER :: b = 4\n a = 1 + b\nEND BLOCK foo" in str(block) -def test_end_block_missing_start_name(f2008_create): # C808 +def test_end_block_missing_start_name(): # C808 """ Test Constraint 808 - that a name on the 'end block' must correspond with the same name on the 'block'. @@ -152,7 +154,7 @@ def test_end_block_missing_start_name(f2008_create): # C808 assert "Name 'foo' has no corresponding starting name" in str(err) -def test_end_block_missing_end_name(f2008_create): # C808 +def test_end_block_missing_end_name(): # C808 """ Test that a named block that is missing a name on its 'end block' statement results in a syntax error. @@ -170,7 +172,7 @@ def test_end_block_missing_end_name(f2008_create): # C808 assert "Expecting name 'foo' but none given" in str(err) -def test_end_block_wrong_name(f2008_create): # C808 +def test_end_block_wrong_name(): # C808 """ Test that an incorrect name on the end block statement results in a syntax error. From 45748c3339a0ab71056a6b0afdad9d16fa6effd2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 22 Mar 2023 10:23:04 +0000 Subject: [PATCH 18/23] #392 fix pylint errors in test_critical --- .../two/tests/fortran2008/test_critical.py | 17 ++++++++++------- src/fparser/two/utils.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/fparser/two/tests/fortran2008/test_critical.py b/src/fparser/two/tests/fortran2008/test_critical.py index 7ffdff82..e9e6d6d2 100644 --- a/src/fparser/two/tests/fortran2008/test_critical.py +++ b/src/fparser/two/tests/fortran2008/test_critical.py @@ -32,16 +32,19 @@ # (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 support of the Fortran2008 +Critical construct.""" + + +import pytest from fparser.api import get_reader from fparser.two.Fortran2003 import Assignment_Stmt from fparser.two.Fortran2008 import Critical_Construct, Critical_Stmt, End_Critical_Stmt from fparser.two.utils import FortranSyntaxError -import pytest - -def test_critical(f2008_create): +def test_critical(): """Test that a basic critical construct is correctly constructed.""" critical = Critical_Construct( get_reader( @@ -59,7 +62,7 @@ def test_critical(f2008_create): assert "CRITICAL\n a = 1 + b\nEND CRITICAL" in str(critical) -def test_named_critical(f2008_create): +def test_named_critical(): """Test that a named critical construct is matched correctly and that its name can be queried.""" critical = Critical_Construct( @@ -75,7 +78,7 @@ def test_named_critical(f2008_create): assert "foo:CRITICAL\n a = 1 + b\nEND CRITICAL foo" in str(critical) -def test_end_critical_missing_start_name(f2008_create): # C809 +def test_end_critical_missing_start_name(): # C809 """Check that a critical construct with an end name but no start name results in a syntax error (C809).""" with pytest.raises(FortranSyntaxError) as err: @@ -90,7 +93,7 @@ def test_end_critical_missing_start_name(f2008_create): # C809 assert "Name 'foo' has no corresponding starting name" in str(err) -def test_end_critical_missing_end_name(f2008_create): # C809 +def test_end_critical_missing_end_name(): # C809 """Test that a named critical construct with the name omitted from the end critical results in a syntax error (C809).""" with pytest.raises(FortranSyntaxError) as err: @@ -105,7 +108,7 @@ def test_end_critical_missing_end_name(f2008_create): # C809 assert "Expecting name 'foo' but none given" in str(err) -def test_end_critical_wrong_name(f2008_create): # C809 +def test_end_critical_wrong_name(): # C809 """Test that mismatched start and end names result in a syntax error (C809)""" with pytest.raises(FortranSyntaxError) as err: Critical_Construct( diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index ca4f2ef8..ca722bb6 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -337,7 +337,7 @@ class Base(ComparableMixin): :param type cls: the class of object to create. :param string: (source of) Fortran string to parse. - :type string: [str | :py:class:`fparser.common.readfortran.FortranReaderBase`] + :type string: str | :py:class:`fparser.common.readfortran.FortranReaderBase` :param parent_cls: the parent class of this object. :type parent_cls: `type` From a6dfc275c17e162c71d274151f9c64a6b2540e68 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 29 Mar 2023 15:19:19 +0100 Subject: [PATCH 19/23] #392 add TODO re names appearing in repr of Blocks --- src/fparser/two/Fortran2008.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index e62397d6..123140ee 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -1418,6 +1418,12 @@ def match(string): # with any regions named in the code. scope_name = f"block:{Block_Stmt.counter}" Block_Stmt.counter += 1 + # TODO #397. Ideally we'd have the name associated with the Block + # Construct here (if any) so that it could be displayed in repr. + # As it is, repr will show scope_name which will not be the same + # as any explicit name given to the Block. (This name *is* shown + # in the repr of the End_Block_Stmt.) This problem is common to + # other block constructs such as Block_Nonlabel_Do_Construct. return block, scope_name def get_scope_name(self): From 098505b8e172d3624466ed164f76a872e6464781 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 3 Apr 2023 10:30:41 +0100 Subject: [PATCH 20/23] #392 update pylint configuration to ignore no-member warnings for generated classes --- src/fparser/.pylintrc | 9 ++++++--- src/fparser/two/Fortran2008.py | 6 ------ src/fparser/two/tests/fortran2008/test_open_stmt_r904.py | 3 +-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/fparser/.pylintrc b/src/fparser/.pylintrc index 7a937382..4d9c1bef 100644 --- a/src/fparser/.pylintrc +++ b/src/fparser/.pylintrc @@ -3,9 +3,12 @@ # 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 +[TYPECHECK] + +# fparser generates *_List classes at runtime so pylint can't +# find them (as it's a static checker). +ignored-modules=fparser.two.Fortran2003,fparser.two.Fortran2008 +generated-members=fparser.two.Fortran2003.*_List,fparser.two.Fortran2008.*_List [DESIGN] # Maximum number of parents for a class (see R0901) diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index 123140ee..c17be7d9 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -93,10 +93,6 @@ WORDClsBase, ) -# The _List classes imported here are auto-generated which confuses pylint -# (because it doesn't actually import modules and therefore the classes are -# not generated). -# pylint: disable=no-name-in-module from fparser.two.Fortran2003 import ( Base, BlockBase, @@ -124,8 +120,6 @@ Use_Stmt, ) -# pylint: enable=no-name-in-module - # Import of F2003 classes that are updated in this standard. from fparser.two.Fortran2003 import ( Action_Stmt as Action_Stmt_2003, diff --git a/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py index 554bd3ed..e928592b 100644 --- a/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py +++ b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Science and Technology Facilities Council +# Copyright (c) 2022-2023 Science and Technology Facilities Council. # All rights reserved. @@ -72,7 +72,6 @@ """ -# pylint: disable=no-member import pytest from fparser.api import get_reader from fparser.two import Fortran2008 From 599ed8af0bc51d9f639f10f07d290f9304341a9a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 3 Apr 2023 11:07:16 +0100 Subject: [PATCH 21/23] #392 update docstrings in End_Block/Critical match methods --- src/fparser/two/Fortran2008.py | 14 ++++++++++---- src/fparser/two/utils.py | 35 ++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index c17be7d9..ddea5401 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -1460,8 +1460,11 @@ def match(string): """ :param str string: Fortran code to check for a match - :return: code line matching the "END BLOCK" statement - :rtype: str + :return: 2-tuple containing "BLOCK" and, optionally, an associated \ + Name or None if no match. + :rtype: Optional[Tuple[ + str, + Optional[:py:class:`fparser.two.Fortran2003.Block_Construct_Name`]]] """ return EndStmtBase.match( @@ -1563,8 +1566,11 @@ def match(string): """ :param str string: Fortran code to check for a match - :returns: code line matching the "END CRITICAL" statement - :rtype: str + :returns: 2-tuple containing "CRITICAL" and, optionally, an associated \ + Name or None if there is no match. + :rtype: Optional[Tuple[ + str, \ + Optional[:py:class:`fparser.two.Fortran2003.Critical_Construct_Name`]]] """ return EndStmtBase.match( diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 412d986a..3c711aee 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -1620,23 +1620,50 @@ class EndStmtBase(StmtBase): @staticmethod def match(stmt_type, stmt_name, string, require_stmt_type=False): + """ + Attempts to match the supplied string as a form of 'END xxx' statement. + + :param str stmt_type: the type of end statement (e.g. "do") that we \ + attempt to match. + :param type stmt_name: a class which should be used to match against \ + the name should this statement be named (e.g. end subroutine sub). + :param str string: the string to attempt to match. + :param bool require_stmt_type: whether or not the string must contain \ + the type of the block that is ending. + + :returns: 2-tuple containing the matched end-statement type (if any) \ + and, optionally, an associated name or None if there is no match. + :rtype: Optional[ + Tuple[Optional[str], + Optional[:py:class:`fparser.two.Fortran2003.Name`]]] + + """ start = string[:3].upper() if start != "END": - return + # string doesn't begin with 'END' + return None line = string[3:].lstrip() start = line[: len(stmt_type)].upper() if start: if start.replace(" ", "") != stmt_type.replace(" ", ""): - return + # Not the correct type of 'END ...' statement. + return None line = line[len(stmt_type) :].lstrip() else: if require_stmt_type: - return + # No type was found but one is required. + return None + # Got a bare "END" and that is a valid match. return None, None if line: if stmt_name is None: - return + # There is content after the 'end xxx' but this block isn't + # named so we fail to match. + return None + # Attempt to match the content after 'end xxx' with the supplied + # name class. return stmt_type, stmt_name(line) + # Successful match with an unnamed 'end xxx'. return stmt_type, None def init(self, stmt_type, stmt_name): From ca11e295d186bc45dfda4c20d478a890c7b1f3f1 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 3 Apr 2023 11:33:40 +0100 Subject: [PATCH 22/23] #392 add tests for EndStmtBase.match --- src/fparser/two/tests/.pylintrc | 9 ++++--- src/fparser/two/tests/test_utils.py | 37 +++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/fparser/two/tests/.pylintrc b/src/fparser/two/tests/.pylintrc index a321354b..bf06335d 100644 --- a/src/fparser/two/tests/.pylintrc +++ b/src/fparser/two/tests/.pylintrc @@ -23,9 +23,12 @@ disable=suppressed-message,duplicate-code,too-many-locals,too-many-lines,protect # 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 +[TYPECHECK] + +# fparser generates *_List classes at runtime so pylint can't +# find them (as it's a static checker). +ignored-modules=fparser.two.Fortran2003,fparser.two.Fortran2008 +generated-members=fparser.two.Fortran2003.*_List,fparser.two.Fortran2008.*_List [DESIGN] # Maximum number of parents for a class (see R0901) diff --git a/src/fparser/two/tests/test_utils.py b/src/fparser/two/tests/test_utils.py index 18f7217a..2803daf1 100644 --- a/src/fparser/two/tests/test_utils.py +++ b/src/fparser/two/tests/test_utils.py @@ -45,7 +45,8 @@ # test BlockBase -def test_blockbase_match_names(f2003_create): +@pytest.mark.usefixtures("f2003_create") +def test_blockbase_match_names(): """Test the blockbase name matching option in its match method. We use the Derived_Type_Def class (which subclasses BlockBase) for this as it sets match_names to True. @@ -78,7 +79,8 @@ def test_blockbase_match_names(f2003_create): ) in str(excinfo.value) -def test_blockbase_match_name_classes(f2003_create): +@pytest.mark.usefixtures("f2003_create") +def test_blockbase_match_name_classes(): """Test the blockbase name matching option in its match method. We use the If_Construct class (which subclasses BlockBase) for this as it sets match_names to True and provides match_name_classes. This is @@ -108,3 +110,34 @@ def test_blockbase_match_name_classes(f2003_create): assert ( "at line 2\n>>>endif label\nName 'label' has no corresponding " "starting name" ) in str(excinfo.value) + + +@pytest.mark.usefixtures("f2003_create") +def test_endstmtbase_match(): + """Tests for the EndStmtBase.match() method.""" + result = utils.EndStmtBase.match("critical", None, "hello") + assert result is None + # No statement type is required by default + result = utils.EndStmtBase.match("CRITICAL", None, "end") + assert result == (None, None) + # Missing statement type. + result = utils.EndStmtBase.match("CRITICAL", None, "end", require_stmt_type=True) + assert result is None + # Matching statement type. + result = utils.EndStmtBase.match( + "CRITICAL", None, "end critical", require_stmt_type=True + ) + assert result == ("CRITICAL", None) + # End construct with name but no class to match it with. + result = utils.EndStmtBase.match( + "SUBROUTINE", None, "end subroutine sub", require_stmt_type=True + ) + assert result is None + # End construct with name that matches with supplied class. + result = utils.EndStmtBase.match( + "SUBROUTINE", + Fortran2003.Subroutine_Name, + "end subroutine sub", + require_stmt_type=True, + ) + assert result == ("SUBROUTINE", Fortran2003.Name("sub")) From a766ac16f43096bf58f77a3068ef4ad3cf5034d8 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Mon, 3 Apr 2023 14:07:04 +0100 Subject: [PATCH 23/23] #392 Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66071843..96b152b2 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 +03/04/2023 PR #392 for #326. Add support for F2008 block and critical constructs. + 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