summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul McGuire <ptmcg@austin.rr.com>2019-07-09 06:20:34 -0500
committerPaul McGuire <ptmcg@austin.rr.com>2019-07-09 06:20:34 -0500
commit95d4aa4c4fc9b285be17d9a3ac15afb28fd01a2a (patch)
tree0683830cdb0cf1410a60d65cdc8d42b4fbad52da
parent3c64e6ed43e8f5f435836cc91983ef1eb65b83c2 (diff)
downloadpyparsing-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--CHANGES28
-rw-r--r--pyparsing.py44
-rw-r--r--unitTests.py98
3 files changed, 150 insertions, 20 deletions
diff --git a/CHANGES b/CHANGES
index e1e98e3..3f094f7 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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):