# Modified work Copyright (c) 2017-2024 Science and Technology
# Facilities Council.
# Original work Copyright (c) 1999-2008 Pearu Peterson
# 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.
# --------------------------------------------------------------------
# The original software (in the f2py project) was 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:
# a. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# b. 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.
# c. Neither the name of the F2PY project 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 REGENTS 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.
"""Base classes and exception handling for Fortran parser.
"""
# Original author: Pearu Peterson <pearu@cens.ioc.ee>
# First version created: Oct 2006
import re
from fparser.common import readfortran
from fparser.common.splitline import string_replace_map
from fparser.common.readfortran import FortranReaderBase
from fparser.two.symbol_table import SYMBOL_TABLES
# A list of supported extensions to the standard(s)
# An X edit descriptor in a format statement specifies the position
# (forward from the current position) at which the next character will
# be transmitted to or from a record. In standard Fortran2003 the X
# edit descriptor must be preceeded by an integer which specifies how
# far forward from the current position. The 'x-format' extension
# allows the X edit descriptor to be specified without a preceeding
# integer.
[docs]
_EXTENSIONS = ["x-format"]
# Cray pointers are a well known extension to the Fortran
# standard. See http://pubs.cray.com/content/S-3901/8.6/
# cray-fortran-reference-manual-s-3901-86/types or
# https://gcc.gnu.org/onlinedocs/gfortran/Cray-pointers.html for
# example. If 'cray-pointer' is specified in EXTENSIONS then this
# extension is allowed in fparser.
_EXTENSIONS += ["cray-pointer"]
# A Hollerith constant is a way of specifying a string as a sequence
# of characters preceded by the string length and separated by an 'H'
# e.g. 5Hhello. See
# https://gcc.gnu.org/onlinedocs/gfortran/Hollerith-constants-support.html,
# for example for more details. fparser currently supports Hollerith
# constants specified in format statements when 'hollerith' is specified
# in the EXTENSIONS list.
_EXTENSIONS += ["hollerith"]
# Many compilers support the use of '$' in a fortran write statement
# to indicate that the carriage return should be suppressed. This is
# an extension to the Fortran standard and is supported by fparser if
# 'dollar-descriptor' is specified in the EXTENSIONS list.
_EXTENSIONS += ["dollar-descriptor"]
# Many compilers support the optional 'CONVERT' argument to OPEN(). This is
# used to indicate any endian-related conversion that must be performed
# when reading/writing data using unformatted IO.
_EXTENSIONS += ["open-convert"]
[docs]
def EXTENSIONS():
"""
:returns: the list of extensions (to the Fortran standard) currently active.
:rtype: list[str]
"""
return _EXTENSIONS
# Set this to True to get verbose output (on stdout) detailing the matches made
# while parsing.
[docs]
_SHOW_MATCH_RESULTS = False
[docs]
class FparserException(Exception):
"""Base class exception for fparser. This allows an external tool to
capture all exceptions if required.
:param str info: a string giving contextual error information.
"""
def __init__(self, info):
Exception.__init__(self, info)
[docs]
class NoMatchError(FparserException):
"""An exception indicating that a particular rule implemented by a
class does not match the provided string. It does not necessary
mean there is an error as another rule may match. This exception
is used internally so should never be visible externally.
"""
[docs]
class FortranSyntaxError(FparserException):
"""An exception indicating that fparser believes the provided code to
be invalid Fortran. Also returns information about the location of
the error if that information is available.
:param reader: input string or reader where the error took \
place. This is used to provide line number and line content \
information.
:type reader: str or :py:class:`FortranReaderBase`
:param str info: a string giving contextual error information.
"""
def __init__(self, reader, info):
output = "at unknown location "
if isinstance(reader, FortranReaderBase):
output = "at line {0}\n>>>{1}\n".format(
reader.linecount, reader.source_lines[reader.linecount - 1]
)
if info:
output += "{0}".format(info)
FparserException.__init__(self, output)
[docs]
class InternalError(FparserException):
"""An exception indicating that an unexpected error has occured in the
parser.
:param str info: a string giving contextual error information.
"""
def __init__(self, info):
new_info = "'{0}'. Please report this to the " "authors.".format(info)
FparserException.__init__(self, new_info)
[docs]
class InternalSyntaxError(FparserException):
"""An exception indicating that a syntax error has been found by the
parser. This is used instead of `FortranSyntaxError` when the
reader object is not available.
"""
[docs]
def show_result(func):
"""
A decorator that enables the matching sequence to be debugged by outputting
the result (to stdout) whenever a new node in the parse tree is successfully
constructed.
:param function func: the functor that is being called.
:returns: the supplied functor.
:rtype: function
"""
if not _SHOW_MATCH_RESULTS:
# Just return the supplied functor unchanged.
return func
# It's not possible to monkeypatch decorators since the functions they are
# wrapping get modified at module-import time. Therefore, we can't get
# coverage of the rest of this routine.
def new_func(cls, string, **kws): # pragma: no cover
"""
New functor to replace the one supplied. Simply wraps the supplied
functor with some code that prints the match if it was successful.
:param type cls: the Class that is being matched.
:param str string: the string we are attempting to match.
:param *kws: additional keyword arguments.
:type *kws: Dict[str, Any]
:returns: new functor object.
:rtype: function
"""
result = func(cls, string, **kws)
if isinstance(result, StmtBase):
if result:
print(f"{cls.__name__}({string}) -> {result}")
else:
print(f"{cls.__name__}({string}) did NOT match")
return result
return new_func # pragma: no cover
#
# BASE CLASSES
#
[docs]
class ComparableMixin:
"""Mixin class to provide rich comparison operators.
This mixin provides a set of rich comparison operators. Each class using
this mixin has to provide a _cmpkey() method that returns a key of objects
that can be compared.
See also http://python3porting.com/preparing.html#richcomparisons
"""
# pylint: disable=too-few-public-methods
[docs]
def _compare(self, other, method):
"""Call the method, if other is able to be used within it.
:param object other: The other object to compare with
:type other: object
:param method: The method to call to compare self and other.
:type method: LambdaType
:return: NotImplemented, when the comparison for the given type
combination can't be performed.
:rtype: :py:class:`types.NotImplementedType`
"""
try:
# This routine's purpose is to access the protected method
# _cmpkey() from client classes, therefore: pylint:
# disable=protected-access
return method(self._cmpkey(), other._cmpkey())
except (AttributeError, TypeError):
# _cmpkey not implemented, or return different type,
# so I can't compare with "other".
# According to the Python Language Reference Manual
# (http://www.network-theory.co.uk/docs/pylang/Coercionrules.html)
# return NotImplemented
return NotImplemented
[docs]
def __lt__(self, other):
return self._compare(other, lambda s, o: s < o)
[docs]
def __le__(self, other):
return self._compare(other, lambda s, o: s <= o)
[docs]
def __eq__(self, other):
return self._compare(other, lambda s, o: s == o)
[docs]
def __ge__(self, other):
return self._compare(other, lambda s, o: s >= o)
[docs]
def __gt__(self, other):
return self._compare(other, lambda s, o: s > o)
[docs]
def __ne__(self, other):
return self._compare(other, lambda s, o: s != o)
[docs]
class DynamicImport:
"""This class imports a set of fparser.two dependencies that can not
be imported during the Python Import time because they have a circular
dependency with this file.
They are imported once when the Fortran2003 is already processed by
calling the import_now() method.
The alternative is to have the equivalent top-level imports in the
Base.__new__ method, but this method is in the parser critical path and
is best to keep expensive operations out of it.
"""
@staticmethod
[docs]
def import_now():
"""Execute the Import of Fortran2003 dependencies."""
# pylint: disable=import-outside-toplevel
from fparser.two.Fortran2003 import (
Else_If_Stmt,
Else_Stmt,
End_If_Stmt,
Masked_Elsewhere_Stmt,
Elsewhere_Stmt,
End_Where_Stmt,
Type_Guard_Stmt,
End_Select_Type_Stmt,
Case_Stmt,
End_Select_Stmt,
Comment,
Include_Stmt,
add_comments_includes_directives,
)
from fparser.two import C99Preprocessor
DynamicImport.Else_If_Stmt = Else_If_Stmt
DynamicImport.Else_Stmt = Else_Stmt
DynamicImport.End_If_Stmt = End_If_Stmt
DynamicImport.Masked_Elsewhere_Stmt = Masked_Elsewhere_Stmt
DynamicImport.Elsewhere_Stmt = Elsewhere_Stmt
DynamicImport.End_Where_Stmt = End_Where_Stmt
DynamicImport.Type_Guard_Stmt = Type_Guard_Stmt
DynamicImport.End_Select_Type_Stmt = End_Select_Type_Stmt
DynamicImport.Case_Stmt = Case_Stmt
DynamicImport.End_Select_Stmt = End_Select_Stmt
DynamicImport.Comment = Comment
DynamicImport.Include_Stmt = Include_Stmt
DynamicImport.C99Preprocessor = C99Preprocessor
DynamicImport.add_comments_includes_directives = (
add_comments_includes_directives
)
[docs]
def _set_parent(parent_node, items):
""" Recursively set the parent of all of the elements
in the list that are a sub-class of Base. (Recursive because
sometimes the list of elements itself contains a list or tuple.)
:param parent_node: the parent of the nodes listed in `items`.
:type parent_node: sub-class of :py:class:`fparser.two.utils.Base`
:param items: list or tuple of nodes for which to set the parent.
:type items: list or tuple of :py:class:`fparser.two.utils.Base` \
or `str` or `list` or `tuple` or NoneType.
"""
for item in items:
if item:
if isinstance(item, Base):
# We can only set the parent of `Base` objects.
# Anything else (e.g. str) is passed over.
item.parent = parent_node
elif isinstance(item, (list, tuple)):
_set_parent(parent_node, item)
[docs]
class Base(ComparableMixin):
"""Base class for Fortran 2003 syntax rules.
All Base classes have the following attributes::
self.string - original argument to construct a class instance, its
type is either str or FortranReaderBase.
self.item - Line instance (holds label) or None.
: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`
:param parent_cls: the parent class of this object.
:type parent_cls: `type`
"""
# This dict of subclasses is populated dynamically by code at the end
# 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.
def __init__(self, string, parent_cls=None):
# pylint:disable=unused-argument
self.parent = None
@show_result
def __new__(cls, string, parent_cls=None):
if parent_cls is None:
parent_cls = [cls]
elif cls not in parent_cls:
parent_cls.append(cls)
# Get the class' match method if it has one
match = getattr(cls, "match", None)
if (
isinstance(string, FortranReaderBase)
and match
and not issubclass(cls, BlockBase)
):
reader = string
item = reader.get_item()
if item is None:
return None
if isinstance(item, readfortran.Comment):
# We got a comment but we weren't after a comment (we handle
# those in Comment.__new__)
obj = None
else:
try:
obj = item.parse_line(cls, parent_cls)
except NoMatchError:
obj = None
if obj is None:
# No match so give the item back to the reader
reader.put_item(item)
return None
obj.item = item
return obj
result = None
if match:
# IMPORTANT: if string is FortranReaderBase then cls must
# restore readers content when no match is found.
try:
result = cls.match(string)
except NoMatchError as msg:
if str(msg) == "%s: %r" % (cls.__name__, string):
# avoid recursion 1.
raise
if isinstance(result, tuple):
obj = object.__new__(cls)
obj.string = string
obj.item = None
# Set-up parent information for the results of the match
_set_parent(obj, result)
if hasattr(cls, "init"):
obj.init(*result)
return obj
if isinstance(result, Base):
return result
if result is None:
# Loop over the possible sub-classes of this class and
# 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
try:
obj = subcls(string, parent_cls=parent_cls)
except NoMatchError:
obj = None
if obj is not None:
return obj
else:
raise AssertionError(repr(result))
# If we get to here then we've failed to match the current line
if isinstance(string, FortranReaderBase):
content = False
for index in range(string.linecount):
# Check all lines up to this one for content. We
# should be able to only check the current line but
# but as the line number returned is not always
# correct (due to coding errors) we cannot assume the
# line pointed to is the line where the error actually
# happened.
if string.source_lines[index].strip():
content = True
break
if not content:
# There are no lines in the input or all lines up to
# this one are empty or contain only white space. This
# is typically accepted by fortran compilers so we
# follow their lead and do not raise an exception.
return
line = string.source_lines[string.linecount - 1]
errmsg = f"at line {string.linecount}\n>>>{line}\n"
else:
errmsg = f"{cls.__name__}: '{string}'"
raise NoMatchError(errmsg)
[docs]
def get_root(self):
"""
Gets the node at the root of the parse tree to which this node belongs.
:returns: the node at the root of the parse tree.
:rtype: :py:class:`fparser.two.utils.Base`
"""
current = self
while current.parent:
current = current.parent
return current
@property
[docs]
def children(self):
"""Return an iterable containing the immediate children of this node in
the parse tree.
If this node represents an expression then its children are
contained in a tuple which is immutable. Therefore, the
manipulation of the children of such a node must be done by
replacing the `items` property of the node directly rather than via the
objects returned by this method.
:returns: the immediate children of this node.
:rtype: list or tuple containing zero or more of \
:py:class:`fparser.two.utils.Base` or NoneType or str
"""
child_list = getattr(self, "content", None)
if child_list is None:
child_list = getattr(self, "items", [])
return child_list
[docs]
def init(self, *items):
"""
Store the supplied list of nodes in the `items` list of this node.
:param items: the children of this node.
:type items: tuple of :py:class:`fparser.two.utils.Base`
"""
self.items = items
[docs]
def torepr(self):
return "%s(%s)" % (self.__class__.__name__, ", ".join(map(repr, self.items)))
[docs]
def __str__(self):
return self.tostr()
[docs]
def __repr__(self):
return self.torepr()
[docs]
def _cmpkey(self):
"""Provides a key of objects to be used for comparing."""
return self.items
[docs]
def tofortran(self, tab="", isfix=None):
"""
Produce the Fortran representation of this Comment.
:param str tab: characters to pre-pend to output.
:param bool isfix: whether or not this is fixed-format code.
:returns: Fortran representation of this comment.
:rtype: str
"""
this_str = str(self)
if this_str.strip():
return tab + this_str
# If this_str is empty (i.e this Comment is a blank line) then
# don't prepend any spaces to it
return this_str
[docs]
def restore_reader(self, reader):
reader.put_item(self.item)
[docs]
class ScopingRegionMixin:
"""
Mixin class for use in all classes that represent a scoping region and
thus have an associated symbol table.
"""
[docs]
def get_scope_name(self):
"""
:returns: the name of this scoping region.
:rtype: str
"""
return self.get_name().string
[docs]
class BlockBase(Base):
"""
Base class for matching all block constructs::
<block-base> = [ <startcls> ]
[ <subcls> ]...
...
[ <subcls> ]...
[ <endcls> ]
"""
@staticmethod
[docs]
def match(
startcls,
subclasses,
endcls,
reader,
match_labels=False,
match_names=False,
match_name_classes=(),
enable_do_label_construct_hook=False,
enable_if_construct_hook=False,
enable_where_construct_hook=False,
strict_order=False,
strict_match_names=False,
):
"""
Checks whether the content in reader matches the given
type of block statement (e.g. DO..END DO, IF...END IF etc.)
:param type startcls: the class marking the beginning of the block
:param list subclasses: list of classes that can be children of \
the block.
:param type endcls: the class marking the end of the block.
:param reader: content to check for match.
:type reader: str or instance of :py:class:`FortranReaderBase`
:param bool match_labels: whether or not the statement terminating \
the block must have a label that matches the opening statement. \
Default is False.
:param bool match_names: TBD
:param tuple match_name_classes: TBD
:param bool enable_do_label_construct_hook: TBD
:param bool enable_if_construct_hook: TBD
:param bool enable_where_construct_hook: TBD
:param bool strict_order: whether to enforce the order of the \
given subclasses.
:param bool strict_match_names: if start name present, end name \
must exist and match.
:return: instance of startcls or None if no match is found
:rtype: startcls
"""
# This implementation uses the DynamicImport class and its instance di
# to access the Fortran2003 and C99Preprocessor classes, this is a
# performance optimization to avoid importing the classes inside this
# method since it is in the hotpath (and it can't be done in the
# 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
DynamicImport.add_comments_includes_directives(content, reader)
# Now attempt to match the start of the block
try:
obj = startcls(reader)
except NoMatchError:
obj = None
if obj is None:
# Ultimately we failed to find a match for the
# start of the block so put back any comments that
# we processed along the way
for obj in reversed(content):
obj.restore_reader(reader)
return
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()
SYMBOL_TABLES.enter_scope(table_name, obj)
# Store the index of the start of this block proper (i.e.
# excluding any comments)
start_idx = len(content)
content.append(obj)
if hasattr(obj, "get_start_label") and enable_do_label_construct_hook:
start_label = obj.get_start_label()
if match_names:
start_name = obj.get_start_name()
# Comments and Include statements are always valid sub-classes
classes = subclasses + [di.Comment, di.Include_Stmt]
# Preprocessor directives are always valid sub-classes
cpp_classes = [
getattr(di.C99Preprocessor, cls_name)
for cls_name in di.C99Preprocessor.CPP_CLASS_NAMES
]
classes += cpp_classes
if endcls is not None:
classes += [endcls]
endcls_all = tuple([endcls] + endcls.subclasses[endcls.__name__])
try:
# Start trying to match the various subclasses, starting from
# the beginning of the list (where else?)
i = 0
had_match = False
found_end = False
while i < len(classes):
if enable_do_label_construct_hook:
# Multiple, labelled DO statements can reference the
# same label.
obj = startcls(reader)
if obj is not None and hasattr(obj, "get_start_label"):
if start_label == obj.get_start_label():
content.append(obj)
continue
obj.restore_reader(reader)
# Attempt to match the i'th subclass
cls = classes[i]
try:
obj = cls(reader)
except NoMatchError:
obj = None
if obj is None:
# No match for this class, continue checking the list
# starting from the i+1'th...
i += 1
continue
# We got a match for this class
had_match = True
content.append(obj)
if match_names and isinstance(obj, match_name_classes):
end_name = obj.get_end_name()
if end_name and not start_name:
raise FortranSyntaxError(
reader,
f"Name '{end_name}' has no corresponding starting name",
)
if (
end_name
and start_name
and end_name.lower() != start_name.lower()
):
raise FortranSyntaxError(
reader, f"Expecting name '{start_name}', got '{end_name}'"
)
if endcls is not None and isinstance(obj, endcls_all):
if match_labels:
start_label, end_label = (
content[start_idx].get_start_label(),
content[-1].get_end_label(),
)
if start_label != end_label:
continue
if match_names:
start_name, end_name = (
content[start_idx].get_start_name(),
content[-1].get_end_name(),
)
if end_name and not start_name:
raise FortranSyntaxError(
reader,
f"Name '{end_name}' has no corresponding starting name",
)
elif strict_match_names and start_name and not end_name:
raise FortranSyntaxError(
reader, f"Expecting name '{start_name}' but none given"
)
elif (
start_name
and end_name
and (start_name.lower() != end_name.lower())
):
raise FortranSyntaxError(
reader,
f"Expecting name '{start_name}', got '{end_name}'",
)
# We've found the enclosing end statement so break out
found_end = True
break
if not strict_order:
# Return to start of classes list now that we've matched.
i = 0
if enable_if_construct_hook:
if isinstance(obj, di.Else_If_Stmt):
# Got an else-if so go back to start of possible
# classes to match
i = 0
if isinstance(obj, (di.Else_Stmt, di.End_If_Stmt)):
# Found end-if
enable_if_construct_hook = False
if enable_where_construct_hook:
if isinstance(obj, di.Masked_Elsewhere_Stmt):
i = 0
if isinstance(obj, (di.Elsewhere_Stmt, di.End_Where_Stmt)):
enable_where_construct_hook = False
continue
except FortranSyntaxError as err:
# We hit trouble so clean up the symbol table
if table_name:
SYMBOL_TABLES.exit_scope()
# Remove any symbol table that we created
SYMBOL_TABLES.remove(table_name)
raise err
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 table_name:
# Remove any symbol table that we created
SYMBOL_TABLES.remove(table_name)
for obj in reversed(content):
obj.restore_reader(reader)
return None
if not content:
# We can only get to here if startcls is None - if startcls is not
# None and fails to match then we will already have returned. If
# it is not None and matches then content will not be empty.
# Since startcls must be None, we won't have created a symbol
# table so we don't have to clean up.
return None
if startcls is not None and endcls is not None:
# check names of start and end statements:
start_stmt = content[start_idx]
end_stmt = content[-1]
if (
isinstance(end_stmt, endcls_all)
and hasattr(end_stmt, "get_name")
and hasattr(start_stmt, "get_name")
):
if end_stmt.get_name() is not None:
if (
start_stmt.get_name().string.lower()
!= end_stmt.get_name().string.lower()
):
end_stmt.item.reader.error(
"expected <%s-name> is %s but got %s. Ignoring."
% (
end_stmt.get_type().lower(),
start_stmt.get_name(),
end_stmt.get_name(),
)
)
return (content,)
[docs]
def init(self, content):
"""
Initialise the `content` attribute with the list of child nodes.
:param content: list of nodes that are children of this one.
:type content: list of :py:class:`fparser.two.utils.Base` or NoneType
"""
self.content = content
[docs]
def _cmpkey(self):
"""Provides a key of objects to be used for comparing."""
return self.content
[docs]
def tostr(self):
return self.tofortran()
[docs]
def torepr(self):
return "%s(%s)" % (self.__class__.__name__, ", ".join(map(repr, self.content)))
[docs]
def tofortran(self, tab="", isfix=None):
"""
Create a string containing the Fortran representation of this class
:param str tab: indent to prefix to code.
:param bool isfix: whether or not to generate fixed-format code.
:return: Fortran representation of this class.
:rtype: str
"""
mylist = []
start = self.content[0]
end = self.content[-1]
extra_tab = ""
if isinstance(end, EndStmtBase):
extra_tab = " "
if start is not None:
mylist.append(start.tofortran(tab=tab, isfix=isfix))
for item in self.content[1:-1]:
mylist.append(item.tofortran(tab=tab + extra_tab, isfix=isfix))
if len(self.content) > 1:
mylist.append(end.tofortran(tab=tab, isfix=isfix))
return "\n".join(mylist)
[docs]
def restore_reader(self, reader):
for obj in reversed(self.content):
obj.restore_reader(reader)
[docs]
class SequenceBase(Base):
"""
Match one or more fparser2 rules separated by a defined separator::
sequence-base is obj [sep obj ] ...
"""
@staticmethod
[docs]
def match(separator, subcls, string):
"""Match one or more 'subcls' fparser2 rules in the string 'string'
separated by 'separator'.
:param str separator: the separator used to split the supplied \
string.
:param subcls: an fparser2 object representing the rule that \
should be matched.
:type subcls: subclass of :py:class:`fparser.two.utils.Base`
:param str string: the input string to match.
:returns: a tuple containing 1) the separator and 2) the \
matched objects in a tuple, or None if there is no match.
:rtype: Optional[(Str, :py:class:`fparser.two.utils.Base`)]
:raises InternalError: if the separator or string arguments \
are not the expected type.
:raises InternalError: if the separator is white space.
"""
if not isinstance(separator, str):
raise InternalError(
f"SequenceBase class match method argument separator expected "
f"to be a string but found '{type(separator)}'."
)
if not isinstance(string, str):
raise InternalError(
f"SequenceBase class match method argument string expected to "
f"be a string but found '{type(string)}'."
)
if separator == " ":
raise InternalError(
"SequenceBase class match method argument separator cannot "
"be white space."
)
line, repmap = string_replace_map(string)
splitted = line.split(separator)
if not splitted:
# There should be at least one entry.
return None
lst = [subcls(repmap(entry.strip())) for entry in splitted]
return separator, tuple(lst)
[docs]
def init(self, separator, items):
"""Store the result of the match method if the match is successful.
:param str separator: the separator used to split the supplied string.
:param items: a tuple containing the matched objects.
:type items: tuple(Subclass of :py:class:`fparser.two.utils.Base`)
"""
self.separator = separator
self.items = items
[docs]
def tostr(self):
"""
:returns: The Fortran representation of this object as a string.
:rtype: str
"""
sep = self.separator
if sep == ",":
sep = sep + " "
elif sep == " ":
pass
else:
sep = " " + sep + " "
return sep.join(map(str, self.items))
[docs]
def torepr(self):
"""
:returns: The Python representation of this object as a string.
:rtype: str
"""
return "{0}('{1}', {2})".format(
self.__class__.__name__, self.separator, self.items
)
# The mixin class is likely to be removed so _cmpkey would not be
# needed. It is not used at the moment. It is only commented out
# at this point, rather than removed, in case it turns out that
# the mixin class is useful.
# def _cmpkey(self):
# """ Provides a key of objects to be used for comparing.
# """
# return (self.separator, self.items)
[docs]
class UnaryOpBase(Base):
"""
::
unary-op-base is unary-op rhs
"""
[docs]
def tostr(self):
return "%s %s" % tuple(self.items)
@staticmethod
[docs]
def match(op_pattern, rhs_cls, string, exclude_op_pattern=None):
m = op_pattern.match(string)
if not m:
return
rhs = string[m.end() :].lstrip()
if not rhs:
return
op = string[: m.end()].rstrip().upper()
if exclude_op_pattern is not None:
if exclude_op_pattern.match(op):
return
return op, rhs_cls(rhs)
[docs]
class BinaryOpBase(Base):
"""
::
binary-op-base is lhs op rhs
Splits the input text into text to the left of the matched
operator and text to the right of the matched operator and tries
to match the lhs text with the supplied lhs class rule and the rhs
text with the supplied rhs class rule.
"""
@staticmethod
[docs]
def match(
lhs_cls, op_pattern, rhs_cls, string, right=True, exclude_op_pattern=None
):
"""Matches the binary-op-base rule.
If the operator defined by argument 'op_pattern' is found in
the string provided in argument 'string' then the text to the
left-hand-side of the operator is matched with the class rule
provided in the 'lhs_cls' argument and the text to the
right-hand-side of the operator is matched with the class rule
provided in the 'rhs_cls' argument.
If the optional 'right' argument is set to true (the default)
then, in the case where the pattern matches multiple times in
the input string, the right-most match will be chosen. If the
'right' argument is set to false then the left-most match will
be chosen.
if a pattern is provided to the optional 'exclude_op_pattern'
argument then there will be no match if the pattern matched by
the 'op_pattern' argument also matches this pattern. The
default (None) does nothing.
:param lhs_cls: an fparser2 object representing the rule that \
should be matched to the lhs text.
:type lhs_cls: subclass of :py:class:`fparser.two.utils.Base`
:param op_pattern: the pattern to match.
:type op_pattern: `str` or \
:py:class:`fparser.two.pattern_tools.Pattern`
:param rhs_cls: an fparser2 object representing the rule that \
should be matched to the rhs text.
:type rhs_cls: subclass of :py:class:`fparser.two.utils.Base`
:param str string: the string to match with the pattern and \
lhs and rhs rules.
:param bool right: in the case where there are multiple \
matches to the pattern in the string this optional \
argument specifies whether the righmost pattern match \
should be chosen (True, the default) or whether the \
leftmost pattern should be chosen (False).
:param exclude_op_pattern: optional argument which specifies a \
particular subpattern to exclude from the match. Defaults \
to None which means there is no subpattern.
:type exclude_op_pattern: :py:class:`fparser.two.pattern_tools.Pattern`
:returns: a tuple containing the matched lhs, the operator and \
the matched rhs of the input string or None if there is \
no match.
:rtype: (:py:class:`fparser.two.utils.Base`, str, \
:py:class:`fparser.two.utils.Base`) or NoneType
"""
line, repmap = string_replace_map(string)
if isinstance(op_pattern, str):
if right:
text_split = line.rsplit(op_pattern, 1)
else:
text_split = line.split(op_pattern, 1)
if len(text_split) != 2:
return None
lhs, rhs = text_split[0].rstrip(), text_split[1].lstrip()
oper = op_pattern
else:
if right:
text_split = op_pattern.rsplit(line)
else:
text_split = op_pattern.lsplit(line)
if not text_split or len(text_split) != 3:
return None
lhs, oper, rhs = text_split
lhs = lhs.rstrip()
rhs = rhs.lstrip()
oper = oper.upper()
if not lhs or not rhs:
return None
if exclude_op_pattern and exclude_op_pattern.match(oper):
return None
# Matching the shorter text first can be much more efficient
# for complex expressions.
if right:
# The split is closest to the right so try to match the
# RHS first.
rhs_obj = rhs_cls(repmap(rhs))
lhs_obj = lhs_cls(repmap(lhs))
else:
# The split is closest to the left so try to match the LHS
# first.
lhs_obj = lhs_cls(repmap(lhs))
rhs_obj = rhs_cls(repmap(rhs))
return (lhs_obj, oper.replace(" ", ""), rhs_obj)
[docs]
def tostr(self):
"""Return the string representation of this object. Uses join() which
is efficient and can make a big performance difference for
complex expressions.
:returns: the string representation of this object.
:rtype: str
"""
return " ".join([str(self.items[0]), str(self.items[1]), str(self.items[2])])
[docs]
class SeparatorBase(Base):
"""
::
separator-base is [ lhs ] : [ rhs ]
"""
@staticmethod
[docs]
def match(lhs_cls, rhs_cls, string, require_lhs=False, require_rhs=False):
line, repmap = string_replace_map(string)
if ":" not in line:
return
lhs, rhs = line.split(":", 1)
lhs = lhs.rstrip()
rhs = rhs.lstrip()
lhs_obj, rhs_obj = None, None
if lhs:
if lhs_cls is None:
return
lhs_obj = lhs_cls(repmap(lhs))
elif require_lhs:
return
if rhs:
if rhs_cls is None:
return
rhs_obj = rhs_cls(repmap(rhs))
elif require_rhs:
return
return lhs_obj, rhs_obj
[docs]
def tostr(self):
s = ""
if self.items[0] is not None:
s += "%s :" % (self.items[0])
else:
s += ":"
if self.items[1] is not None:
s += " %s" % (self.items[1])
return s
[docs]
class KeywordValueBase(Base):
"""
::
keyword-value-base is [ lhs = ] rhs
where::
R215 keyword is name.
"""
@staticmethod
[docs]
def match(lhs_cls, rhs_cls, string, require_lhs=True, upper_lhs=False):
"""
Attempts to match the supplied `string` with `lhs_cls` = `rhs_cls`.
If `lhs_cls` is a str then it is compared with the content to the
left of the first '=' character in `string`. If that content is a
valid Fortran name but does *not* match `lhs_cls` then the match
fails, irrespective of the setting of `require_lhs`.
:param lhs_cls: list, tuple or single value of classes to attempt to \
match LHS against (in order), or string containing \
keyword to match.
:type lhs_cls: names of classes deriving from `:py:class:Base` or str
:param rhs_cls: name of class to match RHS against.
:type rhs_cls: name of a class deriving from `:py:class:Base`
:param str string: text to be matched.
:param bool require_lhs: whether the expression to be matched must \
contain a LHS that is assigned to.
:param bool upper_lhs: whether or not to convert the LHS of the \
matched expression to upper case.
:return: instances of the classes representing quantities on the LHS \
and RHS (LHS is optional) or None if no match is found.
:rtype: 2-tuple of objects or NoneType
"""
if require_lhs and "=" not in string:
return None
if isinstance(lhs_cls, (list, tuple)):
for cls in lhs_cls:
obj = KeywordValueBase.match(
cls, rhs_cls, string, require_lhs=require_lhs, upper_lhs=upper_lhs
)
if obj:
return obj
return obj
# We can't just blindly check whether 'string' contains an '='
# character as it could itself hold a string constant containing
# an '=', e.g. FMT='("Hello = False")'.
# Therefore we only split on the left-most '=' character
pieces = string.split("=", 1)
lhs = None
if len(pieces) == 2:
# It does contain at least one '='. Proceed to attempt to match
# the content on the LHS of it.
lhs = pieces[0].strip()
if isinstance(lhs_cls, str):
# lhs_cls is a keyword
if upper_lhs:
lhs = lhs.upper()
if lhs != lhs_cls:
# The content to the left of the '=' does not match the
# supplied keyword
lhs = None
else:
lhs = lhs_cls(lhs)
if not lhs:
# We haven't matched the LHS and therefore proceed to treat the
# whole string as a RHS if the LHS is not strictly required.
if require_lhs:
return None
rhs = string.strip()
else:
rhs = pieces[-1].strip()
if rhs:
rhs = rhs_cls(rhs)
if not rhs:
return None
return lhs, rhs
[docs]
def tostr(self):
if self.items[0] is None:
return str(self.items[1])
return "%s = %s" % tuple(self.items)
[docs]
class BracketBase(Base):
"""
bracket-base is left-bracket something right-bracket.
This class is able to cope with nested brackets as long as they
are correctly nested. Brackets in strings are ignored.
The 'something' can be specified as being optional.
"""
@staticmethod
[docs]
def match(brackets, cls, string, require_cls=True):
"""A generic match method for all types of bracketed
expressions.
:param str brackets: the format of the left and right brackets \
provided as a string, for example '()'
:param cls: the class to match the content within the brackets \
:type cls: subclass of :py:class:`fparser.two.utils.Base`
:param str string: the content to match
:param bool require_cls: whether the class and associated \
content is mandatory (True) or optional (False). The default \
is True.
:return: None if there is no match, otherwise a tuple with the \
first and third entries being strings containing the left and \
right brackets respectively and the second entry being either \
None or an instance of the class provided as the second \
argument (cls).
:rtype: 'NoneType', ( `str`, `NoneType`, `str`) or ( `str`, \
`cls`, `str` )
"""
if not cls and require_cls:
return None
if not string:
return None
string_strip = string.strip()
if not brackets:
return None
brackets_nospc = brackets.replace(" ", "")
if not brackets_nospc:
return None
if len(brackets_nospc) % 2 == 1:
# LHS and RHS bracketing must be the same size
return None
bracket_len = len(brackets_nospc) // 2
left = brackets_nospc[:bracket_len]
right = brackets_nospc[-bracket_len:]
if len(string_strip) < bracket_len * 2:
return None
if not (string_strip.startswith(left) and string_strip.endswith(right)):
return None
# Check whether or not there's anything between the open
# and close brackets
line = string_strip[bracket_len:-bracket_len].lstrip()
if (not line and cls and require_cls) or (line and not cls):
return None
if not line and (not cls or not require_cls):
return left, None, right
return left, cls(line), right
[docs]
def tostr(self):
"""
:raises InternalError: if the internal items list variable is \
not the expected size.
:raises InternalError: if the first element of the internal \
items list is None or is an empty string.
"""
if len(self.items) != 3:
raise InternalError(
"Class BracketBase method tostr() has '{0}' items, "
"but expecting 3.".format(len(self.items))
)
if not self.items[0]:
raise InternalError(
"Class BracketBase method tostr(). 'Items' entry 0 "
"should be a string containing the left hand bracket "
"but it is empty or None"
)
if not self.items[2]:
raise InternalError(
"Class BracketBase method tostr(). 'Items' entry 2 "
"should be a string containing the right hand bracket "
"but it is empty or None"
)
if self.items[1] is None:
return "{0}{1}".format(self.items[0], self.items[2])
return "{0}{1}{2}".format(self.items[0], self.items[1], self.items[2])
[docs]
class NumberBase(Base):
"""
::
number-base is number [ _ kind-param ]
"""
@staticmethod
[docs]
def match(number_pattern, string):
m = number_pattern.match(string.replace(" ", ""))
if m is None:
return
d = m.groupdict()
return d["value"].upper(), d.get("kind_param")
[docs]
def tostr(self):
if self.items[1] is None:
return str(self.items[0])
return "%s_%s" % tuple(self.items)
[docs]
def _cmpkey(self):
"""Provides a key of objects to be used for comparing."""
return self.items[0]
[docs]
class CallBase(Base):
"""
::
call-base is lhs ( [ rhs ] )
"""
@staticmethod
[docs]
def match(lhs_cls, rhs_cls, string, upper_lhs=False, require_rhs=False):
"""
:param lhs_cls: the class to match with the lhs.
:type lhs_cls: str | class
:param rhs_cls: the class to match with the rhs.
:type rhs_cls: str | class
:param str string: the string to attempt to match.
:param bool upper_lhs: whether or not to convert the lhs to uppercase \
before attempting the match.
:param bool require_rhs: whether the rhs (the part within parentheses) \
must be present.
:returns: a tuple containing the lhs and rhs matches or None if there is \
no match.
:rtype: Optional[Tuple[:py:class:`fparser.two.utils.Base`, \
Optional[:py:class:`fparser.two.utils.Base`]]]
"""
if not string.rstrip().endswith(")"):
return None
line, repmap = string_replace_map(string)
open_idx = line.rfind("(")
if open_idx == -1:
return None
lhs = line[:open_idx].rstrip()
if not lhs:
return
close_idx = line.rfind(")")
rhs = line[open_idx + 1 : close_idx].strip()
lhs = repmap(lhs)
if upper_lhs:
lhs = lhs.upper()
rhs = repmap(rhs)
if isinstance(lhs_cls, str):
if lhs_cls != lhs:
return None
else:
lhs = lhs_cls(lhs)
if rhs:
if isinstance(rhs_cls, str):
if rhs_cls != rhs:
return None
else:
rhs = rhs_cls(rhs)
return lhs, rhs
if require_rhs:
return None
return lhs, None
[docs]
def tostr(self):
if self.items[1] is None:
return "%s()" % (self.items[0])
return "%s(%s)" % (self.items[0], self.items[1])
[docs]
class CALLBase(CallBase):
"""
::
CALL-base is LHS ( [ rhs ] )
"""
@staticmethod
[docs]
def match(lhs_cls, rhs_cls, string, require_rhs=False):
return CallBase.match(
lhs_cls, rhs_cls, string, upper_lhs=True, require_rhs=require_rhs
)
[docs]
class StringBase(Base):
"""
::
string-base is xyz
"""
@staticmethod
[docs]
def match(pattern, string):
if isinstance(pattern, (list, tuple)):
for p in pattern:
obj = StringBase.match(p, string)
if obj is not None:
return obj
return
if isinstance(pattern, str):
if len(pattern) == len(string) and pattern == string:
return (string,)
return
if pattern.match(string):
return (string,)
return None
[docs]
def init(self, string):
self.string = string
[docs]
def tostr(self):
return str(self.string)
[docs]
def torepr(self):
return "%s(%r)" % (self.__class__.__name__, self.string)
[docs]
def _cmpkey(self):
"""Provides a key of objects to be used for comparing."""
return self.string
[docs]
class STRINGBase(StringBase):
"""STRINGBase matches an upper case version of the input string with
another a pattern (typically taken from pattern_tools.py) and
returns the string in upper case if there is a match.
"""
@staticmethod
[docs]
def match(my_pattern, string):
"""Matches an input string with a specified pattern. Casts the string
to upper case before performing a match and, if there is a
match, returns the string in upper case.
The pattern can be a regular expression, a string, a list or a
tuple. If the input pattern is a regular expression or a
string, a direct equivalence is performed. If the input pattern is a
list or a tuple, then all of the contents of the list
or tuple are searched for a match (by recursing). The list or tuple may
contain regular expressions, strings, lists or tuples. This
functionality can be used to recurse down a tree of lists and
or tuples until regular expressions or strings are found (at
the leaves of the tree) on which to match. The patterns used
to match in fparser can be found in patterns_tools.py. These
make use of the pattern class, whose match method behaves like
a regular expression. For example:
from fparser.two import pattern_tools
pattern = pattern_tools.intrinsic_type_name
result = STRINGBase.match(pattern, "logical")
:param pattern: the pattern to match
:type pattern: `list`, `tuple`, `str` or an `re` expression
:param str string: the string to match with the pattern
:return: None if there is no match, or a tuple containing the \
matched string in upper case.
:rtype: `NoneType` or ( `str` )
"""
if string is None:
return None
if not isinstance(string, str):
raise InternalError(
f"Supplied string should be of type str, but found {type(string)}"
)
if isinstance(my_pattern, (list, tuple)):
for child in my_pattern:
result = STRINGBase.match(child, string)
if result:
return result
return None
string_upper = string.upper()
if isinstance(my_pattern, str):
if len(my_pattern) == len(string) and my_pattern == string_upper:
return (string_upper,)
return None
try:
if my_pattern.match(string_upper):
return (string_upper,)
except AttributeError:
raise InternalError(
f"Supplied pattern should be a list, tuple, str or regular "
f"expression but found {type(my_pattern)}"
)
return None
[docs]
class StmtBase(Base):
"""
::
[ [ label ] [ construct-name : ] ] stmt
Attributes::
item : readfortran.Line
"""
[docs]
def tofortran(self, tab="", isfix=None):
label = None
name = None
if self.item is not None:
label = self.item.label
name = self.item.name
if isfix:
c = " "
else:
c = ""
if label:
t = c + str(label)
if isfix:
while len(t) < 6:
t += " "
else:
tab = tab[len(t) :] or " "
else:
# BUG allow for fixed format here
t = ""
if name:
return t + tab + name + ":" + str(self)
return t + tab + str(self)
[docs]
def get_end_label(self):
return self.item.label
[docs]
class EndStmtBase(StmtBase):
"""
::
end-stmt-base = END [ stmt [ stmt-name] ]
"""
@staticmethod
[docs]
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":
# 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(" ", ""):
# Not the correct type of 'END ...' statement.
return None
line = line[len(stmt_type) :].lstrip()
else:
if require_stmt_type:
# 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:
# 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
[docs]
def init(self, stmt_type, stmt_name):
"""
Initialise this EndStmtBase object.
:param str stmt_type: the type of statement, e.g. 'PROGRAM'.
:param stmt_name: the name associated with the statement or None.
:type stmt_name: :py:class:`fparser.two.Fortran2003.Name`
"""
self.items = [stmt_type, stmt_name]
[docs]
def get_name(self):
return self.items[1]
[docs]
def get_type(self):
return self.items[0]
[docs]
def tostr(self):
if self.items[1] is not None:
return "END %s %s" % tuple(self.items)
if self.items[0] is not None:
return "END %s" % (self.items[0])
return "END"
[docs]
def torepr(self):
return "%s(%r, %r)" % (
self.__class__.__name__,
self.get_type(),
self.get_name(),
)
[docs]
def get_end_name(self):
name = self.items[1]
if name is not None:
return name.string
[docs]
def isalnum(c):
return c.isalnum() or c == "_"
[docs]
class WORDClsBase(Base):
"""Base class to support situations where there is a keyword which is
optionally followed by further text, potentially separated by
a double colon. For example::
'program fred', or 'import :: a,b'
WORD-cls is WORD [ [ :: ] cls ]
"""
@staticmethod
[docs]
def match(keyword, cls, string, colons=False, require_cls=False):
"""Checks whether the content in string matches the expected
WORDClsBase format with 'keyword' providing the keyword, 'cls'
providing the following text, 'colons' specifying whether an
optional double colon is allowed as a separator between the keyword
and cls and 'require_cls' specifying whether cls must have
content or not.
Note, if the optional double colon is allowed and exists in the string
then 1) cls must also have content i.e. it implies
`require_cls=True` and 2) white space is not required between
the keyword and the double colon and the double colon and cls.
The simplest form of keyword pattern is a string. However this
method can also match more complex patterns as specified by
the Pattern class in pattern_tools.py. As patterns can be
built from combinations of other patterns (again see
pattern_tool.py) this method also supports a hierarchy of
lists and/or tuples of patterns.
:param keyword: the pattern of the WORD to match. This can be \
a Pattern, string, list or tuple, with a list or tuple \
containing one or more Pattern, string, list or tuple.
:type keyword: :py:class:`fparser.two.pattern_tools.Pattern`, \
str, tuple of str/Pattern/tuple/list or list of \
str/Pattern/tuple/list
:param cls: the class to match.
:type cls: a subclass of :py:class:`fparser.two.utils.Base`
:param str string: Text that we are trying to match.
:param bool colons: whether a double colon is allowed as an optional \
separator between between WORD and cls.
:param bool require_cls: whether content for cls is required \
or not.
:returns: None if there is no match or, if there is a match, a \
2-tuple containing a string matching the 'WORD' and an \
instance of 'cls' (or None if an instance of cls is not \
required and not provided).
:rtype: Optional[Tupe[Str, Optional[Cls]]]
"""
if isinstance(keyword, (tuple, list)):
for child in keyword:
try:
obj = WORDClsBase.match(
child, cls, string, colons=colons, require_cls=require_cls
)
except NoMatchError:
obj = None
if obj is not None:
return obj
return None
if isinstance(keyword, str):
line = string.lstrip()
if line[: len(keyword)].upper() != keyword.upper():
return None
line = line[len(keyword) :]
pattern_value = keyword
else:
my_match = keyword.match(string)
if my_match is None:
return None
line = string[len(my_match.group()) :]
pattern_value = keyword.value
if not line:
if require_cls:
# no text found but it is required
return None
return pattern_value, None
if isalnum(line[0]):
return None
line = line.lstrip()
has_colons = False
if colons and line.startswith("::"):
has_colons = True
line = line[2:].lstrip()
if not line:
if has_colons or require_cls:
# colons without following content is not allowed.
return None
return pattern_value, None
if cls is None:
return None
return pattern_value, cls(line)
[docs]
def tostr(self):
"""Convert the class into Fortran.
:return: String representation of this class without any \
optional double colon.
:rtype: str
"""
if self.items[1] is None:
return str(self.items[0])
s = str(self.items[1])
if s and s[0] in "(*":
return "%s%s" % (self.items[0], s)
return "%s %s" % (self.items[0], s)
[docs]
def tostr_a(self):
"""Convert the class into Fortran, adding in the double colon.
:return: String representation of this class including an \
optional double colon.
:rtype: str
"""
if self.items[1] is None:
return str(self.items[0])
return "%s :: %s" % (self.items[0], self.items[1])
[docs]
class Type_Declaration_StmtBase(StmtBase):
"""
::
type-declaration-stmt is declaration-type-spec [ [ ,
attr-spec ]... :: ] entity-decl-list
"""
[docs]
use_names = None # derived class must define this list
@staticmethod
[docs]
def match(decl_type_spec_cls, attr_spec_list_cls, entity_decl_list_cls, string):
line, repmap = string_replace_map(string)
i = line.find("::")
if i != -1:
j = line[:i].find(",")
if j != -1:
i = j
else:
if line[:6].upper() == "DOUBLE":
m = re.search(r"\s[a-z_]", line[6:].lstrip(), re.I)
if m is None:
return
i = m.start() + len(line) - len(line[6:].lstrip())
else:
m = re.search(r"\s[a-z_]", line, re.I)
if m is None:
return
i = m.start()
type_spec = decl_type_spec_cls(repmap(line[:i].rstrip()))
if type_spec is None:
return
line = line[i:].lstrip()
if line.startswith(","):
i = line.find("::")
if i == -1:
return
attr_specs = attr_spec_list_cls(repmap(line[1:i].strip()))
if attr_specs is None:
return
line = line[i:]
else:
attr_specs = None
if line.startswith("::"):
line = line[2:].lstrip()
entity_decls = entity_decl_list_cls(repmap(line))
if entity_decls is None:
return
return type_spec, attr_specs, entity_decls
[docs]
def tostr(self):
"""
:returns: the text representation of this node.
:rtype: str
"""
if self.items[1] is None:
return f"{self.items[0]} :: {self.items[2]}"
return f"{self.items[0]}, {self.items[1]} :: {self.items[2]}"
[docs]
def walk(node_list, types=None, indent=0, debug=False):
"""
Walk down the parse tree produced by fparser2. Returns a list of all
nodes with the specified type(s).
:param node_list: node or list of nodes from which to walk.
:type node_list: (list of) :py:class:fparser.two.utils.Base
:param types: type or tuple of types of Node to return. (Default is to \
return all nodes.)
:type types: type or tuple of types
:param int indent: extent to which to indent debug output.
:param bool debug: whether or not to write textual representation of AST \
to stdout.
:returns: a list of nodes
:rtype: `list` of :py:class:`fparser.two.utils.Base`
"""
local_list = []
if not isinstance(node_list, (list, tuple)):
node_list = [node_list]
for child in node_list:
if debug:
if isinstance(child, str):
print(indent * " " + "child type = ", type(child), repr(child))
else:
print(indent * " " + "child type = ", type(child))
if types is None or isinstance(child, types):
local_list.append(child)
# Recurse down
if isinstance(child, Base):
local_list += walk(child.children, types, indent + 1, debug)
elif isinstance(child, tuple):
for component in child:
local_list += walk(component, types, indent + 1, debug)
return local_list
[docs]
def get_child(node, node_type):
"""
Searches for the first, immediate child of the supplied node that is of
the specified type.
:param node: the node whose children will be searched.
:type node: :py:class:`fparser.two.utils.Base`
:param type node_type: the class of child node to search for.
:returns: the first child node of type node_type that is encountered \
or None.
:rtype: :py:class:`fparser.two.utils.Base`
"""
for child in node.children:
if isinstance(child, node_type):
return child
return None