diff options
author | Paul McGuire <ptmcg@austin.rr.com> | 2019-07-09 06:20:34 -0500 |
---|---|---|
committer | Paul McGuire <ptmcg@austin.rr.com> | 2019-07-09 06:20:34 -0500 |
commit | 95d4aa4c4fc9b285be17d9a3ac15afb28fd01a2a (patch) | |
tree | 0683830cdb0cf1410a60d65cdc8d42b4fbad52da | |
parent | 3c64e6ed43e8f5f435836cc91983ef1eb65b83c2 (diff) | |
download | pyparsing-git-95d4aa4c4fc9b285be17d9a3ac15afb28fd01a2a.tar.gz |
Cleaned up CHANGES to accurately describe the pre/post 2.3.0 bugfix behavior; added file argument to runTests; added conditionAsParseAction helper
-rw-r--r-- | CHANGES | 28 | ||||
-rw-r--r-- | pyparsing.py | 44 | ||||
-rw-r--r-- | unitTests.py | 98 |
3 files changed, 150 insertions, 20 deletions
@@ -27,9 +27,12 @@ Version 2.4.1 - July, 2019 . __compat__.collect_all_And_tokens will not be settable to False to revert to pre-2.3.1 results name behavior - - review use of names for hierarchical groups, and be sure - to use explicit pyparsing Groups to implement - hierarchical structures + review use of names for MatchFirst and Or expressions + containing And expressions, as they will return the + complete list of parsed tokens, not just the first one. + Use __diag__.warn_multiple_tokens_in_named_alternation + (described below) to help identify those expressions + in your parsers that will have changed as a result. - A new shorthand notation has been added for repetition expressions: expr[min, max], with '...' valid as a min @@ -118,9 +121,8 @@ Version 2.4.1 - July, 2019 warn_multiple_tokens_in_named_alternation is intended to help those who currently have set __compat__.collect_all_And_tokens to - False as a workaround for using the pre-2.3.0 code with ungrouped - results names nested in an outer expression also having a results - name. + False as a workaround for using the pre-2.3.1 code with named + MatchFirst or Or expressions containing an And expression. - Added ParseResults.from_dict classmethod, to simplify creation of a ParseResults with results names. May be called with a dict @@ -131,6 +133,20 @@ Version 2.4.1 - July, 2019 - Added asKeyword argument (default=False) to oneOf, to force keyword-style matching on the generated expressions. +- ParserElement.runTests now accepts an optional 'file' argument to + redirect test output to a file-like object (such as a StringIO, + or opened file). Default is to write to sys.stdout. + +- conditionAsParseAction is a helper method for constructing a + parse action method from a predicate function that simply + returns a boolean result. Useful for those places where a + predicate cannot be added using addCondition, but must be + converted to a parse action (such as in infixNotation). May be + used as a decorator if default message and exception types + can be used. See ParserElement.addCondition for more details + about the expected signature and behavior for predicate condition + methods. + - While investigating issue #93, I found that Or and addCondition could interact to select an alternative that is not the longest match. This is because Or first checks diff --git a/pyparsing.py b/pyparsing.py index 77617d2..13d747f 100644 --- a/pyparsing.py +++ b/pyparsing.py @@ -96,7 +96,7 @@ classes inherit from. Use the docstrings for examples of how to: """ __version__ = "2.4.1" -__versionTime__ = "08 Jul 2019 02:00 UTC" +__versionTime__ = "09 Jul 2019 10:47 UTC" __author__ = "Paul McGuire <ptmcg@users.sourceforge.net>" import string @@ -113,6 +113,7 @@ import types from datetime import datetime from operator import itemgetter import itertools +from functools import wraps try: # Python 3 @@ -274,6 +275,19 @@ alphanums = alphas + nums _bslash = chr(92) printables = "".join(c for c in string.printable if c not in string.whitespace) + +def conditionAsParseAction(fn, message=None, fatal=False): + msg = message if message is not None else "failed user-defined condition" + exc_type = ParseFatalException if fatal else ParseException + fn = _trim_arity(fn) + + @wraps(fn) + def pa(s, l, t): + if not bool(fn(s, l, t)): + raise exc_type(s, l, msg) + + return pa + class ParseBaseException(Exception): """base exception class for all parsing runtime exceptions""" # Performance tuning: we construct a *lot* of these, so keep this @@ -1539,14 +1553,10 @@ class ParserElement(object): result = date_str.parseString("1999/12/31") # -> Exception: Only support years 2000 and later (at char 0), (line:1, col:1) """ - msg = kwargs.get("message", "failed user-defined condition") - exc_type = ParseFatalException if kwargs.get("fatal", False) else ParseException for fn in fns: - fn = _trim_arity(fn) - def pa(s,l,t): - if not bool(fn(s,l,t)): - raise exc_type(s,l,msg) - self.parseAction.append(pa) + self.parseAction.append(conditionAsParseAction(fn, message=kwargs.get('message'), + fatal=kwargs.get('fatal', False))) + self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False) return self @@ -1679,7 +1689,6 @@ class ParserElement(object): #~ print ("Matched",self,"->",retTokens.asList()) if self.debugActions[MATCH]: self.debugActions[MATCH]( instring, tokensStart, loc, self, retTokens ) - print("do_actions =", doActions) return loc, retTokens @@ -2560,7 +2569,8 @@ class ParserElement(object): return False def runTests(self, tests, parseAll=True, comment='#', - fullDump=True, printResults=True, failureTests=False, postParse=None): + fullDump=True, printResults=True, failureTests=False, postParse=None, + file=None): """ Execute the parse expression on a series of test strings, showing each test, the parsed results or where the parse failed. Quick and easy way to @@ -2577,6 +2587,8 @@ class ParserElement(object): - failureTests - (default= ``False``) indicates if these tests are expected to fail parsing - postParse - (default= ``None``) optional callback for successful parse results; called as `fn(test_string, parse_results)` and returns a string to be added to the test output + - file - (default=``None``) optional file-like object to which test output will be written; + if None, will default to ``sys.stdout`` Returns: a (success, results) tuple, where success indicates that all tests succeeded (or failed if ``failureTests`` is True), and the results contain a list of lines of each @@ -2656,6 +2668,10 @@ class ParserElement(object): tests = list(map(str.strip, tests.rstrip().splitlines())) if isinstance(comment, basestring): comment = Literal(comment) + if file is None: + file = sys.stdout + print_ = file.write + allResults = [] comments = [] success = True @@ -2708,7 +2724,7 @@ class ParserElement(object): if printResults: if fullDump: out.append('') - print('\n'.join(out)) + print_('\n'.join(out)) allResults.append((t, result)) @@ -4097,7 +4113,8 @@ class Or(ParseExpression): and __diag__.warn_multiple_tokens_in_named_alternation): if any(isinstance(e, And) for e in self.exprs): warnings.warn("{0}: setting results name {1!r} on {2} expression " - "may only return a single token for an And alternative".format( + "may only return a single token for an And alternative, " + "in future will return the full list of tokens".format( "warn_multiple_tokens_in_named_alternation", name, type(self).__name__), stacklevel=3) @@ -4182,7 +4199,8 @@ class MatchFirst(ParseExpression): and __diag__.warn_multiple_tokens_in_named_alternation): if any(isinstance(e, And) for e in self.exprs): warnings.warn("{0}: setting results name {1!r} on {2} expression " - "may only return a single token for an And alternative".format( + "may only return a single token for an And alternative, " + "in future will return the full list of tokens".format( "warn_multiple_tokens_in_named_alternation", name, type(self).__name__), stacklevel=3) diff --git a/unitTests.py b/unitTests.py index 252cdd6..0c1ebee 100644 --- a/unitTests.py +++ b/unitTests.py @@ -4387,9 +4387,14 @@ class ParseResultsWithNameOr(ParseTestCase): # test compatibility mode, restoring pre-2.3.1 behavior with AutoReset(pp.__compat__, "collect_all_And_tokens"): pp.__compat__.collect_all_And_tokens = False + pp.__diag__.warn_multiple_tokens_in_named_alternation = True expr_a = pp.Literal('not') + pp.Literal('the') + pp.Literal('bird') expr_b = pp.Literal('the') + pp.Literal('bird') - expr = (expr_a ^ expr_b)('rexp') + if PY_3: + with self.assertWarns(UserWarning, msg="failed to warn of And within alternation"): + expr = (expr_a ^ expr_b)('rexp') + else: + expr = (expr_a ^ expr_b)('rexp') expr.runTests("""\ not the bird the bird @@ -4522,6 +4527,97 @@ class OneOfKeywordsTest(ParseTestCase): self.assertTrue(success, "failed keyword oneOf failure tests") +class WarnUngroupedNamedTokensTest(ParseTestCase): + """ + - warn_ungrouped_named_tokens_in_collection - flag to enable warnings when a results + name is defined on a containing expression with ungrouped subexpressions that also + have results names (default=True) + """ + def runTest(self): + import pyparsing as pp + ppc = pp.pyparsing_common + + pp.__diag__.warn_ungrouped_named_tokens_in_collection = True + + COMMA = pp.Suppress(',').setName("comma") + coord = (ppc.integer('x') + COMMA + ppc.integer('y')) + + # this should emit a warning + if PY_3: + with self.assertWarns(UserWarning, msg="failed to warn with named repetition of" + " ungrouped named expressions"): + path = coord[...].setResultsName('path') + + +class WarnNameSetOnEmptyForwardTest(ParseTestCase): + """ + - warn_name_set_on_empty_Forward - flag to enable warnings whan a Forward is defined + with a results name, but has no contents defined (default=False) + """ + def runTest(self): + import pyparsing as pp + + pp.__diag__.warn_name_set_on_empty_Forward = True + + base = pp.Forward() + + if PY_3: + with self.assertWarns(UserWarning, msg="failed to warn when naming an empty Forward expression"): + base("x") + + +class WarnOnMultipleStringArgsToOneOfTest(ParseTestCase): + """ + - warn_on_multiple_string_args_to_oneof - flag to enable warnings whan oneOf is + incorrectly called with multiple str arguments (default=True) + """ + def runTest(self): + import pyparsing as pp + + pp.__diag__.warn_on_multiple_string_args_to_oneof = True + + if PY_3: + with self.assertWarns(UserWarning, msg="failed to warn when incorrectly calling oneOf(string, string)"): + a = pp.oneOf('A', 'B') + + +class EnableDebugOnNamedExpressionsTest(ParseTestCase): + """ + - enable_debug_on_named_expressions - flag to auto-enable debug on all subsequent + calls to ParserElement.setName() (default=False) + """ + def runTest(self): + import pyparsing as pp + import textwrap + + test_stdout = StringIO() + + with AutoReset(sys, 'stdout', 'stderr'): + sys.stdout = test_stdout + sys.stderr = test_stdout + + pp.__diag__.enable_debug_on_named_expressions = True + integer = pp.Word(pp.nums).setName('integer') + + integer[...].parseString("1 2 3") + + expected_debug_output = textwrap.dedent("""\ + Match integer at loc 0(1,1) + Matched integer -> ['1'] + Match integer at loc 1(1,2) + Matched integer -> ['2'] + Match integer at loc 3(1,4) + Matched integer -> ['3'] + Match integer at loc 5(1,6) + Exception raised:Expected integer, found end of text (at char 5), (line:1, col:6) + """) + output = test_stdout.getvalue() + print_(output) + self.assertEquals(output, + expected_debug_output, + "failed to auto-enable debug on named expressions " + "using enable_debug_on_named_expressions") + class MiscellaneousParserTests(ParseTestCase): def runTest(self): |