summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul McGuire <ptmcg@austin.rr.com>2019-03-30 02:28:19 -0500
committerPaul McGuire <ptmcg@austin.rr.com>2019-03-30 02:28:19 -0500
commit5a2fd3bd482d7c3c61d054601c4a17a1111e7747 (patch)
tree412cc2641682f168bd3c9ec7f917161307025283
parent897696352f4b0a03152058c01403a42b07be650c (diff)
parentfed0f3da669a9a81829651080b6dfe32ce44c1b0 (diff)
downloadpyparsing-git-5a2fd3bd482d7c3c61d054601c4a17a1111e7747.tar.gz
Merge remote-tracking branch 'origin/master'
# Conflicts: # pyparsing.py
-rw-r--r--pyparsing.py22
-rw-r--r--simple_unit_tests.py27
-rw-r--r--unitTests.py87
3 files changed, 113 insertions, 23 deletions
diff --git a/pyparsing.py b/pyparsing.py
index 578a07c..af88035 100644
--- a/pyparsing.py
+++ b/pyparsing.py
@@ -4647,18 +4647,18 @@ class Forward(ParseElementEnhance):
def __str__( self ):
if hasattr(self,"name"):
return self.name
- return self.__class__.__name__ + ": ..."
- # stubbed out for now - creates awful memory and perf issues
- self._revertClass = self.__class__
- self.__class__ = _ForwardNoRecurse
+ # Avoid infinite recursion by setting a temporary name
+ self.name = self.__class__.__name__ + ": ..."
+
+ # Use the string representation of main expression.
try:
if self.expr is not None:
retString = _ustr(self.expr)
else:
retString = "None"
finally:
- self.__class__ = self._revertClass
+ del self.name
return self.__class__.__name__ + ": " + retString
def copy(self):
@@ -4669,10 +4669,6 @@ class Forward(ParseElementEnhance):
ret <<= self
return ret
-class _ForwardNoRecurse(Forward):
- def __str__( self ):
- return "..."
-
class TokenConverter(ParseElementEnhance):
"""
Abstract subclass of :class:`ParseExpression`, for converting parsed results.
@@ -5869,12 +5865,17 @@ def indentedBlock(blockStatementExpr, indentStack, indent=True):
':',
[[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]]
"""
+ backup_stack = indentStack[:]
+
+ def reset_stack():
+ indentStack[:] = backup_stack
+
def checkPeerIndent(s,l,t):
if l >= len(s): return
curCol = col(l,s)
if curCol != indentStack[-1]:
if curCol > indentStack[-1]:
- raise ParseFatalException(s,l,"illegal nesting")
+ raise ParseException(s,l,"illegal nesting")
raise ParseException(s,l,"not a peer entry")
def checkSubIndent(s,l,t):
@@ -5902,6 +5903,7 @@ def indentedBlock(blockStatementExpr, indentStack, indent=True):
else:
smExpr = Group( Optional(NL) +
(OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) )
+ smExpr.setFailAction(lambda a, b, c, d: reset_stack())
blockStatementExpr.ignore(_bslash + LineEnd())
return smExpr.setName('indented block')
diff --git a/simple_unit_tests.py b/simple_unit_tests.py
index fee6d16..888b4a8 100644
--- a/simple_unit_tests.py
+++ b/simple_unit_tests.py
@@ -29,6 +29,15 @@ class PyparsingExpressionTestCase(unittest.TestCase):
given text strings. Subclasses must define a class attribute 'tests' which
is a list of PpTestSpec instances.
"""
+
+ if not hasattr(unittest.TestCase, 'subTest'):
+ # Python 2 compatibility
+ from contextlib import contextmanager
+ @contextmanager
+ def subTest(self, **params):
+ print('subTest:', params)
+ yield
+
tests = []
def runTest(self):
if self.__class__ is PyparsingExpressionTestCase:
@@ -44,9 +53,9 @@ class PyparsingExpressionTestCase(unittest.TestCase):
# the location against an expected value
with self.subTest(test_spec=test_spec):
test_spec.expr.streamline()
- print("\n{} - {}({})".format(test_spec.desc,
- type(test_spec.expr).__name__,
- test_spec.expr))
+ print("\n{0} - {1}({2})".format(test_spec.desc,
+ type(test_spec.expr).__name__,
+ test_spec.expr))
parsefn = getattr(test_spec.expr, test_spec.parse_fn)
if test_spec.expected_fail_locn is None:
@@ -69,12 +78,16 @@ class PyparsingExpressionTestCase(unittest.TestCase):
# compare results against given list and/or dict
if test_spec.expected_list is not None:
self.assertEqual([result], test_spec.expected_list)
-
else:
# expect fail
try:
parsefn(test_spec.text)
except Exception as exc:
+ if not hasattr(exc, '__traceback__'):
+ # Python 2 compatibility
+ from sys import exc_info
+ etype, value, traceback = exc_info()
+ exc.__traceback__ = traceback
print(pp.ParseException.explain(exc))
self.assertEqual(exc.loc, test_spec.expected_fail_locn)
else:
@@ -434,12 +447,6 @@ class TestCommonHelperExpressions(PyparsingExpressionTestCase):
#============ MAIN ================
if __name__ == '__main__':
- # we use unittest features that are in Py3 only, bail out if run on Py2
- import sys
- if sys.version_info[0] < 3:
- print("simple_unit_tests.py runs on Python 3 only")
- sys.exit(0)
-
import inspect
def get_decl_line_no(cls):
return inspect.getsourcelines(cls)[1]
diff --git a/unitTests.py b/unitTests.py
index caf538d..9a44193 100644
--- a/unitTests.py
+++ b/unitTests.py
@@ -2891,7 +2891,8 @@ class UnicodeExpressionTest(ParseTestCase):
class SetNameTest(ParseTestCase):
def runTest(self):
from pyparsing import (oneOf,infixNotation,Word,nums,opAssoc,delimitedList,countedArray,
- nestedExpr,makeHTMLTags,anyOpenTag,anyCloseTag,commonHTMLEntity,replaceHTMLEntity)
+ nestedExpr,makeHTMLTags,anyOpenTag,anyCloseTag,commonHTMLEntity,replaceHTMLEntity,
+ Forward,ZeroOrMore)
a = oneOf("a b c")
b = oneOf("d e f")
@@ -2904,6 +2905,8 @@ class SetNameTest(ParseTestCase):
[
(('?',':'),3,opAssoc.LEFT),
])
+ recursive = Forward()
+ recursive <<= a + ZeroOrMore(b + recursive)
tests = [
a,
@@ -2913,6 +2916,7 @@ class SetNameTest(ParseTestCase):
arith_expr.expr,
arith_expr2,
arith_expr2.expr,
+ recursive,
delimitedList(Word(nums).setName("int")),
countedArray(Word(nums).setName("int")),
nestedExpr(),
@@ -2926,10 +2930,11 @@ class SetNameTest(ParseTestCase):
a | b | c
d | e | f
{a | b | c | d | e | f}
- Forward: ...
+ Forward: + | - term
+ | - term
- Forward: ...
+ Forward: ?: term
?: term
+ Forward: {a | b | c [{d | e | f Forward: ...}]...}
int [, int]...
(len) int...
nested () expression
@@ -3842,6 +3847,82 @@ class IndentedBlockTest(ParseTestCase):
self.assertEqual(result.c.c1, 200, "invalid indented block result")
self.assertEqual(result.c.c2.c21, 999, "invalid indented block result")
+
+class IndentedBlockScanTest(ParseTestCase):
+ def get_parser(self):
+ """
+ A valid statement is the word "block:", followed by an indent, followed by the letter A only, or another block
+ """
+ stack = [1]
+ block = pp.Forward()
+ body = pp.indentedBlock(pp.Literal('A') ^ block, indentStack=stack, indent=True)
+ block <<= pp.Literal('block:') + body
+ return block
+
+ def runTest(self):
+ from textwrap import dedent
+
+ # This input string is a perfect match for the parser, so a single match is found
+ p1 = self.get_parser()
+ r1 = list(p1.scanString(dedent("""\
+ block:
+ A
+ """)))
+ self.assertEqual(len(r1), 1)
+
+ # This input string is a perfect match for the parser, except for the letter B instead of A, so this will fail (and should)
+ p2 = self.get_parser()
+ r2 = list(p2.scanString(dedent("""\
+ block:
+ B
+ """)))
+ self.assertEqual(len(r2), 0)
+
+ # This input string contains both string A and string B, and it finds one match (as it should)
+ p3 = self.get_parser()
+ r3 = list(p3.scanString(dedent("""\
+ block:
+ A
+ block:
+ B
+ """)))
+ self.assertEqual(len(r3), 1)
+
+ # This input string contains both string A and string B, but in a different order.
+ p4 = self.get_parser()
+ r4 = list(p4.scanString(dedent("""\
+ block:
+ B
+ block:
+ A
+ """)))
+ self.assertEqual(len(r4), 1)
+
+ # This is the same as case 3, but with nesting
+ p5 = self.get_parser()
+ r5 = list(p5.scanString(dedent("""\
+ block:
+ block:
+ A
+ block:
+ block:
+ B
+ """)))
+ self.assertEqual(len(r5), 1)
+
+ # This is the same as case 4, but with nesting
+ p6 = self.get_parser()
+ r6 = list(p6.scanString(dedent("""\
+ block:
+ block:
+ B
+ block:
+ block:
+ A
+ """)))
+ self.assertEqual(len(r6), 1)
+
+
class ParseResultsWithNameMatchFirst(ParseTestCase):
def runTest(self):
import pyparsing as pp