McUtils.Parsers

Utilities for writing parsers of structured text. An entirely standalone package which is used extensively by GaussianInterface. Three main threads are handled:

  1. A FileStreamer interface which allows for efficient searching for blocks of text in large files with no pattern matching
  2. A Regex interface that provides declarative tools for building and manipulating a regular expression as a python tree
  3. A StringParser/StructuredTypeArray interface that takes the Regex tools and allows for automatic construction of complicated NumPy-backed arrays from the parsed data. Generally works well but the problem is complicated and there are no doubt many unhandled edge cases. This is used extensively with (1.) to provide efficient parsing of data from Gaussian .log files by using a streamer to match chunks and a parser to extract data from the matched chunks.

Members

Examples

RegexPattern

A RegexPattern is a higher-level interface to work with the regular expression (regex) string pattern matching language. Python provides support for regular expressions through the re module. Being comfortable with regex is not a requirement for working with RegexPattern but will help explain some of the more confusing design decisions.

There are a bunch of different RegexPattern instances that cover different cases, e.g.

  • Word: matches a string of characters that are generally considered text
  • PositiveInteger: matches a string of characters that are only digits
  • Integer: a PositiveInteger with and optional sign
  • Number: matches Integer.PositiveInteger
  • VariableName: matches a string of digits or text as the first character is a letter
  • Optional: represents an optional pattern to match

Capturing/Named

When matching pieces of text it is also important to specify which pieces of text we would like to actually get back out. For this there are two main RegexPattern instances. The simplest one is Capturing. This just specifies that we would like to capture a piece of text. There is a slightly more sophisticated instance called Named which allows us to attach a name to a group.

key_value_matcher = RegexPattern([Named(Word, "key"), "=", Named(Word, "value")])
print(key_value_matcher)
(?P<key>\w+)(?:=)(?P<value>\w+)

This can be used directly to pull info out of files

test_data = os.path.join(os.path.dirname(McUtils.__file__), 'ci', 'tests', 'TestData')
with open(os.path.join(test_data, 'water_OH_scan.log')) as log_dat:
    sample_data = log_dat.read()

matches = list(key_value_matcher.finditer(sample_data))
for match in matches[:5]:
    print(match.groupdict())
{'key': '0', 'value': 'g09'}
{'key': 'Input', 'value': 'water_OH_scan'}
{'key': 'Output', 'value': 'water_OH_scan'}
{'key': 'Chk', 'value': 'water_OH_scan'}
{'key': 'NProc', 'value': '8'}

StringParser

A more powerful interface than RegexPattern is through a StringParser instance. This provides a wrapper on RegexPattern that handles the process of turning matches into NumPy arrays of the appropriate type. The actual interface is quite simple, e.g. we can take our matcher from before and use it directly

key_vals = StringParser(key_value_matcher).parse_all(sample_data)
print(key_vals)
StructuredTypeArray(shape=[(11493, 0), (11493, 0)], dtype=OrderedDict([('key', StructuredType(<class 'str'>, shape=(None,))), ('value', StructuredType(<class 'str'>, shape=(None,)))]))

This StructuredTypeArray is basically a version of NumPy record arrays, but was written without knowing about them. A smarter reimplementation of this portion of the parsing process would make use of recarray instead of this custom array type.

That said, getting the raw ndarray objects out is straight-forward

key_vals['key'].array
array(['0', 'Input', 'Output', ..., 'State', 'RMSD', 'PG'], dtype='<U7')

NOTE: 90% of all bugs in the StringParser ecosystem will come from the design of StructuredTypeArray. The need to be efficient in data handling can lead to some difficult implementation details. As the data type has organically evolved it has become potentially tough to understand. A reimplementation based on recarray would potentially solve some issues.

Block Handlers

For efficiency sake, StringParser objects also provide a block_handlers argument (and handlers can be defined on RegexPatterns directly). A handler is a function that can be applied to a parsed piece of text and should directly return a NumPy array so that it can be worked into the returned StructuredTypeArray. The simplest handlers are already provided for convenience on StringParser, e.g. from GaussianLogComponents.py

Named(
    Repeating(
        Capturing(Number),
        min = 3, max = 3,
        prefix=Optional(Whitespace),
        joiner = Whitespace
    ),
    "Coordinates", handler=StringParser.array_handler(dtype=float)
)

Here StringParser.array_handler(dtype=float) provides efficient parsing of data through np.loadtxt with a float as the target dtype. We also see the prefix and joiner options to RegexPattern in action.

LLM Examples

Build a declarative parser for numerical records

from McUtils.Parsers import RegexPattern, Repeating, Capturing
from McUtils.Parsers import Number, Whitespace, Optional, StringParser

pattern = RegexPattern(
    ("Eigenvalues --", Repeating(Capturing(Number), suffix=Optional(Whitespace))),
    joiner=Whitespace
)
parser = StringParser(pattern)
values = parser.parse("Eigenvalues --  -0.1423  0.0781  0.2114")
print(values.array)

Stream structures from an XYZ trajectory

from McUtils.Parsers import XYZParser

with XYZParser("trajectory.xyz") as parser:
    structures = parser.parse()
for comment, atoms, coords in structures:
    print(comment, len(atoms), coords.shape)
first_geometry = structures[0][2]

Parse selected fields from a CIF

from McUtils.ExternalPrograms import CIFParser, CIFConverter

fields = ["cell_length_a", "cell_length_b", "cell_length_c",
          "atom_site_label", "atom_site_fract_x", "atom_site_fract_y", "atom_site_fract_z"]
with CIFParser("crystal.cif", fields=fields) as parser:
    blocks = parser.parse()
crystal = CIFConverter(blocks)
atoms, coordinates = crystal.atoms()
print("cell:", crystal.cell_properties())
print("expanded structure:", len(atoms), coordinates.shape)

Compose named fields into structured data

from McUtils.Parsers import RegexPattern, Named, Number, VariableName, Whitespace
from McUtils.Parsers import StringParser

record = RegexPattern((Named(VariableName, "label"), Named(Number, "value")),
                      joiner=Whitespace)
parser = StringParser(record)
parsed = parser.parse("Energy -76.2413")
print(parsed["label"].array, parsed["value"].array)

Search a large file without loading it all

from McUtils.Parsers import FileStreamReader, FileStreamerTag

with FileStreamReader("large-output.log") as stream:
    tag = FileStreamerTag("Standard orientation:", follow_ups=["-----"])
    block = stream.get_tagged_block("geometry", tag)
print("matched block length:", len(block))

Before we can run our examples we should get a bit of setup out of the way. Since these examples were harvested from the unit tests not all pieces will be necessary for all situations.

All tests are wrapped in a test class

class ParserTests(TestCase):

RegexGroups

    def test_RegexGroups(self):
        # tests whether we capture subgroups or not (by default _not_)

        test_str = "1 2 3 4 a b c d "
        pattern = RegexPattern(
            (
                Capturing(
                    Repeating(
                        Capturing(Repeating(PositiveInteger, 2, 2, suffix=Optional(Whitespace)))
                    )
                ),
                Repeating(Capturing(ASCIILetter), suffix=Whitespace)
            )
        )
        self.assertEquals(len(pattern.search(test_str).groups()), 2)

OptScan

    def test_OptScan(self):

        eigsPattern = RegexPattern(
            (
                "Eigenvalues --",
                Repeating(Capturing(Number), suffix=Optional(Whitespace))
            ),
            joiner=Whitespace
        )

        coordsPattern = RegexPattern(
            (
                Capturing(VariableName),
                Repeating(Capturing(Number), suffix=Optional(Whitespace))
            ),
            prefix=Whitespace,
            joiner=Whitespace
        )

        full_pattern = RegexPattern(
            (
                Named(eigsPattern,
                      "Eigenvalues"
                      #parser=lambda t: np.array(Number.findall(t), 'float')
                      ),
                Named(Repeating(coordsPattern, suffix=Optional(Newline)), "Coordinates")
            ),
            joiner=Newline
        )

        with open(TestManager.test_data('scan_params_test.txt')) as test:
            test_str = test.read()

        parser = StringParser(full_pattern)
        parse_res = parser.parse_all(test_str)
        parse_single = parser.parse(test_str)
        parse_its = list(parser.parse_iter(test_str))

        self.assertEquals(parse_res.shape, [(4, 5), [(4, 32), (4, 32, 5)]])
        self.assertIsInstance(parse_res["Coordinates"][1].array, np.ndarray)
        self.assertEquals(int(parse_res["Coordinates"][1, 0].sum()), 3230)

XYZ

    def test_XYZ(self):

        with open(TestManager.test_data('test_100.xyz')) as test:
            test_str = test.read()

        # print(
        #     "\n".join(test_str.splitlines()[:15]),
        #     "\n",
        #     XYZParser.regex.search(test_str),
        #     file=sys.stderr
        # )

        res = XYZParser.parse_all(
            test_str
        )
        # print(
        #     res["Atoms"],
        #     file=sys.stderr
        # )

        atom_coords = res["Atoms"].array[1].array
        self.assertIsInstance(atom_coords, np.ndarray)
        self.assertEquals(atom_coords.shape, (100, 13, 3))

BasicParse

    def test_BasicParse(self):
        regex = RegexPattern(
            (
                Named(PositiveInteger, "NumAtoms"),
                Named(
                    Repeating(Any, min = None), "Comment", dtype=str
                ),
                Named(
                    Repeating(
                        Capturing(
                            Repeating(Capturing(Number), 3, 3, prefix = Whitespace, suffix = Optional(Whitespace)),
                            handler= StringParser.array_handler(shape = (None, 3))
                        ),
                        suffix = Optional(Newline)
                    ),
                    "Atoms"
                )
            ),
            "XYZ",
            joiner=Newline
        )

        with open(TestManager.test_data('coord_parse.txt')) as test:
            test_str = test.read()

        res = StringParser(regex).parse(test_str)

        comment_string = res["Comment"].array[0]
        self.assertTrue('comment' in comment_string)
        self.assertEquals(res['Atoms'].array.shape, (4, 3))

StructuredArray_HasIndeterminateShape

    def test_StructuredArray_HasIndeterminateShape(self):
        # this is the literal AttributeError from the traceback:
        #   AttributeError: 'StructuredTypeArray' object has no attribute
        #   'has_indeterminate_shape'
        arr = StructuredTypeArray(StructuredType(float))
        if not hasattr(arr, "has_indeterminate_shape"):
            raise Exception("StructuredTypeArray is missing `has_indeterminate_shape`")
        before = arr.has_indeterminate_shape
        self.assertIsInstance(before, (bool, np.bool_))

StructuredArray_AxisShapeIndeterminate

    def test_StructuredArray_AxisShapeIndeterminate(self):
        arr = StructuredTypeArray(StructuredType(float, shape=(None,)))
        if not hasattr(arr, "axis_shape_indeterminate"):
            raise Exception("StructuredTypeArray is missing `axis_shape_indeterminate`")
        result = arr.axis_shape_indeterminate(0)
        self.assertIsInstance(result, (bool, np.bool_))

StructuredArray_BlockSize

    def test_StructuredArray_BlockSize(self):
        arr = StructuredTypeArray(StructuredType(float, shape=(None, 3)))
        if not hasattr(arr, "block_size"):
            raise Exception("StructuredTypeArray is missing `block_size`")
        _ = arr.block_size

StructuredArray_AppendDepth

    def test_StructuredArray_AppendDepth(self):
        arr = StructuredTypeArray(StructuredType(float))
        if not hasattr(arr, "append_depth"):
            raise Exception("StructuredTypeArray is missing `append_depth`")
        arr.append_depth = 0
        self.assertEquals(arr.append_depth, 0)
        arr.append_depth = 1
        self.assertEquals(arr.append_depth, 1)

StructuredArray_CanCast

    def test_StructuredArray_CanCast(self):
        arr = StructuredTypeArray(StructuredType(float))
        if not hasattr(arr, "can_cast"):
            raise Exception("StructuredTypeArray is missing `can_cast`")
        _ = arr.can_cast(1.0)

StructuredArray_GetCastingShape

    def test_StructuredArray_GetCastingShape(self):
        arr = StructuredTypeArray(StructuredType(float, shape=(None,)))
        if not hasattr(arr, "_get_casting_shape"):
            raise Exception("StructuredTypeArray is missing `_get_casting_shape`")

StructuredArray_AppendWritesScalars

    def test_StructuredArray_AppendWritesScalars(self):
        # the specific bug found in an earlier rewrite attempt: a naive
        # patch silently dropped scalar-append writes by nesting the array
        # assignment inside a branch that never fires for scalars. Pin the
        # correct behavior down explicitly against the real class.
        arr = StructuredTypeArray(StructuredType(float))
        arr.add_axis()
        for i in range(10):
            arr.append(np.array([float(i)]))
        self.assertEquals(len(arr), 10)
        self.assertEquals(list(arr.array), [float(i) for i in range(10)])

StructuredArray_ExtendAppendsBlock

    def test_StructuredArray_ExtendAppendsBlock(self):
        arr = StructuredTypeArray(StructuredType(float))
        arr.add_axis()
        arr.fill(np.array([1.0, 2.0, 3.0]))
        arr.extend(np.array([4.0, 5.0]))
        self.assertEquals(list(arr.array), [1.0, 2.0, 3.0, 4.0, 5.0])

StructuredArray_FillSetsContents

    def test_StructuredArray_FillSetsContents(self):
        arr = StructuredTypeArray(StructuredType(float))
        arr.add_axis()
        arr.fill(np.array([9.0, 8.0, 7.0]))
        self.assertEquals(list(arr.array), [9.0, 8.0, 7.0])

StructuredArray_SetPartGetItemRoundTrip

    def test_StructuredArray_SetPartGetItemRoundTrip(self):
        arr = StructuredTypeArray(StructuredType(float))
        arr.add_axis()
        arr.fill(np.array([1.0, 2.0, 3.0]))
        self.assertEquals(arr[0], 1.0)
        self.assertEquals(arr[2], 3.0)

StructuredArray_CastToArrayScalar

    def test_StructuredArray_CastToArrayScalar(self):
        arr = StructuredTypeArray(StructuredType(float))
        result = arr.cast_to_array("3.14")
        self.assertTrue(np.isclose(np.asarray(result).flatten()[0], 3.14))

StructuredArray_CastToArrayVector

    def test_StructuredArray_CastToArrayVector(self):
        arr = StructuredTypeArray(StructuredType(float, shape=(3,)))
        result = arr.cast_to_array("1.0 2.0 3.0")
        self.assertTrue(np.allclose(np.asarray(result).flatten(), [1.0, 2.0, 3.0]))

StructuredArray_AddAxisFromScalarType

    def test_StructuredArray_AddAxisFromScalarType(self):
        arr = StructuredTypeArray(StructuredType(float))
        arr.add_axis()  # must not raise
        arr.append(np.array([1.0]))
        arr.append(np.array([2.0]))
        self.assertEquals(list(arr.array), [1.0, 2.0])

StructuredArray_AddAxisFromShapedType

    def test_StructuredArray_AddAxisFromShapedType(self):
        arr = StructuredTypeArray(StructuredType(float, shape=(3,)))
        arr.add_axis()
        arr.append(np.array([1.0, 2.0, 3.0]))
        arr.append(np.array([4.0, 5.0, 6.0]))
        self.assertEquals(arr.array.shape[0], 2)
        self.assertEquals(list(arr.array[0]), [1.0, 2.0, 3.0])
        self.assertEquals(list(arr.array[1]), [4.0, 5.0, 6.0])

StructuredArray_LenMatchesAppendedCount

    def test_StructuredArray_LenMatchesAppendedCount(self):
        arr = StructuredTypeArray(StructuredType(float))
        arr.add_axis()
        for i in range(37):
            arr.append(np.array([float(i)]))
        self.assertEquals(len(arr), 37)

StructuredArray_DictLikeCompound

    def test_StructuredArray_DictLikeCompound(self):
        stype = StructuredType({"a": StructuredType(float), "b": StructuredType(int)})
        arr = StructuredTypeArray(stype)
        self.assertTrue(arr.dict_like or isinstance(arr.array, dict))

DipoleBlock_SingleRow

    def test_DipoleBlock_SingleRow(self):
        parser = StringParser(_dipole_pattern())
        res = parser.parse(" 0.123456 -0.234567 0.345678")
        if res is None:
            raise Exception("dipole-shaped pattern failed to match a single well-formed row")

DipoleBlock_ParseAll

    def test_DipoleBlock_ParseAll(self):
        # this is the actual regression scenario:
        # parser.parse_all("\n".join(moms))
        block = _make_dipole_block(25, seed=1)
        parser = StringParser(_dipole_pattern())
        res = parser.parse_all(block)  # must NOT raise AttributeError
        if res is None:
            raise Exception("parse_all returned None for a well-formed dipole block")

DipoleBlock_LargeBlock

    def test_DipoleBlock_LargeBlock(self):
        block = _make_dipole_block(5000, seed=2)
        parser = StringParser(_dipole_pattern())
        res = parser.parse_all(block)
        if res is None:
            raise Exception("parse_all returned None for a 5000-row dipole block")

DipoleBlock_SingleRowBlock

    def test_DipoleBlock_SingleRowBlock(self):
        block = _make_dipole_block(1, seed=3)
        parser = StringParser(_dipole_pattern())
        res = parser.parse_all(block)
        if res is None:
            raise Exception("parse_all returned None for a single-row dipole block")

DipoleBlock_EmptyBlock

    def test_DipoleBlock_EmptyBlock(self):
        # exploratory: pin down current empty-input behavior without
        # asserting a specific outcome (either a clean empty result or a
        # clear, catchable exception is acceptable; hanging is not)
        parser = StringParser(_dipole_pattern())
        try:
            res = parser.parse_all("")
            print("empty block parse_all() ->", res, file=sys.stderr)
        except Exception as e:
            print("empty block parse_all() raised:", repr(e), file=sys.stderr)

Stress_RepeatingWithinRepeating

    def test_Stress_RepeatingWithinRepeating(self):
        inner = Repeating(Capturing(Number), min=1, suffix=Optional(Whitespace))
        outer = Repeating(
            RegexPattern((inner,), joiner=Whitespace),
            suffix=Optional(Newline),
        )
        block = "\n".join(
            " ".join(str(x) for x in range(random.randint(1, 5)))
            for _ in range(20)
        )
        parser = StringParser(outer)
        res = parser.parse_all(block)
        if res is None:
            raise Exception("nested Repeating(Repeating(...)) pattern failed to parse")

Stress_ZeroRepeatsOfRepeatingBlock

    def test_Stress_ZeroRepeatsOfRepeatingBlock(self):
        pat = Repeating(Capturing(Number), min=0, suffix=Optional(Whitespace))
        parser = StringParser(pat)
        res = parser.parse_all("")
        if res is None:
            raise Exception("zero-match Repeating block returned None instead of an empty result")

Stress_NamedAndCapturingTogether

    def test_Stress_NamedAndCapturingTogether(self):
        pat = RegexPattern(
            (
                Named(PositiveInteger, "NumberOfAtoms"),
                Named(Repeating(Any, min=0), "Comment", dtype=str),
            ),
            joiner=Newline,
        )
        parser = StringParser(pat)
        res = parser.parse("42\nsomecomment")
        if res is None:
            raise Exception("Named+Capturing combination pattern failed to parse")

Stress_MalformedNumericField

    def test_Stress_MalformedNumericField(self):
        # exploratory: documents current behavior for malformed numeric
        # fields under the numpy backend without asserting a specific
        # outcome -- this is exactly the behavior the backend patch is
        # meant to make configurable (padding_mode='ragged' / backend='python')
        block = _make_dipole_block(50, seed=4, malformed_rate=0.2)
        parser = StringParser(_dipole_pattern())
        try:
            res = parser.parse_all(block)
            print("malformed dipole block parse_all() ->", res, file=sys.stderr)
        except Exception as e:
            print("malformed dipole block parse_all() raised:", repr(e), file=sys.stderr)

Stress_RepeatedParsesAreIndependent

    def test_Stress_RepeatedParsesAreIndependent(self):
        # guards against any shared mutable state leaking between
        # independent StringParser/StructuredTypeArray instances
        block_a = _make_dipole_block(10, seed=10)
        block_b = _make_dipole_block(15, seed=20)
        parser_a = StringParser(_dipole_pattern())
        parser_b = StringParser(_dipole_pattern())
        res_a = parser_a.parse_all(block_a)
        res_b = parser_b.parse_all(block_b)
        if res_a is None or res_b is None:
            raise Exception("independent parses of two separate StringParser instances failed")

Stress_TenThousandRowDipoleBlock

    def test_Stress_TenThousandRowDipoleBlock(self):
        block = _make_dipole_block(10_000, seed=100)
        parser = StringParser(_dipole_pattern())
        res = parser.parse_all(block)
        if res is None:
            raise Exception("parse_all returned None for a 10,000-row dipole block")

Stress_ManySmallParsesNoLeakOrSlowdown

    def test_Stress_ManySmallParsesNoLeakOrSlowdown(self):
        pattern = _dipole_pattern()
        t0 = time.perf_counter()
        for i in range(200):
            block = _make_dipole_block(20, seed=i)
            parser = StringParser(pattern)
            parser.parse_all(block)
        elapsed = time.perf_counter() - t0
        # generous bound -- this is a regression guard against accidental
        # O(n^2) growth reappearing, not a tight perf assertion
        if elapsed >= 30.0:
            raise Exception(f"200 small parses took {elapsed:.1f}s (expected < 30s) -- possible perf regression")

Backend_DefaultIsNumpyAndUnchanged

    def test_Backend_DefaultIsNumpyAndUnchanged(self):
        arr = StructuredTypeArray(StructuredType(float))
        self.assertEquals(getattr(arr.backend, "name", "numpy"), "numpy")
        arr.add_axis()
        arr.append(np.array([1.0]))
        self.assertIsInstance(arr._array, np.ndarray)

Backend_PythonSelectableOnStructuredArray

    def test_Backend_PythonSelectableOnStructuredArray(self):
        arr = StructuredTypeArray(StructuredType(float), backend="python")
        arr.add_axis()
        arr.append(1.0)
        self.assertEquals(len(arr), 1)

Backend_PythonSelectableOnStringParser

    def test_Backend_PythonSelectableOnStringParser(self):
        try:
            parser = StringParser(_dipole_pattern(), backend="python")
        except TypeError as e:
            raise Exception(
                "StringParser doesn't accept backend= yet -- patch not applied to StringParser.py"
            ) from e
        block = _make_dipole_block(20, seed=5)
        res = parser.parse_all(block)
        if res is None:
            raise Exception("python-backend parser returned None for a well-formed block")

Backend_PythonToleratesMalformedDataNumpyDoesNot

    def test_Backend_PythonToleratesMalformedDataNumpyDoesNot(self):
        try:
            robust_parser = StringParser(_dipole_pattern(), backend="python")
        except TypeError as e:
            raise Exception("StringParser doesn't accept backend= yet") from e
        strict_parser = StringParser(_dipole_pattern())

        block = _make_dipole_block(100, seed=6, malformed_rate=0.15)

        strict_raised = False
        try:
            strict_parser.parse_all(block)
        except Exception:
            strict_raised = True

        # the whole point of the python backend: it should get through the
        # same malformed data without raising
        robust_result = robust_parser.parse_all(block)
        if robust_result is None:
            raise Exception("python backend returned None on malformed data instead of a tolerant result")

        if strict_raised:
            print("numpy backend raised on malformed data; python backend did not (expected)", file=sys.stderr)

Backend_CastFailuresRecordedOnPythonBackend

    def test_Backend_CastFailuresRecordedOnPythonBackend(self):
        arr = StructuredTypeArray(StructuredType(float), backend="python")
        arr.add_axis()
        for tok in ["1.0", "********", "3.0", "garbage", "5.0"]:
            try:
                arr.append(tok)
            except Exception:
                pass
        if not hasattr(arr, "cast_failures"):
            raise Exception("python-backed StructuredTypeArray is missing `cast_failures`")

Backend_PropagatesIntoCompoundChildren

    def test_Backend_PropagatesIntoCompoundChildren(self):
        stype = StructuredType({"a": StructuredType(float), "b": StructuredType(float)})
        arr = StructuredTypeArray(stype, backend="python")
        children = list(arr._array.values()) if isinstance(arr._array, dict) else list(arr._array)
        if len(children) == 0:
            raise Exception("compound stype produced no children to check backend propagation on")
        for child in children:
            if getattr(child.backend, "name", None) != "python":
                raise Exception("backend did not propagate into a compound stype's children")

ParseTex

    def test_ParseTex(self):
        import McUtils.Devutils as dev
        root_text = dev.read_file(TestManager.test_data('samp.tex'))
        with TeXParser(TestManager.test_data('samp.tex')) as parser:
            print()
            for i in range(6):
                (s, e), text = parser.parse_tex_call(return_end_points=True)
                print((s, e), text)
                if e > 0:
                    print(root_text[s:e])
                else:
                    print(root_text[s:])

        with TeXParser(TestManager.test_data('samp.tex')) as parser:
            print()
            # print(parser.parse_tex_call("func"))
            (s, e), text = parser.parse_tex_environment(return_end_points=True)
            print((s, e), text)
            if e > 0:
                print(root_text[s:e])
            else:
                print(root_text[s:])

ParseBib

    def test_ParseBib(self):
        import McUtils.Devutils as dev

        bib_file = TestManager.test_data('TeXPaper/bibliography/alt.bib')
        root_text = dev.read_file(bib_file)

        samp_bib = """
@article{Goodfellow2014,
   author = {Ian J. Goodfellow and Jean Pouget-Abadie and Mehdi Mirza and Bing Xu and David Warde-Farley and Sherjil Ozair and Aaron Courville and Yoshua Bengio},
   journal = {arXiv e-prints},
   month = {6},
   title = {Generative Adversarial Networks},
   url = {http://arxiv.org/abs/1406.2661},
   year = {2014},
}
"""
        # with dev.StreamInterface(samp_bib, file_backed=True) as stream:
        #     with BibItemParser(stream) as item_parser:
        #         print(":::", item_parser.parse_bib_line())
        #
        # return
        import pprint

        with BibTeXParser(bib_file) as parser:
            print()
            for i in range(6):
                (s, e), text = parser.parse_bib_item(return_end_points=True)
                if text is not None:
                    print("="*100)
                    print((s, e), text)
                    # if e > 0:
                    #     print(root_text[s:e])
                    # else:
                    #     print(root_text[s:])

                    pprint.pprint(parser.parse_bib_body(text))

LineByLineParser

    def test_LineByLineParser(self):
        import re
        Tags = FileLineByLineReader.LineReaderTags


        BATCH_RE = re.compile(
            r"^\[(?P<phase>train|val)\]\s*batch\s+(?P<batch>\d+)\s*\|\s*"
            r"rows seen\s+(?P<rows>[\d,]+)\s*\|\s*"
            r"running loss\s+(?P<loss>[\d.]+)\s*\|\s*"
            r"running acc\s+(?P<acc>[\d.]+)\s*$"
        )
        EPOCH_RE = re.compile(r"^epoch\s+(?P<epoch>\d+)\s*\|.*$")

        class ImprovedTrainingLogParser:
            def __init__(self):
                self.header_done = False
                self.epoch = 0

            def _parse_batch(self, stripped):
                m = BATCH_RE.match(stripped)
                if m:
                    return m['phase'], {
                        'batch': int(m['batch']), 'rows_seen': int(m['rows'].replace(',', '')),
                        'loss': float(m['loss']), 'acc': float(m['acc']),
                    }
                return None

            def __call__(self, line, depth=0, active_tag=None, label=None, history=None):
                stripped = line.strip()
                parsed = self._parse_batch(stripped)
                is_epoch_line = EPOCH_RE.match(stripped) is not None

                if label is None:
                    if not self.header_done:
                        self.header_done = True
                        if parsed is not None or is_epoch_line:
                            return (Tags.BLOCK_START, self.epoch, Tags.USE_HANDLER), parsed
                        return (Tags.BLOCK_START, "header", line), None
                    return (Tags.BLOCK_START, self.epoch, Tags.USE_HANDLER), parsed

                if label == "header":
                    if parsed is not None or is_epoch_line:
                        return Tags.RESETTING_BLOCK_END, None
                    if len(stripped) == 0:
                        return Tags.SKIP, None
                    return None, line

                if is_epoch_line:
                    self.epoch += 1
                    return Tags.RESETTING_BLOCK_END, None
                if parsed is not None:
                    phase, data = parsed
                    return (Tags.GROUP, phase, data), None
                return Tags.SKIP, None

            def handle_block(self, label, block, depth=0):
                if label in ("train", "val"):
                    return block
                if label == "header":
                    return "\n".join(block)
                result = {"train": [], "val": []}
                for item in block:
                    if item is None:
                        continue
                    if isinstance(item, dict):
                        for phase, vals in item.items():
                            result[phase].extend(vals)
                    else:
                        phase, data = item
                        result[phase].append(data)
                return result

        with line_by_line_parser(
            TestManager.test_data('line_sample.out'),
            ImprovedTrainingLogParser()
        ) as parser:
            import pprint
            pprint.pprint(list(parser))

Feedback

Examples

Templates

Documentation