diff options
author | ptmcg <ptmcg@austin.rr.com> | 2022-07-11 08:43:59 -0500 |
---|---|---|
committer | ptmcg <ptmcg@austin.rr.com> | 2022-07-11 08:43:59 -0500 |
commit | ff2b4c876b2a3032b17ed2c43221c231e3b5e2bb (patch) | |
tree | 5da8b1d4d9682b70854cd67330f45d1c8c5eaf01 | |
parent | 97b30229dcdebddc341df114d9d438431179f4bb (diff) | |
download | pyparsing-git-ff2b4c876b2a3032b17ed2c43221c231e3b5e2bb.tar.gz |
Add recurse option to set_debug(), fixes #399
-rw-r--r-- | CHANGES | 4 | ||||
-rw-r--r-- | pyparsing/__init__.py | 11 | ||||
-rw-r--r-- | pyparsing/core.py | 26 | ||||
-rw-r--r-- | pyparsing/helpers.py | 1 | ||||
-rw-r--r-- | tests/test_unit.py | 44 |
5 files changed, 78 insertions, 8 deletions
@@ -56,6 +56,10 @@ help from Devin J. Pohly in structuring the code to enable this peaceful transit HTML source. (Useful when embedding a call to `create_diagram()` in a PyScript HTML page.) +- Added `recurse` argument to `ParserElement.set_debug` to set the + debug flag on an expression and all of its sub-expressions. Requested + by multimeric in Issue #399. + - Fixed bug in `Word` when `max=2`. Also added performance enhancement when specifying `exact` argument. Reported in issue #409 by panda-34, nice catch! diff --git a/pyparsing/__init__.py b/pyparsing/__init__.py index dd66063..0d33d64 100644 --- a/pyparsing/__init__.py +++ b/pyparsing/__init__.py @@ -105,13 +105,10 @@ class version_info(NamedTuple): @property def __version__(self): - return ( - f"{self.major}.{self.minor}.{self.micro}" - + ( - f"{'r' if self.releaselevel[0] == 'c' else ''}{self.releaselevel[0]}{self.serial}", - "", - )[self.releaselevel == "final"] - ) + return f"{self.major}.{self.minor}.{self.micro}" + ( + f"{'r' if self.releaselevel[0] == 'c' else ''}{self.releaselevel[0]}{self.serial}", + "", + )[self.releaselevel == "final"] def __str__(self): return f"{__name__} {self.__version__} / {__version_time__}" diff --git a/pyparsing/core.py b/pyparsing/core.py index 1866fc2..0b0b2c8 100644 --- a/pyparsing/core.py +++ b/pyparsing/core.py @@ -1,6 +1,7 @@ # # core.py # +from collections import deque import os import typing from typing import ( @@ -488,6 +489,23 @@ class ParserElement(ABC): self.suppress_warnings_.append(warning_type) return self + def visit_all(self): + """General-purpose method to yield all expressions and sub-expressions + in a grammar. Typically just for internal use. + """ + to_visit = deque([self]) + seen = set() + while to_visit: + cur = to_visit.popleft() + + # guard against looping forever through recursive grammars + if cur in seen: + continue + seen.add(cur) + + to_visit.extend(cur.recurse()) + yield cur + def copy(self) -> "ParserElement": """ Make a copy of this :class:`ParserElement`. Useful for defining @@ -1751,10 +1769,11 @@ class ParserElement(ABC): self.debug = True return self - def set_debug(self, flag: bool = True) -> "ParserElement": + def set_debug(self, flag: bool = True, recurse: bool = False) -> "ParserElement": """ Enable display of debugging messages while doing pattern matching. Set ``flag`` to ``True`` to enable, ``False`` to disable. + Set ``recurse`` to ``True`` to set the debug flag on this expression and all sub-expressions. Example:: @@ -1788,6 +1807,11 @@ class ParserElement(ABC): which makes debugging and exception messages easier to understand - for instance, the default name created for the :class:`Word` expression without calling ``set_name`` is ``"W:(A-Za-z)"``. """ + if recurse: + for expr in self.visit_all(): + expr.set_debug(flag, recurse=False) + return self + if flag: self.set_debug_actions( _default_start_debug_action, diff --git a/pyparsing/helpers.py b/pyparsing/helpers.py index ecb382b..f3e0ab5 100644 --- a/pyparsing/helpers.py +++ b/pyparsing/helpers.py @@ -49,6 +49,7 @@ def delimited_list( def make_deep_name_copy(expr): from collections import deque + MAX_EXPRS = sys.getrecursionlimit() seen = set() to_visit = deque([(None, expr)]) diff --git a/tests/test_unit.py b/tests/test_unit.py index d955c48..cdb9691 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -8627,6 +8627,50 @@ class Test02_WithoutPackrat(ppt.TestParseResultsAsserts, TestCase): ), ) + def testSetDebugRecursively(self): + expr = pp.Word(pp.alphas) + contained = expr + pp.Empty().set_name("innermost") + depth = 4 + for _ in range(depth): + contained = pp.Group(contained + pp.Empty()) + contained.set_debug(recurse=True) + self.assertTrue(expr.debug) + # contained.parse_string("ABC") + test_stdout = StringIO() + with resetting(sys, "stdout", "stderr"): + sys.stdout = test_stdout + sys.stderr = test_stdout + contained.parseString("aba", parseAll=True) + + output = test_stdout.getvalue() + print(output) + self.assertEqual(depth, output.count("Matched Empty -> []")) + self.assertEqual(1, output.count("Matched innermost -> []")) + + def testSetDebugRecursivelyWithForward(self): + expr = pp.Word(pp.alphas).set_name("innermost") + contained = pp.infix_notation(expr, [ + ('NOT', 1, pp.opAssoc.RIGHT), + ('AND', 2, pp.opAssoc.LEFT), + ('OR', 2, pp.opAssoc.LEFT), + ]) + + contained.set_debug(recurse=True) + self.assertTrue(expr.debug) + + # contained.parse_string("ABC") + test_stdout = StringIO() + with resetting(sys, "stdout", "stderr"): + sys.stdout = test_stdout + sys.stderr = test_stdout + contained.parseString("aba", parseAll=True) + + output = test_stdout.getvalue() + print(output) + # count of matches varies with packrat state, can't match exact count, but at least test if contains + # self.assertEqual(4, output.count("Matched innermost -> ['aba']")) + self.assertTrue("Matched innermost -> ['aba']" in output) + def testUndesirableButCommonPractices(self): # While these are valid constructs, and they are not encouraged # there is apparently a lot of code out there using these |