summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorptmcg <ptmcg@austin.rr.com>2022-07-11 08:43:59 -0500
committerptmcg <ptmcg@austin.rr.com>2022-07-11 08:43:59 -0500
commitff2b4c876b2a3032b17ed2c43221c231e3b5e2bb (patch)
tree5da8b1d4d9682b70854cd67330f45d1c8c5eaf01
parent97b30229dcdebddc341df114d9d438431179f4bb (diff)
downloadpyparsing-git-ff2b4c876b2a3032b17ed2c43221c231e3b5e2bb.tar.gz
Add recurse option to set_debug(), fixes #399
-rw-r--r--CHANGES4
-rw-r--r--pyparsing/__init__.py11
-rw-r--r--pyparsing/core.py26
-rw-r--r--pyparsing/helpers.py1
-rw-r--r--tests/test_unit.py44
5 files changed, 78 insertions, 8 deletions
diff --git a/CHANGES b/CHANGES
index 9a4baa8..843e840 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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