diff options
author | Paul McGuire <ptmcg@austin.rr.com> | 2019-03-30 02:28:19 -0500 |
---|---|---|
committer | Paul McGuire <ptmcg@austin.rr.com> | 2019-03-30 02:28:19 -0500 |
commit | 5a2fd3bd482d7c3c61d054601c4a17a1111e7747 (patch) | |
tree | 412cc2641682f168bd3c9ec7f917161307025283 | |
parent | 897696352f4b0a03152058c01403a42b07be650c (diff) | |
parent | fed0f3da669a9a81829651080b6dfe32ce44c1b0 (diff) | |
download | pyparsing-git-5a2fd3bd482d7c3c61d054601c4a17a1111e7747.tar.gz |
Merge remote-tracking branch 'origin/master'
# Conflicts:
# pyparsing.py
-rw-r--r-- | pyparsing.py | 22 | ||||
-rw-r--r-- | simple_unit_tests.py | 27 | ||||
-rw-r--r-- | unitTests.py | 87 |
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 |