From f80565a285c1cc321344d3da8280c09560091491 Mon Sep 17 00:00:00 2001 From: Eric Wald Date: Sun, 10 Mar 2019 14:38:43 -0600 Subject: Support for simple_unit_tests under Python 2 (#72) --- pyparsing.py | 6 +++++- simple_unit_tests.py | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pyparsing.py b/pyparsing.py index 4ba5b11..3befc7c 100644 --- a/pyparsing.py +++ b/pyparsing.py @@ -350,7 +350,11 @@ class ParseException(ParseBaseException): callers = inspect.getinnerframes(exc.__traceback__, context=depth) seen = set() for i, ff in enumerate(callers[-depth:]): - frm = ff.frame + if isinstance(ff, tuple): + # Python 2 compatibility + frm = ff[0] + else: + frm = ff.frame f_self = frm.f_locals.get('self', None) if isinstance(f_self, ParserElement): diff --git a/simple_unit_tests.py b/simple_unit_tests.py index fee6d16..d5181fd 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: @@ -75,6 +84,11 @@ class PyparsingExpressionTestCase(unittest.TestCase): 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 +448,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] -- cgit v1.2.1 From 64cbc7245410223e33d81666381821526be48650 Mon Sep 17 00:00:00 2001 From: ptmcg Date: Sun, 10 Mar 2019 15:46:06 -0500 Subject: Py2 simple_unit_tests.py compat changes - part 2 --- simple_unit_tests.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/simple_unit_tests.py b/simple_unit_tests.py index d5181fd..888b4a8 100644 --- a/simple_unit_tests.py +++ b/simple_unit_tests.py @@ -53,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: @@ -78,7 +78,6 @@ 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: -- cgit v1.2.1 From ed2f5ec744ddc25242f947be8ba798d9fca6a674 Mon Sep 17 00:00:00 2001 From: Eric Wald Date: Sun, 10 Mar 2019 14:53:14 -0600 Subject: Descriptive names for Forward expressions (#71) Resolves the infinite recursion potential by setting a temporary name during resolution. --- pyparsing.py | 14 +++++--------- unitTests.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pyparsing.py b/pyparsing.py index 3befc7c..4a79904 100644 --- a/pyparsing.py +++ b/pyparsing.py @@ -4636,18 +4636,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): @@ -4658,10 +4658,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. diff --git a/unitTests.py b/unitTests.py index 4786255..b7d11bc 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 -- cgit v1.2.1 From fed0f3da669a9a81829651080b6dfe32ce44c1b0 Mon Sep 17 00:00:00 2001 From: TMiguelT Date: Wed, 13 Mar 2019 10:41:23 +1100 Subject: Fix cases where an indentedBlock partially parsing will cause all other correct indentedBlocks to fail (#75) * Add failing test for indentedBlock * Simplify fix by storing a backup stack --- pyparsing.py | 8 ++++++- unitTests.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/pyparsing.py b/pyparsing.py index 4a79904..8781ff7 100644 --- a/pyparsing.py +++ b/pyparsing.py @@ -5854,12 +5854,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): @@ -5887,6 +5892,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/unitTests.py b/unitTests.py index b7d11bc..be13631 100644 --- a/unitTests.py +++ b/unitTests.py @@ -3847,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 -- cgit v1.2.1