stfc-fparser-d93d18d/0000775000175000017500000000000015156742256014734 5ustar alastairalastairstfc-fparser-d93d18d/pyproject.toml0000664000175000017500000000246115156742256017653 0ustar alastairalastair[build-system] requires = [ "setuptools >= 61", "setuptools_scm[toml] >= 6.2", "setuptools_scm_git_archive", "wheel >= 0.29.0", ] build-backend = "setuptools.build_meta" [project] name = "fparser" authors = [{name = "Pearu Peterson"}, {name = "Rupert Ford"}, {name = "Andrew Porter", email = "andrew.porter@stfc.ac.uk"}] license = {text = "BSD-3-Clause"} description = "Python implementation of a Fortran parser" readme = "README.md" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Natural Language :: English", "Programming Language :: Fortran", "Programming Language :: Python", "Topic :: Scientific/Engineering", "Topic :: Software Development", "Topic :: Utilities", "Operating System :: POSIX", "Operating System :: Unix", "Operating System :: MacOS", ] keywords = ["fortran", "parser"] dynamic = ["version"] requires-python = ">=3.6" dependencies = ["setuptools_scm"] [project.optional-dependencies] doc = ["sphinx", "sphinxcontrib.bibtex", "autoapi", "sphinx-autoapi", "sphinx_rtd_theme"] tests = ["pytest >= 3.3.0"] [project.scripts] fparser2 = "fparser.scripts.fparser2:main" [tool.setuptools_scm] write_to = "src/fparser/_version.py" stfc-fparser-d93d18d/src/0000775000175000017500000000000015156742256015523 5ustar alastairalastairstfc-fparser-d93d18d/src/fparser/0000775000175000017500000000000015156742256017165 5ustar alastairalastairstfc-fparser-d93d18d/src/fparser/common/0000775000175000017500000000000015156742256020455 5ustar alastairalastairstfc-fparser-d93d18d/src/fparser/common/utils.py0000664000175000017500000003036515156742256022176 0ustar alastairalastair# Modified work Copyright (c) 2017-2022 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. """ Various utility functions. Permission to use, modify, and distribute this software is given under the terms of the NumPy License. See http://scipy.org. NO WARRANTY IS EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. Author: Pearu Peterson Created: May 2006 """ __all__ = [ "split_comma", "specs_split_comma", "ParseError", "AnalyzeError", "get_module_file", "parse_bind", "parse_result", "is_name", "parse_array_spec", "CHAR_BIT", "str2stmt", "classes", ] import logging import glob import io import os import re import traceback class ParseError(Exception): pass class AnalyzeError(Exception): pass is_name = re.compile(r"^[a-z_]\w*$", re.I).match name_re = re.compile(r"[a-z_]\w*", re.I).match is_entity_decl = re.compile(r"^[a-z_]\w*", re.I).match is_int_literal_constant = re.compile(r"^\d+(_\w+|)$").match module_file_extensions = [".f", ".f90", ".f95", ".f03", ".f08"] def split_comma(line, item=None, comma=",", keep_empty=False, brackets=None): """Split (an optionally bracketed) comma-separated list into items and return a list containing them. If supplied then brackets must be a list of containing two strings, the first being the opening bracket and the second the closing bracket.""" # we may have blank space so strip the line line = line.strip() if not line: return [] if brackets: if not isinstance(brackets, tuple): raise ParseError("split_comma: brackets must be a tuple") if len(brackets) != 2: raise ParseError( "split_comma: brackets tuple must contain " "just two items but got: {0}", brackets, ) open = brackets[0] close = brackets[1] if not line.startswith(open) or not line.endswith(close): return [] line = line.strip(brackets[0]) line = line.strip(brackets[1]) items = [] if item is None: for s in line.split(comma): s = s.strip() if not s and not keep_empty: continue items.append(s) return items newitem = item.copy(line, True) apply_map = newitem.apply_map for s in newitem.get_line().split(comma): s = apply_map(s).strip() if not s and not keep_empty: continue items.append(s) return items def extract_bracketed_list_items(line, item=None): """Takes any line that contains "xxx (a,b,...) yyy" and returns a list of items corresponding to a, b, ... Anything outside of the parentheses is ignored. Only works for strings containing a single set of parentheses.""" if line.count("(") > 1 or line.count(")") > 1: raise ParseError( "parse_bracketed_list: more than one opening/closing parenthesis " "found in string '{0}'; this is not supported".format(line) ) idx1 = line.find("(") idx2 = line.rfind(")") if idx1 < 0 or idx2 < 0 or idx2 < idx1: raise ParseError( "parse_bracketed_list: failed to find expression within " "parentheses in '{0}'".format(line) ) items = split_comma(line[idx1 : idx2 + 1], item, brackets=("(", ")")) if item: for idx in range(len(items)): itm = item.copy(items[idx]) rlst = [] for rpart in itm.get_line().split(":"): rlst.append(itm.apply_map(rpart.strip())) items[idx] = rlst return items def parse_array_spec(line, item=None): items = [] for spec in split_comma(line, item): items.append(tuple(split_comma(spec, item, comma=":", keep_empty=True))) return items def specs_split_comma(line, item=None, upper=False): specs0 = split_comma(line, item) specs = [] for spec in specs0: i = spec.find("=") if i != -1: kw = spec[:i].strip().upper() v = spec[i + 1 :].strip() specs.append("%s = %s" % (kw, v)) else: if upper: spec = spec.upper() specs.append(spec) return specs def parse_bind(line, item=None): if not line.lower().startswith("bind"): return None, line if item is not None: newitem = item.copy(line, apply_map=True) newline = newitem.get_line() else: newitem = None newline = newline[4:].lstrip() i = newline.find(")") assert i != -1, repr(newline) args = [] for a in specs_split_comma(newline[1:i].strip(), newitem, upper=True): args.append(a) rest = newline[i + 1 :].lstrip() if item is not None: rest = newitem.apply_map(rest) return args, rest def parse_result(line, item=None): if not line.lower().startswith("result"): return None, line line = line[6:].lstrip() i = line.find(")") assert i != -1, repr(line) name = line[1:i].strip() assert is_name(name), repr(name) return name, line[i + 1 :].lstrip() def filter_stmts(content, classes): """Pop and return classes instances from content.""" stmts = [] indices = [] for i in range(len(content)): stmt = content[i] if isinstance(stmt, classes): stmts.append(stmt) indices.append(i) indices.reverse() for i in indices: del content[i] return stmts def get_module_files(directory, _cache={}): if directory in _cache: return _cache[directory] module_line = re.compile(r"(\A|^)module\s+(?P\w+)\s*(!.*|)$", re.I | re.M) d = {} files = [] for ext in module_file_extensions: files += glob.glob(os.path.join(directory, "*" + ext)) for fn in files: f = open(fn, "r") for name in module_line.findall(f.read()): name = name[1] if name in d: print(d[name], "already defines", name) continue d[name] = fn _cache[directory] = d return d def get_module_file(name, directory, _cache={}): fn = _cache.get(name, None) if fn is not None: return fn if name.endswith("_module"): for ext in module_file_extensions: f1 = os.path.join(directory, name[:-7] + ext) if os.path.isfile(f1): _cache[name] = fn return f1 files = [] for ext in module_file_extensions: files += glob.glob(os.path.join(directory, "*" + ext)) for fn in files: if module_in_file(name, fn): _cache[name] = fn return fn return None def module_in_file(name, filename): name = name.lower() pattern = re.compile(r"\s*module\s+(?P[a-z]\w*)", re.I).match encoding = {"encoding": "UTF-8"} f = io.open(filename, "r", **encoding) for line in f: m = pattern(line) if m and m.group("name").lower() == name: f.close() return filename f.close() def str2stmt(string, isfree=True, isstrict=False): """Convert Fortran code to Statement tree.""" from .readfortran import Line, FortranStringReader from .parsefortran import FortranParser reader = FortranStringReader(string, isfree, isstrict) parser = FortranParser(reader) parser.parse() parser.analyze() block = parser.block while len(block.content) == 1: block = block.content[0] return block def show_item_on_failure(func, _exception_depth=[0]): """ Decorator for analyze methods. """ def new_func(self): try: func(self) except AnalyzeError as msg: clsname = self.__class__.__name__ self.error("%s.analyze error: %s" % (clsname, msg)) traceback.print_exc() except ParseError as msg: self.error("parse error: %s" % (msg)) except Exception as msg: _exception_depth[0] += 1 if _exception_depth[0] == 1: self.error("exception triggered here: %s %s" % (Exception, msg)) raise _exception_depth[0] = 0 return new_func _classes_cache = {} class meta_classes(type): """Meta class for ``classes``.""" __abstractmethods__ = False def __getattr__(self, name): # Expose created classes only as attributes to ``classes`` type. cls = _classes_cache.get(name) if cls is None: raise AttributeError("instance does not have attribute %r" % (name)) return cls class classes(type, metaclass=meta_classes): """Make classes available as attributes of this class. To add a class to the attributes list, one must use:: class Name(metaclass=classes): in the definition of the class. In addition, apply the following tasks: * decorate analyze methods with show_item_on_failure """ def __new__(metacls, name, bases, dict): if "analyze" in dict: dict["analyze"] = show_item_on_failure(dict["analyze"]) cls = type.__new__(metacls, name, bases, dict) _classes_cache[name] = cls return cls stfc-fparser-d93d18d/src/fparser/common/__init__.py0000664000175000017500000000000015156742256022554 0ustar alastairalastairstfc-fparser-d93d18d/src/fparser/common/tests/0000775000175000017500000000000015156742256021617 5ustar alastairalastairstfc-fparser-d93d18d/src/fparser/common/tests/test_splitline.py0000664000175000017500000003600515156742256025237 0ustar alastairalastair# Modified work Copyright (c) 2017-2021 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. # Original author: Pearu Peterson # First version created: May 2006 # Modified by J. Henrichs, Bureau of Meteorology """ Test parsing single Fortran lines. """ import pytest from fparser.common.splitline import ( _next_quote, splitparen, splitquote, string_replace_map, StringReplaceDict, ) def test_splitparen(): """Unit tests for splitparen function.""" assert splitparen("abc") == ["abc"] assert splitparen("abc(1)") == ["abc", "(1)"] assert splitparen("abc(1) xyz") == ["abc", "(1)", " xyz"] assert splitparen("a(b) = b(x,y(1)) b((a))") == [ "a", "(b)", " = b", "(x,y(1))", " b", "((a))", ] # pylint: disable=anomalous-backslash-in-string assert splitparen(r"a(b) = b(x,y(1)) b\((a)\)") == [ "a", "(b)", " = b", "(x,y(1))", " b\\(", "(a)", "\\)", ] # pylint: enable=anomalous-backslash-in-string assert splitparen("abc[1]") == ["abc", "[1]"] assert splitparen("abc[1,2,3]") == ["abc", "[1,2,3]"] assert splitparen("a[b] = b[x,y(1)] b((a))") == [ "a", "[b]", " = b", "[x,y(1)]", " b", "((a))", ] # pylint: disable=anomalous-backslash-in-string assert splitparen(r"a[b] = b[x,y(1)] b\((a)\)") == [ "a", "[b]", " = b", "[x,y(1)]", " b\\(", "(a)", "\\)", ] # pylint: enable=anomalous-backslash-in-string assert splitparen('integer a(3) = (/"a", "b", "c"/)') == [ "integer a", "(3)", " = ", '(/"a", "b", "c"/)', ] assert splitparen('character(len=40) :: a(3) = (/"a[),", ",b,[(", "c,][)("/)') == [ "character", "(len=40)", " :: a", "(3)", " = ", '(/"a[),", ",b,[(", "c,][)("/)', ] assert splitparen('integer a(3) = ["a", "b", "c"]') == [ "integer a", "(3)", " = ", '["a", "b", "c"]', ] assert splitparen('character(len=40) :: a(3) = ["a[),", ",b,[(", "c,][)("]') == [ "character", "(len=40)", " :: a", "(3)", " = ", '["a[),", ",b,[(", "c,][)("]', ] # pylint: disable=anomalous-backslash-in-string result = splitparen('a(1),b\\((2,3),c\\\\((1)),c"("') expected = ["a", "(1)", ",b\\(", "(2,3)", ",c\\\\", "((1))", ',c"("'] # pylint: enable=anomalous-backslash-in-string assert result == expected # Useful for debugging: # for i in range(len(EXPECTED)): # print i,l[i],EXPECTED[i],l[i]==EXPECTED[i] def test_next_quote(): """Test the _next_quote() method.""" # By default, both ' and " are considered. assert _next_quote("hello 'andy'") == 6 assert _next_quote('hello "andy"') == 6 assert _next_quote("hello 'andy'", quote_char="'") == 6 assert _next_quote("hello 'andy'", quote_char="'", start=7) == 11 assert _next_quote("hello 'andy'", quote_char='"') == -1 @pytest.mark.parametrize( "input_line, expected_parts, expected_unterm", [ # Simple double quoted string ('PRINT *, "Hello"', ["PRINT *, ", '"Hello"'], None), # Simple single quoted string ("PRINT *, 'Hello'", ["PRINT *, ", "'Hello'"], None), # Multiple quoted strings ( 'PRINT *, "Hello", VAR, "World!"', ["PRINT *, ", '"Hello"', ", VAR, ", '"World!"'], None, ), # Escaped double quotes inside double quoted string ( 'WRITE(*,*) "He said ""Hello"""', ["WRITE(*,*) ", '"He said ""Hello"""'], None, ), # Escaped single quotes inside single quoted string ("WRITE(*,*) 'It''s fine'", ["WRITE(*,*) ", "'It''s fine'"], None), # Both types in one line ("PRINT *, \"A\", B, 'C'", ["PRINT *, ", '"A"', ", B, ", "'C'"], None), # Mixed with adjacent text ('X="foo""bar"', ["X=", '"foo""bar"'], None), # No quoted strings ("DO 10 I = 1, N", ["DO 10 I = 1, N"], None), # Quoted string at start ('"abc" is a string', ['"abc"', " is a string"], None), # Quoted string at end ('label = "xyz"', ["label = ", '"xyz"'], None), # Embedded commas ('DATA STR /"A,B,C"/', ["DATA STR /", '"A,B,C"', "/"], None), # Fortran character kind (should treat as unquoted) ("character(len=5, kind=1) :: foo", ["character(len=5, kind=1) :: foo"], None), # Unterminated double-quoted string at end ('PRINT *, "unterminated', ["PRINT *, ", '"unterminated'], '"'), # Unterminated single-quoted string at end ("PRINT *, 'unterminated", ["PRINT *, ", "'unterminated"], "'"), # Unterminated string with leading whitespace ( 'PRINT *, "still unterminated', ["PRINT *, ", '"still unterminated'], '"', ), # Unterminated string only ('"oops', ['"oops'], '"'), # Unterminated with content before and after ('VAR = "unterminated and more', ["VAR = ", '"unterminated and more'], '"'), # Properly terminated with doubled quotes ( "PRINT *, 'He said, ''Hello!'''", ["PRINT *, ", "'He said, ''Hello!'''"], None, ), ("'value = 1.0d-3'", ["'value = 1.0d-3'"], None), ("a()", ["a()"], None), # Empty string. ( "print *, 'test', '', 'the end'", ["print *, ", "'test'", ", ", "''", ", ", "'the end'"], None, ), # String contains single quote char ("'", ["'"], "'"), ("'\\'", ["'\\'"], None), ], ) def test_splitquote(input_line, expected_parts, expected_unterm): """Tests the splitquote() method.""" parts, unterminated = splitquote(input_line) assert parts == expected_parts, ( f"For input: {input_line!r} got parts: {parts!r} but expected: " f"{expected_parts!r}" ) assert unterminated == expected_unterm, ( f"For input: {input_line!r} got unterminated: {unterminated!r} but " f"expected: {expected_unterm!r}" ) @pytest.mark.parametrize( "input_line, expected_parts, expected_unterm, stopchar, lower", [ ("this is STILL a quote'", ["this is STILL a quote'"], None, "'", True), ("'' STILL a quote'", ["'' STILL a quote'"], None, "'", True), ("'' STILL a', Quote", ["'' STILL a'", ", quote"], None, "'", True), ("'' STILL a', Quote", ["'' STILL a'", ", Quote"], None, "'", False), ("no quotes HERE", ["no quotes here"], None, None, True), ("' no quotes HERE", ["'", " no quotes here"], None, "'", True), # A continued quote without a closing quote. (" no quotes HERE", [" no quotes HERE"], "'", "'", True), # Line ends with a different, opening quotation mark. ("'' STILL a', Quote, \"", ["'' STILL a'", ", Quote, ", '"'], '"', "'", False), # Line ends with a new quotation that itself contains a quotation mark. ( " STILL a', Quote, \"old'", [" STILL a'", ", Quote, ", "\"old'"], '"', "'", False, ), ], ) def test_splitquote_with_stopchar( input_line, expected_parts, expected_unterm, stopchar, lower ): """Tests the splitquote() method when the stopchar argument is provided (i.e. for a continued, quoted line). """ parts, unterminated = splitquote(input_line, stopchar=stopchar, lower=lower) assert parts == expected_parts assert unterminated == expected_unterm @pytest.mark.parametrize( "test_str, result, result_map", [ ("a()", "a()", {}), ("a(b + c)", "a(F2PY_EXPR_TUPLE_1)", {"F2PY_EXPR_TUPLE_1": "b + c"}), ("0.5d0*a", "F2PY_REAL_CONSTANT_1_*a", {"F2PY_REAL_CONSTANT_1_": "0.5d0"}), (".5d0*a", "F2PY_REAL_CONSTANT_1_*a", {"F2PY_REAL_CONSTANT_1_": ".5d0"}), ( "a + 1.0e-10*c", "a + F2PY_REAL_CONSTANT_1_*c", {"F2PY_REAL_CONSTANT_1_": "1.0e-10"}, ), ( "a + 1.0e-10*c + 1.0e-10*d", "a + F2PY_REAL_CONSTANT_1_*c + F2PY_REAL_CONSTANT_1_*d", {"F2PY_REAL_CONSTANT_1_": "1.0e-10"}, ), ( "a + 1.0E-10*c + 1.0e-11*d", "a + F2PY_REAL_CONSTANT_1_*c + " "F2PY_REAL_CONSTANT_2_*d", {"F2PY_REAL_CONSTANT_1_": "1.0E-10", "F2PY_REAL_CONSTANT_2_": "1.0e-11"}, ), ("a1e-3*1e3", "a1e-3*F2PY_REAL_CONSTANT_1_", {"F2PY_REAL_CONSTANT_1_": "1e3"}), ( "3.0 - .32D+3", "3.0 - F2PY_REAL_CONSTANT_1_", {"F2PY_REAL_CONSTANT_1_": ".32D+3"}, ), ( "var=1.0d-3", "var=F2PY_REAL_CONSTANT_1_", {"F2PY_REAL_CONSTANT_1_": "1.0d-3"}, ), (".5e3_wp*a", "F2PY_REAL_CONSTANT_1_*a", {"F2PY_REAL_CONSTANT_1_": ".5e3_wp"}), ( "5.e+3_wp*a", "F2PY_REAL_CONSTANT_1_*a", {"F2PY_REAL_CONSTANT_1_": "5.e+3_wp"}, ), ( "IF(ABS( zds) <= 1.e-20_wp) zds = 1.e-20_wp", "IF(F2PY_EXPR_TUPLE_1) zds = F2PY_REAL_CONSTANT_1_", { "F2PY_EXPR_TUPLE_1": "ABS( zds) <= 1.e-20_wp", "F2PY_REAL_CONSTANT_1_": "1.e-20_wp", }, ), ( "1.e-1+2.e-1+3.e-1+4.e-1+5.e-1+6.e-1+7.e-1+8.e-1+9.e-1+1.1e-1+1.2e-2", "F2PY_REAL_CONSTANT_1_+F2PY_REAL_CONSTANT_2_+F2PY_REAL_CONSTANT_3_+" "F2PY_REAL_CONSTANT_4_+F2PY_REAL_CONSTANT_5_+F2PY_REAL_CONSTANT_6_+" "F2PY_REAL_CONSTANT_7_+F2PY_REAL_CONSTANT_8_+F2PY_REAL_CONSTANT_9_+" "F2PY_REAL_CONSTANT_10_+F2PY_REAL_CONSTANT_11_", { "F2PY_REAL_CONSTANT_1_": "1.e-1", "F2PY_REAL_CONSTANT_2_": "2.e-1", "F2PY_REAL_CONSTANT_3_": "3.e-1", "F2PY_REAL_CONSTANT_4_": "4.e-1", "F2PY_REAL_CONSTANT_5_": "5.e-1", "F2PY_REAL_CONSTANT_6_": "6.e-1", "F2PY_REAL_CONSTANT_7_": "7.e-1", "F2PY_REAL_CONSTANT_8_": "8.e-1", "F2PY_REAL_CONSTANT_9_": "9.e-1", "F2PY_REAL_CONSTANT_10_": "1.1e-1", "F2PY_REAL_CONSTANT_11_": "1.2e-2", }, ), ( "'value = 1.0d-3'", "'_F2PY_STRING_CONSTANT_1_'", {"_F2PY_STRING_CONSTANT_1_": "value = 1.0d-3"}, ), ( "Met(L:L) == '\\' .and. L <= MaxLen", "Met(F2PY_EXPR_TUPLE_1) == '_F2PY_STRING_CONSTANT_1_' .and. L <= MaxLen", {"_F2PY_STRING_CONSTANT_1_": "\\", "F2PY_EXPR_TUPLE_1": "L:L"}, ), ], ) def test_string_replace_map(test_str, result, result_map): """Tests string_replace_map function for various expressions.""" string, string_map = string_replace_map(test_str) assert string == result assert string_map == result_map assert string_map(string) == test_str def test_string_replace_dict(): """Tests for the StringReplaceDict class.""" repmap = StringReplaceDict() assert repmap == {} repmap["F2PY_REAL_CONSTANT_1_"] = "a_value" new_line = repmap("some text with F2PY_REAL_CONSTANT_1_") assert new_line == "some text with a_value" repmap["F2PY_EXPR_TUPLE_1"] = "3 + 5" new_line = repmap("some text with F2PY_EXPR_TUPLE_1") assert new_line == "some text with 3 + 5" repmap["_F2PY_STRING_CONSTANT_1_"] = "blah = 0.01e+4" new_line = repmap("some text with _F2PY_STRING_CONSTANT_1_") assert new_line == "some text with blah = 0.01e+4" repmap["F2PY_EXPR_TUPLE_11"] = "0.5d0*val" new_line = repmap("text with F2PY_EXPR_TUPLE_11 and F2PY_EXPR_TUPLE_1") assert new_line == "text with 0.5d0*val and 3 + 5" stfc-fparser-d93d18d/src/fparser/common/tests/__init__.py0000664000175000017500000000025015156742256023725 0ustar alastairalastair# An "init" file is needed here but not in any other test directories. This is # because the module "logging_utils.py" is imported by "conftest.py" at the # top level. stfc-fparser-d93d18d/src/fparser/common/tests/test_readfortran.py0000664000175000017500000016542015156742256025547 0ustar alastairalastair# -*- coding: utf-8 -*- ############################################################################## # Copyright (c) 2017-2024 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. ############################################################################## # Modified M. Hambley and P. Elson, Met Office # Modified R. W. Ford and A. R. Porter, STFC Daresbury Lab # Modified by J. Henrichs, Bureau of Meteorology ############################################################################## """ Test battery associated with fparser.common.readfortran package. """ import io import os.path import pytest from fparser.common.readfortran import ( FortranFileReader, FortranStringReader, FortranReaderBase, FortranReaderError, Line, extract_label, extract_construct_name, CppDirective, Comment, ) from fparser.common.sourceinfo import FortranFormat @pytest.fixture(scope="module", name="f2py_enabled", params=[True, False]) def f2py_enabled_fixture(request): """Fixture that returns whether or not to enable reader support for f2py directives.""" return request.param def test_empty_line_err(): """Check that we raise the expected error if we try and create an empty Line""" with pytest.raises(FortranReaderError) as err: _ = Line(" ", 1, "", "a_name", None) assert "Got empty line: ' '" in str(err.value) def test_line_map(): """Check the application and reversal of tokenisation of a Line for strings, expressions in parentheses and literal constants with an exponent. """ my_code = ( "program test\n" " real :: var\n" " var = 1.0e-3\n" " var = var * (var + 1.0d-4)\n" " write(*,*) 'var = ', var\n" "end program test\n" ) reader = FortranStringReader(my_code, ignore_comments=True) for _ in range(3): line = reader.next() assert line.get_line() == "var = F2PY_REAL_CONSTANT_1_" assert line.get_line(apply_map=True) == "var = 1.0e-3" line = reader.next() assert line.get_line() == "var = var * (F2PY_EXPR_TUPLE_1)" assert line.get_line(apply_map=True) == "var = var * (var + 1.0d-4)" line = reader.next() assert line.get_line() == ( "write(F2PY_EXPR_TUPLE_1) " "'_F2PY_STRING_CONSTANT_1_', var" ) assert line.get_line(apply_map=True) == "write(*,*) 'var = ', var" def test_fortranreaderbase_logging(log, monkeypatch): """ Tests the logging functionality of the FortranReaderBase class. """ class FailFile: """ A "file-like" object which returns a line of Fortran source followed by raising a StopIteration exception. """ _stuff = ["x=1"] def __next__(self): """ :returns: the next line of source. :rtype: str """ return self._stuff.pop() monkeypatch.setattr( "fparser.common.readfortran.FortranReaderBase.id", lambda x: "foo", raising=False, ) mode = FortranFormat(True, False) unit_under_test = FortranReaderBase(FailFile(), mode, True) assert str(unit_under_test.next()) == "line #1'x=1'" with pytest.raises(StopIteration): unit_under_test.next() assert log.messages["info"] == [] assert log.messages["warning"] == [] assert log.messages["error"] == [] result = log.messages["critical"][0].split("\n")[1] assert result == " 1:x=1 <== while processing line" assert log.messages["critical"][1] == "STOPPED READING" expected = "Traceback\n" assert log.messages["debug"][0][: len(expected)] == expected def test_include_not_found(): """Tests that FortranReaderBase.next() provides the include line when the included file is not found. """ code = "include 'nonexistant.f90'" unit_under_test = FortranStringReader(code) line = unit_under_test.next() assert str(line.line) == code def test_base_next_good_include(log): """ Tests that FortranReaderBase.next() causes a message to be logged when a file is included. """ code = "include 'modfile.f95'\nx=2" include_directories = [os.path.dirname(__file__)] unit_under_test = FortranStringReader( code, include_dirs=include_directories, ignore_comments=False ) line = unit_under_test.next() assert str(line)[:19] == "Comment('! Modified" # First line of inclusion assert log.messages["debug"] == [] assert log.messages["error"] == [] assert log.messages["warning"] == [] assert log.messages["critical"] == [] expected = ( " 1:include 'modfile.f95' " + "<== including file '{path}/modfile.f95'" ) result = log.messages["info"][0].split("\n")[1] assert result == expected.format(path=include_directories[0]) def test_fortranreaderbase_info(log): """ Tests that FortranReaderBase.info() causes a message to be logged. """ unit_under_test = FortranStringReader("x=3") thing = unit_under_test.get_source_item() unit_under_test.info("Mighty Whirlitzer", thing) assert log.messages["debug"] == [] assert log.messages["error"] == [] assert log.messages["warning"] == [] assert log.messages["critical"] == [] expected = " 1:x=3 <== Mighty Whirlitzer" result = log.messages["info"][0].split("\n")[1] assert result == expected def test_fortranreaderbase_error(log): """ Tests that FortranReaderBase.error() causes a message to be logged. """ unit_under_test = FortranStringReader("x=2") thing = unit_under_test.get_source_item() with pytest.raises(SystemExit): unit_under_test.error("Thundering Chalmer", thing) assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["warning"] == [] assert log.messages["critical"] == [] expected = " 1:x=2 <== Thundering Chalmer" result = log.messages["error"][0].split("\n")[1] assert result == expected def test_fortranreaderbase_warning(log): """ Tests that FortranReaderBase.warning() causes a message to be logged. """ unit_under_test = FortranStringReader("x=1") thing = unit_under_test.get_source_item() unit_under_test.warning("Flatulent Hermit", thing) assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] expected = " 1:x=1 <== Flatulent Hermit" result = log.messages["warning"][0].split("\n")[1] assert result == expected def test_base_handle_multilines(log): """ Tests that FortranReaderBase.get_source_item() logs the correct messages when there are quote discrepancies. """ code = 'character(8) :: test = \'foo"""bar' log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(True, True) unit_under_test.set_format(mode) # Force strict free format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] expected = 'multiline prefix contains odd number of "\'" characters' result = log.messages["warning"][0].split("<==")[1].lstrip() assert result == expected code = 'goo """boo\n doo""" soo \'foo' log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(True, True) unit_under_test.set_format(mode) # Force strict free format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] expected = 'following character continuation: "\'", expected None.' result = log.messages["warning"][0].split("<==")[1].lstrip() assert result == expected def test_fortranreaderbase_is_comment_line(): """ Tests for the is_comment_line() utility method. """ reader = FortranStringReader(" ") # Make the reader free-format. reader.set_format(FortranFormat(True, True)) assert not reader.is_comment_line(" ") assert reader.is_comment_line("!") assert reader.is_comment_line("! ") assert not reader.is_comment_line("call a_func()") # Make the reader fixed-format. reader.set_format(FortranFormat(False, True)) assert not reader.is_comment_line(" ") assert reader.is_comment_line("! a comment") assert reader.is_comment_line("c a comment") assert reader.is_comment_line("c ") def test_base_handle_quoted_backslashes(log): """ Test that the reader isn't tripped-up when a string contains a backslash. """ log.reset() code = "If (MetFolder(L:L) == '\\' .and. L <= MaxFileNameLength) Then" reader = FortranStringReader(code) mode = FortranFormat(True, True) reader.set_format(mode) # Force strict free format reader.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] assert log.messages["warning"] == [] def test_base_fixed_nonlabel(log): """ Tests that FortranReaderBase.get_source_item() logs the correct messages when there is an unexpected character in the initial 6 columns. """ # Checks that a bad character in the first column causes an event to be # logged. code = "w integer :: i" log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(False, True) unit_under_test.set_format(mode) # Force fixed format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] result = log.messages["warning"][0].split("<==")[1].lstrip() expected = ( "non-space/digit char 'w' found in column 1 of fixed " + "Fortran code, interpreting line as comment line" ) assert result == expected # Checks a bad character in columns 2-6 for i in range(1, 5): code = " " * i + "w" + " " * (5 - i) + "integer :: i" log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(False, True) unit_under_test.set_format(mode) # Force strict fixed format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] result = log.messages["warning"][0].split("<==")[1].lstrip() expected = ( "non-space/digit char 'w' found in column {col} " + "of fixed Fortran code" ) assert result == expected.format(col=i + 1) # Checks for a bad character, not in the first column, with "sloppy" mode # engaged. code = " w integer :: i" log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(False, False) unit_under_test.set_format(mode) # Force sloppy fixed format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] expected = ( "non-space/digit char 'w' found in column 2 " + "of fixed Fortran code, switching to free format mode" ) result = log.messages["warning"][0].split("<==")[1].lstrip() assert result == expected def test_base_fixed_continuation(log): """ Tests that FortranReaderBase.get_source_item() logs the correct messages when there are quote mismatches across a continuation in fixed format. """ code = ' character(4) :: cheese = "a & !\n & b' log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(False, False) unit_under_test.set_format(mode) # Force sloppy fixed format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] expected = "following character continuation: '\"', expected None." result = log.messages["warning"][0].split("<==")[1].lstrip() assert result == expected code = " x=1 &\n +1 &\n -2" log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(False, False) unit_under_test.set_format(mode) # Force sloppy fixed format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["error"] == [] assert log.messages["critical"] == [] expected = ( "free format line continuation character `&' detected " + "in fix format code\n 2: +1 &\n 3: -2" ) result = log.messages["warning"][0].split("<==")[1].lstrip() assert result == expected def test_base_free_continuation(log): """ Tests that FortranReaderBase.get_source_item() logs the correct messages when there are quote mismatches across a continuation in free format. """ code = 'character(4) :: "boo & que' log.reset() unit_under_test = FortranStringReader(code) mode = FortranFormat(True, False) unit_under_test.set_format(mode) # Force sloppy free format unit_under_test.get_source_item() assert log.messages["debug"] == [] assert log.messages["info"] == [] assert log.messages["warning"] == [] assert log.messages["critical"] == [] expected = "following character continuation: '\"', expected None." result = log.messages["error"][0].split("<==")[1].lstrip() assert result == expected def check_include_works( fortran_filename, fortran_code, include_info, expected, tmpdir, ignore_comments=True ): """Utility function used by a number of tests to check that include files work as expected. :param str fortran_filename: the name of the fortran file that is \ going to be created in the 'tmpdir' directory. :param str fortran_code: the fortran code to put in the fortran \ file specified by 'fortran_filename'. :param include_info: a list of 2-tuples each with an include \ filename as a string followed by include code as a string. :type include_info: List[str] :param str expected: the expected output after parsing the code. :param str tmpdir: the temporary directory in which to create and \ process the Fortran files. :param bool ignore_comments: whether to ignore (skip) comments in \ the Fortran code or not. Defaults to ignore them. """ try: oldpwd = tmpdir.chdir() cwd = str(tmpdir) # Create the program with open(os.path.join(cwd, fortran_filename), "w") as cfile: cfile.write(fortran_code) for include_filename in include_info.keys(): with open(os.path.join(cwd, include_filename), "w") as cfile: cfile.write(include_info[include_filename]) reader = FortranFileReader(fortran_filename, ignore_comments=ignore_comments) for orig_line in expected.split("\n"): new_line = reader.next().line assert new_line == orig_line with pytest.raises(StopIteration): reader.next() finally: oldpwd.chdir() FORTRAN_CODE = ( "program test\n" " ! prog comment 1\n" " print *, 'Hello'\n" " ! prog comment 2\n" "end program" ) EXPECTED_CODE = "program test\n" "print *, 'Hello'\n" "end program" def test_include1(tmpdir): """Test that FortranReaderBase can parse an include file when the original program consists only of an include. """ fortran_filename = "prog.f90" include_filename = "prog.inc" fortran_code = "include '{0}'".format(include_filename) include_code = EXPECTED_CODE include_info = {include_filename: include_code} check_include_works( fortran_filename, fortran_code, include_info, EXPECTED_CODE, tmpdir ) def test_include2(tmpdir): """Test that FortranReaderBase can parse an include file when the original and include files both have multiple lines. """ fortran_filename = "prog.f90" include_filename = "my-include.h" fortran_code = ( "module include_test\n" " include '{0}'\n" "end module include_test".format(include_filename) ) include_code = ( "interface mpi_sizeof\n" "subroutine simple()\n" "end subroutine simple\n" "end interface mpi_sizeof" ) split_code = fortran_code.split("\n") expected = split_code[0] + "\n" + include_code + "\n" + split_code[2] include_info = {include_filename: include_code} check_include_works(fortran_filename, fortran_code, include_info, expected, tmpdir) def test_include3(tmpdir): """Test that FortranReaderBase can parse an include file when the original program is invalid without the include. """ fortran_filename = "prog.f90" include_filename = "prog.inc" fortran_code = "program test\n" "include '{0}'".format(include_filename) include_code = "print *, 'Hello'\n" "end program" include_info = {include_filename: include_code} check_include_works( fortran_filename, fortran_code, include_info, EXPECTED_CODE, tmpdir ) def test_include4(tmpdir): """Test that FortranReaderBase can parse input containing multiple include files. """ fortran_filename = "prog.f90" include_filename1 = "prog.inc1" include_filename2 = "prog.inc2" fortran_code = ( "program test\n" "include '{0}'\n" "include '{1}'".format(include_filename1, include_filename2) ) include_code1 = "print *, 'Hello'\n" include_code2 = "end program" expected = fortran_code.split("\n")[0] + "\n" + include_code1 + include_code2 include_info = {include_filename1: include_code1, include_filename2: include_code2} check_include_works(fortran_filename, fortran_code, include_info, expected, tmpdir) def test_include5(tmpdir): """Test that FortranReaderBase can parse nested include files.""" fortran_filename = "prog.f90" include_filename1 = "prog.inc1" include_filename2 = "prog.inc2" fortran_code = "program test\n" "include '{0}'".format(include_filename1) include_code1 = "print *, 'Hello'\n" "include '{0}'".format(include_filename2) include_code2 = "end program" include_info = {include_filename1: include_code1, include_filename2: include_code2} check_include_works( fortran_filename, fortran_code, include_info, EXPECTED_CODE, tmpdir ) def test_include6(tmpdir, ignore_comments): """Check that FortranReaderBase can parse an include file correctly when it contains comments. Test with and without comments being ignored. """ fortran_filename = "prog.f90" include_filename = "prog.inc" fortran_code = ( "program test\n" " ! prog comment 1\n" " include '{0}' ! this is an include\n" " ! prog comment 2\n" "end program".format(include_filename) ) include_code = "! include comment 1\n" "print *, 'Hello'\n" "! include comment 2" include_info = {include_filename: include_code} if ignore_comments: expected = EXPECTED_CODE else: expected = ( "program test\n" "! prog comment 1\n" "! include comment 1\n" "print *, 'Hello'\n" "! include comment 2\n" "! this is an include\n" "! prog comment 2\n" "end program" ) check_include_works( fortran_filename, fortran_code, include_info, expected, tmpdir, ignore_comments=ignore_comments, ) def test_multi_stmt_line1(): """Check that simple statements separated by ; on a single line are correctly split into multiple lines by FortranReaderBase """ code = "do i=1,10;b=20 ; c=30" reader = FortranStringReader(code) mode = FortranFormat(True, False) reader.set_format(mode) line1 = reader.next() assert isinstance(line1, Line) assert line1.line == "do i=1,10" assert line1.span == (1, 1) assert line1.label is None assert line1.name is None assert line1.reader is reader line2 = reader.next() assert isinstance(line2, Line) assert line2.line == "b=20" assert line2.span is line1.span assert line2.label is None assert line2.name is None assert line2.reader is reader line3 = reader.next() assert isinstance(line3, Line) assert line3.line == "c=30" assert line3.span is line1.span assert line3.label is None assert line3.name is None assert line3.reader is reader def test_multi_stmt_line2(): """Check that format statements separated by ; on a single line are correctly split into multiple lines by FortranReaderBase """ code = "10 format(a); 20 format(b)" reader = FortranStringReader(code) mode = FortranFormat(True, False) reader.set_format(mode) line1 = reader.next() assert isinstance(line1, Line) assert line1.line == "format(a)" assert line1.span == (1, 1) assert line1.label == 10 assert line1.name is None assert line1.reader is reader line2 = reader.next() assert line2.line == "format(b)" assert line2.span is line1.span assert line2.label == 20 assert line2.name is None assert line2.reader is reader def test_multi_stmt_line3(): """Check that named do loops separated by ; on a single line are correctly split into multiple lines by FortranReaderBase """ code = "name:do i=1,10;name2 : do j=1,10" reader = FortranStringReader(code) mode = FortranFormat(True, False) reader.set_format(mode) line1 = reader.next() assert isinstance(line1, Line) assert line1.line == "do i=1,10" assert line1.span == (1, 1) assert line1.label is None assert line1.name == "name" assert line1.reader is reader line2 = reader.next() assert line2.line == "do j=1,10" assert line2.span is line1.span assert line2.label is None assert line2.name == "name2" assert line2.reader is reader def test_get_item(ignore_comments): """Check the get_item() function works as expected. Test with and without comments being ignored. """ if ignore_comments: expected = EXPECTED_CODE else: expected = ( "program test\n" "! prog comment 1\n" "print *, 'Hello'\n" "! prog comment 2\n" "end program" ) reader = FortranStringReader(FORTRAN_CODE, ignore_comments=ignore_comments) for expected_line in expected.split("\n"): output_line = reader.get_item() assert expected_line in output_line.line assert not reader.get_item() def test_put_item(ignore_comments): """Check that when a line is consumed it can be pushed back so it can be consumed again. Test with and without comments being ignored. """ reader = FortranStringReader(FORTRAN_CODE, ignore_comments=ignore_comments) while True: orig_line = reader.get_item() if not orig_line: break reader.put_item(orig_line) fifo_line = reader.get_item() assert fifo_line == orig_line def test_put_item_include(ignore_comments, tmpdir): """Check that when a line that has been included via an include statement is consumed it can be pushed back so it can be consumed again. Test with and without ignoring comments. """ _ = tmpdir.chdir() cwd = str(tmpdir) include_code = """ var1 = 1 var2 = 2 var3 = 3 """ with open(os.path.join(cwd, "my_include.h"), "w") as cfile: cfile.write(include_code) code = """ program my_prog integer :: var1, var2, var3 include 'my_include.h' end program my_prog """ reader = FortranStringReader(code, ignore_comments=ignore_comments) lines = [] while True: lines.append(reader.get_item()) # Try immediately putting the line back and then requesting it again. # This checks that the correct FIFO buffer is being used. reader.put_item(lines[-1]) assert reader.get_item().line == lines[-1].line if "var3 =" in lines[-1].line: # Stop reading while we're still in the INCLUDE file so that we # have a stack of readers when calling `put_item` below. break # Put all the lines back in the same order that we saw them. for line in reversed(lines): reader.put_item(line) # Check that the reader returns all those lines in the same order that # we saw them originally. for line in lines: assert reader.get_item().line == line.line def test_multi_put_item(ignore_comments): """Check that multiple lines can be pushed back and will be returned correctly in the specified order (actually the reverse of the original). Test with and without ignoring comments. """ reader = FortranStringReader(FORTRAN_CODE, ignore_comments=ignore_comments) orig_lines = [] while True: orig_line = reader.get_item() if not orig_line: break # Make sure our original lines are kept in reverse order. orig_lines.insert(0, orig_line) # Put back original lines in reverse order as that is what we # would expect when processing and rolling back. for line in orig_lines: reader.put_item(line) # Lines should now be returned in the correct order (so compare in # reverse order with the original line list) while True: filo_line = reader.get_item() if not filo_line: break assert filo_line == orig_lines.pop(-1) assert not orig_lines # Issue 177: get_item(ignore_comments) - how does ignore_comments affect # processing? # Issue 178: Why is there a next() as well as a get_item()? How do they # (and put_item()) interact? ############################################################################## FULL_FREE_SOURCE = """ !> Unicode comment: ❤ ✓ ☂ ♞ ☯ program test implicit none character, parameter :: nature = 'free format' end program test """ FULL_FREE_EXPECTED = [ "!> Unicode comment: ❤ ✓ ☂ ♞ ☯", "program test", " implicit none", " character, parameter :: nature = 'free format'", "end program test", ] ############################################################################## def test_filename_reader(tmpdir): """ Tests that a Fortran source file can be read given its filename. """ filename = f"{tmpdir}/out.f90" try: with io.open(filename, mode="w", encoding="UTF-8") as source_file: source_file.write(FULL_FREE_SOURCE) unit_under_test = FortranFileReader(filename) expected = FortranFormat(True, False) assert unit_under_test.format == expected for expected in FULL_FREE_EXPECTED: found = unit_under_test.get_single_line(ignore_empty=True) assert found == expected except Exception: os.unlink(filename) raise ############################################################################## def test_file_reader(tmpdir): """ Tests that a Fortran source file can be read given a file object of it. """ filename = f"{tmpdir}/out.f90" try: with io.open(filename, mode="w", encoding="UTF-8") as source_file: source_file.write(FULL_FREE_SOURCE) with io.open(filename, mode="r", encoding="UTF-8") as source_file: unit_under_test = FortranFileReader(source_file) expected = FortranFormat(True, False) assert unit_under_test.format == expected for expected in FULL_FREE_EXPECTED: assert unit_under_test.get_single_line(ignore_empty=True) == expected except Exception: os.unlink(filename) raise def test_none_in_fifo(tmpdir, log): """Check that a None entry in the reader FIFO buffer is handled correctly.""" log.reset() filename = f"{tmpdir}/out.f90" with io.open(filename, mode="w", encoding="UTF-8") as source_file: source_file.write(FULL_FREE_SOURCE) with io.open(filename, mode="r", encoding="UTF-8") as source_file: unit_under_test = FortranFileReader(source_file) while True: try: _ = unit_under_test.next(ignore_comments=False) except StopIteration: break # Erroneously push a None to the FIFO buffer unit_under_test.put_item(None) # Attempt to read the next item with pytest.raises(StopIteration): _ = unit_under_test.next(ignore_comments=False) # Check that nothing has been logged for log_level in ["debug", "info", "warning", "error", "critical"]: assert log.messages[log_level] == [] def test_bad_file_reader(): """ Tests that the file reader can spot when it is given something to read which is neither file nor filename. """ with pytest.raises(ValueError) as ex: _ = FortranFileReader(42) expected = "FortranFileReader is used with a filename or file-like object." assert expected in str(ex.value) ############################################################################## def test_string_reader(): """ Tests that Fortran source can be read from a string. """ unit_under_test = FortranStringReader(FULL_FREE_SOURCE) expected = FortranFormat(True, False) assert unit_under_test.format == expected for expected in FULL_FREE_EXPECTED: assert unit_under_test.get_single_line(ignore_empty=True) == expected @pytest.mark.parametrize("reader_cls", [FortranStringReader, FortranFileReader]) def test_reader_ignore_encoding(reader_cls, tmp_path): """ Tests that the Fortran{String,File}Reader can be configured to take notice of Python-style encoding information. """ source = "! -*- f77 -*-\n" + FULL_FREE_SOURCE if reader_cls is FortranFileReader: sfile = tmp_path / "my_test.f90" sfile.write_text(source) # File location with full path. rinput = str(sfile.absolute()) else: rinput = source reader = reader_cls(rinput) # By default the encoding information is ignored so the format should be # free format, not strict. assert reader.format == FortranFormat(True, False) # Check that explicitly setting ignore_encoding=True gives # the same behaviour. reader1 = reader_cls(rinput, ignore_encoding=True) assert reader1.format == FortranFormat(True, False) # Now test when the reader takes notice of the encoding information. reader2 = reader_cls(rinput, ignore_encoding=False) # Should be fixed format, strict. assert reader2.format == FortranFormat(False, True) def test_inherited_f77(tmpdir): """ A grab bag of functional tests inherited from readfortran.py. """ string_f77 = """c -*- f77 -*- c12346 comment subroutine foo call foo 'bar a 'g abc=2 call you ! hi end '""" expected = [ "Comment('c -*- f77 -*-',(1, 1))", "Comment('c12346 comment',(2, 2))", "line #3'subroutine foo'", "line #4'call foobar'", 'Comment("a \'g",(6, 6))', "line #7'abc=2'", "line #8'call you ! hi'", "line #9'end'", ] # Reading from buffer reader = FortranStringReader( string_f77, ignore_comments=False, ignore_encoding=False ) assert reader.format.mode == "f77", repr(reader.format.mode) stack = expected[:] for item in reader: assert str(item) == stack.pop(0) # Reading from file filename = f"{tmpdir}/out.f" with open(filename, "w") as fortran_file: print(string_f77, file=fortran_file) reader = FortranFileReader(filename, ignore_comments=False, ignore_encoding=False) stack = expected[:] for item in reader: assert str(item) == stack.pop(0) def test_pyf(): """ Tests inherited from implementation. """ string_pyf = """! -*- pyf -*- python module foo interface beginml '''1st line 2nd line end line'''endml='tere!fake comment'!should be a comment a = 2 'charc\"onstant' ''' single line mline '''a='hi!fake comment'!should be a comment a=\\\\\\\\\\'''not a multiline''' !blah='''never ending multiline b=3! hey, fake line continuation:& c=4& !line cont &45 thisis_label_2 : c = 3 xxif_isotropic_2 : if ( string_upper_compare ( o%opt_aniso, 'ISOTROPIC' ) ) then g=3 endif end interface if ( pc_get_lun() .ne. 6) & write ( pc_get_lun(), '( & & /, a, /, " p=", i4, " stopping c_flag=", a, & & /, " print unit=", i8)') & trim(title), pcpsx_i_pel(), trim(c_flag), pc_get_lun() end python module foo ! end of file """ expected = [ "Comment('! -*- pyf -*-',(1, 1))", "line #2'python module foo'", "line #3'interface'", "MultiLine(' beginml '," + "['1st line', ' 2nd line', ' end line']," + "\"endml='tere!fake comment'\",(4, 6))", "Comment('!should be a comment',(6, 6))", "line #7'a = 2'", "MultiLine(' \\'charc\"onstant\\' '," + "[' single line mline ']," + "\"a='hi!fake comment'\",(8, 8))", "Comment('!should be a comment',(8, 8))", "line #9\"a=\\\\\\\\\\\\\\\\\\\\'''not a multiline'''\"", "Comment(\"!blah='''never ending multiline\",(10, 10))", "line #11'b=3'", "Comment('! hey, fake line continuation:&',(11, 11))", "line #12'c=445'", "Comment('!line cont',(12, 12))", "line #14thisis_label_2: 'c = 3'", "line #15xxif_isotropic_2: " + '"if ( string_upper_compare ( o%opt_aniso,' + " 'ISOTROPIC' ) ) then\"", "line #16'g=3'", "line #17'endif'", "line #18'end interface'", "line #19'if ( pc_get_lun() .ne. 6)" + ' write ( pc_get_lun(), \\\'( /, a, /, " p=", i4,' + ' " stopping c_flag=", a, /, " print unit=", i8)\\\')' + " trim(title), pcpsx_i_pel(), trim(c_flag)," + " pc_get_lun()'", "line #25'end python module foo'", "Comment('! end of file',(26, 26))", ] reader = FortranStringReader( string_pyf, ignore_comments=False, ignore_encoding=False ) assert reader.format.mode == "pyf", repr(reader.format.mode) for item in reader: assert str(item) == expected.pop(0) def test_fix90(): """ Tests inherited from implementation. """ string_fix90 = """c -*- fix -*- subroutine foo cComment 1234 a = 3 !inline comment b = 3 ! !4!line cont. with comment symbol &5 ! KDMO write (obj%print_lun, *) ' KDMO : ' write (obj%print_lun, *) ' COORD = ',coord, ' BIN_WID = ', & obj%bin_wid,' VEL_DMO = ', obj%vel_dmo end subroutine foo subroutine & foo end """ expected = [ "Comment('c -*- fix -*-',(1, 1))", "line #2'subroutine foo'", "Comment('cComment',(3, 3))", "line #4 1234 'a = 3'", "Comment('!inline comment',(4, 4))", "line #5'b = 345'", "Comment('!',(6, 6))", "Comment('!line cont. with comment symbol',(7, 7))", "Comment('! KDMO',(9, 9))", "line #10\"write (obj%print_lun, *) ' KDMO : '\"", "line #11\"write (obj%print_lun, *) ' COORD = ',coord," + " ' BIN_WID = ', &\"", "line #12\"obj%bin_wid,' VEL_DMO = ', obj%vel_dmo\"", "line #13'end subroutine foo'", "line #14'subroutine foo'", "Comment('',(15, 15))", "line #17'end'", ] reader = FortranStringReader( string_fix90, ignore_comments=False, ignore_encoding=False ) assert reader.format.mode == "fix", repr(reader.format.mode) for item in reader: assert str(item) == expected.pop(0) def test_f2py_directive_fixf90(f2py_enabled): """Test the handling of the f2py directive in fixed-format f90.""" string_fix90 = """c -*- fix -*- subroutine foo a = 3!f2py.14 ! pi! !f2py a = 0.0 end subroutine foo""" reader = FortranStringReader(string_fix90, ignore_comments=False) reader.set_format(FortranFormat(False, False, f2py_enabled)) expected = ["Comment('c -*- fix -*-',(1, 1))", "line #2'subroutine foo'"] if f2py_enabled: expected.extend( ["line #3'a = 3.14'", "Comment('! pi!',(3, 3))", "line #4'a = 0.0'"] ) else: expected.extend( [ "line #3'a = 3'", "Comment('!f2py.14 ! pi!',(3, 3))", "Comment('!f2py a = 0.0',(4, 4))", ] ) expected.append("line #5'end subroutine foo'") for item in reader: assert str(item) == expected.pop(0) def test_f2py_freef90(f2py_enabled): """Test the handling of f2py directives in free-format f90, in both in-line and full-line comments.""" lines = [ "subroutine foo", " a = 3!f2py.14 ! pi!", "!f2py a = 0.0", "end subroutine foo", ] reader = FortranStringReader("\n".join(lines), ignore_comments=False) reader.set_format(FortranFormat(True, False, f2py_enabled)) expected = ["line #1'subroutine foo'"] if f2py_enabled: expected.extend( ["line #2'a = 3.14'", "Comment('! pi!',(2, 2))", "line #3'a = 0.0'"] ) else: expected.extend( [ "line #2'a = 3'", "Comment('!f2py.14 ! pi!',(2, 2))", "Comment('!f2py a = 0.0',(3, 3))", ] ) expected.append("line #4'end subroutine foo'") for item in reader: assert str(item) == expected.pop(0) @pytest.mark.xfail(reason="Issue #270: f2py directives not working in F77 " "code.") def test_f2py_directive_f77(f2py_enabled): """Test the handling of the f2py directive in fixed-format f77.""" string_f77 = """c -*- f77 -*- subroutine foo cf2py call me ! hey end""" expected = ["Comment('c -*- f77 -*-',(1, 1))", "line #2'subroutine foo'"] if f2py_enabled: expected.append("line #3'call me ! hey'") else: expected.append("Comment('cf2py call me ! hey',(3, 3))") expected.append("line #4'end'") # Reading from buffer reader = FortranStringReader(string_f77, ignore_comments=False) for item in reader: assert str(item) == expected.pop(0) def test_utf_char_in_code(log): """Check that we cope with Fortran code that contains UTF characters. This is not valid Fortran but most compilers cope with it.""" log.reset() fort_file = os.path.join(os.path.dirname(__file__), "utf_in_code.f90") reader = FortranFileReader(fort_file, ignore_comments=True) out_line = reader.get_item() while out_line: out_line = reader.get_item() assert log.messages["critical"] == [] def test_extract_label(): """Test the extract label function in readfortran.py.""" text_input = "no label" label, text_result = extract_label(text_input) assert label is None assert text_result is text_input text_input = " 80stuff" label, text_result = extract_label(text_input) assert label is None assert text_result is text_input text_input = " 80 stuff" label, text_result = extract_label(text_input) assert label == 80 assert text_result == "stuff" def test_extract_construct_name(): """Test the extract construct name function in readfortran.py.""" text_input = "no construct name" name, text_result = extract_construct_name(text_input) assert name is None assert text_result is text_input text_input = "name:stuff" name, text_result = extract_construct_name(text_input) assert name == "name" assert text_result == "stuff" text_input = " name : stuff" name, text_result = extract_construct_name(text_input) assert name == "name" assert text_result == "stuff" @pytest.mark.parametrize( "input_text, ref", [ ('#include "abc"', True), (' #include "abc"', True), (' # include "abc"', True), ("#define ABC 1", True), ("#ifdef ABC", True), ("#if !defined(ABC)", True), ("abc #define", False), ('"#"', False), ("! #", False), ], ) def test_handle_cpp_directive(input_text, ref): """Test the function that detects cpp directives in readfortran.py.""" reader = FortranStringReader(input_text) output_text, result = reader.handle_cpp_directive(input_text) assert result == ref assert output_text is input_text def test_reader_cpp_directives(): """Test that CPP directives are read correctly in readfortran.py.""" input_text = [ "program test", "#define ABC 123", "character :: c = '#'", "#ifdef ABC", "! Some comment that should be ignored", "integer :: a", "#endif", "#if !defined(ABC)", "integer :: b", "#endif", "end program test", ] ref_text = "\n".join(input_text[:4] + input_text[5:]).strip() reader = FortranStringReader("\n".join(input_text)) lines = list(reader) pp_lines = [1, 3, 5, 6, 8] for i, line in enumerate(lines): if i in pp_lines: assert isinstance(line, CppDirective) else: assert isinstance(line, Line) assert "\n".join(item.line for item in lines) == ref_text def test_multiline_cpp_directives(): """Test that multiline CPP directives are read correctly.""" input_text = [ "program test", "#define ABC \\ ", " 123 ", "integer a", "end program test", ] ref_text = "program test\n#define ABC 123\ninteger a\nend program test" reader = FortranStringReader("\n".join(input_text)) lines = list(reader) assert len(lines) == 4 assert isinstance(lines[1], CppDirective) assert lines[1].span == (2, 3) assert "\n".join(item.line for item in lines) == ref_text # Tests for the get_source_item method in class FortranReaderBase : # Comments def test_single_comment(): """Test that a free format single line comment is output as expected""" input_text = "! a comment\n" reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 1 assert isinstance(lines[0], Comment) assert lines[0].span == (1, 1) assert lines[0].line + "\n" == input_text reader = FortranStringReader(input_text, ignore_comments=True) lines = list(reader) assert len(lines) == 0 def test_many_comments(): """Test that a large consecutive number of single line comments can be successfully processed. Earlier versions of the reader used recursion for multiple consecutive free format single line comments which resulted in a recursion error in this case. """ number_of_comments = 1000 input_text = "program hello\n" for index in range(number_of_comments): input_text += "! comment{0}\n".format(index + 1) input_text += "end program hello\n" reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == number_of_comments + 2 for index in range(1, number_of_comments): assert isinstance(lines[index], Comment) assert lines[index].span == (index + 1, index + 1) assert lines[index].line + "\n" == "! comment{0}\n".format(index) reader = FortranStringReader(input_text, ignore_comments=True) lines = list(reader) assert len(lines) == 2 @pytest.mark.parametrize("inline_comment", [' "', " '", " 'andy' '"]) def test_quotes_in_inline_comments(inline_comment): """Test that an in-line comment containing a quotation mark is read successfully.""" input_text = f"""\ character(*) :: a='hello' &!{inline_comment} & b """ reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 2 assert isinstance(lines[1], Comment) assert lines[1].line.endswith(inline_comment) def test_comments_within_continuation(): """Test that comments and multi-line statements are processed correctly. """ input_text = ( " ! Comment1\n" " real :: a &\n" " ! Comment2\n" " ,b\n" " ! Comment3\n" ) reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 4 assert isinstance(lines[0], Comment) assert lines[0].span == (1, 1) assert lines[0].line == "! Comment1" assert lines[1].span == (2, 4) assert lines[1].line == "real :: a ,b" assert isinstance(lines[2], Comment) assert lines[2].span == (3, 3) assert lines[2].line == "! Comment2" assert isinstance(lines[3], Comment) assert lines[3].span == (5, 5) assert lines[3].line == "! Comment3" reader = FortranStringReader(input_text, ignore_comments=True) lines = list(reader) assert len(lines) == 1 assert lines[0].span == (2, 4) assert lines[0].line == "real :: a ,b" # Tests for the get_source_item method in class FortranReaderBase : # Blank lines @pytest.mark.parametrize("input_text", ["\n", " \n"]) def test_single_blank_line(input_text): """Test that a single blank line with or without white space is output as an empty Comment object. """ reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 1 assert isinstance(lines[0], Comment) assert lines[0].span == (1, 1) assert lines[0].line == "" reader = FortranStringReader(input_text, ignore_comments=True) lines = list(reader) assert len(lines) == 0 def test_multiple_blank_lines(): """Test that multiple blank lines with or without white space are output as an empty Comment objects. """ input_text = " \n\nprogram test\n \n\nend program test\n \n\n" reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 8 for index in [0, 1, 3, 4, 6, 7]: assert isinstance(lines[index], Comment) assert lines[index].span == (index + 1, index + 1) assert lines[index].line == "" assert isinstance(lines[2], Line) assert lines[2].span == (3, 3) assert lines[2].line == "program test" assert isinstance(lines[5], Line) assert lines[5].span == (6, 6) assert lines[5].line == "end program test" reader = FortranStringReader(input_text, ignore_comments=True) lines = list(reader) assert len(lines) == 2 assert isinstance(lines[0], Line) assert lines[0].span == (3, 3) assert lines[0].line == "program test" assert isinstance(lines[1], Line) assert lines[1].span == (6, 6) assert lines[1].line == "end program test" def test_blank_lines_within_continuation(): """Test that blank lines and multi-line statements are processed correctly. Note, empty lines within a multi-line statement are removed. """ input_text = " \n real :: a &\n \n\n ,b\n \n real :: c\n" reader = FortranStringReader(input_text, ignore_comments=False) lines = list(reader) assert len(lines) == 4 assert isinstance(lines[0], Comment) assert lines[0].span == (1, 1) assert lines[0].line == "" assert isinstance(lines[1], Line) assert lines[1].span == (2, 5) assert lines[1].line == "real :: a ,b" assert isinstance(lines[2], Comment) assert lines[2].span == (6, 6) assert lines[2].line == "" assert isinstance(lines[3], Line) assert lines[3].span == (7, 7) assert lines[3].line == "real :: c" reader = FortranStringReader(input_text, ignore_comments=True) lines = list(reader) assert len(lines) == 2 assert isinstance(lines[0], Line) assert lines[0].span == (2, 5) assert lines[0].line == "real :: a ,b" assert isinstance(lines[1], Line) assert lines[1].span == (7, 7) assert lines[1].line == "real :: c" def test_conditional_include_omp_conditional_liness_fixed_format_single_line(): """Test handling of conditional OMP sentinels in a single line with source code in fixed format.""" for sentinel in ["!$", "c$", "C$", "*$"]: input_text = f"{sentinel} bla" # 1. By default (not ignoring comments), the line is just a comment: reader = FortranStringReader(input_text, ignore_comments=False) comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == input_text # 2. And if comments are ignored, nothing should be returned and a # StopIteration exception will be raised. reader = FortranStringReader(input_text, ignore_comments=True) with pytest.raises(StopIteration): comment = reader.next() # 3. When omp-sentinels are accepted, we should get a line, # not a comment: reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" # 4. If omp-sentinels are accepted, and comments ignored, # we should still get the line (with the sentinel removed): reader = FortranStringReader( input_text, ignore_comments=True, include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" # 5. Make sure that a real omp directive stays a comment: input_text = f"{sentinel}omp something" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) line = reader.next() # This is not a conditional sentinel, so it must be returned # as a comment line: assert isinstance(line, Comment) assert line.line == input_text # 6. Test some corner cases (all of which are not valid sentinels): for sentinel in ["!!$", "! $", " !$", " ! $"]: input_text = f"{sentinel} bla" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) # Enforce fixed format, otherwise fparser will silently switch # to free format and suddenly interpret comments differently reader.set_format(FortranFormat(False, False)) comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == input_text def test_conditional_include_omp_conditional_liness_free_format_single_line(): """Test handling of conditional OMP sentinels in a single line with source code in free format.""" # 1. By default, a omp sentinel will be returned as a comment input_text = " !$ bla" reader = FortranStringReader(input_text, ignore_comments=False) comment = reader.next() reader.set_format(FortranFormat(True, True)) assert isinstance(comment, Comment) assert comment.comment == input_text.strip() # 2. And if comments are ignored, nothing should be returned and a # StopIteration exception will be raised. reader = FortranStringReader(input_text, ignore_comments=True) reader.set_format(FortranFormat(True, True)) with pytest.raises(StopIteration): comment = reader.next() # 3. When omp-sentinels are accepted, we should get a line, # not a comment: reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) reader.set_format(FortranFormat(True, True)) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" # 4. If omp-sentinels are accepted, and comments ignored, # we should still get the line (with the sentinel removed): reader = FortranStringReader( input_text, ignore_comments=True, include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) assert line.line == "bla" # 5. Make sure that a real omp directive stays a comment: input_text = " !$omp something" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) reader.set_format(FortranFormat(True, True)) line = reader.next() # This is not a conditional sentinel, so it must be returned # as a comment line: assert isinstance(line, Comment) assert line.line == input_text.strip() # 6. Test some corner cases (all of which are not valid sentinels): for sentinel in ["!!$", "! $", " ! $"]: input_text = f"{sentinel} bla" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) reader.set_format(FortranFormat(True, True)) comment = reader.next() assert isinstance(comment, Comment) # Since fparser will remove leading white spaces, we need to # compare with the input text after removing its white spaces: assert comment.comment == input_text.strip() def test_conditional_include_omp_conditional_liness_fixed_format_multiple(): """Test handling of conditional OMP sentinels with continuation lines with source code in fixed format.""" input_text = "!$ bla\n!$ &bla" reader = FortranStringReader(input_text, ignore_comments=True) with pytest.raises(StopIteration): reader.next() reader = FortranStringReader(input_text, ignore_comments=False) # Without handling of sentinels, this should return # two comment lines: comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == "!$ bla" comment = reader.next() assert isinstance(comment, Comment) assert comment.comment == "!$ &bla" # Now enable handling of sentinels, which will result # in returning only one line with both concatenated. input_text = "!$ bla\n!$ &bla" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) assert line.line == "blabla" # Ignoring comments must not change the behaviour: reader = FortranStringReader( input_text, ignore_comments=True, include_omp_conditional_lines=True ) line = reader.next() assert isinstance(line, Line) assert line.line == "blabla" # Add invalid sentinels in continuation lines: input_text = "!$ bla\n! $ &bla" reader = FortranStringReader( input_text, ignore_comments=True, include_omp_conditional_lines=True ) line = reader.next() assert line.line == "bla" # The second line is just a comment line, so it must be ignored: with pytest.raises(StopIteration): reader.next() reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) line = reader.next() assert line.line == "bla" line = reader.next() assert line.line == "! $ &bla" def test_conditional_include_omp_conditional_liness_free_format_multiple(): """Test handling of conditional OMP sentinels with continuation lines with source code in free format.""" # Test with the optional & in the continuation line: # --------------------------------------------------- input_text = "!$ bla &\n!$& bla" reader = FortranStringReader(input_text, ignore_comments=False) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) comment = reader.next() assert comment.comment == "!$ bla &" comment = reader.next() assert comment.comment == "!$& bla" input_text = "!$ bla &\n!$& bla" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() assert line.line == "bla bla" # Test without the optional & in the continuation line: # ----------------------------------------------------- input_text = "!$ bla &\n!$ bla" reader = FortranStringReader(input_text, ignore_comments=False) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) comment = reader.next() assert comment.comment == "!$ bla &" comment = reader.next() assert comment.comment == "!$ bla" reader = FortranStringReader(input_text, ignore_comments=True) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) with pytest.raises(StopIteration): comment = reader.next() input_text = "!$ bla &\n!$ bla" reader = FortranStringReader( input_text, ignore_comments=False, include_omp_conditional_lines=True ) # Make sure to enforce free format reader.set_format(FortranFormat(True, True)) line = reader.next() # 8 spaces in input_text, plus two for replacing the !$ assert line.line == "bla bla" def test_process_directives_option_read_fortran(): """Test handling of the process_directives option. Note that the funcionality tests for this option are in fparser/two/tests/test_comments_and_directives.py""" input_text = "!$omp target\n! comment" reader = FortranStringReader(input_text) assert not reader.process_directives assert reader._ignore_comments reader = FortranStringReader(input_text, process_directives=True) assert reader.process_directives assert not reader._ignore_comments reader = FortranStringReader( input_text, ignore_comments=False, process_directives=True ) assert reader.process_directives assert not reader._ignore_comments stfc-fparser-d93d18d/src/fparser/common/tests/modfile.f950000664000175000017500000000663515156742256023575 0ustar alastairalastair! Modified work Copyright (c) 2017 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. module testmod implicit none integer, parameter :: i = 20 end module testmod stfc-fparser-d93d18d/src/fparser/common/tests/test_sourceinfo.py0000664000175000017500000003123015156742256025403 0ustar alastairalastair# -*- coding: utf-8 -*- ############################################################################## # Copyright (c) 2017-2023 Science and Technology Facilities Council. # # All rights reserved. # # Modifications made as part of the fparser project are distributed # under the following license: # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ############################################################################## # Modified M.Hambley, UK Met Office # Modified by J. Henrichs, Bureau of Meteorology ############################################################################## """ Test battery associated with fparser.sourceinfo package. """ import os import tempfile import pytest from fparser.common.sourceinfo import ( FortranFormat, get_source_info_str, get_source_info, ) ############################################################################## def test_format_constructor_faults(): """ Tests that the constructor correctly rejects attempts to create an object with "None" arguments. """ with pytest.raises(Exception): _unit_under_test = FortranFormat(True, None) with pytest.raises(Exception): _unit_under_test = FortranFormat(None, True) ############################################################################## @pytest.fixture( scope="module", params=[ (False, False, "fix", "Non-strict fixed format"), (False, True, "f77", "Strict fixed format"), (True, False, "free", "Non-strict free format"), (True, True, "pyf", "Strict free format"), ], ) def pretty(request): """ Returns all possible permutations of flags and their corresponding mode and descriptive strings. """ return request.param ############################################################################## def test_fortranformat_constructor(pretty): # pylint: disable=redefined-outer-name """ Tests the constructor correctly sets up the object. """ unit_under_test = FortranFormat(pretty[0], pretty[1]) assert str(unit_under_test) == pretty[3] assert unit_under_test.is_free == pretty[0] assert unit_under_test.is_fixed == (not pretty[0]) assert unit_under_test.is_strict == pretty[1] assert unit_under_test.is_f77 == (not pretty[0] and pretty[1]) assert unit_under_test.is_fix == (not pretty[0] and not pretty[1]) assert unit_under_test.is_pyf == (pretty[0] and pretty[1]) assert unit_under_test.mode == pretty[2] assert not unit_under_test.f2py_enabled @pytest.mark.parametrize( "permutations", [(False, False), (False, True), (True, False), (True, True)] ) def test_fortranformat_equality(permutations, pretty): # pylint: disable=redefined-outer-name """ Tests that the equality operator works as expected. """ expected = (permutations[0] == pretty[0]) and (permutations[1] == pretty[1]) unit_under_test = FortranFormat(permutations[0], permutations[1]) candidate = FortranFormat(pretty[0], pretty[1]) assert (unit_under_test == candidate) == expected ############################################################################## def test_fortranformat_invalid(): """ Tests that the equality operator understands that it can't compare apples and oranges. """ unit_under_test = FortranFormat(True, False) with pytest.raises(NotImplementedError): if unit_under_test == "oranges": raise Exception("That shouldn't have happened") ############################################################################## @pytest.fixture( scope="module", params=[ ("free", True, False), ("f77", False, True), ("fix", False, False), ("pyf", True, True), ], ) def mode(request): """ Returns all possible mode strings and their corresponding flags. """ return request.param ############################################################################## def test_fortranformat_from_mode(mode): # pylint: disable=redefined-outer-name """ Tests that the object is correctly created by the from_mode function. """ unit_under_test = FortranFormat.from_mode(mode[0]) assert unit_under_test.mode == mode[0] assert unit_under_test.is_free == mode[1] assert unit_under_test.is_fixed == (not mode[1]) assert unit_under_test.is_strict == mode[2] assert unit_under_test.is_f77 == (not mode[1] and mode[2]) assert unit_under_test.is_fix == (not mode[1] and not mode[2]) assert unit_under_test.is_pyf == (mode[1] and mode[2]) assert str(unit_under_test.mode) == mode[0] assert not unit_under_test.f2py_enabled def test_format_from_mode_bad(): """ Tests that an exception is thrown for an unrecognised mode string. """ with pytest.raises(NotImplementedError): _unit_under_test = FortranFormat.from_mode("cheese") ############################################################################## # Setting up a pytest fixture in this way is a mechanism for creating # parameterised tests. # # Normally when a test includes a fixture in its argument list the # corresponding function will be called and the result passed in to the test. # # When used like this the test will be called once for each value the fixture # function returns. So in this case any test including "header" in its # argument list will be run once with "header" equal to "! -*- fortran -*-", # then with "header" equal to "! -*- f77 -*-" and so on. # # If a test includes multiple parameter fixtures it will be called for every # permutation thus afforded. # @pytest.fixture( scope="module", params=[ (None, FortranFormat(True, True)), ("! -*- fortran -*-", FortranFormat(False, True)), ("! -*- f77 -*-", FortranFormat(False, True)), ("! -*- f90 -*-", FortranFormat(True, False)), ("! -*- f03 -*-", FortranFormat(True, False)), ("! -*- f08 -*-", FortranFormat(True, False)), ("! -*- fix -*-", FortranFormat(False, False)), ("! -*- pyf -*-", FortranFormat(True, True)), ], name="header", ) def header_fixture(request): """ Returns parameters for header tests. """ return request.param ############################################################################## _FIXED_SOURCE = """ program main end program main """ _FREE_SOURCE = """program main end program main """ _FIXED_WITH_CONTINUE = """ program main implicit none integer :: foo, & bar end program main """ _FIXED_WITH_COMMENTS = """! The program program main c Enforce explicit variable declaration implicit none * variables integer :: foo end program main """ # Tabs are not actually in the Fortran character set but fparser has handled # them in the past even if it shouldn't have. We have to continue handling # them for the time being until we work out why they were supported. # _INITIAL_TAB = "\tprogram main\n" _MIDDLE_TAB = " \tprogram main\n" # Another parameterised test fixture. See "header" above. # @pytest.fixture( scope="module", params=[ (None, FortranFormat(False, False)), (_FIXED_SOURCE, FortranFormat(False, False)), (_FREE_SOURCE, FortranFormat(True, False)), (_FIXED_WITH_CONTINUE, FortranFormat(True, False)), (_FIXED_WITH_COMMENTS, FortranFormat(False, False)), (_INITIAL_TAB, FortranFormat(False, False)), (_MIDDLE_TAB, FortranFormat(True, False)), ], ) def content(request): """ Returns parameters for content tests. """ return request.param ############################################################################## def test_get_source_info_str(header, content): # pylint: disable=redefined-outer-name """ Tests that source format is correctly identified when read from a string. """ full_source = "" if header[0] is not None: full_source += header[0] + "\n" if content[0] is not None: full_source += content[0] source_info = get_source_info_str(full_source, ignore_encoding=False) if header[0]: assert source_info == header[1] else: # No header assert source_info == content[1] ############################################################################## # Another parameterised test fixture. See "header" above. # @pytest.fixture( scope="module", params=[ (".f", None), (".f90", None), (".pyf", FortranFormat(True, True)), (".guff", None), ], ) def extension(request): """ Returns parameters for extension tests. """ return request.param ############################################################################## def test_get_source_info_filename(tmpdir, extension, header, content): # pylint: disable=redefined-outer-name """ Tests that source format is correctly identified when read from a file. """ full_source = "" if header[0] is not None: full_source += header[0] + "\n" if content[0] is not None: full_source += content[0] filename = f"{tmpdir}/out{extension[0]}" with open(filename, "w") as source_file: print(full_source, file=source_file) try: source_info = get_source_info(filename, ignore_encoding=False) if extension[1] is not None: assert source_info == extension[1] elif header[0] is not None: assert source_info == header[1] else: # No header assert source_info == content[1] except Exception as ex: os.remove(filename) raise ex ############################################################################## def test_get_source_info_file(extension, header, content): # pylint: disable=redefined-outer-name """ Tests that source format is correctly identified when read from a file. """ full_source = "" if header[0] is not None: full_source += header[0] + "\n" if content[0] is not None: full_source += content[0] with tempfile.TemporaryFile(mode="w+", suffix=extension[0]) as source_file: print(full_source, file=source_file) source_file.seek(0) source_info = get_source_info(source_file, ignore_encoding=False) if header[0] is not None: assert source_info == header[1] else: # No header assert source_info == content[1] def test_get_source_info_utf8(): """ Tests that Fortran code containing a unicode character can be read by the get_source_info method. """ encoding = dict(encoding="UTF-8") with tempfile.NamedTemporaryFile(mode="w", **encoding) as tmp_file: content = ' ! A fortran comment with a unicode character "\u2014"' tmp_file.write(content) tmp_file.flush() source_info = get_source_info(tmp_file.name) assert source_info is not None ############################################################################## def test_get_source_info_wrong(): """ Tests that get_source_info throws an exception if passed the wrong type of argument. """ with pytest.raises(ValueError): _source_info = get_source_info(42) # Obviously wrong with pytest.raises(ValueError): _source_info = get_source_info(["one"]) # Less obviously wrong ############################################################################## stfc-fparser-d93d18d/src/fparser/common/tests/conftest.py0000664000175000017500000000371215156742256024021 0ustar alastairalastair# Copyright (c) 2019 Science and Technology Facilities Council # All rights reserved. # Modifications made as part of the fparser project are distributed # under the following license: # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Module which provides pytest fixtures for use by files in this directory """ import pytest @pytest.fixture(scope="module", params=[True, False]) def ignore_comments(request): """Fixture for testing with and without the parser ignoring comments. Returns the content of params in turn. """ return request.param stfc-fparser-d93d18d/src/fparser/common/tests/test_base_classes.py0000664000175000017500000001537015156742256025665 0ustar alastairalastair# -*- coding: utf-8 -*- ############################################################################## # Copyright (c) 2017 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. ############################################################################## # Modified M.Hambley, UK Met Office ############################################################################## """ Test battery associated with fparser.common.base_classes package. """ import re import pytest import fparser.common.base_classes import fparser.common.readfortran import fparser.common.sourceinfo import fparser.common.utils from fparser import api def test_statement_logging(log, monkeypatch): """ Tests the Statement class' logging methods. """ class DummyParser: """ Null parser harness. """ def __init__(self, reader): self.reader = reader reader = fparser.common.readfortran.FortranStringReader("dummy = 1") parser = DummyParser(reader) monkeypatch.setattr( fparser.common.base_classes.Statement, "process_item", lambda x: None, raising=False, ) unit_under_test = fparser.common.base_classes.Statement(parser, None) unit_under_test.error("Scary biscuits") assert log.messages == { "critical": [], "debug": [], "error": ["Scary biscuits"], "info": [], "warning": [], } log.reset() unit_under_test.warning("Trepidacious Cetations") assert log.messages == { "critical": [], "debug": [], "error": [], "info": [], "warning": ["Trepidacious Cetations"], } log.reset() unit_under_test.info("Hilarious Ontologies") assert log.messages == { "critical": [], "debug": [], "error": [], "info": ["Hilarious Ontologies"], "warning": [], } def test_log_comment_mix(log): """ Tests that unexpected Fortran 90 comment in fixed format source is logged. """ class EndDummy(fparser.common.base_classes.EndStatement): """ Dummy EndStatement. """ match = re.compile(r"\s*end(\s*thing\s*\w*|)\s*\Z", re.I).match class BeginHarness(fparser.common.base_classes.BeginStatement): """ Dummy BeginStatement. """ end_stmt_cls = EndDummy classes = [] match = re.compile(r"\s*thing\s+(\w*)\s*\Z", re.I).match def get_classes(self): """ Returns an empty list of contained statements. """ return [] code = " x=1 ! Cheese" parent = fparser.common.readfortran.FortranStringReader(code, ignore_comments=False) parent.set_format(fparser.common.sourceinfo.FortranFormat(False, True)) item = fparser.common.readfortran.Line(code, (1, 1), None, None, parent) with pytest.raises(fparser.common.utils.AnalyzeError): __ = BeginHarness(parent, item) expected = ( " 1: x=1 ! Cheese <== " + 'no parse pattern found for "x=1 ! cheese" ' + "in 'BeginHarness' block, " + "trying to remove inline comment (not in Fortran 77)." ) result = log.messages["warning"][0].split("\n")[1] assert result == expected def test_log_unexpected(log): """ Tests that an unexpected thing between begin and end statements logs an event. """ class EndThing(fparser.common.base_classes.EndStatement): """ Dummy EndStatement class. """ isvalid = True match = re.compile(r"\s*end(\s+thing(\s+\w+)?)?\s*$", re.I).match class BeginThing(fparser.common.base_classes.BeginStatement): """ Dummy BeginStatement class. """ end_stmt_cls = EndThing classes = [] match = re.compile(r"\s*thing\s+(\w+)?\s*$", re.I).match def get_classes(self): """ Returns an empty list of contained classes. """ return [] code = [" jumper", " end thing"] parent = fparser.common.readfortran.FortranStringReader("\n".join(code)) parent.set_format(fparser.common.sourceinfo.FortranFormat(False, True)) item = fparser.common.readfortran.Line(code[0], (1, 1), None, None, parent) with pytest.raises(fparser.common.utils.AnalyzeError): __ = BeginThing(parent, item) expected = ( ' 1: jumper <== no parse pattern found for "jumper" ' "in 'BeginThing' block." ) result = log.messages["warning"][0].split("\n")[1] assert result == expected def test_space_after_enddo(): """Make sure that there is no space after an 'END DO' without name, but there is a space if there is a name after 'END DO'. """ # Unnamed loop: source_str = """\ subroutine foo integer i, r do i = 1,100 r = r + 1 end do end subroutine foo """ tree = api.parse(source_str, isfree=True, isstrict=False) assert "END DO " not in tree.tofortran() # Named loop: source_str = """\ subroutine foo integer i, r loop1: do i = 1,100 r = r + 1 end do loop1 end subroutine foo """ tree = api.parse(source_str, isfree=True, isstrict=False) assert "END DO loop1" in tree.tofortran() stfc-fparser-d93d18d/src/fparser/common/tests/test_utils.py0000664000175000017500000001173215156742256024374 0ustar alastairalastair# Copyright (c) 2017-2022 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. """ Test the various utility functions """ import pytest from fparser.common.utils import split_comma, ParseError def test_split_comma(): """Test the split_comma() function""" items = split_comma("hello, goodbye") print(items) assert items[0] == "hello" assert items[1] == "goodbye" # With trailing and leading white space items = split_comma(" hello, goodbye ") print(items) assert items[0] == "hello" assert items[1] == "goodbye" items = split_comma(" ") assert not items def test_split_comma_exceptions(): """Test that we raise the expected exceptions if we don't supply the brackets in the right form""" with pytest.raises(ParseError) as excinfo: _ = split_comma("one, two", brackets="()") assert "brackets must be a tuple" in str(excinfo.value) with pytest.raises(ParseError) as excinfo: _ = split_comma("one, two", brackets=("()",)) assert "brackets tuple must contain just two items" in str(excinfo.value) with pytest.raises(ParseError) as excinfo: _ = split_comma("one, two", brackets=("(", "(", "(")) assert "brackets tuple must contain just two items" in str(excinfo.value) def test_split_bracketed_list(): """Test the splitting of a list bracketed with parentheses""" items = split_comma("(well(1), this(is), it)", brackets=("(", ")")) print(items) assert items[0] == "well(1)" assert items[1] == "this(is)" # With superfluous white space items = split_comma(" ( well(1), this(is), it ) ", brackets=("(", ")")) print(items) assert items[0] == "well(1)" assert items[1] == "this(is)" assert items[2] == "it" # Square brackets items = split_comma("[well(1), this(is), it]", brackets=("[", "]")) print(items) assert items[0] == "well(1)" assert items[1] == "this(is)" assert items[2] == "it" # Mis-matched brackets items = split_comma("[well(1), this(is), it)", brackets=("[", "]")) assert not items def test_extract_bracketed_list(): """Test the extraction and parsing of a list within parentheses within a larger string""" from fparser.common.utils import extract_bracketed_list_items items = extract_bracketed_list_items("hello (this, is, a) test") assert items[0] == "this" assert items[1] == "is" assert items[2] == "a" def test_extract_bracketed_list_err(): """Test that we get the expected errors if the string passed into extract_bracketed_list_items() does not have the correct format""" from fparser.common.utils import extract_bracketed_list_items with pytest.raises(ParseError) as excinfo: _ = extract_bracketed_list_items("hello (this, is, wrong(") assert "more than one opening/closing parenthesis found" in str(excinfo.value) with pytest.raises(ParseError) as excinfo: _ = extract_bracketed_list_items("hello )this, is, wrong)") assert "more than one opening/closing parenthesis found" in str(excinfo.value) with pytest.raises(ParseError) as excinfo: _ = extract_bracketed_list_items("hello (this, is, wrong) (too)") assert "more than one opening/closing parenthesis found" in str(excinfo.value) with pytest.raises(ParseError) as excinfo: _ = extract_bracketed_list_items("hello )this, is, wrong( too") assert "failed to find expression within parentheses in" in str(excinfo.value) stfc-fparser-d93d18d/src/fparser/common/tests/utf.f900000664000175000017500000000345015156742256022737 0ustar alastairalastair! ----------------------------------------------------------------------------- ! BSD 3-Clause License ! ! Copyright (c) 2019, Science and Technology Facilities Council ! All rights reserved. ! ! Redistribution and use in source and binary forms, with or without ! modification, are permitted provided that the following conditions are met: ! ! * Redistributions of source code must retain the above copyright notice, this ! list of conditions and the following disclaimer. ! ! * 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. ! ! * 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. ! ----------------------------------------------------------------------------- ! Author R. W. Ford STFC Daresbury Lab program utf ! N²<0 end program utf stfc-fparser-d93d18d/src/fparser/common/tests/utf_in_code.f900000664000175000017500000000351115156742256024415 0ustar alastairalastair! ----------------------------------------------------------------------------- ! BSD 3-Clause License ! ! Copyright (c) 2019, Science and Technology Facilities Council ! All rights reserved. ! ! Redistribution and use in source and binary forms, with or without ! modification, are permitted provided that the following conditions are met: ! ! * Redistributions of source code must retain the above copyright notice, this ! list of conditions and the following disclaimer. ! ! * 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. ! ! * 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. ! ----------------------------------------------------------------------------- ! Authors: R. W. Ford and A. R. Porter, STFC Daresbury Lab program utf write(*,*) 'N²<0' end program utf stfc-fparser-d93d18d/src/fparser/common/tests/logging_utils.py0000664000175000017500000000674615156742256025054 0ustar alastairalastair# -*- coding: utf-8 -*- ############################################################################## # Copyright (c) 2017 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. ############################################################################## # Modified M.Hambley, UK Met Office ############################################################################## """ Helps with testing methods which write to the standard logger. """ import logging class CaptureLoggingHandler(logging.Handler): """ Records logged output for later examination. This is a standard handler for the built-in Python logging system. To make use of it simply register an instance with the logger using a command such as "logging.getLogger(__class__).addHandler( CaptureLoggingHandler() )" Any log message raised while this handler is in use will be recorded to a memory buffer for later use. This object has attributes 'debug', 'info', 'warning', 'error' and 'critical', each of which is a list of logged messages. Only the message text is recorded, everything else is lost. """ def __init__(self, *args, **kwargs): """ Constructs an instance of CaptureLoggingHandler using the arguments accepted by logging.Handler. """ super(CaptureLoggingHandler, self).__init__(*args, **kwargs) self.reset() def emit(self, record): """ Handles a logged event. The event is passed from the logging system and recorded for future inspection. :param :py:class:`logging.LogRecord` record The event being logged. """ self.messages[record.levelname.lower()].append(record.getMessage()) def reset(self): """ Empties the log of previous messages. """ self.messages = { "debug": [], "info": [], "warning": [], "error": [], "critical": [], } stfc-fparser-d93d18d/src/fparser/common/sourceinfo.py0000664000175000017500000002774715156742256023224 0ustar alastairalastair# Modified work Copyright (c) 2017-2023 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. """ Provides functions to determine whether a piece of Fortran source is free or fixed format. It also tries to differentiate between strict and "pyf" although I'm not sure what that is. """ import os import re ############################################################################## class FortranFormat: """ Describes the nature of a piece of Fortran source. Source can be fixed or free format. It can also be "strict" or "not strict" although it's not entirely clear what that means. It may refer to the strictness of adherance to fixed format although what that means in the context of free format I don't know. :param bool is_free: True for free format, False for fixed. :param bool is_strict: some amount of strictness. :param bool enable_f2py: whether f2py directives are enabled or treated \ as comments (the default). """ def __init__(self, is_free, is_strict, enable_f2py=False): if is_free is None: raise Exception("FortranFormat does not accept a None is_free") if is_strict is None: raise Exception("FortranFormat does not accept a None is_strict") self._is_free = is_free self._is_strict = is_strict self._f2py_enabled = enable_f2py @classmethod def from_mode(cls, mode): """ Constructs a FortranFormat object from a mode string. Arguments: mode - (String) One of 'free', 'fix', 'f77' or 'pyf' """ if mode == "free": is_free, is_strict = True, False elif mode == "fix": is_free, is_strict = False, False elif mode == "f77": is_free, is_strict = False, True elif mode == "pyf": is_free, is_strict = True, True else: raise NotImplementedError(repr(mode)) return cls(is_free, is_strict) def __eq__(self, other): if isinstance(other, FortranFormat): return ( self.is_free == other.is_free and self.is_strict == other.is_strict and self.f2py_enabled == other.f2py_enabled ) raise NotImplementedError def __str__(self): if self.is_strict: string = "Strict" else: string = "Non-strict" if self.is_free: string += " free" else: string += " fixed" return string + " format" @property def is_free(self): """ Returns true for free format. """ return self._is_free @property def is_fixed(self): """ Returns true for fixed format. """ return not self._is_free @property def is_strict(self): """ Returns true for strict format. """ return self._is_strict @property def is_f77(self): """ Returns true for strict fixed format. """ return not self._is_free and self._is_strict @property def is_fix(self): """ Returns true for slack fixed format. """ return not self._is_free and not self._is_strict @property def is_pyf(self): """ Returns true for strict free format. """ return self._is_free and self._is_strict @property def f2py_enabled(self): """ :returns: whether or not f2py directives are enabled. :rtype: bool """ return self._f2py_enabled @property def mode(self): """ Returns a string representing this format. """ if self._is_free and self._is_strict: mode = "pyf" elif self._is_free: mode = "free" elif self.is_fix: mode = "fix" elif self.is_f77: mode = "f77" # While mode is determined by is_free and is_strict all permutations # are covered. There is no need for a final "else" clause as the # object cannot get wedged in an invalid mode. return mode ############################################################################## _HAS_F_EXTENSION = re.compile(r".*[.](for|ftn|f77|f)\Z", re.I).match _HAS_F_HEADER = re.compile(r"-[*]-\s*(fortran|f77)\s*-[*]-", re.I).search _HAS_F90_HEADER = re.compile(r"-[*]-\s*f90\s*-[*]-", re.I).search _HAS_F03_HEADER = re.compile(r"-[*]-\s*f03\s*-[*]-", re.I).search _HAS_F08_HEADER = re.compile(r"-[*]-\s*f08\s*-[*]-", re.I).search _HAS_FREE_HEADER = re.compile(r"-[*]-\s*(f90|f95|f03|f08)\s*-[*]-", re.I).search _HAS_FIX_HEADER = re.compile(r"-[*]-\s*fix\s*-[*]-", re.I).search _HAS_PYF_HEADER = re.compile(r"-[*]-\s*pyf\s*-[*]-", re.I).search _FREE_FORMAT_START = re.compile(r"[^c*!]\s*[^\s\d\t]", re.I).match def get_source_info_str(source, ignore_encoding=True): """ Determines the format of Fortran source held in a string. :param bool ignore_encoding: whether or not to ignore any Python-style \ encoding information in the first line of the file. :returns: a FortranFormat object. :rtype: :py:class:`fparser.common.sourceinfo.FortranFormat` """ lines = source.splitlines() if not lines: return FortranFormat(False, False) if not ignore_encoding: # We check to see whether the file contains a comment describing its # encoding. This has nothing to do with the Fortran standard (see e.g. # https://peps.python.org/pep-0263/) and hence is not done by default. firstline = lines[0].lstrip() if _HAS_F_HEADER(firstline): # -*- fortran -*- implies Fortran77 so fixed format. return FortranFormat(False, True) if _HAS_FIX_HEADER(firstline): return FortranFormat(False, False) if _HAS_FREE_HEADER(firstline): return FortranFormat(True, False) if _HAS_PYF_HEADER(firstline): return FortranFormat(True, True) line_tally = 10000 # Check up to this number of non-comment lines is_free = False while line_tally > 0 and lines: line = lines.pop(0).rstrip() if line and line[0] != "!": line_tally -= 1 if line[0] != "\t" and _FREE_FORMAT_START(line[:5]) or line[-1:] == "&": is_free = True break return FortranFormat(is_free, False) ############################################################################## def get_source_info(file_candidate, ignore_encoding=True): """ Determines the format of Fortran source held in a file. :param file_candidate: a filename or a file object :type file_candidate: str or _io.TextIOWrapper (py3) :returns: the Fortran format encoded as a string. :rtype: str """ if hasattr(file_candidate, "name") and hasattr(file_candidate, "read"): filename = file_candidate.name # Under Python 3 file.name holds an integer file handle when # associated with a file without a name. if isinstance(filename, int): filename = None elif isinstance(file_candidate, str): filename = file_candidate else: message = "Argument must be a filename or file-like object." raise ValueError(message) if filename: _, ext = os.path.splitext(filename) if ext == ".pyf": return FortranFormat(True, True) if hasattr(file_candidate, "read"): # If the candidate object has a "read" method we assume it's a file # object. # # If it is a file object then it may be in the process of being read. # As such we need to take a note of the current state of the file # pointer so we can restore it when we've finished what we're doing. # pointer = file_candidate.tell() file_candidate.seek(0) source_info = get_source_info_str( file_candidate.read(), ignore_encoding=ignore_encoding ) file_candidate.seek(pointer) return source_info # It isn't a file and it passed the type check above so it must be # a string. # # If it's a string we assume it is a filename. In which case we need # to open the named file so we can read it. # # It is closed on completion so as to return it to the state it was # found in. # # The 'fparser-logging' handler is setup in fparser/__init__.py and # ensures any occurrences of invalid characters are skipped and # logged. with open( file_candidate, "r", encoding="utf-8", errors="fparser-logging" ) as file_object: string = get_source_info_str( file_object.read(), ignore_encoding=ignore_encoding ) return string ############################################################################## stfc-fparser-d93d18d/src/fparser/common/readfortran.py0000664000175000017500000021463615156742256023352 0ustar alastairalastair#!/usr/bin/env python # -*- coding: utf-8 -*- # Modified work Copyright (c) 2017-2025 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. # # Author: Pearu Peterson # Created: May 2006 # Modified by R. W. Ford and A. R. Porter, STFC Daresbury Lab # Modified by P. Elson, Met Office # Modified by J. Henrichs, Bureau of Meteorology """Provides Fortran reader classes. Overview Provides FortranReader classes for reading Fortran codes from files and strings. FortranReader handles comments and line continuations of both fix and free format Fortran codes. Examples:: >> from fparser.common.readfortran import FortranFileReader >>> import os >>> reader = FortranFileReader(os.path.expanduser('~/src/blas/daxpy.f')) >>> print reader.next() line #1 'subroutine daxpy(n,da,dx,incx,dy,incy)' >>> print `reader.next()` Comment('c constant times a vector plus a vector.\\n c uses unrolled loops for increments equal to one.\\n c jack dongarra, linpack, 3/11/78.\\n c modified 12/3/93, array(1) declarations changed to array(*)',(3, 6)) >>> print `reader.next()` Line('double precision dx(*),dy(*),da',(8, 8),'') >>> print `reader.next()` Line('integer i,incx,incy,ix,iy,m,mp1,n',(9, 9),'') Note that the ``.next()`` method may return `Line`, `SyntaxErrorLine`, `Comment`, `MultiLine`, or `SyntaxErrorMultiLine` instance. Let us continue with the above example session to illustrate the `Line` methods and attributes:: >>> item = reader.next() >>> item Line('if (da .eq. 0.0d0) return',(12, 12),'') >>> item.line 'if (da .eq. 0.0d0) return' >>> item.strline 'if (F2PY_EXPR_TUPLE_5) return' >>> item.strlinemap {'F2PY_EXPR_TUPLE_5': 'da .eq. 0.0d0'} >>> item.span (12, 12) >>> item.get_line() 'if (F2PY_EXPR_TUPLE_5) return' To read a Fortran code from a string, use `FortranStringReader` class:: >>> from fparser.common.sourceinfo import FortranFormat >>> from fparser.common.readfortran import FortranStringReader >>> code = ''' ... subroutine foo(a) ... integer a ... print*,\"a=\",a ... end ... ''' >>> reader = FortranStringReader(code) >>> reader.set_format(FortranFormat(False, True)) >>> reader.next() Line('subroutine foo(a)',(2, 2),'') >>> reader.next() Line('integer a',(3, 3),'') >>> reader.next() Line('print*,\"a=\",a',(4, 4),'') """ import logging import os import re import sys import traceback from typing import Optional, Tuple from io import StringIO import fparser.common.sourceinfo from fparser.common.splitline import String, string_replace_map, splitquote __all__ = [ "FortranFileReader", "FortranStringReader", "FortranReaderError", "Line", "SyntaxErrorLine", "Comment", "MultiLine", "SyntaxErrorMultiLine", ] _SPACEDIGITS = " 0123456789" _CF2PY_RE = re.compile(r"(?P\s*)!f2py(?P.*)", re.I) _LABEL_RE = re.compile(r"\s*(?P