Imported Upstream version 2.4.1.1 upstream/2.4.1.1
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 18 Jul 2022 05:42:31 +0000 (14:42 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 18 Jul 2022 05:42:31 +0000 (14:42 +0900)
21 files changed:
CHANGES
PKG-INFO
examples/LAparser.py
examples/chemicalFormulas.py
examples/commasep.py
examples/deltaTime.py [deleted file]
examples/delta_time.py [new file with mode: 0644]
examples/eval_arith.py
examples/httpServerLogParser.py
examples/lucene_grammar.py
examples/nested_markup.py [new file with mode: 0644]
examples/protobuf_parser.py
examples/removeLineBreaks.py
examples/select_parser.py
examples/sexpParser.py
examples/wordsToNum.py
pyparsing.egg-info/PKG-INFO
pyparsing.egg-info/SOURCES.txt
pyparsing.py
simple_unit_tests.py
unitTests.py

diff --git a/CHANGES b/CHANGES
index e1fa06eb0a993f683c8946b82fc686587b0ae0b5..397af9c5c5369c35fab8c6bee02b578d8348b851 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -2,6 +2,258 @@
 Change Log
 ==========
 
+Version 2.4.1.1 - July 24, 2019
+-------------------------------
+This is a re-release of version 2.4.1 to restore the release history
+in PyPI, since the 2.4.1 release was deleted.
+
+There are 3 known issues in this release, which are fixed in
+the upcoming 2.4.2:
+
+- API change adding support for `expr[...]` - the original
+  code in 2.4.1 incorrectly implemented this as OneOrMore.
+  Code using this feature under this relase should explicitly
+  use `expr[0, ...]` for ZeroOrMore and `expr[1, ...]` for
+  OneOrMore. In 2.4.2 you will be able to write `expr[...]`
+  equivalent to `ZeroOrMore(expr)`.
+
+- Bug if composing And, Or, MatchFirst, or Each expressions
+  using an expression. This only affects code which uses
+  explicit expression construction using the And, Or, etc.
+  classes instead of using overloaded operators '+', '^', and
+  so on. If constructing an And using a single expression,
+  you may get an error that "cannot multiply ParserElement by
+  0 or (0, 0)" or a Python `IndexError`. Change code like
+
+    cmd = Or(Word(alphas))
+
+  to
+
+    cmd = Or([Word(alphas)])
+
+  (Note that this is not the recommended style for constructing
+  Or expressions.)
+
+- Some newly-added `__diag__` switches are enabled by default,
+  which may give rise to noisy user warnings for existing parsers.
+  You can disable them using:
+
+    import pyparsing as pp
+    pp.__diag__.warn_multiple_tokens_in_named_alternation = False
+    pp.__diag__.warn_ungrouped_named_tokens_in_collection = False
+    pp.__diag__.warn_name_set_on_empty_Forward = False
+    pp.__diag__.warn_on_multiple_string_args_to_oneof = False
+    pp.__diag__.enable_debug_on_named_expressions = False
+
+  In 2.4.2 these will all be set to False by default.
+
+
+Version 2.4.1 - July, 2019
+--------------------------
+- NOTE: Deprecated functions and features that will be dropped
+  in pyparsing 2.5.0 (planned next release):
+
+  . support for Python 2 - ongoing users running with
+    Python 2 can continue to use pyparsing 2.4.1
+
+  . ParseResults.asXML() - if used for debugging, switch
+    to using ParseResults.dump(); if used for data transfer,
+    use ParseResults.asDict() to convert to a nested Python
+    dict, which can then be converted to XML or JSON or
+    other transfer format
+
+  . operatorPrecedence synonym for infixNotation -
+    convert to calling infixNotation
+
+  . commaSeparatedList - convert to using
+    pyparsing_common.comma_separated_list
+
+  . upcaseTokens and downcaseTokens - convert to using
+    pyparsing_common.upcaseTokens and downcaseTokens
+
+  . __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 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
+  or max value:
+     - expr[...] is equivalent to OneOrMore(expr)
+     - expr[0, ...] is equivalent to ZeroOrMore(expr)
+     - expr[1, ...] is equivalent to OneOrMore(expr)
+     - expr[n, ...] or expr[n,] is equivalent
+          to expr*n + ZeroOrMore(expr)
+          (read as "n or more instances of expr")
+     - expr[..., n] is equivalent to expr*(0, n)
+     - expr[m, n] is equivalent to expr*(m, n)
+  Note that expr[..., n] and expr[m, n] do not raise an exception
+  if more than n exprs exist in the input stream.  If this
+  behavior is desired, then write expr[..., n] + ~expr.
+
+- '...' can also be used as short hand for SkipTo when used
+  in adding parse expressions to compose an And expression.
+
+      Literal('start') + ... + Literal('end')
+      And(['start', ..., 'end'])
+
+  are both equivalent to:
+
+      Literal('start') + SkipTo('end')("_skipped*") + Literal('end')
+
+  The '...' form has the added benefit of not requiring repeating
+  the skip target expression. Note that the skipped text is
+  returned with '_skipped' as a results name, and that the contents of
+  `_skipped` will contain a list of text from all `...`s in the expression.
+
+- '...' can also be used as a "skip forward in case of error" expression:
+
+        expr = "start" + (Word(nums).setName("int") | ...) + "end"
+
+        expr.parseString("start 456 end")
+        ['start', '456', 'end']
+
+        expr.parseString("start 456 foo 789 end")
+        ['start', '456', 'foo 789 ', 'end']
+        - _skipped: ['foo 789 ']
+
+        expr.parseString("start foo end")
+        ['start', 'foo ', 'end']
+        - _skipped: ['foo ']
+
+        expr.parseString("start end")
+        ['start', '', 'end']
+        - _skipped: ['missing <int>']
+
+  Note that in all the error cases, the '_skipped' results name is
+  present, showing a list of the extra or missing items.
+
+  This form is only valid when used with the '|' operator.
+
+- Improved exception messages to show what was actually found, not
+  just what was expected.
+
+    word = pp.Word(pp.alphas)
+    pp.OneOrMore(word).parseString("aaa bbb 123", parseAll=True)
+
+  Former exception message:
+
+    pyparsing.ParseException: Expected end of text (at char 8), (line:1, col:9)
+
+  New exception message:
+
+    pyparsing.ParseException: Expected end of text, found '1' (at char 8), (line:1, col:9)
+
+- Added diagnostic switches to help detect and warn about common
+  parser construction mistakes, or enable additional parse
+  debugging. Switches are attached to the pyparsing.__diag__
+  namespace object:
+     - warn_multiple_tokens_in_named_alternation - flag to enable warnings when a results
+       name is defined on a MatchFirst or Or expression with one or more And subexpressions
+       (default=True)
+     - 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)
+     - 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)
+     - warn_on_multiple_string_args_to_oneof - flag to enable warnings whan oneOf is
+       incorrectly called with multiple str arguments (default=True)
+     - enable_debug_on_named_expressions - flag to auto-enable debug on all subsequent
+       calls to ParserElement.setName() (default=False)
+
+  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.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 using a dict, which may be nested.
+  This makes it easy to add a sub-level of named items to the parsed
+  tokens in a parse action.
+
+- 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
+  all alternatives for matches without running attached
+  parse actions or conditions, orders by longest match, and
+  then rechecks for matches with conditions and parse actions.
+  Some expressions, when checking with conditions, may end
+  up matching on a shorter token list than originally matched,
+  but would be selected because of its original priority.
+  This matching code has been expanded to do more extensive
+  searching for matches when a second-pass check matches a
+  smaller list than in the first pass.
+
+- Fixed issue #87, a regression in indented block.
+  Reported by Renz Bagaporo, who submitted a very nice repro
+  example, which makes the bug-fixing process a lot easier,
+  thanks!
+
+- Fixed MemoryError issue #85 and #91 with str generation for
+  Forwards. Thanks decalage2 and Harmon758 for your patience.
+
+- Modified setParseAction to accept None as an argument,
+  indicating that all previously-defined parse actions for the
+  expression should be cleared.
+
+- Modified pyparsing_common.real and sci_real to parse reals
+  without leading integer digits before the decimal point,
+  consistent with Python real number formats. Original PR #98
+  submitted by ansobolev.
+
+- Modified runTests to call postParse function before dumping out
+  the parsed results - allows for postParse to add further results,
+  such as indications of additional validation success/failure.
+
+- Updated statemachine example: refactored state transitions to use
+  overridden classmethods; added <statename>Mixin class to simplify
+  definition of application classes that "own" the state object and
+  delegate to it to model state-specific properties and behavior.
+
+- Added example nested_markup.py, showing a simple wiki markup with
+  nested markup directives, and illustrating the use of '...' for
+  skipping over input to match the next expression. (This example
+  uses syntax that is not valid under Python 2.)
+
+- Rewrote delta_time.py example (renamed from deltaTime.py) to
+  fix some omitted formats and upgrade to latest pyparsing idioms,
+  beginning with writing an actual BNF.
+
+- With the help and encouragement from several contributors, including
+  Matěj Cepl and Cengiz Kaygusuz, I've started cleaning up the internal
+  coding styles in core pyparsing, bringing it up to modern coding
+  practices from pyparsing's early development days dating back to
+  2003. Whitespace has been largely standardized along PEP8 guidelines,
+  removing extra spaces around parentheses, and adding them around
+  arithmetic operators and after colons and commas. I was going to hold
+  off on doing this work until after 2.4.1, but after cleaning up a
+  few trial classes, the difference was so significant that I continued
+  on to the rest of the core code base. This should facilitate future
+  work and submitted PRs, allowing them to focus on substantive code
+  changes, and not get sidetracked by whitespace issues.
+
+
 Version 2.4.0 - April, 2019
 ---------------------------
 - Well, it looks like the API change that was introduced in 2.3.1 was more
@@ -1428,7 +1680,7 @@ finally time to bump the minor rev number on pyparsing - so
         stack (multiple indentedBlock expressions
         within a single grammar should share a common indentStack)
     - indent - boolean indicating whether block must be indented
-        beyond the the current level; set to False for block of
+        beyond the current level; set to False for block of
         left-most statements (default=True)
 
   A valid block must contain at least one indented statement.
index 20ce5c23346087ba32b4eafd12753eb7b27d1374..113698f8f49b711ee1a77b989a18750d11b1c15a 100644 (file)
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: pyparsing
-Version: 2.4.0
+Version: 2.4.1.1
 Summary: Python parsing module
 Home-page: https://github.com/pyparsing/pyparsing/
 Author: Paul McGuire
index 41e8b4ff2d4805e0bfda65c595704f09aee3ea86..330b8f5c0c7a6db84ac34b01835e2aaea4ab6e71 100644 (file)
-"""\r
-Purpose:   Linear Algebra Parser\r
-Based on:  SimpleCalc.py example (author Paul McGuire) in pyparsing-1.3.3\r
-Author:    Mike Ellis\r
-Copyright: Ellis & Grant, Inc. 2005\r
-License:   You may freely use, modify, and distribute this software.\r
-Warranty:  THIS SOFTWARE HAS NO WARRANTY WHATSOEVER. USE AT YOUR OWN RISK.\r
-Notes: Parses infix linear algebra (LA) notation for vectors, matrices, and scalars.\r
-       Output is C code function calls.  The parser can be run as an interactive\r
-       interpreter or included as module to use for in-place substitution into C files\r
-       containing LA equations.\r
-\r
-       Supported operations are:\r
-       OPERATION:              INPUT                    OUTPUT\r
-       Scalar addition:        "a = b+c"                "a=(b+c)"\r
-       Scalar subtraction:     "a = b-c"                "a=(b-c)"\r
-       Scalar multiplication:  "a = b*c"                "a=b*c"\r
-       Scalar division:        "a = b/c"                "a=b/c"\r
-       Scalar exponentiation:  "a = b^c"                "a=pow(b,c)"\r
-       Vector scaling:         "V3_a = V3_b * c"        "vCopy(a,vScale(b,c))"\r
-       Vector addition:        "V3_a = V3_b + V3_c"     "vCopy(a,vAdd(b,c))"\r
-       Vector subtraction:     "V3_a = V3_b - V3_c"     "vCopy(a,vSubtract(b,c))"\r
-       Vector dot product:     "a = V3_b * V3_c"        "a=vDot(b,c)"\r
-       Vector outer product:   "M3_a = V3_b @ V3_c"     "a=vOuterProduct(b,c)"\r
-       Vector magn. squared:   "a = V3_b^Mag2"          "a=vMagnitude2(b)"\r
-       Vector magnitude:       "a = V3_b^Mag"           "a=sqrt(vMagnitude2(b))"\r
-       Matrix scaling:         "M3_a = M3_b * c"        "mCopy(a,mScale(b,c))"\r
-       Matrix addition:        "M3_a = M3_b + M3_c"     "mCopy(a,mAdd(b,c))"\r
-       Matrix subtraction:     "M3_a = M3_b - M3_c"     "mCopy(a,mSubtract(b,c))"\r
-       Matrix multiplication:  "M3_a = M3_b * M3_c"     "mCopy(a,mMultiply(b,c))"\r
-       Matrix by vector mult.: "V3_a = M3_b * V3_c"     "vCopy(a,mvMultiply(b,c))"\r
-       Matrix inversion:       "M3_a = M3_b^-1"         "mCopy(a,mInverse(b))"\r
-       Matrix transpose:       "M3_a = M3_b^T"          "mCopy(a,mTranspose(b))"\r
-       Matrix determinant:     "a = M3_b^Det"           "a=mDeterminant(b)"\r
-\r
-       The parser requires the expression to be an equation.  Each non-scalar variable\r
-       must be prefixed with a type tag, 'M3_' for 3x3 matrices and 'V3_' for 3-vectors.\r
-       For proper compilation of the C code, the variables need to be declared without\r
-       the prefix as float[3] for vectors and float[3][3] for matrices. The operations do\r
-       not modify any variables on the right-hand side of the equation.\r
-\r
-       Equations may include nested expressions within parentheses. The allowed binary\r
-       operators are '+-*/^' for scalars, and '+-*^@' for vectors and matrices with the\r
-       meanings defined in the table above.\r
-\r
-       Specifying an improper combination of operands, e.g. adding a vector to a matrix,\r
-       is detected by the parser and results in a Python TypeError Exception. The usual cause\r
-       of this is omitting one or more tag prefixes. The parser knows nothing about a\r
-       a variable's C declaration and relies entirely on the type tags. Errors in C\r
-       declarations are not caught until compile time.\r
-\r
-Usage: To process LA equations embedded in source files, import this module and\r
-       pass input and output file objects to the fprocess() function.  You can\r
-       can also invoke the parser from the command line, e.g. 'python LAparser.py',\r
-       to run a small test suite and enter an interactive loop where you can enter\r
-       LA equations and see the resulting C code.\r
-\r
-"""\r
-\r
-import re,sys\r
-from pyparsing import Word, alphas, ParseException, Literal, CaselessLiteral \\r
-, Combine, Optional, nums, Forward, ZeroOrMore, \\r
-  StringEnd, alphanums\r
-\r
-# Debugging flag can be set to either "debug_flag=True" or "debug_flag=False"\r
-debug_flag=False\r
-\r
-#----------------------------------------------------------------------------\r
-# Variables that hold intermediate parsing results and a couple of\r
-# helper functions.\r
-exprStack = []      # Holds operators and operands parsed from input.\r
-targetvar = None    # Holds variable name to left of '=' sign in LA equation.\r
-\r
-\r
-def _pushFirst( str, loc, toks ):\r
-    if debug_flag: print("pushing ", toks[0], "str is ", str)\r
-    exprStack.append( toks[0] )\r
-\r
-def _assignVar( str, loc, toks ):\r
-    global targetvar\r
-    targetvar =  toks[0]\r
-\r
-#-----------------------------------------------------------------------------\r
-# The following statements define the grammar for the parser.\r
-\r
-point = Literal('.')\r
-e = CaselessLiteral('E')\r
-plusorminus = Literal('+') | Literal('-')\r
-number = Word(nums)\r
-integer = Combine( Optional(plusorminus) + number )\r
-floatnumber = Combine( integer +\r
-                       Optional( point + Optional(number) ) +\r
-                       Optional( e + integer )\r
-                     )\r
-\r
-lbracket = Literal("[")\r
-rbracket = Literal("]")\r
-ident = Forward()\r
-## The definition below treats array accesses as identifiers. This means your expressions\r
-## can include references to array elements, rows and columns, e.g., a = b[i] + 5.\r
-## Expressions within []'s are not presently supported, so a = b[i+1] will raise\r
-## a ParseException.\r
-ident = Combine(Word(alphas + '-',alphanums + '_') + \\r
-                ZeroOrMore(lbracket + (Word(alphas + '-',alphanums + '_')|integer) + rbracket) \\r
-                )\r
-\r
-plus  = Literal( "+" )\r
-minus = Literal( "-" )\r
-mult  = Literal( "*" )\r
-div   = Literal( "/" )\r
-outer = Literal( "@" )\r
-lpar  = Literal( "(" ).suppress()\r
-rpar  = Literal( ")" ).suppress()\r
-addop  = plus | minus\r
-multop = mult | div | outer\r
-expop = Literal( "^" )\r
-assignop = Literal( "=" )\r
-\r
-expr = Forward()\r
-atom = ( ( e | floatnumber | integer | ident  ).setParseAction(_pushFirst) |\r
-         ( lpar + expr.suppress() + rpar )\r
-       )\r
-factor = Forward()\r
-factor << atom + ZeroOrMore( ( expop + factor ).setParseAction( _pushFirst ) )\r
-\r
-term = factor + ZeroOrMore( ( multop + factor ).setParseAction( _pushFirst ) )\r
-expr << term + ZeroOrMore( ( addop + term ).setParseAction( _pushFirst ) )\r
-equation = (ident + assignop).setParseAction(_assignVar) + expr + StringEnd()\r
-\r
-# End of grammar definition\r
-#-----------------------------------------------------------------------------\r
-## The following are helper variables and functions used by the Binary Infix Operator\r
-## Functions described below.\r
-\r
-vprefix = 'V3_'\r
-vplen = len(vprefix)\r
-mprefix = 'M3_'\r
-mplen = len(mprefix)\r
-\r
-## We don't support unary negation for vectors and matrices\r
-class UnaryUnsupportedError(Exception): pass\r
-\r
-def _isvec(ident):\r
-   if ident[0] == '-' and ident[1:vplen+1] == vprefix:\r
-      raise UnaryUnsupportedError\r
-   else: return ident[0:vplen] == vprefix\r
-\r
-def _ismat(ident):\r
-   if ident[0] == '-' and ident[1:mplen+1] == mprefix:\r
-      raise UnaryUnsupportedError\r
-   else: return ident[0:mplen] == mprefix\r
-\r
-def _isscalar(ident): return not (_isvec(ident) or _ismat(ident))\r
-\r
-## Binary infix operator (BIO) functions.  These are called when the stack evaluator\r
-## pops a binary operator like '+' or '*".  The stack evaluator pops the two operand, a and b,\r
-## and calls the function that is mapped to the operator with a and b as arguments.  Thus,\r
-## 'x + y' yields a call to addfunc(x,y). Each of the BIO functions checks the prefixes of its\r
-## arguments to determine whether the operand is scalar, vector, or matrix.  This information\r
-## is used to generate appropriate C code.  For scalars, this is essentially the input string, e.g.\r
-## 'a + b*5' as input yields 'a + b*5' as output.  For vectors and matrices, the input is translated to\r
-## nested function calls, e.g. "V3_a + V3_b*5"  yields "V3_vAdd(a,vScale(b,5)".  Note that prefixes are\r
-## stripped from operands and function names within the argument list to the outer function and\r
-## the appropriate prefix is placed on the outer function for removal later as the stack evaluation\r
-## recurses toward the final assignment statement.\r
-\r
-def _addfunc(a,b):\r
-   if _isscalar(a) and _isscalar(b): return "(%s+%s)"%(a,b)\r
-   if _isvec(a) and _isvec(b): return "%svAdd(%s,%s)"%(vprefix,a[vplen:],b[vplen:])\r
-   if _ismat(a) and _ismat(b): return "%smAdd(%s,%s)"%(mprefix,a[mplen:],b[mplen:])\r
-   else: raise TypeError\r
-\r
-def _subfunc(a,b):\r
-   if _isscalar(a) and _isscalar(b): return "(%s-%s)"%(a,b)\r
-   if _isvec(a) and _isvec(b): return "%svSubtract(%s,%s)"%(vprefix,a[vplen:],b[vplen:])\r
-   if _ismat(a) and _ismat(b): return "%smSubtract(%s,%s)"%(mprefix,a[mplen:],b[mplen:])\r
-   else: raise TypeError\r
-\r
-def _mulfunc(a,b):\r
-   if _isscalar(a) and _isscalar(b): return "%s*%s"%(a,b)\r
-   if _isvec(a) and _isvec(b):    return "vDot(%s,%s)"%(a[vplen:],b[vplen:])\r
-   if _ismat(a) and _ismat(b):    return "%smMultiply(%s,%s)"%(mprefix,a[mplen:],b[mplen:])\r
-   if _ismat(a) and _isvec(b):    return "%smvMultiply(%s,%s)"%(vprefix,a[mplen:],b[vplen:])\r
-   if _ismat(a) and _isscalar(b): return "%smScale(%s,%s)"%(mprefix,a[mplen:],b)\r
-   if _isvec(a) and _isscalar(b): return "%svScale(%s,%s)"%(vprefix,a[mplen:],b)\r
-   else: raise TypeError\r
-\r
-def _outermulfunc(a,b):\r
-   ## The '@' operator is used for the vector outer product.\r
-   if _isvec(a) and _isvec(b):\r
-     return "%svOuterProduct(%s,%s)"%(mprefix,a[vplen:],b[vplen:])\r
-   else: raise TypeError\r
-\r
-def _divfunc(a,b):\r
-   ## The '/' operator is used only for scalar division\r
-   if _isscalar(a) and _isscalar(b): return "%s/%s"%(a,b)\r
-   else: raise TypeError\r
-\r
-def _expfunc(a,b):\r
-  ## The '^' operator is used for exponentiation on scalars and\r
-  ## as a marker for unary operations on vectors and matrices.\r
-  if _isscalar(a) and _isscalar(b): return "pow(%s,%s)"%(str(a),str(b))\r
-  if _ismat(a) and b=='-1':         return "%smInverse(%s)"%(mprefix,a[mplen:])\r
-  if _ismat(a) and b=='T':          return "%smTranspose(%s)"%(mprefix,a[mplen:])\r
-  if _ismat(a) and b=='Det':        return "mDeterminant(%s)"%(a[mplen:])\r
-  if _isvec(a) and b=='Mag':        return "sqrt(vMagnitude2(%s))"%(a[vplen:])\r
-  if _isvec(a) and b=='Mag2':       return "vMagnitude2(%s)"%(a[vplen:])\r
-  else: raise TypeError\r
-\r
-def _assignfunc(a,b):\r
-   ## The '=' operator is used for assignment\r
-   if _isscalar(a) and _isscalar(b): return "%s=%s"%(a,b)\r
-   if _isvec(a) and _isvec(b): return "vCopy(%s,%s)"%(a[vplen:],b[vplen:])\r
-   if _ismat(a) and _ismat(b): return "mCopy(%s,%s)"%(a[mplen:],b[mplen:])\r
-   else: raise TypeError\r
-\r
-## End of BIO func definitions\r
-##----------------------------------------------------------------------------\r
-\r
-# Map  operator symbols to corresponding BIO funcs\r
-opn = { "+" : ( _addfunc ),\r
-        "-" : ( _subfunc ),\r
-        "*" : ( _mulfunc ),\r
-        "@" : ( _outermulfunc ),\r
-        "/" : ( _divfunc),\r
-        "^" : ( _expfunc ), }\r
-\r
-\r
-##----------------------------------------------------------------------------\r
-# Recursive function that evaluates the expression stack\r
-def _evaluateStack( s ):\r
-  op = s.pop()\r
-  if op in "+-*/@^":\r
-    op2 = _evaluateStack( s )\r
-    op1 = _evaluateStack( s )\r
-    result = opn[op]( op1, op2 )\r
-    if debug_flag: print(result)\r
-    return result\r
-  else:\r
-    return op\r
-\r
-##----------------------------------------------------------------------------\r
-# The parse function that invokes all of the above.\r
-def parse(input_string):\r
-    """\r
-    Accepts an input string containing an LA equation, e.g.,\r
-    "M3_mymatrix = M3_anothermatrix^-1" returns C code function\r
-    calls that implement the expression.\r
-    """\r
-\r
-    global  exprStack\r
-    global targetvar\r
-\r
-    # Start with a blank exprStack and a blank targetvar\r
-    exprStack = []\r
-    targetvar=None\r
-\r
-    if input_string != '':\r
-      # try parsing the input string\r
-      try:\r
-        L=equation.parseString( input_string )\r
-      except ParseException as err:\r
-        print('Parse Failure', file=sys.stderr)\r
-        print(err.line, file=sys.stderr)\r
-        print(" "*(err.column-1) + "^", file=sys.stderr)\r
-        print(err, file=sys.stderr)\r
-        raise\r
-\r
-      # show result of parsing the input string\r
-      if debug_flag:\r
-        print(input_string, "->", L)\r
-        print("exprStack=", exprStack)\r
-\r
-      # Evaluate the stack of parsed operands, emitting C code.\r
-      try:\r
-        result=_evaluateStack(exprStack)\r
-      except TypeError:\r
-        print("Unsupported operation on right side of '%s'.\nCheck for missing or incorrect tags on non-scalar operands."%input_string, file=sys.stderr)\r
-        raise\r
-      except UnaryUnsupportedError:\r
-        print("Unary negation is not supported for vectors and matrices: '%s'"%input_string, file=sys.stderr)\r
-        raise\r
-\r
-      # Create final assignment and print it.\r
-      if debug_flag: print("var=",targetvar)\r
-      if targetvar != None:\r
-          try:\r
-            result = _assignfunc(targetvar,result)\r
-          except TypeError:\r
-            print("Left side tag does not match right side of '%s'"%input_string, file=sys.stderr)\r
-            raise\r
-          except UnaryUnsupportedError:\r
-            print("Unary negation is not supported for vectors and matrices: '%s'"%input_string, file=sys.stderr)\r
-            raise\r
-\r
-          return  result\r
-      else:\r
-        print("Empty left side in '%s'"%input_string, file=sys.stderr)\r
-        raise TypeError\r
-\r
-##-----------------------------------------------------------------------------------\r
-def fprocess(infilep,outfilep):\r
-   """\r
-   Scans an input file for LA equations between double square brackets,\r
-   e.g. [[ M3_mymatrix = M3_anothermatrix^-1 ]], and replaces the expression\r
-   with a comment containing the equation followed by nested function calls\r
-   that implement the equation as C code. A trailing semi-colon is appended.\r
-   The equation within [[ ]] should NOT end with a semicolon as that will raise\r
-   a ParseException. However, it is ok to have a semicolon after the right brackets.\r
-\r
-   Other text in the file is unaltered.\r
-\r
-   The arguments are file objects (NOT file names) opened for reading and\r
-   writing, respectively.\r
-   """\r
-   pattern = r'\[\[\s*(.*?)\s*\]\]'\r
-   eqn = re.compile(pattern,re.DOTALL)\r
-   s = infilep.read()\r
-   def parser(mo):\r
-      ccode = parse(mo.group(1))\r
-      return "/* %s */\n%s;\nLAParserBufferReset();\n"%(mo.group(1),ccode)\r
-\r
-   content = eqn.sub(parser,s)\r
-   outfilep.write(content)\r
-\r
-##-----------------------------------------------------------------------------------\r
-def test():\r
-   """\r
-   Tests the parsing of various supported expressions. Raises\r
-   an AssertError if the output is not what is expected. Prints the\r
-   input, expected output, and actual output for all tests.\r
-   """\r
-   print("Testing LAParser")\r
-   testcases = [\r
-     ("Scalar addition","a = b+c","a=(b+c)"),\r
-     ("Vector addition","V3_a = V3_b + V3_c","vCopy(a,vAdd(b,c))"),\r
-     ("Vector addition","V3_a=V3_b+V3_c","vCopy(a,vAdd(b,c))"),\r
-     ("Matrix addition","M3_a = M3_b + M3_c","mCopy(a,mAdd(b,c))"),\r
-     ("Matrix addition","M3_a=M3_b+M3_c","mCopy(a,mAdd(b,c))"),\r
-     ("Scalar subtraction","a = b-c","a=(b-c)"),\r
-     ("Vector subtraction","V3_a = V3_b - V3_c","vCopy(a,vSubtract(b,c))"),\r
-     ("Matrix subtraction","M3_a = M3_b - M3_c","mCopy(a,mSubtract(b,c))"),\r
-     ("Scalar multiplication","a = b*c","a=b*c"),\r
-     ("Scalar division","a = b/c","a=b/c"),\r
-     ("Vector multiplication (dot product)","a = V3_b * V3_c","a=vDot(b,c)"),\r
-     ("Vector multiplication (outer product)","M3_a = V3_b @ V3_c","mCopy(a,vOuterProduct(b,c))"),\r
-     ("Matrix multiplication","M3_a = M3_b * M3_c","mCopy(a,mMultiply(b,c))"),\r
-     ("Vector scaling","V3_a = V3_b * c","vCopy(a,vScale(b,c))"),\r
-     ("Matrix scaling","M3_a = M3_b * c","mCopy(a,mScale(b,c))"),\r
-     ("Matrix by vector multiplication","V3_a = M3_b * V3_c","vCopy(a,mvMultiply(b,c))"),\r
-     ("Scalar exponentiation","a = b^c","a=pow(b,c)"),\r
-     ("Matrix inversion","M3_a = M3_b^-1","mCopy(a,mInverse(b))"),\r
-     ("Matrix transpose","M3_a = M3_b^T","mCopy(a,mTranspose(b))"),\r
-     ("Matrix determinant","a = M3_b^Det","a=mDeterminant(b)"),\r
-     ("Vector magnitude squared","a = V3_b^Mag2","a=vMagnitude2(b)"),\r
-     ("Vector magnitude","a = V3_b^Mag","a=sqrt(vMagnitude2(b))"),\r
-     ("Complicated expression", "myscalar = (M3_amatrix * V3_bvector)^Mag + 5*(-xyz[i] + 2.03^2)","myscalar=(sqrt(vMagnitude2(mvMultiply(amatrix,bvector)))+5*(-xyz[i]+pow(2.03,2)))"),\r
-     ("Complicated Multiline", "myscalar = \n(M3_amatrix * V3_bvector)^Mag +\n 5*(xyz + 2.03^2)","myscalar=(sqrt(vMagnitude2(mvMultiply(amatrix,bvector)))+5*(xyz+pow(2.03,2)))")\r
-\r
-     ]\r
-\r
-   for t in testcases:\r
-      name,input,expected = t\r
-      print(name)\r
-      print("   %s input"%input)\r
-      print("   %s expected"%expected)\r
-      result = parse(input)\r
-      print("   %s received"%result)\r
-      print("")\r
-      assert expected == result\r
-\r
-   ##TODO: Write testcases with invalid expressions and test that the expected\r
-   ## exceptions are raised.\r
-\r
-   print("Tests completed!")\r
-##----------------------------------------------------------------------------\r
-## The following is executed only when this module is executed as\r
-## command line script.  It runs a small test suite (see above)\r
-## and then enters an interactive loop where you\r
-## can enter expressions and see the resulting C code as output.\r
-\r
-if __name__ == '__main__':\r
-  # run testcases\r
-  test()\r
-\r
-  # input_string\r
-  input_string=''\r
-\r
-  # Display instructions on how to use the program interactively\r
-  interactiveusage = """\r
-  Entering interactive mode:\r
-  Type in an equation to be parsed or 'quit' to exit the program.\r
-  Type 'debug on' to print parsing details as each string is processed.\r
-  Type 'debug off' to stop printing parsing details\r
-  """\r
-  print(interactiveusage)\r
-  input_string = input("> ")\r
-\r
-  while input_string != 'quit':\r
-    if input_string == "debug on":\r
-       debug_flag = True\r
-    elif input_string == "debug off":\r
-       debug_flag = False\r
-    else:\r
-      try:\r
-        print(parse(input_string))\r
-      except Exception:\r
-        pass\r
-\r
-    # obtain new input string\r
-    input_string = input("> ")\r
-\r
-  # if user types 'quit' then say goodbye\r
-  print("Good bye!")\r
+"""
+Purpose:   Linear Algebra Parser
+Based on:  SimpleCalc.py example (author Paul McGuire) in pyparsing-1.3.3
+Author:    Mike Ellis
+Copyright: Ellis & Grant, Inc. 2005
+License:   You may freely use, modify, and distribute this software.
+Warranty:  THIS SOFTWARE HAS NO WARRANTY WHATSOEVER. USE AT YOUR OWN RISK.
+Notes: Parses infix linear algebra (LA) notation for vectors, matrices, and scalars.
+       Output is C code function calls.  The parser can be run as an interactive
+       interpreter or included as module to use for in-place substitution into C files
+       containing LA equations.
+
+       Supported operations are:
+       OPERATION:              INPUT                    OUTPUT
+       Scalar addition:        "a = b+c"                "a=(b+c)"
+       Scalar subtraction:     "a = b-c"                "a=(b-c)"
+       Scalar multiplication:  "a = b*c"                "a=b*c"
+       Scalar division:        "a = b/c"                "a=b/c"
+       Scalar exponentiation:  "a = b^c"                "a=pow(b,c)"
+       Vector scaling:         "V3_a = V3_b * c"        "vCopy(a,vScale(b,c))"
+       Vector addition:        "V3_a = V3_b + V3_c"     "vCopy(a,vAdd(b,c))"
+       Vector subtraction:     "V3_a = V3_b - V3_c"     "vCopy(a,vSubtract(b,c))"
+       Vector dot product:     "a = V3_b * V3_c"        "a=vDot(b,c)"
+       Vector outer product:   "M3_a = V3_b @ V3_c"     "a=vOuterProduct(b,c)"
+       Vector magn. squared:   "a = V3_b^Mag2"          "a=vMagnitude2(b)"
+       Vector magnitude:       "a = V3_b^Mag"           "a=sqrt(vMagnitude2(b))"
+       Matrix scaling:         "M3_a = M3_b * c"        "mCopy(a,mScale(b,c))"
+       Matrix addition:        "M3_a = M3_b + M3_c"     "mCopy(a,mAdd(b,c))"
+       Matrix subtraction:     "M3_a = M3_b - M3_c"     "mCopy(a,mSubtract(b,c))"
+       Matrix multiplication:  "M3_a = M3_b * M3_c"     "mCopy(a,mMultiply(b,c))"
+       Matrix by vector mult.: "V3_a = M3_b * V3_c"     "vCopy(a,mvMultiply(b,c))"
+       Matrix inversion:       "M3_a = M3_b^-1"         "mCopy(a,mInverse(b))"
+       Matrix transpose:       "M3_a = M3_b^T"          "mCopy(a,mTranspose(b))"
+       Matrix determinant:     "a = M3_b^Det"           "a=mDeterminant(b)"
+
+       The parser requires the expression to be an equation.  Each non-scalar variable
+       must be prefixed with a type tag, 'M3_' for 3x3 matrices and 'V3_' for 3-vectors.
+       For proper compilation of the C code, the variables need to be declared without
+       the prefix as float[3] for vectors and float[3][3] for matrices. The operations do
+       not modify any variables on the right-hand side of the equation.
+
+       Equations may include nested expressions within parentheses. The allowed binary
+       operators are '+-*/^' for scalars, and '+-*^@' for vectors and matrices with the
+       meanings defined in the table above.
+
+       Specifying an improper combination of operands, e.g. adding a vector to a matrix,
+       is detected by the parser and results in a Python TypeError Exception. The usual cause
+       of this is omitting one or more tag prefixes. The parser knows nothing about a
+       a variable's C declaration and relies entirely on the type tags. Errors in C
+       declarations are not caught until compile time.
+
+Usage: To process LA equations embedded in source files, import this module and
+       pass input and output file objects to the fprocess() function.  You can
+       can also invoke the parser from the command line, e.g. 'python LAparser.py',
+       to run a small test suite and enter an interactive loop where you can enter
+       LA equations and see the resulting C code.
+
+"""
+
+import re,sys
+from pyparsing import Word, alphas, ParseException, Literal, CaselessLiteral \
+, Combine, Optional, nums, Forward, ZeroOrMore, \
+  StringEnd, alphanums
+
+# Debugging flag can be set to either "debug_flag=True" or "debug_flag=False"
+debug_flag=False
+
+#----------------------------------------------------------------------------
+# Variables that hold intermediate parsing results and a couple of
+# helper functions.
+exprStack = []      # Holds operators and operands parsed from input.
+targetvar = None    # Holds variable name to left of '=' sign in LA equation.
+
+
+def _pushFirst( str, loc, toks ):
+    if debug_flag: print("pushing ", toks[0], "str is ", str)
+    exprStack.append( toks[0] )
+
+def _assignVar( str, loc, toks ):
+    global targetvar
+    targetvar =  toks[0]
+
+#-----------------------------------------------------------------------------
+# The following statements define the grammar for the parser.
+
+point = Literal('.')
+e = CaselessLiteral('E')
+plusorminus = Literal('+') | Literal('-')
+number = Word(nums)
+integer = Combine( Optional(plusorminus) + number )
+floatnumber = Combine( integer +
+                       Optional( point + Optional(number) ) +
+                       Optional( e + integer )
+                     )
+
+lbracket = Literal("[")
+rbracket = Literal("]")
+ident = Forward()
+## The definition below treats array accesses as identifiers. This means your expressions
+## can include references to array elements, rows and columns, e.g., a = b[i] + 5.
+## Expressions within []'s are not presently supported, so a = b[i+1] will raise
+## a ParseException.
+ident = Combine(Word(alphas + '-',alphanums + '_') + \
+                ZeroOrMore(lbracket + (Word(alphas + '-',alphanums + '_')|integer) + rbracket) \
+                )
+
+plus  = Literal( "+" )
+minus = Literal( "-" )
+mult  = Literal( "*" )
+div   = Literal( "/" )
+outer = Literal( "@" )
+lpar  = Literal( "(" ).suppress()
+rpar  = Literal( ")" ).suppress()
+addop  = plus | minus
+multop = mult | div | outer
+expop = Literal( "^" )
+assignop = Literal( "=" )
+
+expr = Forward()
+atom = ( ( e | floatnumber | integer | ident  ).setParseAction(_pushFirst) |
+         ( lpar + expr.suppress() + rpar )
+       )
+factor = Forward()
+factor << atom + ZeroOrMore( ( expop + factor ).setParseAction( _pushFirst ) )
+
+term = factor + ZeroOrMore( ( multop + factor ).setParseAction( _pushFirst ) )
+expr << term + ZeroOrMore( ( addop + term ).setParseAction( _pushFirst ) )
+equation = (ident + assignop).setParseAction(_assignVar) + expr + StringEnd()
+
+# End of grammar definition
+#-----------------------------------------------------------------------------
+## The following are helper variables and functions used by the Binary Infix Operator
+## Functions described below.
+
+vprefix = 'V3_'
+vplen = len(vprefix)
+mprefix = 'M3_'
+mplen = len(mprefix)
+
+## We don't support unary negation for vectors and matrices
+class UnaryUnsupportedError(Exception): pass
+
+def _isvec(ident):
+   if ident[0] == '-' and ident[1:vplen+1] == vprefix:
+      raise UnaryUnsupportedError
+   else: return ident[0:vplen] == vprefix
+
+def _ismat(ident):
+   if ident[0] == '-' and ident[1:mplen+1] == mprefix:
+      raise UnaryUnsupportedError
+   else: return ident[0:mplen] == mprefix
+
+def _isscalar(ident): return not (_isvec(ident) or _ismat(ident))
+
+## Binary infix operator (BIO) functions.  These are called when the stack evaluator
+## pops a binary operator like '+' or '*".  The stack evaluator pops the two operand, a and b,
+## and calls the function that is mapped to the operator with a and b as arguments.  Thus,
+## 'x + y' yields a call to addfunc(x,y). Each of the BIO functions checks the prefixes of its
+## arguments to determine whether the operand is scalar, vector, or matrix.  This information
+## is used to generate appropriate C code.  For scalars, this is essentially the input string, e.g.
+## 'a + b*5' as input yields 'a + b*5' as output.  For vectors and matrices, the input is translated to
+## nested function calls, e.g. "V3_a + V3_b*5"  yields "V3_vAdd(a,vScale(b,5)".  Note that prefixes are
+## stripped from operands and function names within the argument list to the outer function and
+## the appropriate prefix is placed on the outer function for removal later as the stack evaluation
+## recurses toward the final assignment statement.
+
+def _addfunc(a,b):
+   if _isscalar(a) and _isscalar(b): return "(%s+%s)"%(a,b)
+   if _isvec(a) and _isvec(b): return "%svAdd(%s,%s)"%(vprefix,a[vplen:],b[vplen:])
+   if _ismat(a) and _ismat(b): return "%smAdd(%s,%s)"%(mprefix,a[mplen:],b[mplen:])
+   else: raise TypeError
+
+def _subfunc(a,b):
+   if _isscalar(a) and _isscalar(b): return "(%s-%s)"%(a,b)
+   if _isvec(a) and _isvec(b): return "%svSubtract(%s,%s)"%(vprefix,a[vplen:],b[vplen:])
+   if _ismat(a) and _ismat(b): return "%smSubtract(%s,%s)"%(mprefix,a[mplen:],b[mplen:])
+   else: raise TypeError
+
+def _mulfunc(a,b):
+   if _isscalar(a) and _isscalar(b): return "%s*%s"%(a,b)
+   if _isvec(a) and _isvec(b):    return "vDot(%s,%s)"%(a[vplen:],b[vplen:])
+   if _ismat(a) and _ismat(b):    return "%smMultiply(%s,%s)"%(mprefix,a[mplen:],b[mplen:])
+   if _ismat(a) and _isvec(b):    return "%smvMultiply(%s,%s)"%(vprefix,a[mplen:],b[vplen:])
+   if _ismat(a) and _isscalar(b): return "%smScale(%s,%s)"%(mprefix,a[mplen:],b)
+   if _isvec(a) and _isscalar(b): return "%svScale(%s,%s)"%(vprefix,a[mplen:],b)
+   else: raise TypeError
+
+def _outermulfunc(a,b):
+   ## The '@' operator is used for the vector outer product.
+   if _isvec(a) and _isvec(b):
+     return "%svOuterProduct(%s,%s)"%(mprefix,a[vplen:],b[vplen:])
+   else: raise TypeError
+
+def _divfunc(a,b):
+   ## The '/' operator is used only for scalar division
+   if _isscalar(a) and _isscalar(b): return "%s/%s"%(a,b)
+   else: raise TypeError
+
+def _expfunc(a,b):
+  ## The '^' operator is used for exponentiation on scalars and
+  ## as a marker for unary operations on vectors and matrices.
+  if _isscalar(a) and _isscalar(b): return "pow(%s,%s)"%(str(a),str(b))
+  if _ismat(a) and b=='-1':         return "%smInverse(%s)"%(mprefix,a[mplen:])
+  if _ismat(a) and b=='T':          return "%smTranspose(%s)"%(mprefix,a[mplen:])
+  if _ismat(a) and b=='Det':        return "mDeterminant(%s)"%(a[mplen:])
+  if _isvec(a) and b=='Mag':        return "sqrt(vMagnitude2(%s))"%(a[vplen:])
+  if _isvec(a) and b=='Mag2':       return "vMagnitude2(%s)"%(a[vplen:])
+  else: raise TypeError
+
+def _assignfunc(a,b):
+   ## The '=' operator is used for assignment
+   if _isscalar(a) and _isscalar(b): return "%s=%s"%(a,b)
+   if _isvec(a) and _isvec(b): return "vCopy(%s,%s)"%(a[vplen:],b[vplen:])
+   if _ismat(a) and _ismat(b): return "mCopy(%s,%s)"%(a[mplen:],b[mplen:])
+   else: raise TypeError
+
+## End of BIO func definitions
+##----------------------------------------------------------------------------
+
+# Map  operator symbols to corresponding BIO funcs
+opn = { "+" : ( _addfunc ),
+        "-" : ( _subfunc ),
+        "*" : ( _mulfunc ),
+        "@" : ( _outermulfunc ),
+        "/" : ( _divfunc),
+        "^" : ( _expfunc ), }
+
+
+##----------------------------------------------------------------------------
+# Recursive function that evaluates the expression stack
+def _evaluateStack( s ):
+  op = s.pop()
+  if op in "+-*/@^":
+    op2 = _evaluateStack( s )
+    op1 = _evaluateStack( s )
+    result = opn[op]( op1, op2 )
+    if debug_flag: print(result)
+    return result
+  else:
+    return op
+
+##----------------------------------------------------------------------------
+# The parse function that invokes all of the above.
+def parse(input_string):
+    """
+    Accepts an input string containing an LA equation, e.g.,
+    "M3_mymatrix = M3_anothermatrix^-1" returns C code function
+    calls that implement the expression.
+    """
+
+    global  exprStack
+    global targetvar
+
+    # Start with a blank exprStack and a blank targetvar
+    exprStack = []
+    targetvar=None
+
+    if input_string != '':
+      # try parsing the input string
+      try:
+        L=equation.parseString( input_string )
+      except ParseException as err:
+        print('Parse Failure', file=sys.stderr)
+        print(err.line, file=sys.stderr)
+        print(" "*(err.column-1) + "^", file=sys.stderr)
+        print(err, file=sys.stderr)
+        raise
+
+      # show result of parsing the input string
+      if debug_flag:
+        print(input_string, "->", L)
+        print("exprStack=", exprStack)
+
+      # Evaluate the stack of parsed operands, emitting C code.
+      try:
+        result=_evaluateStack(exprStack)
+      except TypeError:
+        print("Unsupported operation on right side of '%s'.\nCheck for missing or incorrect tags on non-scalar operands."%input_string, file=sys.stderr)
+        raise
+      except UnaryUnsupportedError:
+        print("Unary negation is not supported for vectors and matrices: '%s'"%input_string, file=sys.stderr)
+        raise
+
+      # Create final assignment and print it.
+      if debug_flag: print("var=",targetvar)
+      if targetvar != None:
+          try:
+            result = _assignfunc(targetvar,result)
+          except TypeError:
+            print("Left side tag does not match right side of '%s'"%input_string, file=sys.stderr)
+            raise
+          except UnaryUnsupportedError:
+            print("Unary negation is not supported for vectors and matrices: '%s'"%input_string, file=sys.stderr)
+            raise
+
+          return  result
+      else:
+        print("Empty left side in '%s'"%input_string, file=sys.stderr)
+        raise TypeError
+
+##-----------------------------------------------------------------------------------
+def fprocess(infilep,outfilep):
+   """
+   Scans an input file for LA equations between double square brackets,
+   e.g. [[ M3_mymatrix = M3_anothermatrix^-1 ]], and replaces the expression
+   with a comment containing the equation followed by nested function calls
+   that implement the equation as C code. A trailing semi-colon is appended.
+   The equation within [[ ]] should NOT end with a semicolon as that will raise
+   a ParseException. However, it is ok to have a semicolon after the right brackets.
+
+   Other text in the file is unaltered.
+
+   The arguments are file objects (NOT file names) opened for reading and
+   writing, respectively.
+   """
+   pattern = r'\[\[\s*(.*?)\s*\]\]'
+   eqn = re.compile(pattern,re.DOTALL)
+   s = infilep.read()
+   def parser(mo):
+      ccode = parse(mo.group(1))
+      return "/* %s */\n%s;\nLAParserBufferReset();\n"%(mo.group(1),ccode)
+
+   content = eqn.sub(parser,s)
+   outfilep.write(content)
+
+##-----------------------------------------------------------------------------------
+def test():
+   """
+   Tests the parsing of various supported expressions. Raises
+   an AssertError if the output is not what is expected. Prints the
+   input, expected output, and actual output for all tests.
+   """
+   print("Testing LAParser")
+   testcases = [
+     ("Scalar addition","a = b+c","a=(b+c)"),
+     ("Vector addition","V3_a = V3_b + V3_c","vCopy(a,vAdd(b,c))"),
+     ("Vector addition","V3_a=V3_b+V3_c","vCopy(a,vAdd(b,c))"),
+     ("Matrix addition","M3_a = M3_b + M3_c","mCopy(a,mAdd(b,c))"),
+     ("Matrix addition","M3_a=M3_b+M3_c","mCopy(a,mAdd(b,c))"),
+     ("Scalar subtraction","a = b-c","a=(b-c)"),
+     ("Vector subtraction","V3_a = V3_b - V3_c","vCopy(a,vSubtract(b,c))"),
+     ("Matrix subtraction","M3_a = M3_b - M3_c","mCopy(a,mSubtract(b,c))"),
+     ("Scalar multiplication","a = b*c","a=b*c"),
+     ("Scalar division","a = b/c","a=b/c"),
+     ("Vector multiplication (dot product)","a = V3_b * V3_c","a=vDot(b,c)"),
+     ("Vector multiplication (outer product)","M3_a = V3_b @ V3_c","mCopy(a,vOuterProduct(b,c))"),
+     ("Matrix multiplication","M3_a = M3_b * M3_c","mCopy(a,mMultiply(b,c))"),
+     ("Vector scaling","V3_a = V3_b * c","vCopy(a,vScale(b,c))"),
+     ("Matrix scaling","M3_a = M3_b * c","mCopy(a,mScale(b,c))"),
+     ("Matrix by vector multiplication","V3_a = M3_b * V3_c","vCopy(a,mvMultiply(b,c))"),
+     ("Scalar exponentiation","a = b^c","a=pow(b,c)"),
+     ("Matrix inversion","M3_a = M3_b^-1","mCopy(a,mInverse(b))"),
+     ("Matrix transpose","M3_a = M3_b^T","mCopy(a,mTranspose(b))"),
+     ("Matrix determinant","a = M3_b^Det","a=mDeterminant(b)"),
+     ("Vector magnitude squared","a = V3_b^Mag2","a=vMagnitude2(b)"),
+     ("Vector magnitude","a = V3_b^Mag","a=sqrt(vMagnitude2(b))"),
+     ("Complicated expression", "myscalar = (M3_amatrix * V3_bvector)^Mag + 5*(-xyz[i] + 2.03^2)","myscalar=(sqrt(vMagnitude2(mvMultiply(amatrix,bvector)))+5*(-xyz[i]+pow(2.03,2)))"),
+     ("Complicated Multiline", "myscalar = \n(M3_amatrix * V3_bvector)^Mag +\n 5*(xyz + 2.03^2)","myscalar=(sqrt(vMagnitude2(mvMultiply(amatrix,bvector)))+5*(xyz+pow(2.03,2)))")
+
+     ]
+
+
+   all_passed = [True]
+
+   def post_test(test, parsed):
+
+      # copy exprStack to evaluate and clear before running next test
+      parsed_stack = exprStack[:]
+      exprStack.clear()
+
+      name, testcase, expected = next(tc for tc in testcases if tc[1] == test)
+
+      this_test_passed = False
+      try:
+          try:
+            result=_evaluateStack(parsed_stack)
+          except TypeError:
+            print("Unsupported operation on right side of '%s'.\nCheck for missing or incorrect tags on non-scalar operands."%input_string, file=sys.stderr)
+            raise
+          except UnaryUnsupportedError:
+            print("Unary negation is not supported for vectors and matrices: '%s'"%input_string, file=sys.stderr)
+            raise
+
+          # Create final assignment and print it.
+          if debug_flag: print("var=",targetvar)
+          if targetvar != None:
+              try:
+                result = _assignfunc(targetvar,result)
+              except TypeError:
+                print("Left side tag does not match right side of '%s'"%input_string, file=sys.stderr)
+                raise
+              except UnaryUnsupportedError:
+                print("Unary negation is not supported for vectors and matrices: '%s'"%input_string, file=sys.stderr)
+                raise
+
+          else:
+            print("Empty left side in '%s'"%input_string, file=sys.stderr)
+            raise TypeError
+
+          parsed['result'] = result
+          parsed['passed'] = this_test_passed = result == expected
+
+      finally:
+          all_passed[0] = all_passed[0] and this_test_passed
+          print('\n' + name)
+
+   equation.runTests((t[1] for t in testcases), postParse=post_test)
+
+
+   ##TODO: Write testcases with invalid expressions and test that the expected
+   ## exceptions are raised.
+
+   print("Tests completed!")
+   print("PASSED" if all_passed[0] else "FAILED")
+   assert all_passed[0]
+
+##----------------------------------------------------------------------------
+## The following is executed only when this module is executed as
+## command line script.  It runs a small test suite (see above)
+## and then enters an interactive loop where you
+## can enter expressions and see the resulting C code as output.
+
+if __name__ == '__main__':
+
+  import sys
+  if not sys.flags.interactive:
+      # run testcases
+      test()
+      sys.exit(0)
+
+  # input_string
+  input_string=''
+
+  # Display instructions on how to use the program interactively
+  interactiveusage = """
+  Entering interactive mode:
+  Type in an equation to be parsed or 'quit' to exit the program.
+  Type 'debug on' to print parsing details as each string is processed.
+  Type 'debug off' to stop printing parsing details
+  """
+  print(interactiveusage)
+  input_string = input("> ")
+
+  while input_string != 'quit':
+    if input_string == "debug on":
+       debug_flag = True
+    elif input_string == "debug off":
+       debug_flag = False
+    else:
+      try:
+        print(parse(input_string))
+      except Exception:
+        pass
+
+    # obtain new input string
+    input_string = input("> ")
+
+  # if user types 'quit' then say goodbye
+  print("Good bye!")
+  import os
+  os._exit(0)
+
index c1df9f3d9f59ed5b706a48b83d285b7cd4285201..753901b897d044aa2a7dc985c3a5854448fe07e2 100644 (file)
@@ -2,11 +2,10 @@
 #\r
 # chemicalFormulas.py\r
 #\r
-# Copyright (c) 2003, Paul McGuire\r
+# Copyright (c) 2003,2019 Paul McGuire\r
 #\r
 \r
-from pyparsing import Word, Optional, OneOrMore, Group, ParseException, Regex\r
-from pyparsing import alphas\r
+import pyparsing as pp\r
 \r
 atomicWeight = {\r
     "O"  : 15.9994,\r
@@ -19,14 +18,14 @@ atomicWeight = {
 digits = "0123456789"\r
 \r
 # Version 1\r
-element = Word( alphas.upper(), alphas.lower(), max=2)\r
+element = pp.Word(pp.alphas.upper(), pp.alphas.lower(), max=2)\r
 # for stricter matching, use this Regex instead\r
 # element = Regex("A[cglmrstu]|B[aehikr]?|C[adeflmorsu]?|D[bsy]|"\r
 #                 "E[rsu]|F[emr]?|G[ade]|H[efgos]?|I[nr]?|Kr?|L[airu]|"\r
 #                 "M[dgnot]|N[abdeiop]?|Os?|P[abdmortu]?|R[abefghnu]|"\r
 #                 "S[bcegimnr]?|T[abcehilm]|U(u[bhopqst])?|V|W|Xe|Yb?|Z[nr]")\r
-elementRef = Group( element + Optional( Word( digits ), default="1" ) )\r
-formula = OneOrMore( elementRef )\r
+elementRef = pp.Group(element + pp.Optional(pp.Word(digits), default="1"))\r
+formula = elementRef[...]\r
 \r
 fn = lambda elemList : sum(atomicWeight[elem]*int(qty) for elem,qty in elemList)\r
 formula.runTests("""\\r
@@ -38,8 +37,8 @@ formula.runTests("""\
 print()\r
 \r
 # Version 2 - access parsed items by results name\r
-elementRef = Group( element("symbol") + Optional( Word( digits ), default="1" )("qty") )\r
-formula = OneOrMore( elementRef )\r
+elementRef = pp.Group(element("symbol") + pp.Optional(pp.Word(digits), default="1")("qty"))\r
+formula = elementRef[...]\r
 \r
 fn = lambda elemList : sum(atomicWeight[elem.symbol]*int(elem.qty) for elem in elemList)\r
 formula.runTests("""\\r
@@ -51,9 +50,9 @@ formula.runTests("""\
 print()\r
 \r
 # Version 3 - convert integers during parsing process\r
-integer = Word( digits ).setParseAction(lambda t:int(t[0]))\r
-elementRef = Group( element("symbol") + Optional( integer, default=1 )("qty") )\r
-formula = OneOrMore( elementRef )\r
+integer = pp.Word(digits).setParseAction(lambda t:int(t[0]))\r
+elementRef = pp.Group(element("symbol") + pp.Optional(integer, default=1)("qty"))\r
+formula = elementRef[...]\r
 \r
 fn = lambda elemList : sum(atomicWeight[elem.symbol]*elem.qty for elem in elemList)\r
 formula.runTests("""\\r
@@ -72,10 +71,10 @@ def cvt_subscript_int(s):
     for c in s[0]:\r
         ret = ret*10 + subscript_int_map[c]\r
     return ret\r
-subscript_int = Word(subscript_digits).addParseAction(cvt_subscript_int)\r
+subscript_int = pp.Word(subscript_digits).addParseAction(cvt_subscript_int)\r
 \r
-elementRef = Group( element("symbol") + Optional(subscript_int, default=1)("qty") )\r
-formula = OneOrMore( elementRef )\r
+elementRef = pp.Group(element("symbol") + pp.Optional(subscript_int, default=1)("qty"))\r
+formula = elementRef[...]\r
 formula.runTests("""\\r
     H₂O\r
     C₆H₅OH\r
index eae6dc1106e566c075c0097e264e905758e6b795..067647dc35d5e7ee7a5929c6832c5e97670d51ba 100644 (file)
@@ -1,7 +1,7 @@
 # commasep.py
 #
 # comma-separated list example, to illustrate the advantages of using
-# the pyparsing commaSeparatedList as opposed to string.split(","):
+# the pyparsing comma_separated_list as opposed to string.split(","):
 # - leading and trailing whitespace is implicitly trimmed from list elements
 # - list elements can be quoted strings, which can safely contain commas without breaking
 #    into separate elements
@@ -9,7 +9,8 @@
 # Copyright (c) 2004-2016, Paul McGuire
 #
 
-from pyparsing import commaSeparatedList
+import pyparsing as pp
+ppc = pp.pyparsing_common
 
 testData = [
     "a,b,c,100.2,,3",
@@ -20,4 +21,4 @@ testData = [
     "",
     ]
 
-commaSeparatedList.runTests(testData)
+ppc.comma_separated_list.runTests(testData)
diff --git a/examples/deltaTime.py b/examples/deltaTime.py
deleted file mode 100644 (file)
index 2fa8769..0000000
+++ /dev/null
@@ -1,208 +0,0 @@
-# deltaTime.py\r
-#\r
-# Parser to convert a conversational time reference such as "in a minute" or\r
-# "noon tomorrow" and convert it to a Python datetime.  The returned\r
-# ParseResults object contains the results name "timeOffset" containing\r
-# the timedelta, and "calculatedTime" containing the computed time relative\r
-# to datetime.now().\r
-#\r
-# Copyright 2010, by Paul McGuire\r
-#\r
-\r
-from datetime import datetime, timedelta\r
-from pyparsing import *\r
-import calendar\r
-\r
-__all__ = ["nlTimeExpression"]\r
-\r
-# string conversion parse actions\r
-def convertToTimedelta(toks):\r
-    unit = toks.timeunit.lower().rstrip("s")\r
-    td = {\r
-        'week'    : timedelta(7),\r
-        'day'    : timedelta(1),\r
-        'hour'   : timedelta(0,0,0,0,0,1),\r
-        'minute' : timedelta(0,0,0,0,1),\r
-        'second' : timedelta(0,1),\r
-        }[unit]\r
-    if toks.qty:\r
-        td *= int(toks.qty)\r
-    if toks.dir:\r
-        td *= toks.dir\r
-    toks["timeOffset"] = td\r
-\r
-def convertToDay(toks):\r
-    now = datetime.now()\r
-    if "wkdayRef" in toks:\r
-        todaynum = now.weekday()\r
-        daynames = [n.lower() for n in calendar.day_name]\r
-        nameddaynum = daynames.index(toks.wkdayRef.day.lower())\r
-        if toks.wkdayRef.dir > 0:\r
-            daydiff = (nameddaynum + 7 - todaynum) % 7\r
-        else:\r
-            daydiff = -((todaynum + 7 - nameddaynum) % 7)\r
-        toks["absTime"] = datetime(now.year, now.month, now.day)+timedelta(daydiff)\r
-    else:\r
-        name = toks.name.lower()\r
-        toks["absTime"] = {\r
-            "now"       : now,\r
-            "today"     : datetime(now.year, now.month, now.day),\r
-            "yesterday" : datetime(now.year, now.month, now.day)+timedelta(-1),\r
-            "tomorrow"  : datetime(now.year, now.month, now.day)+timedelta(+1),\r
-            }[name]\r
-\r
-def convertToAbsTime(toks):\r
-    now = datetime.now()\r
-    if "dayRef" in toks:\r
-        day = toks.dayRef.absTime\r
-        day = datetime(day.year, day.month, day.day)\r
-    else:\r
-        day = datetime(now.year, now.month, now.day)\r
-    if "timeOfDay" in toks:\r
-        if isinstance(toks.timeOfDay,str):\r
-            timeOfDay = {\r
-                "now"      : timedelta(0, (now.hour*60+now.minute)*60+now.second, now.microsecond),\r
-                "noon"     : timedelta(0,0,0,0,0,12),\r
-                "midnight" : timedelta(),\r
-                }[toks.timeOfDay]\r
-        else:\r
-            hhmmss = toks.timeparts\r
-            if hhmmss.miltime:\r
-                hh,mm = hhmmss.miltime\r
-                ss = 0\r
-            else:\r
-                hh,mm,ss = (hhmmss.HH % 12), hhmmss.MM, hhmmss.SS\r
-                if not mm: mm = 0\r
-                if not ss: ss = 0\r
-                if toks.timeOfDay.ampm == 'pm':\r
-                    hh += 12\r
-            timeOfDay = timedelta(0, (hh*60+mm)*60+ss, 0)\r
-    else:\r
-        timeOfDay = timedelta(0, (now.hour*60+now.minute)*60+now.second, now.microsecond)\r
-    toks["absTime"] = day + timeOfDay\r
-\r
-def calculateTime(toks):\r
-    if toks.absTime:\r
-        absTime = toks.absTime\r
-    else:\r
-        absTime = datetime.now()\r
-    if toks.timeOffset:\r
-        absTime += toks.timeOffset\r
-    toks["calculatedTime"] = absTime\r
-\r
-# grammar definitions\r
-CL = CaselessLiteral\r
-today, tomorrow, yesterday, noon, midnight, now = map( CL,\r
-    "today tomorrow yesterday noon midnight now".split())\r
-plural = lambda s : Combine(CL(s) + Optional(CL("s")))\r
-week, day, hour, minute, second = map( plural,\r
-    "week day hour minute second".split())\r
-am = CL("am")\r
-pm = CL("pm")\r
-COLON = Suppress(':')\r
-\r
-# are these actually operators?\r
-in_ = CL("in").setParseAction(replaceWith(1))\r
-from_ = CL("from").setParseAction(replaceWith(1))\r
-before = CL("before").setParseAction(replaceWith(-1))\r
-after = CL("after").setParseAction(replaceWith(1))\r
-ago = CL("ago").setParseAction(replaceWith(-1))\r
-next_ = CL("next").setParseAction(replaceWith(1))\r
-last_ = CL("last").setParseAction(replaceWith(-1))\r
-at_ = CL("at")\r
-on_ = CL("on")\r
-\r
-couple = (Optional(CL("a")) + CL("couple") + Optional(CL("of"))).setParseAction(replaceWith(2))\r
-a_qty = CL("a").setParseAction(replaceWith(1))\r
-integer = Word(nums).setParseAction(lambda t:int(t[0]))\r
-int4 = Group(Word(nums,exact=4).setParseAction(lambda t: [int(t[0][:2]),int(t[0][2:])] ))\r
-def fill_timefields(t):\r
-    t[0]['HH'] = t[0][0]\r
-    t[0]['MM'] = t[0][1]\r
-    t[0]['ampm'] = ('am','pm')[t[0].HH >= 12]\r
-int4.addParseAction(fill_timefields)\r
-qty = integer | couple | a_qty\r
-dayName = oneOf( list(calendar.day_name) )\r
-\r
-dayOffset = (qty("qty") + (week | day)("timeunit"))\r
-dayFwdBack = (from_ + now.suppress() | ago)("dir")\r
-weekdayRef = (Optional(next_ | last_,1)("dir") + dayName("day"))\r
-dayRef = Optional( (dayOffset + (before | after | from_)("dir") ).setParseAction(convertToTimedelta) ) + \\r
-            ((yesterday | today | tomorrow)("name")|\r
-             weekdayRef("wkdayRef")).setParseAction(convertToDay)\r
-todayRef = (dayOffset + dayFwdBack).setParseAction(convertToTimedelta) | \\r
-            (in_("dir") + qty("qty") + day("timeunit")).setParseAction(convertToTimedelta)\r
-\r
-dayTimeSpec = dayRef | todayRef\r
-dayTimeSpec.setParseAction(calculateTime)\r
-\r
-relativeTimeUnit = (week | day | hour | minute | second)\r
-\r
-timespec = Group(ungroup(int4) |\r
-                 integer("HH") +\r
-                 ungroup(Optional(COLON + integer,[0]))("MM") +\r
-                 ungroup(Optional(COLON + integer,[0]))("SS") +\r
-                 (am | pm)("ampm")\r
-                 )\r
-\r
-absTimeSpec = ((noon | midnight | now | timespec("timeparts"))("timeOfDay") +\r
-                Optional(on_) + Optional(dayRef)("dayRef") |\r
-                dayRef("dayRef") + at_ +\r
-                (noon | midnight | now | timespec("timeparts"))("timeOfDay"))\r
-absTimeSpec.setParseAction(convertToAbsTime,calculateTime)\r
-\r
-relTimeSpec = qty("qty") + relativeTimeUnit("timeunit") + \\r
-                (from_ | before | after)("dir") + \\r
-                Optional(at_) + \\r
-                absTimeSpec("absTime") | \\r
-              qty("qty") + relativeTimeUnit("timeunit") + ago("dir") | \\r
-              in_ + qty("qty") + relativeTimeUnit("timeunit")\r
-relTimeSpec.setParseAction(convertToTimedelta,calculateTime)\r
-\r
-nlTimeExpression = (absTimeSpec + Optional(dayTimeSpec) |\r
-                    dayTimeSpec + Optional(Optional(at_) + absTimeSpec) |\r
-                    relTimeSpec + Optional(absTimeSpec))\r
-\r
-if __name__ == "__main__":\r
-    # test grammar\r
-    tests = """\\r
-    today\r
-    tomorrow\r
-    yesterday\r
-    in a couple of days\r
-    a couple of days from now\r
-    a couple of days from today\r
-    in a day\r
-    3 days ago\r
-    3 days from now\r
-    a day ago\r
-    in 2 weeks\r
-    in 3 days at 5pm\r
-    now\r
-    10 minutes ago\r
-    10 minutes from now\r
-    in 10 minutes\r
-    in a minute\r
-    in a couple of minutes\r
-    20 seconds ago\r
-    in 30 seconds\r
-    20 seconds before noon\r
-    20 seconds before noon tomorrow\r
-    noon\r
-    midnight\r
-    noon tomorrow\r
-    6am tomorrow\r
-    0800 yesterday\r
-    12:15 AM today\r
-    3pm 2 days from today\r
-    a week from today\r
-    a week from now\r
-    3 weeks ago\r
-    noon next Sunday\r
-    noon Sunday\r
-    noon last Sunday\r
-    2pm next Sunday\r
-    next Sunday at 2pm"""\r
-\r
-    print("(relative to %s)" % datetime.now())\r
-    nlTimeExpression.runTests(tests)\r
diff --git a/examples/delta_time.py b/examples/delta_time.py
new file mode 100644 (file)
index 0000000..e079094
--- /dev/null
@@ -0,0 +1,363 @@
+# deltaTime.py
+#
+# Parser to convert a conversational time reference such as "in a minute" or
+# "noon tomorrow" and convert it to a Python datetime. The returned
+# ParseResults object contains
+#   - original - the original time expression string
+#   - computed_dt - the Python datetime representing the computed time
+#   - relative_to - the reference "now" time
+#   - time_offset - the difference between the reference time and the computed time
+#
+# BNF:
+#     time_and_day ::= time_reference [day_reference] | day_reference 'at' absolute_time_of_day
+#     day_reference ::= absolute_day_reference | relative_day_reference
+#     absolute_day_reference ::= 'today' | 'tomorrow' | 'yesterday' | ('next' | 'last') weekday_name
+#     relative_day_reference ::= 'in' qty day_units
+#                                | qty day_units 'ago'
+#                                | 'qty day_units ('from' | 'before' | 'after') absolute_day_reference
+#     day_units ::= 'days' | 'weeks'
+#
+#     time_reference ::= absolute_time_of_day | relative_time_reference
+#     relative_time_reference ::= qty time_units ('from' | 'before' | 'after') absolute_time_of_day
+#                                 | qty time_units 'ago'
+#                                 | 'in' qty time_units
+#     time_units ::= 'hours' | 'minutes' | 'seconds'
+#     absolute_time_of_day ::= 'noon' | 'midnight' | 'now' | absolute_time
+#     absolute_time ::=  24hour_time | hour ("o'clock" | ':' minute) ('AM'|'PM')
+#
+#     qty ::= integer | integer_words | 'a couple of' | 'a' | 'the'
+#
+# Copyright 2010, 2019 by Paul McGuire
+#
+
+from datetime import datetime, time, timedelta
+import pyparsing as pp
+import calendar
+
+__all__ = ["time_expression"]
+
+# basic grammar definitions
+def make_integer_word_expr(int_name, int_value):
+    return pp.CaselessKeyword(int_name).addParseAction(pp.replaceWith(int_value))
+integer_word = pp.MatchFirst(make_integer_word_expr(int_str, int_value)
+                     for int_value, int_str
+                     in enumerate("one two three four five six seven eight nine ten"
+                                  " eleven twelve thirteen fourteen fifteen sixteen"
+                                  " seventeen eighteen nineteen twenty".split(), start=1))
+integer = pp.pyparsing_common.integer | integer_word
+
+CK = pp.CaselessKeyword
+CL = pp.CaselessLiteral
+today, tomorrow, yesterday, noon, midnight, now = map(CK, "today tomorrow yesterday noon midnight now".split())
+def plural(s):
+    return CK(s) | CK(s + 's').addParseAction(pp.replaceWith(s))
+week, day, hour, minute, second = map(plural, "week day hour minute second".split())
+am = CL("am")
+pm = CL("pm")
+COLON = pp.Suppress(':')
+
+in_ = CK("in").setParseAction(pp.replaceWith(1))
+from_ = CK("from").setParseAction(pp.replaceWith(1))
+before = CK("before").setParseAction(pp.replaceWith(-1))
+after = CK("after").setParseAction(pp.replaceWith(1))
+ago = CK("ago").setParseAction(pp.replaceWith(-1))
+next_ = CK("next").setParseAction(pp.replaceWith(1))
+last_ = CK("last").setParseAction(pp.replaceWith(-1))
+at_ = CK("at")
+on_ = CK("on")
+
+couple = (pp.Optional(CK("a")) + CK("couple") + pp.Optional(CK("of"))).setParseAction(pp.replaceWith(2))
+a_qty = (CK("a") | CK("an")).setParseAction(pp.replaceWith(1))
+the_qty = CK("the").setParseAction(pp.replaceWith(1))
+qty = pp.ungroup(integer | couple | a_qty | the_qty)
+time_ref_present = pp.Empty().addParseAction(pp.replaceWith(True))('time_ref_present')
+
+def fill_24hr_time_fields(t):
+    t['HH'] = t[0]
+    t['MM'] = t[1]
+    t['SS'] = 0
+    t['ampm'] = ('am','pm')[t.HH >= 12]
+
+def fill_default_time_fields(t):
+    for fld in 'HH MM SS'.split():
+        if fld not in t:
+            t[fld] = 0
+
+weekday_name_list = list(calendar.day_name)
+weekday_name = pp.oneOf(weekday_name_list)
+
+_24hour_time = pp.Word(pp.nums, exact=4).addParseAction(lambda t: [int(t[0][:2]),int(t[0][2:])],
+                                                        fill_24hr_time_fields)
+_24hour_time.setName("0000 time")
+ampm = am | pm
+timespec = (integer("HH")
+                    + pp.Optional(CK("o'clock")
+                       |
+                       COLON + integer("MM")
+                       + pp.Optional(COLON + integer("SS"))
+                       )
+                   + (am | pm)("ampm")
+                 ).addParseAction(fill_default_time_fields)
+absolute_time = _24hour_time | timespec
+
+absolute_time_of_day = noon | midnight | now | absolute_time
+
+def add_computed_time(t):
+    if t[0] in 'now noon midnight'.split():
+        t['computed_time'] = {'now': datetime.now().time().replace(microsecond=0),
+                              'noon': time(hour=12),
+                              'midnight': time()}[t[0]]
+    else:
+        t['HH'] = {'am': int(t['HH']) % 12,
+                   'pm': int(t['HH']) % 12 + 12}[t.ampm]
+        t['computed_time'] = time(hour=t.HH, minute=t.MM, second=t.SS)
+
+absolute_time_of_day.addParseAction(add_computed_time)
+
+
+#     relative_time_reference ::= qty time_units ('from' | 'before' | 'after') absolute_time_of_day
+#                                 | qty time_units 'ago'
+#                                 | 'in' qty time_units
+time_units = hour | minute | second
+relative_time_reference = (qty('qty') + time_units('units') + ago('dir')
+                           | qty('qty') + time_units('units')
+                             + (from_ | before | after)('dir')
+                             + pp.Group(absolute_time_of_day)('ref_time')
+                           | in_('dir') + qty('qty') + time_units('units')
+                           )
+
+def compute_relative_time(t):
+    if 'ref_time' not in t:
+        t['ref_time'] = datetime.now().time().replace(microsecond=0)
+    else:
+        t['ref_time'] = t.ref_time.computed_time
+    delta_seconds = {'hour': 3600,
+                     'minute': 60,
+                     'second': 1}[t.units] * t.qty
+    t['time_delta'] = timedelta(seconds=t.dir * delta_seconds)
+
+relative_time_reference.addParseAction(compute_relative_time)
+
+time_reference = absolute_time_of_day | relative_time_reference
+def add_default_time_ref_fields(t):
+    if 'time_delta' not in t:
+        t['time_delta'] = timedelta()
+time_reference.addParseAction(add_default_time_ref_fields)
+
+#     absolute_day_reference ::= 'today' | 'tomorrow' | 'yesterday' | ('next' | 'last') weekday_name
+#     day_units ::= 'days' | 'weeks'
+
+day_units = day | week
+weekday_reference = pp.Optional(next_ | last_, 1)('dir') + weekday_name('day_name')
+
+def convert_abs_day_reference_to_date(t):
+    now = datetime.now().replace(microsecond=0)
+
+    # handle day reference by weekday name
+    if 'day_name' in t:
+        todaynum = now.weekday()
+        daynames = [n.lower() for n in weekday_name_list]
+        nameddaynum = daynames.index(t.day_name.lower())
+        # compute difference in days - if current weekday name is referenced, then
+        # computed 0 offset is changed to 7
+        if t.dir > 0:
+            daydiff = (nameddaynum + 7 - todaynum) % 7 or 7
+        else:
+            daydiff = -((todaynum + 7 - nameddaynum) % 7 or 7)
+        t["abs_date"] = datetime(now.year, now.month, now.day) + timedelta(daydiff)
+    else:
+        name = t[0]
+        t["abs_date"] = {
+            "now"       : now,
+            "today"     : datetime(now.year, now.month, now.day),
+            "yesterday" : datetime(now.year, now.month, now.day) + timedelta(days=-1),
+            "tomorrow"  : datetime(now.year, now.month, now.day) + timedelta(days=+1),
+            }[name]
+
+absolute_day_reference = today | tomorrow | yesterday | now + time_ref_present | weekday_reference
+absolute_day_reference.addParseAction(convert_abs_day_reference_to_date)
+
+
+#     relative_day_reference ::=  'in' qty day_units
+#                                   | qty day_units 'ago'
+#                                   | 'qty day_units ('from' | 'before' | 'after') absolute_day_reference
+relative_day_reference = (in_('dir') + qty('qty') + day_units('units')
+                          | qty('qty') + day_units('units') + ago('dir')
+                          | qty('qty') + day_units('units') + (from_ | before | after)('dir')
+                            + absolute_day_reference('ref_day')
+                          )
+
+def compute_relative_date(t):
+    now = datetime.now().replace(microsecond=0)
+    if 'ref_day' in t:
+        t['computed_date'] = t.ref_day
+    else:
+        t['computed_date'] = now.date()
+    day_diff = t.dir * t.qty * {'week': 7, 'day': 1}[t.units]
+    t['date_delta'] = timedelta(days=day_diff)
+relative_day_reference.addParseAction(compute_relative_date)
+
+# combine expressions for absolute and relative day references
+day_reference = relative_day_reference | absolute_day_reference
+def add_default_date_fields(t):
+    if 'date_delta' not in t:
+        t['date_delta'] = timedelta()
+day_reference.addParseAction(add_default_date_fields)
+
+# combine date and time expressions into single overall parser
+time_and_day = (time_reference + time_ref_present + pp.Optional(pp.Optional(on_) + day_reference)
+               | day_reference + pp.Optional(at_ + absolute_time_of_day + time_ref_present))
+
+# parse actions for total time_and_day expression
+def save_original_string(s, l, t):
+    # save original input string and reference time
+    t['original'] = ' '.join(s.strip().split())
+    t['relative_to'] = datetime.now().replace(microsecond=0)
+
+def compute_timestamp(t):
+    # accumulate values from parsed time and day subexpressions - fill in defaults for omitted parts
+    now = datetime.now().replace(microsecond=0)
+    if 'computed_time' not in t:
+        t['computed_time'] = t.ref_time or now.time()
+    if 'abs_date' not in t:
+        t['abs_date'] = now
+
+    # roll up all fields and apply any time or day deltas
+    t['computed_dt'] = (
+        t.abs_date.replace(hour=t.computed_time.hour, minute=t.computed_time.minute, second=t.computed_time.second)
+        + (t.time_delta or timedelta(0))
+        + (t.date_delta or timedelta(0))
+    )
+
+    # if time just given in terms of day expressions, zero out time fields
+    if not t.time_ref_present:
+        t['computed_dt'] = t.computed_dt.replace(hour=0, minute=0, second=0)
+
+    # add results name compatible with previous version
+    t['calculatedTime'] = t.computed_dt
+
+    # add time_offset fields
+    t['time_offset'] = t.computed_dt - t.relative_to
+
+def remove_temp_keys(t):
+    # strip out keys that are just used internally
+    all_keys = list(t.keys())
+    for k in all_keys:
+        if k not in ('computed_dt', 'original', 'relative_to', 'time_offset', 'calculatedTime'):
+            del t[k]
+
+time_and_day.addParseAction(save_original_string, compute_timestamp, remove_temp_keys)
+
+
+time_expression = time_and_day
+
+
+if __name__ == "__main__":
+    current_time = datetime.now()
+    # test grammar
+    tests = """\
+        today
+        tomorrow
+        yesterday
+        the day before yesterday
+        the day after tomorrow
+        2 weeks after today
+        in a couple of days
+        a couple of days from now
+        a couple of days from today
+        in a day
+        3 days ago
+        3 days from now
+        a day ago
+        an hour ago
+        in 2 weeks
+        in 3 days at 5pm
+        now
+        10 minutes ago
+        10 minutes from now
+        in 10 minutes
+        in a minute
+        in a couple of minutes
+        20 seconds ago
+        in 30 seconds
+        in an hour
+        in a couple hours
+        in a couple days
+        20 seconds before noon
+        ten seconds before noon tomorrow
+        noon
+        midnight
+        noon tomorrow
+        6am tomorrow
+        0800 yesterday
+        1700 tomorrow
+        12:15 AM today
+        3pm 2 days from today
+        a week from today
+        a week from now
+        three weeks ago
+        noon next Sunday
+        noon Sunday
+        noon last Sunday
+        2pm next Sunday
+        next Sunday at 2pm
+        last Sunday at 2pm
+    """
+
+    time_of_day = timedelta(hours=current_time.hour,
+                            minutes=current_time.minute,
+                            seconds=current_time.second)
+    expected = {
+        'now' : timedelta(0),
+        '10 minutes ago': timedelta(minutes=-10),
+        '10 minutes from now': timedelta(minutes=10),
+        'in 10 minutes': timedelta(minutes=10),
+        'in a minute': timedelta(minutes=1),
+        'in a couple of minutes': timedelta(minutes=2),
+        '20 seconds ago': timedelta(seconds=-20),
+        'in 30 seconds': timedelta(seconds=30),
+        'in an hour': timedelta(hours=1),
+        'in a couple hours': timedelta(hours=2),
+        'a week from now': timedelta(days=7),
+        '3 days from now': timedelta(days=3),
+        'a couple of days from now': timedelta(days=2),
+        'an hour ago': timedelta(hours=-1),
+        'in a couple days': timedelta(days=2) - time_of_day,
+        'a week from today': timedelta(days=7) - time_of_day,
+        'three weeks ago': timedelta(days=-21) - time_of_day,
+        'a day ago': timedelta(days=-1) - time_of_day,
+        'in a couple of days': timedelta(days=2) - time_of_day,
+        'a couple of days from today': timedelta(days=2) - time_of_day,
+        '2 weeks after today': timedelta(days=14) - time_of_day,
+        'in 2 weeks': timedelta(days=14) - time_of_day,
+        'the day after tomorrow': timedelta(days=2) - time_of_day,
+        'tomorrow': timedelta(days=1) - time_of_day,
+        'the day before yesterday': timedelta(days=-2) - time_of_day,
+        'yesterday': timedelta(days=-1) - time_of_day,
+        'today': -time_of_day,
+        'midnight': -time_of_day,
+        'in a day': timedelta(days=1) - time_of_day,
+        '3 days ago': timedelta(days=-3) - time_of_day,
+        'noon tomorrow': timedelta(days=1) - time_of_day + timedelta(hours=12),
+        '6am tomorrow': timedelta(days=1) - time_of_day + timedelta(hours=6),
+        '0800 yesterday': timedelta(days=-1) - time_of_day + timedelta(hours=8),
+        '1700 tomorrow':  timedelta(days=1) - time_of_day + timedelta(hours=17),
+        '12:15 AM today': -time_of_day + timedelta(minutes=15),
+        '3pm 2 days from today': timedelta(days=2) - time_of_day + timedelta(hours=15),
+        'ten seconds before noon tomorrow': timedelta(days=1) - time_of_day
+                                            + timedelta(hours=12) + timedelta(seconds=-10),
+        '20 seconds before noon': -time_of_day + timedelta(hours=12) + timedelta(seconds=-20),
+        'in 3 days at 5pm': timedelta(days=3) - time_of_day + timedelta(hours=17),
+    }
+
+    def verify_offset(instring, parsed):
+        time_epsilon = timedelta(seconds=1)
+        if instring in expected:
+            # allow up to a second time discrepancy due to test processing time
+            if (parsed.time_offset - expected[instring]) <= time_epsilon:
+                parsed['verify_offset'] = 'PASS'
+            else:
+                parsed['verify_offset'] = 'FAIL'
+
+    print("(relative to %s)" % datetime.now())
+    time_expression.runTests(tests, postParse=verify_offset)
index 133f6c25b5850564131eebf010aecae886bb067a..0896c010ea6c09b3a6677ae97cce59db9a51ca26 100644 (file)
-# eval_arith.py\r
-#\r
-# Copyright 2009, 2011 Paul McGuire\r
-#\r
-# Expansion on the pyparsing example simpleArith.py, to include evaluation\r
-# of the parsed tokens.\r
-#\r
-# Added support for exponentiation, using right-to-left evaluation of\r
-# operands\r
-#\r
-from pyparsing import Word, nums, alphas, Combine, oneOf, \\r
-    opAssoc, infixNotation, Literal\r
-\r
-class EvalConstant(object):\r
-    "Class to evaluate a parsed constant or variable"\r
-    vars_ = {}\r
-    def __init__(self, tokens):\r
-        self.value = tokens[0]\r
-    def eval(self):\r
-        if self.value in EvalConstant.vars_:\r
-            return EvalConstant.vars_[self.value]\r
-        else:\r
-            return float(self.value)\r
-\r
-class EvalSignOp(object):\r
-    "Class to evaluate expressions with a leading + or - sign"\r
-    def __init__(self, tokens):\r
-        self.sign, self.value = tokens[0]\r
-    def eval(self):\r
-        mult = {'+':1, '-':-1}[self.sign]\r
-        return mult * self.value.eval()\r
-\r
-def operatorOperands(tokenlist):\r
-    "generator to extract operators and operands in pairs"\r
-    it = iter(tokenlist)\r
-    while 1:\r
-        try:\r
-            yield (next(it), next(it))\r
-        except StopIteration:\r
-            break\r
-\r
-class EvalPowerOp(object):\r
-    "Class to evaluate multiplication and division expressions"\r
-    def __init__(self, tokens):\r
-        self.value = tokens[0]\r
-    def eval(self):\r
-        res = self.value[-1].eval()\r
-        for val in self.value[-3::-2]:\r
-            res = val.eval()**res\r
-        return res\r
-\r
-class EvalMultOp(object):\r
-    "Class to evaluate multiplication and division expressions"\r
-    def __init__(self, tokens):\r
-        self.value = tokens[0]\r
-    def eval(self):\r
-        prod = self.value[0].eval()\r
-        for op,val in operatorOperands(self.value[1:]):\r
-            if op == '*':\r
-                prod *= val.eval()\r
-            if op == '/':\r
-                prod /= val.eval()\r
-        return prod\r
-\r
-class EvalAddOp(object):\r
-    "Class to evaluate addition and subtraction expressions"\r
-    def __init__(self, tokens):\r
-        self.value = tokens[0]\r
-    def eval(self):\r
-        sum = self.value[0].eval()\r
-        for op,val in operatorOperands(self.value[1:]):\r
-            if op == '+':\r
-                sum += val.eval()\r
-            if op == '-':\r
-                sum -= val.eval()\r
-        return sum\r
-\r
-class EvalComparisonOp(object):\r
-    "Class to evaluate comparison expressions"\r
-    opMap = {\r
-        "<" : lambda a,b : a < b,\r
-        "<=" : lambda a,b : a <= b,\r
-        ">" : lambda a,b : a > b,\r
-        ">=" : lambda a,b : a >= b,\r
-        "!=" : lambda a,b : a != b,\r
-        "=" : lambda a,b : a == b,\r
-        "LT" : lambda a,b : a < b,\r
-        "LE" : lambda a,b : a <= b,\r
-        "GT" : lambda a,b : a > b,\r
-        "GE" : lambda a,b : a >= b,\r
-        "NE" : lambda a,b : a != b,\r
-        "EQ" : lambda a,b : a == b,\r
-        "<>" : lambda a,b : a != b,\r
-        }\r
-    def __init__(self, tokens):\r
-        self.value = tokens[0]\r
-    def eval(self):\r
-        val1 = self.value[0].eval()\r
-        for op,val in operatorOperands(self.value[1:]):\r
-            fn = EvalComparisonOp.opMap[op]\r
-            val2 = val.eval()\r
-            if not fn(val1,val2):\r
-                break\r
-            val1 = val2\r
-        else:\r
-            return True\r
-        return False\r
-\r
-\r
-# define the parser\r
-integer = Word(nums)\r
-real = Combine(Word(nums) + "." + Word(nums))\r
-variable = Word(alphas,exact=1)\r
-operand = real | integer | variable\r
-\r
-signop = oneOf('+ -')\r
-multop = oneOf('* /')\r
-plusop = oneOf('+ -')\r
-expop = Literal('**')\r
-\r
-# use parse actions to attach EvalXXX constructors to sub-expressions\r
-operand.setParseAction(EvalConstant)\r
-arith_expr = infixNotation(operand,\r
-    [\r
-     (signop, 1, opAssoc.RIGHT, EvalSignOp),\r
-     (expop, 2, opAssoc.LEFT, EvalPowerOp),\r
-     (multop, 2, opAssoc.LEFT, EvalMultOp),\r
-     (plusop, 2, opAssoc.LEFT, EvalAddOp),\r
-    ])\r
-\r
-comparisonop = oneOf("< <= > >= != = <> LT GT LE GE EQ NE")\r
-comp_expr = infixNotation(arith_expr,\r
-    [\r
-    (comparisonop, 2, opAssoc.LEFT, EvalComparisonOp),\r
-    ])\r
-\r
-def main():\r
-    # sample expressions posted on comp.lang.python, asking for advice\r
-    # in safely evaluating them\r
-    rules=[\r
-             '( A - B ) = 0',\r
-             '(A + B + C + D + E + F + G + H + I) = J',\r
-             '(A + B + C + D + E + F + G + H) = I',\r
-             '(A + B + C + D + E + F) = G',\r
-             '(A + B + C + D + E) = (F + G + H + I + J)',\r
-             '(A + B + C + D + E) = (F + G + H + I)',\r
-             '(A + B + C + D + E) = F',\r
-             '(A + B + C + D) = (E + F + G + H)',\r
-             '(A + B + C) = (D + E + F)',\r
-             '(A + B) = (C + D + E + F)',\r
-             '(A + B) = (C + D)',\r
-             '(A + B) = (C - D + E - F - G + H + I + J)',\r
-             '(A + B) = C',\r
-             '(A + B) = 0',\r
-             '(A+B+C+D+E) = (F+G+H+I+J)',\r
-             '(A+B+C+D) = (E+F+G+H)',\r
-             '(A+B+C+D)=(E+F+G+H)',\r
-             '(A+B+C)=(D+E+F)',\r
-             '(A+B)=(C+D)',\r
-             '(A+B)=C',\r
-             '(A-B)=C',\r
-             '(A/(B+C))',\r
-             '(B/(C+D))',\r
-             '(G + H) = I',\r
-             '-0.99 LE ((A+B+C)-(D+E+F+G)) LE 0.99',\r
-             '-0.99 LE (A-(B+C)) LE 0.99',\r
-             '-1000.00 LE A LE 0.00',\r
-             '-5000.00 LE A LE 0.00',\r
-             'A < B',\r
-             'A < 7000',\r
-             'A = -(B)',\r
-             'A = C',\r
-             'A = 0',\r
-             'A GT 0',\r
-             'A GT 0.00',\r
-             'A GT 7.00',\r
-             'A LE B',\r
-             'A LT -1000.00',\r
-             'A LT -5000',\r
-             'A LT 0',\r
-             'A=(B+C+D)',\r
-             'A=B',\r
-             'I = (G + H)',\r
-             '0.00 LE A LE 4.00',\r
-             '4.00 LT A LE 7.00',\r
-             '0.00 LE A LE 4.00 LE E > D',\r
-             '2**2**(A+3)',\r
-         ]\r
-    vars_={'A': 0, 'B': 1.1, 'C': 2.2, 'D': 3.3, 'E': 4.4, 'F': 5.5, 'G':\r
-    6.6, 'H':7.7, 'I':8.8, 'J':9.9}\r
-\r
-    # define tests from given rules\r
-    tests = []\r
-    for t in rules:\r
-        t_orig = t\r
-        t = t.replace("=","==")\r
-        t = t.replace("EQ","==")\r
-        t = t.replace("LE","<=")\r
-        t = t.replace("GT",">")\r
-        t = t.replace("LT","<")\r
-        t = t.replace("GE",">=")\r
-        t = t.replace("LE","<=")\r
-        t = t.replace("NE","!=")\r
-        t = t.replace("<>","!=")\r
-        tests.append( (t_orig,eval(t,vars_)) )\r
-\r
-    # copy vars_ to EvalConstant lookup dict\r
-    EvalConstant.vars_ = vars_\r
-    failed = 0\r
-    for test,expected in tests:\r
-        ret = comp_expr.parseString(test)[0]\r
-        parsedvalue = ret.eval()\r
-        print(test, expected, parsedvalue)\r
-        if parsedvalue != expected:\r
-            print("<<< FAIL")\r
-            failed += 1\r
-        else:\r
-            print('')\r
-\r
-    print('')\r
-    if failed:\r
-        print(failed, "tests FAILED")\r
-    else:\r
-        print("all tests PASSED")\r
-\r
-if __name__=='__main__':\r
-    main()\r
+# eval_arith.py
+#
+# Copyright 2009, 2011 Paul McGuire
+#
+# Expansion on the pyparsing example simpleArith.py, to include evaluation
+# of the parsed tokens.
+#
+# Added support for exponentiation, using right-to-left evaluation of
+# operands
+#
+from pyparsing import Word, nums, alphas, Combine, oneOf, \
+    opAssoc, infixNotation, Literal
+
+class EvalConstant(object):
+    "Class to evaluate a parsed constant or variable"
+    vars_ = {}
+    def __init__(self, tokens):
+        self.value = tokens[0]
+    def eval(self):
+        if self.value in EvalConstant.vars_:
+            return EvalConstant.vars_[self.value]
+        else:
+            return float(self.value)
+
+class EvalSignOp(object):
+    "Class to evaluate expressions with a leading + or - sign"
+    def __init__(self, tokens):
+        self.sign, self.value = tokens[0]
+    def eval(self):
+        mult = {'+':1, '-':-1}[self.sign]
+        return mult * self.value.eval()
+
+def operatorOperands(tokenlist):
+    "generator to extract operators and operands in pairs"
+    it = iter(tokenlist)
+    while 1:
+        try:
+            yield (next(it), next(it))
+        except StopIteration:
+            break
+
+class EvalPowerOp(object):
+    "Class to evaluate multiplication and division expressions"
+    def __init__(self, tokens):
+        self.value = tokens[0]
+    def eval(self):
+        res = self.value[-1].eval()
+        for val in self.value[-3::-2]:
+            res = val.eval()**res
+        return res
+
+class EvalMultOp(object):
+    "Class to evaluate multiplication and division expressions"
+    def __init__(self, tokens):
+        self.value = tokens[0]
+    def eval(self):
+        prod = self.value[0].eval()
+        for op,val in operatorOperands(self.value[1:]):
+            if op == '*':
+                prod *= val.eval()
+            if op == '/':
+                prod /= val.eval()
+        return prod
+
+class EvalAddOp(object):
+    "Class to evaluate addition and subtraction expressions"
+    def __init__(self, tokens):
+        self.value = tokens[0]
+    def eval(self):
+        sum = self.value[0].eval()
+        for op,val in operatorOperands(self.value[1:]):
+            if op == '+':
+                sum += val.eval()
+            if op == '-':
+                sum -= val.eval()
+        return sum
+
+class EvalComparisonOp(object):
+    "Class to evaluate comparison expressions"
+    opMap = {
+        "<" : lambda a,b : a < b,
+        "<=" : lambda a,b : a <= b,
+        ">" : lambda a,b : a > b,
+        ">=" : lambda a,b : a >= b,
+        "!=" : lambda a,b : a != b,
+        "=" : lambda a,b : a == b,
+        "LT" : lambda a,b : a < b,
+        "LE" : lambda a,b : a <= b,
+        "GT" : lambda a,b : a > b,
+        "GE" : lambda a,b : a >= b,
+        "NE" : lambda a,b : a != b,
+        "EQ" : lambda a,b : a == b,
+        "<>" : lambda a,b : a != b,
+        }
+    def __init__(self, tokens):
+        self.value = tokens[0]
+    def eval(self):
+        val1 = self.value[0].eval()
+        for op,val in operatorOperands(self.value[1:]):
+            fn = EvalComparisonOp.opMap[op]
+            val2 = val.eval()
+            if not fn(val1,val2):
+                break
+            val1 = val2
+        else:
+            return True
+        return False
+
+
+# define the parser
+integer = Word(nums)
+real = Combine(Word(nums) + "." + Word(nums))
+variable = Word(alphas,exact=1)
+operand = real | integer | variable
+
+signop = oneOf('+ -')
+multop = oneOf('* /')
+plusop = oneOf('+ -')
+expop = Literal('**')
+
+# use parse actions to attach EvalXXX constructors to sub-expressions
+operand.setParseAction(EvalConstant)
+arith_expr = infixNotation(operand,
+    [
+     (signop, 1, opAssoc.RIGHT, EvalSignOp),
+     (expop, 2, opAssoc.LEFT, EvalPowerOp),
+     (multop, 2, opAssoc.LEFT, EvalMultOp),
+     (plusop, 2, opAssoc.LEFT, EvalAddOp),
+    ])
+
+comparisonop = oneOf("< <= > >= != = <> LT GT LE GE EQ NE")
+comp_expr = infixNotation(arith_expr,
+    [
+    (comparisonop, 2, opAssoc.LEFT, EvalComparisonOp),
+    ])
+
+def main():
+    # sample expressions posted on comp.lang.python, asking for advice
+    # in safely evaluating them
+    rules=[
+             '( A - B ) = 0',
+             '(A + B + C + D + E + F + G + H + I) = J',
+             '(A + B + C + D + E + F + G + H) = I',
+             '(A + B + C + D + E + F) = G',
+             '(A + B + C + D + E) = (F + G + H + I + J)',
+             '(A + B + C + D + E) = (F + G + H + I)',
+             '(A + B + C + D + E) = F',
+             '(A + B + C + D) = (E + F + G + H)',
+             '(A + B + C) = (D + E + F)',
+             '(A + B) = (C + D + E + F)',
+             '(A + B) = (C + D)',
+             '(A + B) = (C - D + E - F - G + H + I + J)',
+             '(A + B) = C',
+             '(A + B) = 0',
+             '(A+B+C+D+E) = (F+G+H+I+J)',
+             '(A+B+C+D) = (E+F+G+H)',
+             '(A+B+C+D)=(E+F+G+H)',
+             '(A+B+C)=(D+E+F)',
+             '(A+B)=(C+D)',
+             '(A+B)=C',
+             '(A-B)=C',
+             '(A/(B+C))',
+             '(B/(C+D))',
+             '(G + H) = I',
+             '-0.99 LE ((A+B+C)-(D+E+F+G)) LE 0.99',
+             '-0.99 LE (A-(B+C)) LE 0.99',
+             '-1000.00 LE A LE 0.00',
+             '-5000.00 LE A LE 0.00',
+             'A < B',
+             'A < 7000',
+             'A = -(B)',
+             'A = C',
+             'A = 0',
+             'A GT 0',
+             'A GT 0.00',
+             'A GT 7.00',
+             'A LE B',
+             'A LT -1000.00',
+             'A LT -5000',
+             'A LT 0',
+             'A=(B+C+D)',
+             'A=B',
+             'I = (G + H)',
+             '0.00 LE A LE 4.00',
+             '4.00 LT A LE 7.00',
+             '0.00 LE A LE 4.00 LE E > D',
+             '2**2**(A+3)',
+         ]
+    vars_={'A': 0, 'B': 1.1, 'C': 2.2, 'D': 3.3, 'E': 4.4, 'F': 5.5, 'G':
+    6.6, 'H':7.7, 'I':8.8, 'J':9.9}
+
+    # define tests from given rules
+    tests = []
+    for t in rules:
+        t_orig = t
+        t = t.replace("=","==")
+        t = t.replace("EQ","==")
+        t = t.replace("LE","<=")
+        t = t.replace("GT",">")
+        t = t.replace("LT","<")
+        t = t.replace("GE",">=")
+        t = t.replace("LE","<=")
+        t = t.replace("NE","!=")
+        t = t.replace("<>","!=")
+        tests.append( (t_orig,eval(t,vars_)) )
+
+    # copy vars_ to EvalConstant lookup dict
+    EvalConstant.vars_ = vars_
+    failed = 0
+    for test,expected in tests:
+        ret = comp_expr.parseString(test)[0]
+        parsedvalue = ret.eval()
+        print(test, expected, parsedvalue)
+        if parsedvalue != expected:
+            print("<<< FAIL")
+            failed += 1
+        else:
+            print('')
+
+    print('')
+    if failed:
+        print(failed, "tests FAILED")
+        return 1
+    else:
+        print("all tests PASSED")
+        return 0
+
+if __name__=='__main__':
+    exit(main())
index 261cea35fd8970b62ca42aff8c6952311e77b3e4..b10678bc53385916b02d58563d1b12178bcb05ce 100644 (file)
@@ -1,73 +1,72 @@
-# httpServerLogParser.py\r
-#\r
-# Copyright (c) 2016, Paul McGuire\r
-#\r
-"""\r
-Parser for HTTP server log output, of the form:\r
-\r
-195.146.134.15 - - [20/Jan/2003:08:55:36 -0800]\r
-"GET /path/to/page.html HTTP/1.0" 200 4649 "http://www.somedomain.com/020602/page.html"\r
-"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"\r
-127.0.0.1 - u.surname@domain.com [12/Sep/2006:14:13:53 +0300]\r
-"GET /skins/monobook/external.png HTTP/1.0" 304 - "http://wiki.mysite.com/skins/monobook/main.css"\r
-"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6"\r
-\r
-You can then break it up as follows:\r
-IP ADDRESS - -\r
-Server Date / Time [SPACE]\r
-"GET /path/to/page\r
-HTTP/Type Request"\r
-Success Code\r
-Bytes Sent To Client\r
-Referer\r
-Client Software\r
-"""\r
-\r
-from pyparsing import alphas,nums, dblQuotedString, Combine, Word, Group, delimitedList, Suppress, removeQuotes\r
-import string\r
-\r
-def getCmdFields( s, l, t ):\r
-    t["method"],t["requestURI"],t["protocolVersion"] = t[0].strip('"').split()\r
-\r
-logLineBNF = None\r
-def getLogLineBNF():\r
-    global logLineBNF\r
-\r
-    if logLineBNF is None:\r
-        integer = Word( nums )\r
-        ipAddress = delimitedList( integer, ".", combine=True )\r
-\r
-        timeZoneOffset = Word("+-",nums)\r
-        month = Word(string.uppercase, string.lowercase, exact=3)\r
-        serverDateTime = Group( Suppress("[") +\r
-                                Combine( integer + "/" + month + "/" + integer +\r
-                                        ":" + integer + ":" + integer + ":" + integer ) +\r
-                                timeZoneOffset +\r
-                                Suppress("]") )\r
-\r
-        logLineBNF = ( ipAddress.setResultsName("ipAddr") +\r
-                       Suppress("-") +\r
-                       ("-" | Word( alphas+nums+"@._" )).setResultsName("auth") +\r
-                       serverDateTime.setResultsName("timestamp") +\r
-                       dblQuotedString.setResultsName("cmd").setParseAction(getCmdFields) +\r
-                       (integer | "-").setResultsName("statusCode") +\r
-                       (integer | "-").setResultsName("numBytesSent")  +\r
-                       dblQuotedString.setResultsName("referrer").setParseAction(removeQuotes) +\r
-                       dblQuotedString.setResultsName("clientSfw").setParseAction(removeQuotes) )\r
-    return logLineBNF\r
-\r
-testdata = """\r
-195.146.134.15 - - [20/Jan/2003:08:55:36 -0800] "GET /path/to/page.html HTTP/1.0" 200 4649 "http://www.somedomain.com/020602/page.html" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"\r
-111.111.111.11 - - [16/Feb/2004:04:09:49 -0800] "GET /ads/redirectads/336x280redirect.htm HTTP/1.1" 304 - "http://www.foobarp.org/theme_detail.php?type=vs&cat=0&mid=27512" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"\r
-11.111.11.111 - - [16/Feb/2004:10:35:12 -0800] "GET /ads/redirectads/468x60redirect.htm HTTP/1.1" 200 541 "http://11.11.111.11/adframe.php?n=ad1f311a&what=zone:56" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) Opera 7.20  [ru\"]"\r
-127.0.0.1 - u.surname@domain.com [12/Sep/2006:14:13:53 +0300] "GET /skins/monobook/external.png HTTP/1.0" 304 - "http://wiki.mysite.com/skins/monobook/main.css" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6"\r
-"""\r
-for line in testdata.split("\n"):\r
-    if not line: continue\r
-    fields = getLogLineBNF().parseString(line)\r
-    print(fields.dump())\r
-    #~ print repr(fields)\r
-    #~ for k in fields.keys():\r
-        #~ print "fields." + k + " =", fields[k]\r
-    print(fields.asXML("LOG"))\r
-    print()\r
+# httpServerLogParser.py
+#
+# Copyright (c) 2016, Paul McGuire
+#
+"""
+Parser for HTTP server log output, of the form:
+
+195.146.134.15 - - [20/Jan/2003:08:55:36 -0800]
+"GET /path/to/page.html HTTP/1.0" 200 4649 "http://www.somedomain.com/020602/page.html"
+"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
+127.0.0.1 - u.surname@domain.com [12/Sep/2006:14:13:53 +0300]
+"GET /skins/monobook/external.png HTTP/1.0" 304 - "http://wiki.mysite.com/skins/monobook/main.css"
+"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6"
+
+You can then break it up as follows:
+IP ADDRESS - -
+Server Date / Time [SPACE]
+"GET /path/to/page
+HTTP/Type Request"
+Success Code
+Bytes Sent To Client
+Referer
+Client Software
+"""
+
+from pyparsing import alphas,nums, dblQuotedString, Combine, Word, Group, delimitedList, Suppress, removeQuotes
+import string
+
+def getCmdFields( s, l, t ):
+    t["method"],t["requestURI"],t["protocolVersion"] = t[0].strip('"').split()
+
+logLineBNF = None
+def getLogLineBNF():
+    global logLineBNF
+
+    if logLineBNF is None:
+        integer = Word( nums )
+        ipAddress = delimitedList( integer, ".", combine=True )
+
+        timeZoneOffset = Word("+-",nums)
+        month = Word(string.ascii_uppercase, string.ascii_lowercase, exact=3)
+        serverDateTime = Group( Suppress("[") +
+                                Combine( integer + "/" + month + "/" + integer +
+                                        ":" + integer + ":" + integer + ":" + integer ) +
+                                timeZoneOffset +
+                                Suppress("]") )
+
+        logLineBNF = ( ipAddress.setResultsName("ipAddr") +
+                       Suppress("-") +
+                       ("-" | Word( alphas+nums+"@._" )).setResultsName("auth") +
+                       serverDateTime.setResultsName("timestamp") +
+                       dblQuotedString.setResultsName("cmd").setParseAction(getCmdFields) +
+                       (integer | "-").setResultsName("statusCode") +
+                       (integer | "-").setResultsName("numBytesSent")  +
+                       dblQuotedString.setResultsName("referrer").setParseAction(removeQuotes) +
+                       dblQuotedString.setResultsName("clientSfw").setParseAction(removeQuotes) )
+    return logLineBNF
+
+testdata = """
+195.146.134.15 - - [20/Jan/2003:08:55:36 -0800] "GET /path/to/page.html HTTP/1.0" 200 4649 "http://www.somedomain.com/020602/page.html" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
+111.111.111.11 - - [16/Feb/2004:04:09:49 -0800] "GET /ads/redirectads/336x280redirect.htm HTTP/1.1" 304 - "http://www.foobarp.org/theme_detail.php?type=vs&cat=0&mid=27512" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
+11.111.11.111 - - [16/Feb/2004:10:35:12 -0800] "GET /ads/redirectads/468x60redirect.htm HTTP/1.1" 200 541 "http://11.11.111.11/adframe.php?n=ad1f311a&what=zone:56" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) Opera 7.20  [ru\"]"
+127.0.0.1 - u.surname@domain.com [12/Sep/2006:14:13:53 +0300] "GET /skins/monobook/external.png HTTP/1.0" 304 - "http://wiki.mysite.com/skins/monobook/main.css" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6"
+"""
+for line in testdata.split("\n"):
+    if not line: continue
+    fields = getLogLineBNF().parseString(line)
+    print(fields.dump())
+    #~ print repr(fields)
+    #~ for k in fields.keys():
+        #~ print "fields." + k + " =", fields[k]
+    print()
index 07eb3192e9c2fa678753e8237753d1ee25a34ee3..bf925096ade6db983a960705d05cb75c7a0a7752 100644 (file)
-#\r
-# lucene_grammar.py\r
-#\r
-# Copyright 2011, Paul McGuire\r
-#\r
-# implementation of Lucene grammar, as decribed\r
-# at http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/docs/queryparsersyntax.html\r
-#\r
-\r
-import pyparsing as pp\r
-from pyparsing import pyparsing_common as ppc\r
-pp.ParserElement.enablePackrat()\r
-\r
-COLON,LBRACK,RBRACK,LBRACE,RBRACE,TILDE,CARAT = map(pp.Literal,":[]{}~^")\r
-LPAR,RPAR = map(pp.Suppress,"()")\r
-and_, or_, not_, to_ = map(pp.CaselessKeyword, "AND OR NOT TO".split())\r
-keyword = and_ | or_ | not_ | to_\r
-\r
-expression = pp.Forward()\r
-\r
-valid_word = pp.Regex(r'([a-zA-Z0-9*_+.-]|\\\\|\\([+\-!(){}\[\]^"~*?:]|\|\||&&))+').setName("word")\r
-valid_word.setParseAction(\r
-    lambda t : t[0].replace('\\\\',chr(127)).replace('\\','').replace(chr(127),'\\')\r
-    )\r
-\r
-string = pp.QuotedString('"')\r
-\r
-required_modifier = pp.Literal("+")("required")\r
-prohibit_modifier = pp.Literal("-")("prohibit")\r
-integer = ppc.integer()\r
-proximity_modifier = pp.Group(TILDE + integer("proximity"))\r
-number = ppc.fnumber()\r
-fuzzy_modifier = TILDE + pp.Optional(number, default=0.5)("fuzzy")\r
-\r
-term = pp.Forward()\r
-field_name = valid_word().setName("fieldname")\r
-incl_range_search = pp.Group(LBRACK + term("lower") + to_ + term("upper") + RBRACK)\r
-excl_range_search = pp.Group(LBRACE + term("lower") + to_ + term("upper") + RBRACE)\r
-range_search = incl_range_search("incl_range") | excl_range_search("excl_range")\r
-boost = (CARAT + number("boost"))\r
-\r
-string_expr = pp.Group(string + proximity_modifier) | string\r
-word_expr = pp.Group(valid_word + fuzzy_modifier) | valid_word\r
-term << (pp.Optional(field_name("field") + COLON)\r
-         + (word_expr | string_expr | range_search | pp.Group(LPAR + expression + RPAR))\r
-         + pp.Optional(boost))\r
-term.setParseAction(lambda t:[t] if 'field' in t or 'boost' in t else None)\r
-\r
-expression << pp.infixNotation(term,\r
-    [\r
-    (required_modifier | prohibit_modifier, 1, pp.opAssoc.RIGHT),\r
-    ((not_ | '!').setParseAction(lambda: "NOT"), 1, pp.opAssoc.RIGHT),\r
-    ((and_ | '&&').setParseAction(lambda: "AND"), 2, pp.opAssoc.LEFT),\r
-    (pp.Optional(or_ | '||').setParseAction(lambda: "OR"), 2, pp.opAssoc.LEFT),\r
-    ])\r
-\r
-# test strings taken from grammar description doc, and TestQueryParser.java\r
-tests = r"""\r
-    # Success tests\r
-    a and b\r
-    a and not b\r
-    a and !b\r
-    a && !b\r
-    a&&!b\r
-    name:a\r
-    name:a and not title:b\r
-    (a^100 c d f) and !z\r
-    name:"blah de blah"\r
-    title:(+return +"pink panther")\r
-    title:"The Right Way" AND text:go\r
-    title:"Do it right" AND right\r
-    title:Do it right\r
-    roam~\r
-    roam~0.8\r
-    "jakarta apache"~10\r
-    mod_date:[20020101 TO 20030101]\r
-    title:{Aida TO Carmen}\r
-    jakarta apache\r
-    jakarta^4 apache\r
-    "jakarta apache"^4 "Apache Lucene"\r
-    "jakarta apache" jakarta\r
-    "jakarta apache" OR jakarta\r
-    "jakarta apache" AND "Apache Lucene"\r
-    +jakarta lucene\r
-    "jakarta apache" NOT "Apache Lucene"\r
-    "jakarta apache" -"Apache Lucene"\r
-    (jakarta OR apache) AND website\r
-    \(1+1\)\:2\r
-    c\:\\windows\r
-    (fieldX:xxxxx OR fieldy:xxxxxxxx)^2 AND (fieldx:the OR fieldy:foo)\r
-    (fieldX:xxxxx fieldy:xxxxxxxx)^2 AND (fieldx:the fieldy:foo)\r
-    (fieldX:xxxxx~0.5 fieldy:xxxxxxxx)^2 AND (fieldx:the fieldy:foo)\r
-    +term -term term\r
-    foo:term AND field:anotherTerm\r
-    germ term^2.0\r
-    (term)^2.0\r
-    (foo OR bar) AND (baz OR boo)\r
-    +(apple \"steve jobs\") -(foo bar baz)\r
-    +title:(dog OR cat) -author:\"bob dole\"\r
-    a AND b\r
-    +a +b\r
-    (a AND b)\r
-    c OR (a AND b)\r
-    c (+a +b)\r
-    a AND NOT b\r
-    +a -b\r
-    a AND -b\r
-    a AND !b\r
-    a && b\r
-    a && ! b\r
-    a OR b\r
-    a b\r
-    a || b\r
-    a OR !b\r
-    a -b\r
-    a OR ! b\r
-    a OR -b\r
-    a - b\r
-    a + b\r
-    a ! b\r
-    +foo:term +anotherterm\r
-    hello\r
-    term^2.0\r
-    (germ term)^2.0\r
-    term^2\r
-    +(foo bar) +(baz boo)\r
-    ((a OR b) AND NOT c) OR d\r
-    (+(a b) -c) d\r
-    field\r
-    a&&b\r
-    .NET\r
-    term\r
-    germ\r
-    3\r
-    term 1.0 1 2\r
-    term term1 term2\r
-    term term term\r
-    term*\r
-    term*^2\r
-    term*^2.0\r
-    term~\r
-    term~2.0\r
-    term~0.7\r
-    term~^3\r
-    term~2.0^3.0\r
-    term*germ\r
-    term*germ^3\r
-    term*germ^3.0\r
-    term~1.1\r
-    [A TO C]\r
-    t*erm*\r
-    *term*\r
-    term term^3.0 term\r
-    term stop^3.0 term\r
-    term +stop term\r
-    term -stop term\r
-    drop AND (stop) AND roll\r
-    +drop +roll\r
-    term +(stop) term\r
-    term -(stop) term\r
-    drop AND stop AND roll\r
-    term phrase term\r
-    term (phrase1 phrase2) term\r
-    term AND NOT phrase term\r
-    +term -(phrase1 phrase2) term\r
-    stop^3\r
-    stop\r
-    (stop)^3\r
-    ((stop))^3\r
-    (stop^3)\r
-    ((stop)^3)\r
-    (stop)\r
-    ((stop))\r
-    term +stop\r
-    [ a TO z]\r
-    [a TO z]\r
-    [ a TO z ]\r
-    { a TO z}\r
-    {a TO z}\r
-    { a TO z }\r
-    { a TO z }^2.0\r
-    {a TO z}^2.0\r
-    [ a TO z] OR bar\r
-    [a TO z] bar\r
-    [ a TO z] AND bar\r
-    +[a TO z] +bar\r
-    ( bar blar { a TO z})\r
-    bar blar {a TO z}\r
-    gack ( bar blar { a TO z})\r
-    gack (bar blar {a TO z})\r
-    [* TO Z]\r
-    [* TO z]\r
-    [A TO *]\r
-    [a TO *]\r
-    [* TO *]\r
-    [\* TO \*]\r
-    \!blah\r
-    \:blah\r
-    blah\r
-    \~blah\r
-    \*blah\r
-    a\r
-    a-b:c\r
-    a+b:c\r
-    a\:b:c\r
-    a\\b:c\r
-    a:b-c\r
-    a:b+c\r
-    a:b\:c\r
-    a:b\\c\r
-    a:b-c*\r
-    a:b+c*\r
-    a:b\:c*\r
-    a:b\\c*\r
-    a:b-c~2.0\r
-    a:b+c~2.0\r
-    a:b\:c~\r
-    a:b\\c~\r
-    [a- TO a+]\r
-    [ a\\ TO a\* ]\r
-    c\:\\temp\\\~foo.txt\r
-    abc\r
-    XYZ\r
-    (item:\\ item:ABCD\\)\r
-    \*\r
-    *\r
-    \\\r
-    \||\r
-    \&&\r
-    a\:b\:c\r
-    a\\b\:c\r
-    a\:b\\c\r
-    a\:b\:c\*\r
-    a\:b\\\\c\*\r
-    a:b-c~\r
-    a:b+c~\r
-    a\:b\:c\~\r
-    a\:b\\c\~\r
-    +weltbank +worlbank\r
-    +term +term +term\r
-    term +term term\r
-    term term +term\r
-    term +term +term\r
-    -term term term\r
-    -term +term +term\r
-    on\r
-    on^1.0\r
-    hello^2.0\r
-    the^3\r
-    the\r
-    some phrase\r
-    xunit~\r
-    one two three\r
-    A AND B OR C AND D\r
-    +A +B +C +D\r
-    foo:zoo*\r
-    foo:zoo*^2\r
-    zoo\r
-    foo:*\r
-    foo:*^2\r
-    *:foo\r
-    a:the OR a:foo\r
-    a:woo OR a:the\r
-    *:*\r
-    (*:*)\r
-    +*:* -*:*\r
-    the wizard of ozzy\r
-    """\r
-\r
-failtests = r"""\r
-    # Failure tests\r
-    field:term:with:colon some more terms\r
-    (sub query)^5.0^2.0 plus more\r
-    a:b:c\r
-    a:b:c~\r
-    a:b:c*\r
-    a:b:c~2.0\r
-    \+blah\r
-    \-blah\r
-    foo \|| bar\r
-    foo \AND bar\r
-    \a\r
-    a\-b:c\r
-    a\+b:c\r
-    a\b:c\r
-    a:b\-c\r
-    a:b\+c\r
-    a\-b\:c\r
-    a\+b\:c\r
-    a:b\c*\r
-    a:b\-c~\r
-    a:b\+c~\r
-    a:b\c\r
-    a:b\-c*\r
-    a:b\+c*\r
-    [ a\- TO a\+ ]\r
-    [a\ TO a*]\r
-    a\\\+b\r
-    a\+b\r
-    c:\temp\~foo.txt\r
-    XY\\r
-    a\u0062c\r
-    a:b\c~2.0\r
-    XY\u005a\r
-    XY\u005A\r
-    item:\ item:ABCD\\r
-    \\r
-    a\ or b\r
-    a\:b\-c\r
-    a\:b\+c\r
-    a\:b\-c\*\r
-    a\:b\+c\*\r
-    a\:b\-c\~\r
-    a\:b\+c\~\r
-    a:b\c~\r
-    [ a\ TO a* ]\r
-    """\r
-\r
-success1, _ = expression.runTests(tests)\r
-success2, _ = expression.runTests(failtests, failureTests=True)\r
-\r
-print(("FAIL", "OK")[success1 and success2])\r
-\r
-if not (success1 and success2):\r
-    raise Exception("failure in lucene grammar parser, check output")\r
+#
+# lucene_grammar.py
+#
+# Copyright 2011, Paul McGuire
+#
+# implementation of Lucene grammar, as decribed
+# at http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/docs/queryparsersyntax.html
+#
+
+import pyparsing as pp
+from pyparsing import pyparsing_common as ppc
+pp.ParserElement.enablePackrat()
+
+COLON,LBRACK,RBRACK,LBRACE,RBRACE,TILDE,CARAT = map(pp.Literal,":[]{}~^")
+LPAR,RPAR = map(pp.Suppress,"()")
+and_, or_, not_, to_ = map(pp.CaselessKeyword, "AND OR NOT TO".split())
+keyword = and_ | or_ | not_ | to_
+
+expression = pp.Forward()
+
+valid_word = pp.Regex(r'([a-zA-Z0-9*_+.-]|\\\\|\\([+\-!(){}\[\]^"~*?:]|\|\||&&))+').setName("word")
+valid_word.setParseAction(
+    lambda t : t[0].replace('\\\\',chr(127)).replace('\\','').replace(chr(127),'\\')
+    )
+
+string = pp.QuotedString('"')
+
+required_modifier = pp.Literal("+")("required")
+prohibit_modifier = pp.Literal("-")("prohibit")
+integer = ppc.integer()
+proximity_modifier = pp.Group(TILDE + integer("proximity"))
+number = ppc.fnumber()
+fuzzy_modifier = TILDE + pp.Optional(number, default=0.5)("fuzzy")
+
+term = pp.Forward()
+field_name = valid_word().setName("fieldname")
+incl_range_search = pp.Group(LBRACK - term("lower") + to_ + term("upper") + RBRACK)
+excl_range_search = pp.Group(LBRACE - term("lower") + to_ + term("upper") + RBRACE)
+range_search = incl_range_search("incl_range") | excl_range_search("excl_range")
+boost = (CARAT - number("boost"))
+
+string_expr = pp.Group(string + proximity_modifier) | string
+word_expr = pp.Group(valid_word + fuzzy_modifier) | valid_word
+term << (pp.Optional(field_name("field") + COLON)
+         + (word_expr | string_expr | range_search | pp.Group(LPAR + expression + RPAR))
+         + pp.Optional(boost))
+term.setParseAction(lambda t:[t] if 'field' in t or 'boost' in t else None)
+
+expression << pp.infixNotation(term,
+    [
+    (required_modifier | prohibit_modifier, 1, pp.opAssoc.RIGHT),
+    ((not_ | '!').setParseAction(lambda: "NOT"), 1, pp.opAssoc.RIGHT),
+    ((and_ | '&&').setParseAction(lambda: "AND"), 2, pp.opAssoc.LEFT),
+    (pp.Optional(or_ | '||').setParseAction(lambda: "OR"), 2, pp.opAssoc.LEFT),
+    ])
+
+if __name__ == '__main__':
+
+    # test strings taken from grammar description doc, and TestQueryParser.java
+    tests = r"""
+        # Success tests
+        a and b
+        a and not b
+        a and !b
+        a && !b
+        a&&!b
+        name:a
+        name:a and not title:b
+        (a^100 c d f) and !z
+        name:"blah de blah"
+        title:(+return +"pink panther")
+        title:"The Right Way" AND text:go
+        title:"Do it right" AND right
+        title:Do it right
+        roam~
+        roam~0.8
+        "jakarta apache"~10
+        mod_date:[20020101 TO 20030101]
+        title:{Aida TO Carmen}
+        jakarta apache
+        jakarta^4 apache
+        "jakarta apache"^4 "Apache Lucene"
+        "jakarta apache" jakarta
+        "jakarta apache" OR jakarta
+        "jakarta apache" AND "Apache Lucene"
+        +jakarta lucene
+        "jakarta apache" NOT "Apache Lucene"
+        "jakarta apache" -"Apache Lucene"
+        (jakarta OR apache) AND website
+        \(1+1\)\:2
+        c\:\\windows
+        (fieldX:xxxxx OR fieldy:xxxxxxxx)^2 AND (fieldx:the OR fieldy:foo)
+        (fieldX:xxxxx fieldy:xxxxxxxx)^2 AND (fieldx:the fieldy:foo)
+        (fieldX:xxxxx~0.5 fieldy:xxxxxxxx)^2 AND (fieldx:the fieldy:foo)
+        +term -term term
+        foo:term AND field:anotherTerm
+        germ term^2.0
+        (term)^2.0
+        (foo OR bar) AND (baz OR boo)
+        +(apple \"steve jobs\") -(foo bar baz)
+        +title:(dog OR cat) -author:\"bob dole\"
+        a AND b
+        +a +b
+        (a AND b)
+        c OR (a AND b)
+        c (+a +b)
+        a AND NOT b
+        +a -b
+        a AND -b
+        a AND !b
+        a && b
+        a && ! b
+        a OR b
+        a b
+        a || b
+        a OR !b
+        a -b
+        a OR ! b
+        a OR -b
+        a - b
+        a + b
+        a ! b
+        +foo:term +anotherterm
+        hello
+        term^2.0
+        (germ term)^2.0
+        term^2
+        +(foo bar) +(baz boo)
+        ((a OR b) AND NOT c) OR d
+        (+(a b) -c) d
+        field
+        a&&b
+        .NET
+        term
+        germ
+        3
+        term 1.0 1 2
+        term term1 term2
+        term term term
+        term*
+        term*^2
+        term*^2.0
+        term~
+        term~2.0
+        term~0.7
+        term~^3
+        term~2.0^3.0
+        term*germ
+        term*germ^3
+        term*germ^3.0
+        term~1.1
+        [A TO C]
+        t*erm*
+        *term*
+        term term^3.0 term
+        term stop^3.0 term
+        term +stop term
+        term -stop term
+        drop AND (stop) AND roll
+        +drop +roll
+        term +(stop) term
+        term -(stop) term
+        drop AND stop AND roll
+        term phrase term
+        term (phrase1 phrase2) term
+        term AND NOT phrase term
+        +term -(phrase1 phrase2) term
+        stop^3
+        stop
+        (stop)^3
+        ((stop))^3
+        (stop^3)
+        ((stop)^3)
+        (stop)
+        ((stop))
+        term +stop
+        [ a TO z]
+        [a TO z]
+        [ a TO z ]
+        { a TO z}
+        {a TO z}
+        { a TO z }
+        { a TO z }^2.0
+        {a TO z}^2.0
+        [ a TO z] OR bar
+        [a TO z] bar
+        [ a TO z] AND bar
+        +[a TO z] +bar
+        ( bar blar { a TO z})
+        bar blar {a TO z}
+        gack ( bar blar { a TO z})
+        gack (bar blar {a TO z})
+        [* TO Z]
+        [* TO z]
+        [A TO *]
+        [a TO *]
+        [* TO *]
+        [\* TO \*]
+        \!blah
+        \:blah
+        blah
+        \~blah
+        \*blah
+        a
+        a-b:c
+        a+b:c
+        a\:b:c
+        a\\b:c
+        a:b-c
+        a:b+c
+        a:b\:c
+        a:b\\c
+        a:b-c*
+        a:b+c*
+        a:b\:c*
+        a:b\\c*
+        a:b-c~2.0
+        a:b+c~2.0
+        a:b\:c~
+        a:b\\c~
+        [a- TO a+]
+        [ a\\ TO a\* ]
+        c\:\\temp\\\~foo.txt
+        abc
+        XYZ
+        (item:\\ item:ABCD\\)
+        \*
+        *
+        \\
+        \||
+        \&&
+        a\:b\:c
+        a\\b\:c
+        a\:b\\c
+        a\:b\:c\*
+        a\:b\\\\c\*
+        a:b-c~
+        a:b+c~
+        a\:b\:c\~
+        a\:b\\c\~
+        +weltbank +worlbank
+        +term +term +term
+        term +term term
+        term term +term
+        term +term +term
+        -term term term
+        -term +term +term
+        on
+        on^1.0
+        hello^2.0
+        the^3
+        the
+        some phrase
+        xunit~
+        one two three
+        A AND B OR C AND D
+        +A +B +C +D
+        foo:zoo*
+        foo:zoo*^2
+        zoo
+        foo:*
+        foo:*^2
+        *:foo
+        a:the OR a:foo
+        a:woo OR a:the
+        *:*
+        (*:*)
+        +*:* -*:*
+        the wizard of ozzy
+        """
+
+    failtests = r"""
+        # Failure tests
+
+        # multiple ':'s in term
+        field:term:with:colon some more terms
+
+        # multiple '^'s in term
+        (sub query)^5.0^2.0 plus more
+        a:b:c
+        a:b:c~
+        a:b:c*
+        a:b:c~2.0
+        \+blah
+        \-blah
+        foo \|| bar
+        foo \AND bar
+        \a
+        a\-b:c
+        a\+b:c
+        a\b:c
+        a:b\-c
+        a:b\+c
+        a\-b\:c
+        a\+b\:c
+        a:b\c*
+        a:b\-c~
+        a:b\+c~
+        a:b\c
+        a:b\-c*
+        a:b\+c*
+        [ a\- TO a\+ ]
+        [a\ TO a*]
+        a\\\+b
+        a\+b
+        c:\temp\~foo.txt
+        XY\
+        a\u0062c
+        a:b\c~2.0
+        XY\u005a
+        XY\u005A
+        item:\ item:ABCD\
+        \
+        a\ or b
+        a\:b\-c
+        a\:b\+c
+        a\:b\-c\*
+        a\:b\+c\*
+        a\:b\-c\~
+        a\:b\+c\~
+        a:b\c~
+        [ a\ TO a* ]
+        """
+
+    success1, _ = expression.runTests(tests)
+    success2, _ = expression.runTests(failtests, failureTests=True)
+
+    print("All tests:", ("FAIL", "OK")[success1 and success2])
+
+    if not (success1 and success2):
+        import sys
+        sys.exit(1)
diff --git a/examples/nested_markup.py b/examples/nested_markup.py
new file mode 100644 (file)
index 0000000..6d83636
--- /dev/null
@@ -0,0 +1,49 @@
+#
+# nested_markup.py
+#
+# Example markup parser to recursively transform nested markup directives.
+#
+# Copyright 2019, Paul McGuire
+#
+import pyparsing as pp
+
+wiki_markup = pp.Forward()
+
+# a method that will construct and return a parse action that will
+# do the proper wrapping in opening and closing HTML, and recursively call
+# wiki_markup.transformString on the markup body text
+def convert_markup_to_html(opening,closing):
+    def conversionParseAction(s, l, t):
+        return opening + wiki_markup.transformString(t[1][1:-1]) + closing
+    return conversionParseAction
+
+# use a nestedExpr with originalTextFor to parse nested braces, but return the
+# parsed text as a single string containing the outermost nested braces instead
+# of a nested list of parsed tokens
+markup_body = pp.originalTextFor(pp.nestedExpr('{', '}'))
+italicized = ('ital' + markup_body).setParseAction(convert_markup_to_html("<I>", "</I>"))
+bolded = ('bold' + markup_body).setParseAction(convert_markup_to_html("<B>", "</B>"))
+
+# another markup and parse action to parse links - again using transform string
+# to recursively parse any markup in the link text
+def convert_link_to_html(s, l, t):
+    link_text, url = t._skipped
+    t['link_text'] = wiki_markup.transformString(link_text)
+    t['url'] = url
+    return '<A href="{url}">{link_text}</A>'.format_map(t)
+
+urlRef = (pp.Keyword('link') + '{' + ... + '->' + ... + '}').setParseAction(convert_link_to_html)
+
+# now inject all the markup bits as possible markup expressions
+wiki_markup <<= urlRef | italicized | bolded
+
+# try it out!
+wiki_input = """
+Here is a simple Wiki input:
+
+  ital{This is in italics}.
+  bold{This is in bold}!
+  bold{This is in ital{bold italics}! But this is just bold.}
+  Here's a URL to link{Pyparsing's bold{Wiki Page}!->https://github.com/pyparsing/pyparsing/wiki}
+"""
+print(wiki_markup.transformString(wiki_input))
index 68a8f633a18a5341296f3c23ba1bcffcfcfda5df..0b3e9098bd921c635f42b633848af98c912d5f7c 100644 (file)
-# protobuf_parser.py\r
-#\r
-#  simple parser for parsing protobuf .proto files\r
-#\r
-#  Copyright 2010, Paul McGuire\r
-#\r
-\r
-from pyparsing import (Word, alphas, alphanums, Regex, Suppress, Forward,\r
-    Group, oneOf, ZeroOrMore, Optional, delimitedList,\r
-    restOfLine, quotedString, Dict)\r
-\r
-ident = Word(alphas+"_",alphanums+"_").setName("identifier")\r
-integer = Regex(r"[+-]?\d+")\r
-\r
-LBRACE,RBRACE,LBRACK,RBRACK,LPAR,RPAR,EQ,SEMI = map(Suppress,"{}[]()=;")\r
-\r
-kwds = """message required optional repeated enum extensions extends extend\r
-          to package service rpc returns true false option import"""\r
-for kw in kwds.split():\r
-    exec("{0}_ = Keyword('{1}')".format(kw.upper(), kw))\r
-\r
-messageBody = Forward()\r
-\r
-messageDefn = MESSAGE_ - ident("messageId") + LBRACE + messageBody("body") + RBRACE\r
-\r
-typespec = oneOf("""double float int32 int64 uint32 uint64 sint32 sint64\r
-                    fixed32 fixed64 sfixed32 sfixed64 bool string bytes""") | ident\r
-rvalue = integer | TRUE_ | FALSE_ | ident\r
-fieldDirective = LBRACK + Group(ident + EQ + rvalue) + RBRACK\r
-fieldDefn = (( REQUIRED_ | OPTIONAL_ | REPEATED_ )("fieldQualifier") -\r
-              typespec("typespec") + ident("ident") + EQ + integer("fieldint") + ZeroOrMore(fieldDirective) + SEMI)\r
-\r
-# enumDefn ::= 'enum' ident '{' { ident '=' integer ';' }* '}'\r
-enumDefn = ENUM_("typespec") - ident('name') + LBRACE + Dict( ZeroOrMore( Group(ident + EQ + integer + SEMI) ))('values') + RBRACE\r
-\r
-# extensionsDefn ::= 'extensions' integer 'to' integer ';'\r
-extensionsDefn = EXTENSIONS_ - integer + TO_ + integer + SEMI\r
-\r
-# messageExtension ::= 'extend' ident '{' messageBody '}'\r
-messageExtension = EXTEND_ - ident + LBRACE + messageBody + RBRACE\r
-\r
-# messageBody ::= { fieldDefn | enumDefn | messageDefn | extensionsDefn | messageExtension }*\r
-messageBody << Group(ZeroOrMore( Group(fieldDefn | enumDefn | messageDefn | extensionsDefn | messageExtension) ))\r
-\r
-# methodDefn ::= 'rpc' ident '(' [ ident ] ')' 'returns' '(' [ ident ] ')' ';'\r
-methodDefn = (RPC_ - ident("methodName") +\r
-              LPAR + Optional(ident("methodParam")) + RPAR +\r
-              RETURNS_ + LPAR + Optional(ident("methodReturn")) + RPAR)\r
-\r
-# serviceDefn ::= 'service' ident '{' methodDefn* '}'\r
-serviceDefn = SERVICE_ - ident("serviceName") + LBRACE + ZeroOrMore(Group(methodDefn)) + RBRACE\r
-\r
-# packageDirective ::= 'package' ident [ '.' ident]* ';'\r
-packageDirective = Group(PACKAGE_ - delimitedList(ident, '.', combine=True) + SEMI)\r
-\r
-comment = '//' + restOfLine\r
-\r
-importDirective = IMPORT_ - quotedString("importFileSpec") + SEMI\r
-\r
-optionDirective = OPTION_ - ident("optionName") + EQ + quotedString("optionValue") + SEMI\r
-\r
-topLevelStatement = Group(messageDefn | messageExtension | enumDefn | serviceDefn | importDirective | optionDirective)\r
-\r
-parser = Optional(packageDirective) + ZeroOrMore(topLevelStatement)\r
-\r
-parser.ignore(comment)\r
-\r
-\r
-test1 = """message Person {\r
-  required int32 id = 1;\r
-  required string name = 2;\r
-  optional string email = 3;\r
-}"""\r
-\r
-test2 = """package tutorial;\r
-\r
-message Person {\r
-  required string name = 1;\r
-  required int32 id = 2;\r
-  optional string email = 3;\r
-\r
-  enum PhoneType {\r
-    MOBILE = 0;\r
-    HOME = 1;\r
-    WORK = 2;\r
-  }\r
-\r
-  message PhoneNumber {\r
-    required string number = 1;\r
-    optional PhoneType type = 2 [default = HOME];\r
-  }\r
-\r
-  repeated PhoneNumber phone = 4;\r
-}\r
-\r
-message AddressBook {\r
-  repeated Person person = 1;\r
-}"""\r
-\r
-parser.runTests([test1, test2])\r
+# protobuf_parser.py
+#
+#  simple parser for parsing protobuf .proto files
+#
+#  Copyright 2010, Paul McGuire
+#
+
+from pyparsing import (Word, alphas, alphanums, Regex, Suppress, Forward,
+    Keyword, Group, oneOf, ZeroOrMore, Optional, delimitedList,
+    restOfLine, quotedString, Dict)
+
+ident = Word(alphas+"_",alphanums+"_").setName("identifier")
+integer = Regex(r"[+-]?\d+")
+
+LBRACE,RBRACE,LBRACK,RBRACK,LPAR,RPAR,EQ,SEMI = map(Suppress,"{}[]()=;")
+
+kwds = """message required optional repeated enum extensions extends extend
+          to package service rpc returns true false option import"""
+for kw in kwds.split():
+    exec("{0}_ = Keyword('{1}')".format(kw.upper(), kw))
+
+messageBody = Forward()
+
+messageDefn = MESSAGE_ - ident("messageId") + LBRACE + messageBody("body") + RBRACE
+
+typespec = oneOf("""double float int32 int64 uint32 uint64 sint32 sint64
+                    fixed32 fixed64 sfixed32 sfixed64 bool string bytes""") | ident
+rvalue = integer | TRUE_ | FALSE_ | ident
+fieldDirective = LBRACK + Group(ident + EQ + rvalue) + RBRACK
+fieldDefn = (( REQUIRED_ | OPTIONAL_ | REPEATED_ )("fieldQualifier") -
+              typespec("typespec") + ident("ident") + EQ + integer("fieldint") + ZeroOrMore(fieldDirective) + SEMI)
+
+# enumDefn ::= 'enum' ident '{' { ident '=' integer ';' }* '}'
+enumDefn = ENUM_("typespec") - ident('name') + LBRACE + Dict( ZeroOrMore( Group(ident + EQ + integer + SEMI) ))('values') + RBRACE
+
+# extensionsDefn ::= 'extensions' integer 'to' integer ';'
+extensionsDefn = EXTENSIONS_ - integer + TO_ + integer + SEMI
+
+# messageExtension ::= 'extend' ident '{' messageBody '}'
+messageExtension = EXTEND_ - ident + LBRACE + messageBody + RBRACE
+
+# messageBody ::= { fieldDefn | enumDefn | messageDefn | extensionsDefn | messageExtension }*
+messageBody << Group(ZeroOrMore( Group(fieldDefn | enumDefn | messageDefn | extensionsDefn | messageExtension) ))
+
+# methodDefn ::= 'rpc' ident '(' [ ident ] ')' 'returns' '(' [ ident ] ')' ';'
+methodDefn = (RPC_ - ident("methodName") +
+              LPAR + Optional(ident("methodParam")) + RPAR +
+              RETURNS_ + LPAR + Optional(ident("methodReturn")) + RPAR)
+
+# serviceDefn ::= 'service' ident '{' methodDefn* '}'
+serviceDefn = SERVICE_ - ident("serviceName") + LBRACE + ZeroOrMore(Group(methodDefn)) + RBRACE
+
+# packageDirective ::= 'package' ident [ '.' ident]* ';'
+packageDirective = Group(PACKAGE_ - delimitedList(ident, '.', combine=True) + SEMI)
+
+comment = '//' + restOfLine
+
+importDirective = IMPORT_ - quotedString("importFileSpec") + SEMI
+
+optionDirective = OPTION_ - ident("optionName") + EQ + quotedString("optionValue") + SEMI
+
+topLevelStatement = Group(messageDefn | messageExtension | enumDefn | serviceDefn | importDirective | optionDirective)
+
+parser = Optional(packageDirective) + ZeroOrMore(topLevelStatement)
+
+parser.ignore(comment)
+
+
+test1 = """message Person {
+  required int32 id = 1;
+  required string name = 2;
+  optional string email = 3;
+}"""
+
+test2 = """package tutorial;
+
+message Person {
+  required string name = 1;
+  required int32 id = 2;
+  optional string email = 3;
+
+  enum PhoneType {
+    MOBILE = 0;
+    HOME = 1;
+    WORK = 2;
+  }
+
+  message PhoneNumber {
+    required string number = 1;
+    optional PhoneType type = 2 [default = HOME];
+  }
+
+  repeated PhoneNumber phone = 4;
+}
+
+message AddressBook {
+  repeated Person person = 1;
+}"""
+
+parser.runTests([test1, test2])
index df07fbab21c5397bc3c3a310ef9ac179076ef99e..1a77231b133c28775dae929b9722e22c4ea4b164 100644 (file)
@@ -1,45 +1,57 @@
-# removeLineBreaks.py\r
-#\r
-# Demonstration of the pyparsing module, converting text files\r
-# with hard line-breaks to text files with line breaks only\r
-# between paragraphs.  (Helps when converting downloads from Project\r
-# Gutenberg - https://www.gutenberg.org/ - to import to word processing apps\r
-# that can reformat paragraphs once hard line-breaks are removed.)\r
-#\r
-# Uses parse actions and transformString to remove unwanted line breaks,\r
-# and to double up line breaks between paragraphs.\r
-#\r
-# Copyright 2006, by Paul McGuire\r
-#\r
-from pyparsing import *\r
-\r
-# define an expression for the body of a line of text - use a parse action to reject any\r
-# empty lines\r
-def mustBeNonBlank(s,l,t):\r
-    if not t[0]:\r
-        raise ParseException(s,l,"line body can't be empty")\r
-lineBody = SkipTo(lineEnd).setParseAction(mustBeNonBlank)\r
-\r
-# now define a line with a trailing lineEnd, to be replaced with a space character\r
-textLine = lineBody + Suppress(lineEnd).setParseAction(replaceWith(" "))\r
-\r
-# define a paragraph, with a separating lineEnd, to be replaced with a double newline\r
-para = OneOrMore(textLine) + Suppress(lineEnd).setParseAction(replaceWith("\n\n"))\r
-\r
-\r
-# run a test\r
-test = """\r
-    Now is the\r
-    time for\r
-    all\r
-    good men\r
-    to come to\r
-\r
-    the aid of their\r
-    country.\r
-"""\r
-print(para.transformString(test))\r
-\r
-# process an entire file\r
-z = para.transformString(file("Successful Methods of Public Speaking.txt").read())\r
-file("Successful Methods of Public Speaking(2).txt","w").write(z)\r
+# removeLineBreaks.py
+#
+# Demonstration of the pyparsing module, converting text files
+# with hard line-breaks to text files with line breaks only
+# between paragraphs.  (Helps when converting downloads from Project
+# Gutenberg - https://www.gutenberg.org/ - to import to word processing apps
+# that can reformat paragraphs once hard line-breaks are removed.)
+#
+# Uses parse actions and transformString to remove unwanted line breaks,
+# and to double up line breaks between paragraphs.
+#
+# Copyright 2006, by Paul McGuire
+#
+import pyparsing as pp
+
+line_end = pp.LineEnd()
+
+# define an expression for the body of a line of text - use a predicate condition to
+# accept only lines with some content.
+def mustBeNonBlank(t):
+    return t[0] != ''
+    # could also be written as
+    # return bool(t[0])
+
+lineBody = pp.SkipTo(line_end).addCondition(mustBeNonBlank, message="line body can't be empty")
+
+# now define a line with a trailing lineEnd, to be replaced with a space character
+textLine = lineBody + line_end().setParseAction(pp.replaceWith(" "))
+
+# define a paragraph, with a separating lineEnd, to be replaced with a double newline
+para = pp.OneOrMore(textLine) + line_end().setParseAction(pp.replaceWith("\n\n"))
+
+# run a test
+test = """
+    Now is the
+    time for
+    all
+    good men
+    to come to
+
+    the aid of their
+    country.
+"""
+print(para.transformString(test))
+
+# process an entire file
+#   Project Gutenberg EBook of Successful Methods of Public Speaking, by Grenville Kleiser
+#   Download from http://www.gutenberg.org/cache/epub/18095/pg18095.txt
+#
+with open("18095-8.txt") as source_file:
+    original = source_file.read()
+
+# use transformString to convert line breaks
+transformed = para.transformString(original)
+
+with open("18095-8_reformatted.txt", "w") as transformed_file:
+    transformed_file.write(transformed)
index 79a336660a60e60b8ca5e5ce5cafe5642fa7d9a5..7f9273c81fb09b4329ee1ca74f762bd7c0469974 100644 (file)
-# select_parser.py\r
-# Copyright 2010, Paul McGuire\r
-#\r
-# a simple SELECT statement parser, taken from SQLite's SELECT statement\r
-# definition at https://www.sqlite.org/lang_select.html\r
-#\r
-from pyparsing import *\r
-ParserElement.enablePackrat()\r
-\r
-LPAR,RPAR,COMMA = map(Suppress,"(),")\r
-select_stmt = Forward().setName("select statement")\r
-\r
-# keywords\r
-(UNION, ALL, AND, INTERSECT, EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER,\r
- CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT, DISTINCT, FROM, WHERE, GROUP, BY,\r
- HAVING, ORDER, BY, LIMIT, OFFSET, OR) =  map(CaselessKeyword, """UNION, ALL, AND, INTERSECT,\r
- EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER, CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT,\r
- DISTINCT, FROM, WHERE, GROUP, BY, HAVING, ORDER, BY, LIMIT, OFFSET, OR""".replace(",","").split())\r
-(CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, END, CASE, WHEN, THEN, EXISTS,\r
- COLLATE, IN, LIKE, GLOB, REGEXP, MATCH, ESCAPE, CURRENT_TIME, CURRENT_DATE,\r
- CURRENT_TIMESTAMP) = map(CaselessKeyword, """CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE,\r
- END, CASE, WHEN, THEN, EXISTS, COLLATE, IN, LIKE, GLOB, REGEXP, MATCH, ESCAPE,\r
- CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP""".replace(",","").split())\r
-keyword = MatchFirst((UNION, ALL, INTERSECT, EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER,\r
- CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT, DISTINCT, FROM, WHERE, GROUP, BY,\r
- HAVING, ORDER, BY, LIMIT, OFFSET, CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, END, CASE, WHEN, THEN, EXISTS,\r
- COLLATE, IN, LIKE, GLOB, REGEXP, MATCH, ESCAPE, CURRENT_TIME, CURRENT_DATE,\r
- CURRENT_TIMESTAMP))\r
-\r
-identifier = ~keyword + Word(alphas, alphanums+"_")\r
-collation_name = identifier.copy()\r
-column_name = identifier.copy()\r
-column_alias = identifier.copy()\r
-table_name = identifier.copy()\r
-table_alias = identifier.copy()\r
-index_name = identifier.copy()\r
-function_name = identifier.copy()\r
-parameter_name = identifier.copy()\r
-database_name = identifier.copy()\r
-\r
-# expression\r
-expr = Forward().setName("expression")\r
-\r
-integer = Regex(r"[+-]?\d+")\r
-numeric_literal = Regex(r"\d+(\.\d*)?([eE][+-]?\d+)?")\r
-string_literal = QuotedString("'")\r
-blob_literal = Regex(r"[xX]'[0-9A-Fa-f]+'")\r
-literal_value = ( numeric_literal | string_literal | blob_literal |\r
-    NULL | CURRENT_TIME | CURRENT_DATE | CURRENT_TIMESTAMP )\r
-bind_parameter = (\r
-    Word("?",nums) |\r
-    Combine(oneOf(": @ $") + parameter_name)\r
-    )\r
-type_name = oneOf("TEXT REAL INTEGER BLOB NULL")\r
-\r
-expr_term = (\r
-    CAST + LPAR + expr + AS + type_name + RPAR |\r
-    EXISTS + LPAR + select_stmt + RPAR |\r
-    function_name.setName("function_name") + LPAR + Optional("*" | delimitedList(expr)) + RPAR |\r
-    literal_value |\r
-    bind_parameter |\r
-    Combine(identifier+('.'+identifier)*(0,2)).setName("ident")\r
-    )\r
-\r
-UNARY,BINARY,TERNARY=1,2,3\r
-expr << infixNotation(expr_term,\r
-    [\r
-    (oneOf('- + ~') | NOT, UNARY, opAssoc.RIGHT),\r
-    (ISNULL | NOTNULL | NOT + NULL, UNARY, opAssoc.LEFT),\r
-    ('||', BINARY, opAssoc.LEFT),\r
-    (oneOf('* / %'), BINARY, opAssoc.LEFT),\r
-    (oneOf('+ -'), BINARY, opAssoc.LEFT),\r
-    (oneOf('<< >> & |'), BINARY, opAssoc.LEFT),\r
-    (oneOf('< <= > >='), BINARY, opAssoc.LEFT),\r
-    (oneOf('= == != <>') | IS | IN | LIKE | GLOB | MATCH | REGEXP, BINARY, opAssoc.LEFT),\r
-    ((BETWEEN,AND), TERNARY, opAssoc.LEFT),\r
-    (IN + LPAR + Group(select_stmt | delimitedList(expr)) + RPAR, UNARY, opAssoc.LEFT),\r
-    (AND, BINARY, opAssoc.LEFT),\r
-    (OR, BINARY, opAssoc.LEFT),\r
-    ])\r
-\r
-compound_operator = (UNION + Optional(ALL) | INTERSECT | EXCEPT)\r
-\r
-ordering_term = Group(expr('order_key') + Optional(COLLATE + collation_name('collate')) + Optional(ASC | DESC)('direction'))\r
-\r
-join_constraint = Group(Optional(ON + expr | USING + LPAR + Group(delimitedList(column_name)) + RPAR))\r
-\r
-join_op = COMMA | Group(Optional(NATURAL) + Optional(INNER | CROSS | LEFT + OUTER | LEFT | OUTER) + JOIN)\r
-\r
-join_source = Forward()\r
-single_source = ( (Group(database_name("database") + "." + table_name("table*")) | table_name("table*")) +\r
-                    Optional(Optional(AS) + table_alias("table_alias*")) +\r
-                    Optional(INDEXED + BY + index_name("name") | NOT + INDEXED)("index") |\r
-                  (LPAR + select_stmt + RPAR + Optional(Optional(AS) + table_alias)) |\r
-                  (LPAR + join_source + RPAR) )\r
-\r
-join_source << (Group(single_source + OneOrMore(join_op + single_source + join_constraint)) |\r
-                single_source)\r
-\r
-result_column = "*" | table_name + "." + "*" | Group(expr + Optional(Optional(AS) + column_alias))\r
-select_core = (SELECT + Optional(DISTINCT | ALL) + Group(delimitedList(result_column))("columns") +\r
-                Optional(FROM + join_source("from*")) +\r
-                Optional(WHERE + expr("where_expr")) +\r
-                Optional(GROUP + BY + Group(delimitedList(ordering_term))("group_by_terms") +\r
-                        Optional(HAVING + expr("having_expr"))))\r
-\r
-select_stmt << (select_core + ZeroOrMore(compound_operator + select_core) +\r
-                Optional(ORDER + BY + Group(delimitedList(ordering_term))("order_by_terms")) +\r
-                Optional(LIMIT + (Group(expr + OFFSET + expr) | Group(expr + COMMA + expr) | expr)("limit")))\r
-\r
-tests = """\\r
-    select * from xyzzy where z > 100\r
-    select * from xyzzy where z > 100 order by zz\r
-    select * from xyzzy\r
-    select z.* from xyzzy\r
-    select a, b from test_table where 1=1 and b='yes'\r
-    select a, b from test_table where 1=1 and b in (select bb from foo)\r
-    select z.a, b from test_table where 1=1 and b in (select bb from foo)\r
-    select z.a, b from test_table where 1=1 and b in (select bb from foo) order by b,c desc,d\r
-    select z.a, b from test_table left join test2_table where 1=1 and b in (select bb from foo)\r
-    select a, db.table.b as BBB from db.table where 1=1 and BBB='yes'\r
-    select a, db.table.b as BBB from test_table,db.table where 1=1 and BBB='yes'\r
-    select a, db.table.b as BBB from test_table,db.table where 1=1 and BBB='yes' limit 50\r
-    select a, b from test_table where (1=1 or 2=3) and b='yes' group by zx having b=2 order by 1\r
-    """\r
-\r
-select_stmt.runTests(tests)\r
+# select_parser.py
+# Copyright 2010,2019 Paul McGuire
+#
+# a simple SELECT statement parser, taken from SQLite's SELECT statement
+# definition at https://www.sqlite.org/lang_select.html
+#
+from pyparsing import *
+ParserElement.enablePackrat()
+
+LPAR,RPAR,COMMA = map(Suppress,"(),")
+DOT,STAR = map(Literal, ".*")
+select_stmt = Forward().setName("select statement")
+
+# keywords
+(UNION, ALL, AND, INTERSECT, EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER,
+ CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT, DISTINCT, FROM, WHERE, GROUP, BY,
+ HAVING, ORDER, BY, LIMIT, OFFSET, OR) =  map(CaselessKeyword, """UNION, ALL, AND, INTERSECT,
+ EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER, CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT,
+ DISTINCT, FROM, WHERE, GROUP, BY, HAVING, ORDER, BY, LIMIT, OFFSET, OR""".replace(",","").split())
+(CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, END, CASE, WHEN, THEN, EXISTS, IN, LIKE, GLOB, REGEXP,
+ MATCH, ESCAPE, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP) = map(CaselessKeyword,
+ """CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, END, CASE, WHEN, THEN, EXISTS, IN, LIKE, GLOB, 
+ REGEXP, MATCH, ESCAPE, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP""".replace(",","").split())
+keyword = MatchFirst((UNION, ALL, INTERSECT, EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER,
+                      CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT, DISTINCT, FROM, WHERE, GROUP, BY,
+                      HAVING, ORDER, BY, LIMIT, OFFSET, CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, END,
+                      CASE, WHEN, THEN, EXISTS, COLLATE, IN, LIKE, GLOB, REGEXP, MATCH, ESCAPE,
+                      CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP))
+
+identifier = ~keyword + Word(alphas, alphanums+"_")
+collation_name = identifier.copy()
+column_name = identifier.copy()
+column_alias = identifier.copy()
+table_name = identifier.copy()
+table_alias = identifier.copy()
+index_name = identifier.copy()
+function_name = identifier.copy()
+parameter_name = identifier.copy()
+database_name = identifier.copy()
+
+# expression
+expr = Forward().setName("expression")
+
+integer = Regex(r"[+-]?\d+")
+numeric_literal = Regex(r"\d+(\.\d*)?([eE][+-]?\d+)?")
+string_literal = QuotedString("'")
+blob_literal = Regex(r"[xX]'[0-9A-Fa-f]+'")
+literal_value = (
+        numeric_literal
+        | string_literal
+        | blob_literal
+        | NULL
+        | CURRENT_TIME
+        | CURRENT_DATE
+        | CURRENT_TIMESTAMP
+        )
+bind_parameter = (
+    Word("?",nums)
+    | Combine(oneOf(": @ $") + parameter_name)
+    )
+type_name = oneOf("TEXT REAL INTEGER BLOB NULL")
+
+expr_term = (
+    CAST + LPAR + expr + AS + type_name + RPAR
+    | EXISTS + LPAR + select_stmt + RPAR
+    | function_name.setName("function_name") + LPAR + Optional(STAR | delimitedList(expr)) + RPAR
+    | literal_value
+    | bind_parameter
+    | Group(identifier('col_db') + DOT + identifier('col_tab') + DOT + identifier('col'))
+    | Group(identifier('col_tab') + DOT + identifier('col'))
+    | Group(identifier('col'))
+    )
+
+UNARY,BINARY,TERNARY=1,2,3
+expr << infixNotation(expr_term,
+    [
+    (oneOf('- + ~') | NOT, UNARY, opAssoc.RIGHT),
+    (ISNULL | NOTNULL | NOT + NULL, UNARY, opAssoc.LEFT),
+    ('||', BINARY, opAssoc.LEFT),
+    (oneOf('* / %'), BINARY, opAssoc.LEFT),
+    (oneOf('+ -'), BINARY, opAssoc.LEFT),
+    (oneOf('<< >> & |'), BINARY, opAssoc.LEFT),
+    (oneOf('< <= > >='), BINARY, opAssoc.LEFT),
+    (oneOf('= == != <>') | IS | IN | LIKE | GLOB | MATCH | REGEXP, BINARY, opAssoc.LEFT),
+    ((BETWEEN,AND), TERNARY, opAssoc.LEFT),
+    (IN + LPAR + Group(select_stmt | delimitedList(expr)) + RPAR, UNARY, opAssoc.LEFT),
+    (AND, BINARY, opAssoc.LEFT),
+    (OR, BINARY, opAssoc.LEFT),
+    ])
+
+compound_operator = (UNION + Optional(ALL) | INTERSECT | EXCEPT)
+
+ordering_term = Group(expr('order_key')
+                      + Optional(COLLATE + collation_name('collate'))
+                      + Optional(ASC | DESC)('direction'))
+
+join_constraint = Group(Optional(ON + expr | USING + LPAR + Group(delimitedList(column_name)) + RPAR))
+
+join_op = COMMA | Group(Optional(NATURAL) + Optional(INNER | CROSS | LEFT + OUTER | LEFT | OUTER) + JOIN)
+
+join_source = Forward()
+single_source = (
+                Group(database_name("database") + DOT + table_name("table*")
+                      | table_name("table*"))
+                    + Optional(Optional(AS) + table_alias("table_alias*"))
+                    + Optional(INDEXED + BY + index_name("name") | NOT + INDEXED)("index")
+                | (LPAR + select_stmt + RPAR + Optional(Optional(AS) + table_alias))
+                | (LPAR + join_source + RPAR)
+                )
+
+join_source <<= (Group(single_source + OneOrMore(join_op + single_source + join_constraint))
+                 | single_source)
+
+# result_column = "*" | table_name + "." + "*" | Group(expr + Optional(Optional(AS) + column_alias))
+result_column = Group(STAR('col')
+                      | table_name('col_table') + DOT + STAR('col')
+                      | expr('col') + Optional(Optional(AS) + column_alias('alias'))
+                      )
+
+select_core = (SELECT + Optional(DISTINCT | ALL) + Group(delimitedList(result_column))("columns")
+               + Optional(FROM + join_source("from*"))
+               + Optional(WHERE + expr("where_expr"))
+               + Optional(GROUP + BY + Group(delimitedList(ordering_term))("group_by_terms")
+                          + Optional(HAVING + expr("having_expr"))))
+
+select_stmt << (select_core + ZeroOrMore(compound_operator + select_core)
+                + Optional(ORDER + BY + Group(delimitedList(ordering_term))("order_by_terms"))
+                + Optional(LIMIT + (Group(expr + OFFSET + expr) | Group(expr + COMMA + expr) | expr)("limit"))
+                )
+
+tests = """\
+    select * from xyzzy where z > 100
+    select * from xyzzy where z > 100 order by zz
+    select * from xyzzy
+    select z.* from xyzzy
+    select a, b from test_table where 1=1 and b='yes'
+    select a, b from test_table where 1=1 and b in (select bb from foo)
+    select z.a, b from test_table where 1=1 and b in (select bb from foo)
+    select z.a, b from test_table where 1=1 and b in (select bb from foo) order by b,c desc,d
+    select z.a, b from test_table left join test2_table where 1=1 and b in (select bb from foo)
+    select a, db.table.b as BBB from db.table where 1=1 and BBB='yes'
+    select a, db.table.b as BBB from test_table,db.table where 1=1 and BBB='yes'
+    select a, db.table.b as BBB from test_table,db.table where 1=1 and BBB='yes' limit 50
+    select a, b from test_table where (1=1 or 2=3) and b='yes' group by zx having b=2 order by 1
+    SELECT emp.ename as e FROM scott.employee as emp
+    SELECT ename as e, fname as f FROM scott.employee as emp
+    SELECT emp.eid, fname,lname FROM scott.employee as emp
+    SELECT ename, lname, emp.eid FROM scott.employee as emp
+    select emp.salary * (1.0 + emp.bonus) as salary_plus_bonus from scott.employee as emp
+"""
+select_stmt.runTests(tests)
index 123b401d629b9626bceada9b7f07fb43c35cff33..fd8ffd3a447ae5e09eb9ec91fb9d700988c5334a 100644 (file)
@@ -18,12 +18,12 @@ BNF reference: http://theory.lcs.mit.edu/~rivest/sexp.txt
 <sexp>         :: <string> | <list>\r
 <string>       :: <display>? <simple-string> ;\r
 <simple-string>        :: <raw> | <token> | <base-64> | <hexadecimal> |\r
-                          <quoted-string> ;\r
+                   <quoted-string> ;\r
 <display>      :: "[" <simple-string> "]" ;\r
 <raw>          :: <decimal> ":" <bytes> ;\r
 <decimal>      :: <decimal-digit>+ ;\r
-               -- decimal numbers should have no unnecessary leading zeros\r
-<bytes>        -- any string of bytes, of the indicated length\r
+        -- decimal numbers should have no unnecessary leading zeros\r
+<bytes>     -- any string of bytes, of the indicated length\r
 <token>        :: <tokenchar>+ ;\r
 <base-64>      :: <decimal>? "|" ( <base-64-char> | <whitespace> )* "|" ;\r
 <hexadecimal>   :: "#" ( <hex-digit> | <white-space> )* "#" ;\r
@@ -43,51 +43,51 @@ BNF reference: http://theory.lcs.mit.edu/~rivest/sexp.txt
 <null>         :: "" ;\r
 """\r
 \r
-from pyparsing import *\r
+import pyparsing as pp\r
 from base64 import b64decode\r
 import pprint\r
 \r
-def verifyLen(s,l,t):\r
+\r
+def verify_length(s, l, t):\r
     t = t[0]\r
     if t.len is not None:\r
         t1len = len(t[1])\r
         if t1len != t.len:\r
-            raise ParseFatalException(s,l,\\r
-                    "invalid data of length %d, expected %s" % (t1len, t.len))\r
+            raise pp.ParseFatalException(s, l, "invalid data of length {0}, expected {1}".format(t1len, t.len))\r
     return t[1]\r
 \r
+\r
 # define punctuation literals\r
-LPAR, RPAR, LBRK, RBRK, LBRC, RBRC, VBAR = map(Suppress, "()[]{}|")\r
-\r
-decimal = Regex(r'0|[1-9]\d*').setParseAction(lambda t: int(t[0]))\r
-hexadecimal = ("#" + OneOrMore(Word(hexnums)) + "#")\\r
-                .setParseAction(lambda t: int("".join(t[1:-1]),16))\r
-bytes = Word(printables)\r
-raw = Group(decimal("len") + Suppress(":") + bytes).setParseAction(verifyLen)\r
-token = Word(alphanums + "-./_:*+=")\r
-base64_ = Group(Optional(decimal|hexadecimal,default=None)("len") + VBAR\r
-    + OneOrMore(Word( alphanums +"+/=" )).setParseAction(lambda t: b64decode("".join(t)))\r
-    + VBAR).setParseAction(verifyLen)\r
-\r
-qString = Group(Optional(decimal,default=None)("len") +\r
-                        dblQuotedString.setParseAction(removeQuotes)).setParseAction(verifyLen)\r
-simpleString = base64_ | raw | decimal | token | hexadecimal | qString\r
-\r
-# extended definitions\r
-decimal = Regex(r'-?0|[1-9]\d*').setParseAction(lambda t: int(t[0]))\r
-real = Regex(r"[+-]?\d+\.\d*([eE][+-]?\d+)?").setParseAction(lambda tokens: float(tokens[0]))\r
-token = Word(alphanums + "-./_:*+=!<>")\r
+LPAR, RPAR, LBRK, RBRK, LBRC, RBRC, VBAR, COLON = (pp.Suppress(c).setName(c) for c in "()[]{}|:")\r
+\r
+decimal = pp.Regex(r'-?0|[1-9]\d*').setParseAction(lambda t: int(t[0]))\r
+hexadecimal = ("#" + pp.Word(pp.hexnums)[...] + "#").setParseAction(lambda t: int("".join(t[1:-1]), 16))\r
+bytes = pp.Word(pp.printables)\r
+raw = pp.Group(decimal("len") + COLON + bytes).setParseAction(verify_length)\r
+base64_ = pp.Group(pp.Optional(decimal | hexadecimal, default=None)("len")\r
+                   + VBAR\r
+                   + pp.Word(pp.alphanums + "+/=")[...].setParseAction(lambda t: b64decode("".join(t)))\r
+                   + VBAR\r
+                   ).setParseAction(verify_length)\r
+\r
+real = pp.Regex(r"[+-]?\d+\.\d*([eE][+-]?\d+)?").setParseAction(lambda tokens: float(tokens[0]))\r
+token = pp.Word(pp.alphanums + "-./_:*+=!<>")\r
+qString = pp.Group(pp.Optional(decimal, default=None)("len")\r
+                   + pp.dblQuotedString.setParseAction(pp.removeQuotes)\r
+                   ).setParseAction(verify_length)\r
 \r
 simpleString = real | base64_ | raw | decimal | token | hexadecimal | qString\r
 \r
 display = LBRK + simpleString + RBRK\r
-string_ = Optional(display) + simpleString\r
+string_ = pp.Optional(display) + simpleString\r
+\r
+sexp = pp.Forward()\r
+sexpList = pp.Group(LPAR + sexp[0, ...] + RPAR)\r
+sexp <<= string_ | sexpList\r
+\r
 \r
-sexp = Forward()\r
-sexpList = Group(LPAR + ZeroOrMore(sexp) + RPAR)\r
-sexp << ( string_ | sexpList )\r
+#  Test data\r
 \r
-######### Test data ###########\r
 test00 = """(snicker "abc" (#03# |YWJj|))"""\r
 test01 = """(certificate\r
  (issuer\r
@@ -142,7 +142,7 @@ test07 = """(defun factorial (x)
 test51 = """(2:XX "abc" (#03# |YWJj|))"""\r
 test51error = """(3:XX "abc" (#03# |YWJj|))"""\r
 \r
-test52 =     """\r
+test52 = """\r
     (and\r
       (or (> uid 1000)\r
           (!= gid 20)\r
@@ -152,7 +152,6 @@ test52 =     """
     """\r
 \r
 # Run tests\r
-t = None\r
-alltests = [ globals()[t] for t in sorted(locals()) if t.startswith("test") ]\r
+alltests = [globals()[testname] for testname in sorted(locals()) if testname.startswith("test")]\r
 \r
 sexp.runTests(alltests, fullDump=False)\r
index cbd3d26a2a35d3db9e697cfb883abe0491fd09d6..d9511da0bd9953623c3edef753567519b2253f67 100644 (file)
@@ -3,13 +3,13 @@
 #\r
 # Sample parser grammar to read a number given in words, and return the numeric value.\r
 #\r
-from pyparsing import *\r
+import pyparsing as pp\r
 from operator import mul\r
 from functools import reduce\r
 \r
-def makeLit(s,val):\r
-    ret = CaselessLiteral(s).setName(s)\r
-    return ret.setParseAction( replaceWith(val) )\r
+def makeLit(s, val):\r
+    ret = pp.CaselessLiteral(s)\r
+    return ret.setParseAction(pp.replaceWith(val))\r
 \r
 unitDefinitions = [\r
     ("zero",       0),\r
@@ -38,7 +38,7 @@ unitDefinitions = [
     ("eighteen",  18),\r
     ("nineteen",  19),\r
     ]\r
-units = Or(makeLit(s,v) for s,v in unitDefinitions)\r
+units = pp.MatchFirst(makeLit(s,v) for s,v in sorted(unitDefinitions, key=lambda d: -len(d[0])))\r
 \r
 tensDefinitions = [\r
     ("ten",     10),\r
@@ -52,7 +52,7 @@ tensDefinitions = [
     ("eighty",  80),\r
     ("ninety",  90),\r
     ]\r
-tens = Or(makeLit(s,v) for s,v in tensDefinitions)\r
+tens = pp.MatchFirst(makeLit(s,v) for s,v in tensDefinitions)\r
 \r
 hundreds = makeLit("hundred", 100)\r
 \r
@@ -64,43 +64,47 @@ majorDefinitions = [
     ("quadrillion", int(1e15)),\r
     ("quintillion", int(1e18)),\r
     ]\r
-mag = Or(makeLit(s,v) for s,v in majorDefinitions)\r
+mag = pp.MatchFirst(makeLit(s,v) for s,v in majorDefinitions)\r
 \r
 wordprod = lambda t: reduce(mul,t)\r
-wordsum = lambda t: sum(t)\r
-numPart = (((( units + Optional(hundreds) ).setParseAction(wordprod) +\r
-               Optional(tens)).setParseAction(wordsum)\r
-               ^ tens )\r
-               + Optional(units) ).setParseAction(wordsum)\r
-numWords = OneOrMore( (numPart + Optional(mag)).setParseAction(wordprod)\r
-                    ).setParseAction(wordsum) + StringEnd()\r
-numWords.ignore(Literal("-"))\r
-numWords.ignore(CaselessLiteral("and"))\r
+numPart = ((((units + pp.Optional(hundreds)).setParseAction(wordprod)\r
+             + pp.Optional(tens)\r
+             ).setParseAction(sum)\r
+            ^ tens)\r
+           + pp.Optional(units)\r
+           ).setParseAction(sum)\r
+numWords = ((numPart + pp.Optional(mag)).setParseAction(wordprod)[...]).setParseAction(sum)\r
+numWords.setName("num word parser")\r
 \r
-def test(s,expected):\r
-    try:\r
-        fail_expected = (expected is None)\r
-        success, results_tup = numWords.runTests(s, failureTests=fail_expected)\r
-        assert success, "Failed test!"\r
-        if not fail_expected:\r
-            teststr, results = results_tup[0]\r
-            observed = results[0]\r
-            assert expected == observed, "incorrect parsed value, {0} -> {1}, should be {2}".format(teststr, observed, expected)\r
-    except Exception as exc:\r
-        print("{0}: {1}".format(type(exc).__name__, exc))\r
+numWords.ignore(pp.Literal("-"))\r
+numWords.ignore(pp.CaselessLiteral("and"))\r
 \r
-test("one hundred twenty hundred", None)\r
-test("one hundred and twennty", None)\r
-test("one hundred and twenty", 120)\r
-test("one hundred and three", 103)\r
-test("one hundred twenty-three", 123)\r
-test("one hundred and twenty three", 123)\r
-test("one hundred twenty three million", 123000000)\r
-test("one hundred and twenty three million", 123000000)\r
-test("one hundred twenty three million and three", 123000003)\r
-test("fifteen hundred and sixty five", 1565)\r
-test("seventy-seven thousand eight hundred and nineteen", 77819)\r
-test("seven hundred seventy-seven thousand seven hundred and seventy-seven", 777777)\r
-test("zero", 0)\r
-test("forty two", 42)\r
-test("fourty two", 42)\r
+tests = """\r
+    one hundred twenty hundred, None\r
+    one hundred and twennty, None\r
+    one hundred and twenty, 120\r
+    one hundred and three, 103\r
+    one hundred twenty-three, 123\r
+    one hundred and twenty three, 123\r
+    one hundred twenty three million, 123000000\r
+    one hundred and twenty three million, 123000000\r
+    one hundred twenty three million and three, 123000003\r
+    fifteen hundred and sixty five, 1565\r
+    seventy-seven thousand eight hundred and nineteen, 77819\r
+    seven hundred seventy-seven thousand seven hundred and seventy-seven, 777777\r
+    zero, 0\r
+    forty two, 42\r
+    fourty two, 42\r
+"""\r
+\r
+# use '| ...' to indicate "if omitted, skip to next" logic\r
+test_expr = (numWords('result') | ...) + ',' + (pp.pyparsing_common.integer('expected') | 'None')\r
+\r
+def verify_result(t):\r
+    if '_skipped' in t:\r
+        t['pass'] = False\r
+    elif 'expected' in t:\r
+        t['pass'] = t.result == t.expected\r
+test_expr.addParseAction(verify_result)\r
+\r
+test_expr.runTests(tests)\r
index 20ce5c23346087ba32b4eafd12753eb7b27d1374..113698f8f49b711ee1a77b989a18750d11b1c15a 100644 (file)
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: pyparsing
-Version: 2.4.0
+Version: 2.4.1.1
 Summary: Python parsing module
 Home-page: https://github.com/pyparsing/pyparsing/
 Author: Paul McGuire
index 7c99a77ec7615bd2d204ff527b0a3d2fd013f89a..b767e5afc68f512731ecc0112a6e6afdd1a52097 100644 (file)
@@ -37,7 +37,7 @@ examples/configParse.py
 examples/cpp_enum_parser.py
 examples/datetimeParseActions.py
 examples/decaf_parser.py
-examples/deltaTime.py
+examples/delta_time.py
 examples/dfmparse.py
 examples/dhcpd_leases_parser.py
 examples/dictExample.py
@@ -71,6 +71,7 @@ examples/matchPreviousDemo.py
 examples/mozilla.ics
 examples/mozillaCalendarParser.py
 examples/nested.py
+examples/nested_markup.py
 examples/numerics.py
 examples/oc.py
 examples/parseListString.py
index 5b5897fb23be0514ad6e8c81b0e2ad6ae36f9faa..fb277fdf223811b1151f8c2f83381e05d730f251 100644 (file)
@@ -1,4 +1,4 @@
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
 # module pyparsing.py
 #
 # Copyright (c) 2003-2019  Paul T. McGuire
@@ -87,14 +87,16 @@ classes inherit from. Use the docstrings for examples of how to:
    more complex ones
  - associate names with your parsed results using
    :class:`ParserElement.setResultsName`
+ - access the parsed data, which is returned as a :class:`ParseResults`
+   object
  - find some helpful expression short-cuts like :class:`delimitedList`
    and :class:`oneOf`
  - find more useful common expressions in the :class:`pyparsing_common`
    namespace class
 """
 
-__version__ = "2.4.0"
-__versionTime__ = "07 Apr 2019 18:28 UTC"
+__version__ = "2.4.1.1"
+__versionTime__ = "25 Jul 2019 01:03 UTC"
 __author__ = "Paul McGuire <ptmcg@users.sourceforge.net>"
 
 import string
@@ -109,6 +111,9 @@ import pprint
 import traceback
 import types
 from datetime import datetime
+from operator import itemgetter
+import itertools
+from functools import wraps
 
 try:
     # Python 3
@@ -124,11 +129,11 @@ except ImportError:
 try:
     # Python 3
     from collections.abc import Iterable
-    from collections.abc import MutableMapping
+    from collections.abc import MutableMapping, Mapping
 except ImportError:
     # Python 2.7
     from collections import Iterable
-    from collections import MutableMapping
+    from collections import MutableMapping, Mapping
 
 try:
     from collections import OrderedDict as _OrderedDict
@@ -146,40 +151,63 @@ except ImportError:
 # version compatibility configuration
 __compat__ = SimpleNamespace()
 __compat__.__doc__ = """
-    A cross-version compatibility configuration for pyparsing features that will be 
-    released in a future version. By setting values in this configuration to True, 
-    those features can be enabled in prior versions for compatibility development 
+    A cross-version compatibility configuration for pyparsing features that will be
+    released in a future version. By setting values in this configuration to True,
+    those features can be enabled in prior versions for compatibility development
     and testing.
-    
+
      - collect_all_And_tokens - flag to enable fix for Issue #63 that fixes erroneous grouping
-       of results names when an And expression is nested within an Or or MatchFirst; set to 
-       True to enable bugfix to be released in pyparsing 2.4
+       of results names when an And expression is nested within an Or or MatchFirst; set to
+       True to enable bugfix released in pyparsing 2.3.0, or False to preserve
+       pre-2.3.0 handling of named results
 """
 __compat__.collect_all_And_tokens = True
 
-
-#~ sys.stderr.write( "testing pyparsing module, version %s, %s\n" % (__version__,__versionTime__ ) )
-
-__all__ = [ '__version__', '__versionTime__', '__author__', '__compat__',
-'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty',
-'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal',
-'PrecededBy', 'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or',
-'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException',
-'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException',
-'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter',
-'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore', 'Char',
-'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',
-'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',
-'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums',
-'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno',
-'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',
-'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',
-'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity',
-'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',
-'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',
-'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation','locatedExpr', 'withClass',
-'CloseMatch', 'tokenMap', 'pyparsing_common', 'pyparsing_unicode', 'unicode_set',
-]
+__diag__ = SimpleNamespace()
+__diag__.__doc__ = """
+Diagnostic configuration
+     - warn_multiple_tokens_in_named_alternation - flag to enable warnings when a results
+       name is defined on a MatchFirst or Or expression with one or more And subexpressions
+       (default=True) (only warns if __compat__.collect_all_And_tokens is False)
+     - 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)
+     - 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)
+     - warn_on_multiple_string_args_to_oneof - flag to enable warnings whan oneOf is
+       incorrectly called with multiple str arguments (default=True)
+     - enable_debug_on_named_expressions - flag to auto-enable debug on all subsequent
+       calls to ParserElement.setName() (default=False)
+"""
+__diag__.warn_multiple_tokens_in_named_alternation = True
+__diag__.warn_ungrouped_named_tokens_in_collection = True
+__diag__.warn_name_set_on_empty_Forward = False
+__diag__.warn_on_multiple_string_args_to_oneof = True
+__diag__.enable_debug_on_named_expressions = False
+
+# ~ sys.stderr.write("testing pyparsing module, version %s, %s\n" % (__version__, __versionTime__))
+
+__all__ = ['__version__', '__versionTime__', '__author__', '__compat__', '__diag__',
+           'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty',
+           'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal',
+           'PrecededBy', 'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or',
+           'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException',
+           'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException',
+           'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter',
+           'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore', 'Char',
+           'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',
+           'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',
+           'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums',
+           'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno',
+           'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',
+           'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',
+           'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity',
+           'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',
+           'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',
+           'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation', 'locatedExpr', 'withClass',
+           'CloseMatch', 'tokenMap', 'pyparsing_common', 'pyparsing_unicode', 'unicode_set',
+           'conditionAsParseAction',
+           ]
 
 system_version = tuple(sys.version_info)[:3]
 PY_3 = system_version[0] == 3
@@ -204,7 +232,7 @@ else:
         < returns the unicode object | encodes it with the default
         encoding | ... >.
         """
-        if isinstance(obj,unicode):
+        if isinstance(obj, unicode):
             return obj
 
         try:
@@ -222,9 +250,10 @@ else:
     # build list of single arg builtins, tolerant of Python version, that can be used as parse actions
     singleArgBuiltins = []
     import __builtin__
+
     for fname in "sum len sorted reversed list tuple set any all min max".split():
         try:
-            singleArgBuiltins.append(getattr(__builtin__,fname))
+            singleArgBuiltins.append(getattr(__builtin__, fname))
         except AttributeError:
             continue
 
@@ -235,23 +264,36 @@ def _xml_escape(data):
 
     # ampersand must be replaced first
     from_symbols = '&><"\''
-    to_symbols = ('&'+s+';' for s in "amp gt lt quot apos".split())
-    for from_,to_ in zip(from_symbols, to_symbols):
+    to_symbols = ('&' + s + ';' for s in "amp gt lt quot apos".split())
+    for from_, to_ in zip(from_symbols, to_symbols):
         data = data.replace(from_, to_)
     return data
 
-alphas     = string.ascii_uppercase + string.ascii_lowercase
-nums       = "0123456789"
-hexnums    = nums + "ABCDEFabcdef"
-alphanums  = alphas + nums
-_bslash    = chr(92)
+alphas = string.ascii_uppercase + string.ascii_lowercase
+nums = "0123456789"
+hexnums = nums + "ABCDEFabcdef"
+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
     # constructor as small and fast as possible
-    def __init__( self, pstr, loc=0, msg=None, elem=None ):
+    def __init__(self, pstr, loc=0, msg=None, elem=None):
         self.loc = loc
         if msg is None:
             self.msg = pstr
@@ -270,27 +312,34 @@ class ParseBaseException(Exception):
         """
         return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)
 
-    def __getattr__( self, aname ):
+    def __getattr__(self, aname):
         """supported attributes by name are:
            - lineno - returns the line number of the exception text
            - col - returns the column number of the exception text
            - line - returns the line containing the exception text
         """
-        if( aname == "lineno" ):
-            return lineno( self.loc, self.pstr )
-        elif( aname in ("col", "column") ):
-            return col( self.loc, self.pstr )
-        elif( aname == "line" ):
-            return line( self.loc, self.pstr )
+        if aname == "lineno":
+            return lineno(self.loc, self.pstr)
+        elif aname in ("col", "column"):
+            return col(self.loc, self.pstr)
+        elif aname == "line":
+            return line(self.loc, self.pstr)
         else:
             raise AttributeError(aname)
 
-    def __str__( self ):
-        return "%s (at char %d), (line:%d, col:%d)" % \
-                ( self.msg, self.loc, self.lineno, self.column )
-    def __repr__( self ):
+    def __str__(self):
+        if self.pstr:
+            if self.loc >= len(self.pstr):
+                foundstr = ', found end of text'
+            else:
+                foundstr = (', found %r' % self.pstr[self.loc:self.loc + 1]).replace(r'\\', '\\')
+        else:
+            foundstr = ''
+        return ("%s%s  (at char %d), (line:%d, col:%d)" %
+                   (self.msg, foundstr, self.loc, self.lineno, self.column))
+    def __repr__(self):
         return _ustr(self)
-    def markInputline( self, markerString = ">!<" ):
+    def markInputline(self, markerString=">!<"):
         """Extracts the exception line from the input string, and marks
            the location of the exception with a special symbol.
         """
@@ -426,21 +475,21 @@ class RecursiveGrammarException(Exception):
     """exception thrown by :class:`ParserElement.validate` if the
     grammar could be improperly recursive
     """
-    def __init__( self, parseElementList ):
+    def __init__(self, parseElementList):
         self.parseElementTrace = parseElementList
 
-    def __str__( self ):
+    def __str__(self):
         return "RecursiveGrammarException: %s" % self.parseElementTrace
 
 class _ParseResultsWithOffset(object):
-    def __init__(self,p1,p2):
-        self.tup = (p1,p2)
-    def __getitem__(self,i):
+    def __init__(self, p1, p2):
+        self.tup = (p1, p2)
+    def __getitem__(self, i):
         return self.tup[i]
     def __repr__(self):
         return repr(self.tup[0])
-    def setOffset(self,i):
-        self.tup = (self.tup[0],i)
+    def setOffset(self, i):
+        self.tup = (self.tup[0], i)
 
 class ParseResults(object):
     """Structured parse results, to provide multiple means of access to
@@ -485,7 +534,7 @@ class ParseResults(object):
         - month: 12
         - year: 1999
     """
-    def __new__(cls, toklist=None, name=None, asList=True, modal=True ):
+    def __new__(cls, toklist=None, name=None, asList=True, modal=True):
         if isinstance(toklist, cls):
             return toklist
         retobj = object.__new__(cls)
@@ -494,7 +543,7 @@ class ParseResults(object):
 
     # Performance tuning: we construct a *lot* of these, so keep this
     # constructor as small and fast as possible
-    def __init__( self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance ):
+    def __init__(self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance):
         if self.__doinit:
             self.__doinit = False
             self.__name = None
@@ -515,85 +564,93 @@ class ParseResults(object):
         if name is not None and name:
             if not modal:
                 self.__accumNames[name] = 0
-            if isinstance(name,int):
-                name = _ustr(name) # will always return a str, but use _ustr for consistency
+            if isinstance(name, int):
+                name = _ustr(name)  # will always return a str, but use _ustr for consistency
             self.__name = name
-            if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None,'',[])):
-                if isinstance(toklist,basestring):
-                    toklist = [ toklist ]
+            if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None, '', [])):
+                if isinstance(toklist, basestring):
+                    toklist = [toklist]
                 if asList:
-                    if isinstance(toklist,ParseResults):
+                    if isinstance(toklist, ParseResults):
                         self[name] = _ParseResultsWithOffset(ParseResults(toklist.__toklist), 0)
                     else:
-                        self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]),0)
+                        self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]), 0)
                     self[name].__name = name
                 else:
                     try:
                         self[name] = toklist[0]
-                    except (KeyError,TypeError,IndexError):
+                    except (KeyError, TypeError, IndexError):
                         self[name] = toklist
 
-    def __getitem__( self, i ):
-        if isinstance( i, (int,slice) ):
+    def __getitem__(self, i):
+        if isinstance(i, (int, slice)):
             return self.__toklist[i]
         else:
             if i not in self.__accumNames:
                 return self.__tokdict[i][-1][0]
             else:
-                return ParseResults([ v[0] for v in self.__tokdict[i] ])
+                return ParseResults([v[0] for v in self.__tokdict[i]])
 
-    def __setitem__( self, k, v, isinstance=isinstance ):
-        if isinstance(v,_ParseResultsWithOffset):
-            self.__tokdict[k] = self.__tokdict.get(k,list()) + [v]
+    def __setitem__(self, k, v, isinstance=isinstance):
+        if isinstance(v, _ParseResultsWithOffset):
+            self.__tokdict[k] = self.__tokdict.get(k, list()) + [v]
             sub = v[0]
-        elif isinstance(k,(int,slice)):
+        elif isinstance(k, (int, slice)):
             self.__toklist[k] = v
             sub = v
         else:
-            self.__tokdict[k] = self.__tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)]
+            self.__tokdict[k] = self.__tokdict.get(k, list()) + [_ParseResultsWithOffset(v, 0)]
             sub = v
-        if isinstance(sub,ParseResults):
+        if isinstance(sub, ParseResults):
             sub.__parent = wkref(self)
 
-    def __delitem__( self, i ):
-        if isinstance(i,(int,slice)):
-            mylen = len( self.__toklist )
+    def __delitem__(self, i):
+        if isinstance(i, (int, slice)):
+            mylen = len(self.__toklist)
             del self.__toklist[i]
 
             # convert int to slice
             if isinstance(i, int):
                 if i < 0:
                     i += mylen
-                i = slice(i, i+1)
+                i = slice(i, i + 1)
             # get removed indices
             removed = list(range(*i.indices(mylen)))
             removed.reverse()
             # fixup indices in token dictionary
-            for name,occurrences in self.__tokdict.items():
+            for name, occurrences in self.__tokdict.items():
                 for j in removed:
                     for k, (value, position) in enumerate(occurrences):
                         occurrences[k] = _ParseResultsWithOffset(value, position - (position > j))
         else:
             del self.__tokdict[i]
 
-    def __contains__( self, k ):
+    def __contains__(self, k):
         return k in self.__tokdict
 
-    def __len__( self ): return len( self.__toklist )
-    def __bool__(self): return ( not not self.__toklist )
+    def __len__(self):
+        return len(self.__toklist)
+
+    def __bool__(self):
+        return (not not self.__toklist)
     __nonzero__ = __bool__
-    def __iter__( self ): return iter( self.__toklist )
-    def __reversed__( self ): return iter( self.__toklist[::-1] )
-    def _iterkeys( self ):
+
+    def __iter__(self):
+        return iter(self.__toklist)
+
+    def __reversed__(self):
+        return iter(self.__toklist[::-1])
+
+    def _iterkeys(self):
         if hasattr(self.__tokdict, "iterkeys"):
             return self.__tokdict.iterkeys()
         else:
             return iter(self.__tokdict)
 
-    def _itervalues( self ):
+    def _itervalues(self):
         return (self[k] for k in self._iterkeys())
 
-    def _iteritems( self ):
+    def _iteritems(self):
         return ((k, self[k]) for k in self._iterkeys())
 
     if PY_3:
@@ -616,24 +673,24 @@ class ParseResults(object):
         iteritems = _iteritems
         """Returns an iterator of all named result key-value tuples (Python 2.x only)."""
 
-        def keys( self ):
+        def keys(self):
             """Returns all named result keys (as a list in Python 2.x, as an iterator in Python 3.x)."""
             return list(self.iterkeys())
 
-        def values( self ):
+        def values(self):
             """Returns all named result values (as a list in Python 2.x, as an iterator in Python 3.x)."""
             return list(self.itervalues())
 
-        def items( self ):
+        def items(self):
             """Returns all named result key-values (as a list of tuples in Python 2.x, as an iterator in Python 3.x)."""
             return list(self.iteritems())
 
-    def haskeys( self ):
+    def haskeys(self):
         """Since keys() returns an iterator, this method is helpful in bypassing
            code that looks for the existence of any defined results names."""
         return bool(self.__tokdict)
 
-    def pop( self, *args, **kwargs):
+    def pop(self, *args, **kwargs):
         """
         Removes and returns item at specified index (default= ``last``).
         Supports both ``list`` and ``dict`` semantics for ``pop()``. If
@@ -672,14 +729,14 @@ class ParseResults(object):
         """
         if not args:
             args = [-1]
-        for k,v in kwargs.items():
+        for k, v in kwargs.items():
             if k == 'default':
                 args = (args[0], v)
             else:
                 raise TypeError("pop() got an unexpected keyword argument '%s'" % k)
-        if (isinstance(args[0], int) or
-                        len(args) == 1 or
-                        args[0] in self):
+        if (isinstance(args[0], int)
+                or len(args) == 1
+                or args[0] in self):
             index = args[0]
             ret = self[index]
             del self[index]
@@ -711,7 +768,7 @@ class ParseResults(object):
         else:
             return defaultValue
 
-    def insert( self, index, insStr ):
+    def insert(self, index, insStr):
         """
         Inserts new element at location index in the list of parsed tokens.
 
@@ -728,11 +785,11 @@ class ParseResults(object):
         """
         self.__toklist.insert(index, insStr)
         # fixup indices in token dictionary
-        for name,occurrences in self.__tokdict.items():
+        for name, occurrences in self.__tokdict.items():
             for k, (value, position) in enumerate(occurrences):
                 occurrences[k] = _ParseResultsWithOffset(value, position + (position > index))
 
-    def append( self, item ):
+    def append(self, item):
         """
         Add single element to end of ParseResults list of elements.
 
@@ -747,7 +804,7 @@ class ParseResults(object):
         """
         self.__toklist.append(item)
 
-    def extend( self, itemseq ):
+    def extend(self, itemseq):
         """
         Add sequence of elements to end of ParseResults list of elements.
 
@@ -766,74 +823,66 @@ class ParseResults(object):
         else:
             self.__toklist.extend(itemseq)
 
-    def clear( self ):
+    def clear(self):
         """
         Clear all elements and results names.
         """
         del self.__toklist[:]
         self.__tokdict.clear()
 
-    def __getattr__( self, name ):
+    def __getattr__(self, name):
         try:
             return self[name]
         except KeyError:
             return ""
 
-        if name in self.__tokdict:
-            if name not in self.__accumNames:
-                return self.__tokdict[name][-1][0]
-            else:
-                return ParseResults([ v[0] for v in self.__tokdict[name] ])
-        else:
-            return ""
-
-    def __add__( self, other ):
+    def __add__(self, other):
         ret = self.copy()
         ret += other
         return ret
 
-    def __iadd__( self, other ):
+    def __iadd__(self, other):
         if other.__tokdict:
             offset = len(self.__toklist)
-            addoffset = lambda a: offset if a<0 else a+offset
+            addoffset = lambda a: offset if a < 0 else a + offset
             otheritems = other.__tokdict.items()
-            otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])) )
-                                for (k,vlist) in otheritems for v in vlist]
-            for k,v in otherdictitems:
+            otherdictitems = [(k, _ParseResultsWithOffset(v[0], addoffset(v[1])))
+                              for k, vlist in otheritems for v in vlist]
+            for k, v in otherdictitems:
                 self[k] = v
-                if isinstance(v[0],ParseResults):
+                if isinstance(v[0], ParseResults):
                     v[0].__parent = wkref(self)
 
         self.__toklist += other.__toklist
-        self.__accumNames.update( other.__accumNames )
+        self.__accumNames.update(other.__accumNames)
         return self
 
     def __radd__(self, other):
-        if isinstance(other,int) and other == 0:
+        if isinstance(other, int) and other == 0:
             # useful for merging many ParseResults using sum() builtin
             return self.copy()
         else:
             # this may raise a TypeError - so be it
             return other + self
 
-    def __repr__( self ):
-        return "(%s, %s)" % ( repr( self.__toklist ), repr( self.__tokdict ) )
+    def __repr__(self):
+        return "(%s, %s)" % (repr(self.__toklist), repr(self.__tokdict))
 
-    def __str__( self ):
+    def __str__(self):
         return '[' + ', '.join(_ustr(i) if isinstance(i, ParseResults) else repr(i) for i in self.__toklist) + ']'
 
-    def _asStringList( self, sep='' ):
+    def _asStringList(self, sep=''):
         out = []
         for item in self.__toklist:
             if out and sep:
                 out.append(sep)
-            if isinstance( item, ParseResults ):
+            if isinstance(item, ParseResults):
                 out += item._asStringList()
             else:
-                out.append( _ustr(item) )
+                out.append(_ustr(item))
         return out
 
-    def asList( self ):
+    def asList(self):
         """
         Returns the parse results as a nested list of matching tokens, all converted to strings.
 
@@ -848,9 +897,9 @@ class ParseResults(object):
             result_list = result.asList()
             print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']
         """
-        return [res.asList() if isinstance(res,ParseResults) else res for res in self.__toklist]
+        return [res.asList() if isinstance(res, ParseResults) else res for res in self.__toklist]
 
-    def asDict( self ):
+    def asDict(self):
         """
         Returns the named parse results as a nested dictionary.
 
@@ -884,27 +933,27 @@ class ParseResults(object):
             else:
                 return obj
 
-        return dict((k,toItem(v)) for k,v in item_fn())
+        return dict((k, toItem(v)) for k, v in item_fn())
 
-    def copy( self ):
+    def copy(self):
         """
         Returns a new copy of a :class:`ParseResults` object.
         """
-        ret = ParseResults( self.__toklist )
+        ret = ParseResults(self.__toklist)
         ret.__tokdict = dict(self.__tokdict.items())
         ret.__parent = self.__parent
-        ret.__accumNames.update( self.__accumNames )
+        ret.__accumNames.update(self.__accumNames)
         ret.__name = self.__name
         return ret
 
-    def asXML( self, doctag=None, namedItemsOnly=False, indent="", formatted=True ):
+    def asXML(self, doctag=None, namedItemsOnly=False, indent="", formatted=True):
         """
         (Deprecated) Returns the parse results as XML. Tags are created for tokens and lists that have defined results names.
         """
         nl = "\n"
         out = []
-        namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items()
-                                                            for v in vlist)
+        namedItems = dict((v[1], k) for (k, vlist) in self.__tokdict.items()
+                          for v in vlist)
         nextLevelIndent = indent + "  "
 
         # collapse out indents if formatting is not desired
@@ -926,20 +975,20 @@ class ParseResults(object):
             else:
                 selfTag = "ITEM"
 
-        out += [ nl, indent, "<", selfTag, ">" ]
+        out += [nl, indent, "<", selfTag, ">"]
 
-        for i,res in enumerate(self.__toklist):
-            if isinstance(res,ParseResults):
+        for i, res in enumerate(self.__toklist):
+            if isinstance(res, ParseResults):
                 if i in namedItems:
-                    out += [ res.asXML(namedItems[i],
-                                        namedItemsOnly and doctag is None,
-                                        nextLevelIndent,
-                                        formatted)]
+                    out += [res.asXML(namedItems[i],
+                                      namedItemsOnly and doctag is None,
+                                      nextLevelIndent,
+                                      formatted)]
                 else:
-                    out += [ res.asXML(None,
-                                        namedItemsOnly and doctag is None,
-                                        nextLevelIndent,
-                                        formatted)]
+                    out += [res.asXML(None,
+                                      namedItemsOnly and doctag is None,
+                                      nextLevelIndent,
+                                      formatted)]
             else:
                 # individual token, see if there is a name for it
                 resTag = None
@@ -951,16 +1000,16 @@ class ParseResults(object):
                     else:
                         resTag = "ITEM"
                 xmlBodyText = _xml_escape(_ustr(res))
-                out += [ nl, nextLevelIndent, "<", resTag, ">",
-                                                xmlBodyText,
-                                                "</", resTag, ">" ]
+                out += [nl, nextLevelIndent, "<", resTag, ">",
+                        xmlBodyText,
+                                                "</", resTag, ">"]
 
-        out += [ nl, indent, "</", selfTag, ">" ]
+        out += [nl, indent, "</", selfTag, ">"]
         return "".join(out)
 
-    def __lookup(self,sub):
-        for k,vlist in self.__tokdict.items():
-            for v,loc in vlist:
+    def __lookup(self, sub):
+        for k, vlist in self.__tokdict.items():
+            for v, loc in vlist:
                 if sub is v:
                     return k
         return None
@@ -998,14 +1047,14 @@ class ParseResults(object):
                 return par.__lookup(self)
             else:
                 return None
-        elif (len(self) == 1 and
-               len(self.__tokdict) == 1 and
-               next(iter(self.__tokdict.values()))[0][1] in (0,-1)):
+        elif (len(self) == 1
+              and len(self.__tokdict) == 1
+              and next(iter(self.__tokdict.values()))[0][1] in (0, -1)):
             return next(iter(self.__tokdict.keys()))
         else:
             return None
 
-    def dump(self, indent='', depth=0, full=True):
+    def dump(self, indent='', full=True, include_list=True, _depth=0):
         """
         Diagnostic method for listing out the contents of
         a :class:`ParseResults`. Accepts an optional ``indent`` argument so
@@ -1028,28 +1077,45 @@ class ParseResults(object):
         """
         out = []
         NL = '\n'
-        out.append( indent+_ustr(self.asList()) )
+        if include_list:
+            out.append(indent + _ustr(self.asList()))
+        else:
+            out.append('')
+
         if full:
             if self.haskeys():
-                items = sorted((str(k), v) for k,v in self.items())
-                for k,v in items:
+                items = sorted((str(k), v) for k, v in self.items())
+                for k, v in items:
                     if out:
                         out.append(NL)
-                    out.append( "%s%s- %s: " % (indent,('  '*depth), k) )
-                    if isinstance(v,ParseResults):
+                    out.append("%s%s- %s: " % (indent, ('  ' * _depth), k))
+                    if isinstance(v, ParseResults):
                         if v:
-                            out.append( v.dump(indent,depth+1) )
+                            out.append(v.dump(indent=indent, full=full, include_list=include_list, _depth=_depth + 1))
                         else:
                             out.append(_ustr(v))
                     else:
                         out.append(repr(v))
-            elif any(isinstance(vv,ParseResults) for vv in self):
+            elif any(isinstance(vv, ParseResults) for vv in self):
                 v = self
-                for i,vv in enumerate(v):
-                    if isinstance(vv,ParseResults):
-                        out.append("\n%s%s[%d]:\n%s%s%s" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),vv.dump(indent,depth+1) ))
+                for i, vv in enumerate(v):
+                    if isinstance(vv, ParseResults):
+                        out.append("\n%s%s[%d]:\n%s%s%s" % (indent,
+                                                            ('  ' * (_depth)),
+                                                            i,
+                                                            indent,
+                                                            ('  ' * (_depth + 1)),
+                                                            vv.dump(indent=indent,
+                                                                    full=full,
+                                                                    include_list=include_list,
+                                                                    _depth=_depth + 1)))
                     else:
-                        out.append("\n%s%s[%d]:\n%s%s%s" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),_ustr(vv)))
+                        out.append("\n%s%s[%d]:\n%s%s%s" % (indent,
+                                                            ('  ' * (_depth)),
+                                                            i,
+                                                            indent,
+                                                            ('  ' * (_depth + 1)),
+                                                            _ustr(vv)))
 
         return "".join(out)
 
@@ -1082,18 +1148,15 @@ class ParseResults(object):
 
     # add support for pickle protocol
     def __getstate__(self):
-        return ( self.__toklist,
-                 ( self.__tokdict.copy(),
-                   self.__parent is not None and self.__parent() or None,
-                   self.__accumNames,
-                   self.__name ) )
+        return (self.__toklist,
+                (self.__tokdict.copy(),
+                 self.__parent is not None and self.__parent() or None,
+                 self.__accumNames,
+                 self.__name))
 
-    def __setstate__(self,state):
+    def __setstate__(self, state):
         self.__toklist = state[0]
-        (self.__tokdict,
-         par,
-         inAccumNames,
-         self.__name) = state[1]
+        self.__tokdict, par, inAccumNames, self.__name = state[1]
         self.__accumNames = {}
         self.__accumNames.update(inAccumNames)
         if par is not None:
@@ -1105,11 +1168,39 @@ class ParseResults(object):
         return self.__toklist, self.__name, self.__asList, self.__modal
 
     def __dir__(self):
-        return (dir(type(self)) + list(self.keys()))
+        return dir(type(self)) + list(self.keys())
+
+    @classmethod
+    def from_dict(cls, other, name=None):
+        """
+        Helper classmethod to construct a ParseResults from a dict, preserving the
+        name-value relations as results names. If an optional 'name' argument is
+        given, a nested ParseResults will be returned
+        """
+        def is_iterable(obj):
+            try:
+                iter(obj)
+            except Exception:
+                return False
+            else:
+                if PY_3:
+                    return not isinstance(obj, (str, bytes))
+                else:
+                    return not isinstance(obj, basestring)
+
+        ret = cls([])
+        for k, v in other.items():
+            if isinstance(v, Mapping):
+                ret += cls.from_dict(v, name=k)
+            else:
+                ret += cls([v], name=k, asList=is_iterable(v))
+        if name is not None:
+            ret = cls([ret], name=name)
+        return ret
 
 MutableMapping.register(ParseResults)
 
-def col (loc,strg):
+def col (loc, strg):
     """Returns current column within a string, counting newlines as line separators.
    The first column is number 1.
 
@@ -1121,9 +1212,9 @@ def col (loc,strg):
    location, and line and column positions within the parsed string.
    """
     s = strg
-    return 1 if 0<loc<len(s) and s[loc-1] == '\n' else loc - s.rfind("\n", 0, loc)
+    return 1 if 0 < loc < len(s) and s[loc-1] == '\n' else loc - s.rfind("\n", 0, loc)
 
-def lineno(loc,strg):
+def lineno(loc, strg):
     """Returns current line number within a string, counting newlines as line separators.
     The first line is number 1.
 
@@ -1133,26 +1224,26 @@ def lineno(loc,strg):
     suggested methods to maintain a consistent view of the parsed string, the
     parse location, and line and column positions within the parsed string.
     """
-    return strg.count("\n",0,loc) + 1
+    return strg.count("\n", 0, loc) + 1
 
-def line( loc, strg ):
+def line(loc, strg):
     """Returns the line of text containing loc within a string, counting newlines as line separators.
        """
     lastCR = strg.rfind("\n", 0, loc)
     nextCR = strg.find("\n", loc)
     if nextCR >= 0:
-        return strg[lastCR+1:nextCR]
+        return strg[lastCR + 1:nextCR]
     else:
-        return strg[lastCR+1:]
+        return strg[lastCR + 1:]
 
-def _defaultStartDebugAction( instring, loc, expr ):
-    print (("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) )))
+def _defaultStartDebugAction(instring, loc, expr):
+    print(("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % (lineno(loc, instring), col(loc, instring))))
 
-def _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ):
-    print ("Matched " + _ustr(expr) + " -> " + str(toks.asList()))
+def _defaultSuccessDebugAction(instring, startloc, endloc, expr, toks):
+    print("Matched " + _ustr(expr) + " -> " + str(toks.asList()))
 
-def _defaultExceptionDebugAction( instring, loc, expr, exc ):
-    print ("Exception raised:" + _ustr(exc))
+def _defaultExceptionDebugAction(instring, loc, expr, exc):
+    print("Exception raised:" + _ustr(exc))
 
 def nullDebugAction(*args):
     """'Do-nothing' debug action, to suppress debugging output during parsing."""
@@ -1183,16 +1274,16 @@ def nullDebugAction(*args):
 'decorator to trim function calls to match the arity of the target'
 def _trim_arity(func, maxargs=2):
     if func in singleArgBuiltins:
-        return lambda s,l,t: func(t)
+        return lambda s, l, t: func(t)
     limit = [0]
     foundArity = [False]
 
     # traceback return data structure changed in Py3.5 - normalize back to plain tuples
-    if system_version[:2] >= (3,5):
+    if system_version[:2] >= (3, 5):
         def extract_stack(limit=0):
             # special handling for Python 3.5.0 - extra deep call stack by 1
-            offset = -3 if system_version == (3,5,0) else -2
-            frame_summary = traceback.extract_stack(limit=-offset+limit-1)[offset]
+            offset = -3 if system_version == (3, 5, 0) else -2
+            frame_summary = traceback.extract_stack(limit=-offset + limit - 1)[offset]
             return [frame_summary[:2]]
         def extract_tb(tb, limit=0):
             frames = traceback.extract_tb(tb, limit=limit)
@@ -1209,7 +1300,7 @@ def _trim_arity(func, maxargs=2):
     # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND
     # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!
     this_line = extract_stack(limit=2)[-1]
-    pa_call_line_synth = (this_line[0], this_line[1]+LINE_DIFF)
+    pa_call_line_synth = (this_line[0], this_line[1] + LINE_DIFF)
 
     def wrapper(*args):
         while 1:
@@ -1227,7 +1318,10 @@ def _trim_arity(func, maxargs=2):
                         if not extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth:
                             raise
                     finally:
-                        del tb
+                        try:
+                            del tb
+                        except NameError:
+                            pass
 
                 if limit[0] <= maxargs:
                     limit[0] += 1
@@ -1245,13 +1339,14 @@ def _trim_arity(func, maxargs=2):
 
     return wrapper
 
+
 class ParserElement(object):
     """Abstract base level parser element class."""
     DEFAULT_WHITE_CHARS = " \n\t\r"
     verbose_stacktrace = False
 
     @staticmethod
-    def setDefaultWhitespaceChars( chars ):
+    def setDefaultWhitespaceChars(chars):
         r"""
         Overrides the default whitespace chars
 
@@ -1288,10 +1383,10 @@ class ParserElement(object):
         """
         ParserElement._literalStringClass = cls
 
-    def __init__( self, savelist=False ):
+    def __init__(self, savelist=False):
         self.parseAction = list()
         self.failAction = None
-        #~ self.name = "<unknown>"  # don't define self.name, let subclasses try/except upcall
+        # ~ self.name = "<unknown>"  # don't define self.name, let subclasses try/except upcall
         self.strRepr = None
         self.resultsName = None
         self.saveAsList = savelist
@@ -1306,12 +1401,12 @@ class ParserElement(object):
         self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index
         self.errmsg = ""
         self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all)
-        self.debugActions = ( None, None, None ) #custom debug actions
+        self.debugActions = (None, None, None)  # custom debug actions
         self.re = None
         self.callPreparse = True # used to avoid redundant calls to preParse
         self.callDuringTry = False
 
-    def copy( self ):
+    def copy(self):
         """
         Make a copy of this :class:`ParserElement`.  Useful for defining
         different parse actions for the same parsing pattern, using copies of
@@ -1320,8 +1415,8 @@ class ParserElement(object):
         Example::
 
             integer = Word(nums).setParseAction(lambda toks: int(toks[0]))
-            integerK = integer.copy().addParseAction(lambda toks: toks[0]*1024) + Suppress("K")
-            integerM = integer.copy().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M")
+            integerK = integer.copy().addParseAction(lambda toks: toks[0] * 1024) + Suppress("K")
+            integerM = integer.copy().addParseAction(lambda toks: toks[0] * 1024 * 1024) + Suppress("M")
 
             print(OneOrMore(integerK | integerM | integer).parseString("5K 100 640K 256M"))
 
@@ -1331,16 +1426,16 @@ class ParserElement(object):
 
         Equivalent form of ``expr.copy()`` is just ``expr()``::
 
-            integerM = integer().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M")
+            integerM = integer().addParseAction(lambda toks: toks[0] * 1024 * 1024) + Suppress("M")
         """
-        cpy = copy.copy( self )
+        cpy = copy.copy(self)
         cpy.parseAction = self.parseAction[:]
         cpy.ignoreExprs = self.ignoreExprs[:]
         if self.copyDefaultWhiteChars:
             cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS
         return cpy
 
-    def setName( self, name ):
+    def setName(self, name):
         """
         Define name for this expression, makes debugging and exception messages clearer.
 
@@ -1351,11 +1446,11 @@ class ParserElement(object):
         """
         self.name = name
         self.errmsg = "Expected " + self.name
-        if hasattr(self,"exception"):
-            self.exception.msg = self.errmsg
+        if __diag__.enable_debug_on_named_expressions:
+            self.setDebug()
         return self
 
-    def setResultsName( self, name, listAllMatches=False ):
+    def setResultsName(self, name, listAllMatches=False):
         """
         Define name for referencing matching tokens as a nested attribute
         of the returned parse results.
@@ -1376,15 +1471,18 @@ class ParserElement(object):
             # equivalent form:
             date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
         """
+        return self._setResultsName(name, listAllMatches)
+
+    def _setResultsName(self, name, listAllMatches=False):
         newself = self.copy()
         if name.endswith("*"):
             name = name[:-1]
-            listAllMatches=True
+            listAllMatches = True
         newself.resultsName = name
         newself.modalResults = not listAllMatches
         return newself
 
-    def setBreak(self,breakFlag = True):
+    def setBreak(self, breakFlag=True):
         """Method to invoke the Python pdb debugger when this element is
            about to be parsed. Set ``breakFlag`` to True to enable, False to
            disable.
@@ -1393,20 +1491,21 @@ class ParserElement(object):
             _parseMethod = self._parse
             def breaker(instring, loc, doActions=True, callPreParse=True):
                 import pdb
+                # this call to pdb.set_trace() is intentional, not a checkin error
                 pdb.set_trace()
-                return _parseMethod( instring, loc, doActions, callPreParse )
+                return _parseMethod(instring, loc, doActions, callPreParse)
             breaker._originalParseMethod = _parseMethod
             self._parse = breaker
         else:
-            if hasattr(self._parse,"_originalParseMethod"):
+            if hasattr(self._parse, "_originalParseMethod"):
                 self._parse = self._parse._originalParseMethod
         return self
 
-    def setParseAction( self, *fns, **kwargs ):
+    def setParseAction(self, *fns, **kwargs):
         """
         Define one or more actions to perform when successfully matching parse element definition.
-        Parse action fn is a callable method with 0-3 arguments, called as ``fn(s,loc,toks)`` ,
-        ``fn(loc,toks)`` , ``fn(toks)`` , or just ``fn()`` , where:
+        Parse action fn is a callable method with 0-3 arguments, called as ``fn(s, loc, toks)`` ,
+        ``fn(loc, toks)`` , ``fn(toks)`` , or just ``fn()`` , where:
 
         - s   = the original string being parsed (see note below)
         - loc = the location of the matching substring
@@ -1416,8 +1515,11 @@ class ParserElement(object):
         value from fn, and the modified list of tokens will replace the original.
         Otherwise, fn does not need to return any value.
 
+        If None is passed as the parse action, all previously added parse actions for this
+        expression are cleared.
+
         Optional keyword arguments:
-        - callDuringTry = (default= ``False`` ) indicate if parse action should be run during lookaheads and alternate testing
+        - callDuringTry = (default= ``False``) indicate if parse action should be run during lookaheads and alternate testing
 
         Note: the default parsing behavior is to expand tabs in the input string
         before starting the parsing process.  See :class:`parseString for more
@@ -1439,11 +1541,16 @@ class ParserElement(object):
             # note that integer fields are now ints, not strings
             date_str.parseString("1999/12/31")  # -> [1999, '/', 12, '/', 31]
         """
-        self.parseAction = list(map(_trim_arity, list(fns)))
-        self.callDuringTry = kwargs.get("callDuringTry", False)
+        if list(fns) == [None,]:
+            self.parseAction = []
+        else:
+            if not all(callable(fn) for fn in fns):
+                raise TypeError("parse actions must be callable")
+            self.parseAction = list(map(_trim_arity, list(fns)))
+            self.callDuringTry = kwargs.get("callDuringTry", False)
         return self
 
-    def addParseAction( self, *fns, **kwargs ):
+    def addParseAction(self, *fns, **kwargs):
         """
         Add one or more parse actions to expression's list of parse actions. See :class:`setParseAction`.
 
@@ -1471,21 +1578,17 @@ 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
 
-    def setFailAction( self, fn ):
+    def setFailAction(self, fn):
         """Define action to perform if parsing fails at this expression.
            Fail acton fn is a callable function that takes the arguments
-           ``fn(s,loc,expr,err)`` where:
+           ``fn(s, loc, expr, err)`` where:
            - s = string being parsed
            - loc = location where expression match was attempted and failed
            - expr = the parse expression that failed
@@ -1495,22 +1598,22 @@ class ParserElement(object):
         self.failAction = fn
         return self
 
-    def _skipIgnorables( self, instring, loc ):
+    def _skipIgnorables(self, instring, loc):
         exprsFound = True
         while exprsFound:
             exprsFound = False
             for e in self.ignoreExprs:
                 try:
                     while 1:
-                        loc,dummy = e._parse( instring, loc )
+                        loc, dummy = e._parse(instring, loc)
                         exprsFound = True
                 except ParseException:
                     pass
         return loc
 
-    def preParse( self, instring, loc ):
+    def preParse(self, instring, loc):
         if self.ignoreExprs:
-            loc = self._skipIgnorables( instring, loc )
+            loc = self._skipIgnorables(instring, loc)
 
         if self.skipWhitespace:
             wt = self.whiteChars
@@ -1520,101 +1623,105 @@ class ParserElement(object):
 
         return loc
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         return loc, []
 
-    def postParse( self, instring, loc, tokenlist ):
+    def postParse(self, instring, loc, tokenlist):
         return tokenlist
 
-    #~ @profile
-    def _parseNoCache( self, instring, loc, doActions=True, callPreParse=True ):
-        debugging = ( self.debug ) #and doActions )
+    # ~ @profile
+    def _parseNoCache(self, instring, loc, doActions=True, callPreParse=True):
+        TRY, MATCH, FAIL = 0, 1, 2
+        debugging = (self.debug)  # and doActions)
 
         if debugging or self.failAction:
-            #~ print ("Match",self,"at loc",loc,"(%d,%d)" % ( lineno(loc,instring), col(loc,instring) ))
-            if (self.debugActions[0] ):
-                self.debugActions[0]( instring, loc, self )
-            if callPreParse and self.callPreparse:
-                preloc = self.preParse( instring, loc )
-            else:
-                preloc = loc
-            tokensStart = preloc
+            # ~ print ("Match", self, "at loc", loc, "(%d, %d)" % (lineno(loc, instring), col(loc, instring)))
+            if self.debugActions[TRY]:
+                self.debugActions[TRY](instring, loc, self)
             try:
-                try:
-                    loc,tokens = self.parseImpl( instring, preloc, doActions )
-                except IndexError:
-                    raise ParseException( instring, len(instring), self.errmsg, self )
-            except ParseBaseException as err:
-                #~ print ("Exception raised:", err)
-                if self.debugActions[2]:
-                    self.debugActions[2]( instring, tokensStart, self, err )
+                if callPreParse and self.callPreparse:
+                    preloc = self.preParse(instring, loc)
+                else:
+                    preloc = loc
+                tokensStart = preloc
+                if self.mayIndexError or preloc >= len(instring):
+                    try:
+                        loc, tokens = self.parseImpl(instring, preloc, doActions)
+                    except IndexError:
+                        raise ParseException(instring, len(instring), self.errmsg, self)
+                else:
+                    loc, tokens = self.parseImpl(instring, preloc, doActions)
+            except Exception as err:
+                # ~ print ("Exception raised:", err)
+                if self.debugActions[FAIL]:
+                    self.debugActions[FAIL](instring, tokensStart, self, err)
                 if self.failAction:
-                    self.failAction( instring, tokensStart, self, err )
+                    self.failAction(instring, tokensStart, self, err)
                 raise
         else:
             if callPreParse and self.callPreparse:
-                preloc = self.preParse( instring, loc )
+                preloc = self.preParse(instring, loc)
             else:
                 preloc = loc
             tokensStart = preloc
             if self.mayIndexError or preloc >= len(instring):
                 try:
-                    loc,tokens = self.parseImpl( instring, preloc, doActions )
+                    loc, tokens = self.parseImpl(instring, preloc, doActions)
                 except IndexError:
-                    raise ParseException( instring, len(instring), self.errmsg, self )
+                    raise ParseException(instring, len(instring), self.errmsg, self)
             else:
-                loc,tokens = self.parseImpl( instring, preloc, doActions )
+                loc, tokens = self.parseImpl(instring, preloc, doActions)
 
-        tokens = self.postParse( instring, loc, tokens )
+        tokens = self.postParse(instring, loc, tokens)
 
-        retTokens = ParseResults( tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults )
+        retTokens = ParseResults(tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults)
         if self.parseAction and (doActions or self.callDuringTry):
             if debugging:
                 try:
                     for fn in self.parseAction:
                         try:
-                            tokens = fn( instring, tokensStart, retTokens )
+                            tokens = fn(instring, tokensStart, retTokens)
                         except IndexError as parse_action_exc:
                             exc = ParseException("exception raised in parse action")
                             exc.__cause__ = parse_action_exc
                             raise exc
 
                         if tokens is not None and tokens is not retTokens:
-                            retTokens = ParseResults( tokens,
+                            retTokens = ParseResults(tokens,
                                                       self.resultsName,
-                                                      asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),
-                                                      modal=self.modalResults )
-                except ParseBaseException as err:
-                    #~ print "Exception raised in user parse action:", err
-                    if (self.debugActions[2] ):
-                        self.debugActions[2]( instring, tokensStart, self, err )
+                                                      asList=self.saveAsList and isinstance(tokens, (ParseResults, list)),
+                                                      modal=self.modalResults)
+                except Exception as err:
+                    # ~ print "Exception raised in user parse action:", err
+                    if self.debugActions[FAIL]:
+                        self.debugActions[FAIL](instring, tokensStart, self, err)
                     raise
             else:
                 for fn in self.parseAction:
                     try:
-                        tokens = fn( instring, tokensStart, retTokens )
+                        tokens = fn(instring, tokensStart, retTokens)
                     except IndexError as parse_action_exc:
                         exc = ParseException("exception raised in parse action")
                         exc.__cause__ = parse_action_exc
                         raise exc
 
                     if tokens is not None and tokens is not retTokens:
-                        retTokens = ParseResults( tokens,
+                        retTokens = ParseResults(tokens,
                                                   self.resultsName,
-                                                  asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),
-                                                  modal=self.modalResults )
+                                                  asList=self.saveAsList and isinstance(tokens, (ParseResults, list)),
+                                                  modal=self.modalResults)
         if debugging:
-            #~ print ("Matched",self,"->",retTokens.asList())
-            if (self.debugActions[1] ):
-                self.debugActions[1]( instring, tokensStart, loc, self, retTokens )
+            # ~ print ("Matched", self, "->", retTokens.asList())
+            if self.debugActions[MATCH]:
+                self.debugActions[MATCH](instring, tokensStart, loc, self, retTokens)
 
         return loc, retTokens
 
-    def tryParse( self, instring, loc ):
+    def tryParse(self, instring, loc):
         try:
-            return self._parse( instring, loc, doActions=False )[0]
+            return self._parse(instring, loc, doActions=False)[0]
         except ParseFatalException:
-            raise ParseException( instring, loc, self.errmsg, self)
+            raise ParseException(instring, loc, self.errmsg, self)
 
     def canParseNext(self, instring, loc):
         try:
@@ -1711,7 +1818,7 @@ class ParserElement(object):
 
     # this method gets repeatedly called during backtracking with the same arguments -
     # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression
-    def _parseCache( self, instring, loc, doActions=True, callPreParse=True ):
+    def _parseCache(self, instring, loc, doActions=True, callPreParse=True):
         HIT, MISS = 0, 1
         lookup = (self, instring, loc, callPreParse, doActions)
         with ParserElement.packrat_cache_lock:
@@ -1732,7 +1839,7 @@ class ParserElement(object):
                 ParserElement.packrat_cache_stats[HIT] += 1
                 if isinstance(value, Exception):
                     raise value
-                return (value[0], value[1].copy())
+                return value[0], value[1].copy()
 
     _parse = _parseNoCache
 
@@ -1777,12 +1884,16 @@ class ParserElement(object):
                 ParserElement.packrat_cache = ParserElement._FifoCache(cache_size_limit)
             ParserElement._parse = ParserElement._parseCache
 
-    def parseString( self, instring, parseAll=False ):
+    def parseString(self, instring, parseAll=False):
         """
         Execute the parse expression with the given string.
         This is the main interface to the client code, once the complete
         expression has been built.
 
+        Returns the parsed data as a :class:`ParseResults` object, which may be
+        accessed as a list, or as a dict or object with attributes if the given parser
+        includes results names.
+
         If you want the grammar to require that the entire input string be
         successfully parsed, then set ``parseAll`` to True (equivalent to ending
         the grammar with ``StringEnd()``).
@@ -1796,7 +1907,7 @@ class ParserElement(object):
 
         - calling ``parseWithTabs`` on your grammar before calling ``parseString``
           (see :class:`parseWithTabs`)
-        - define your parse action using the full ``(s,loc,toks)`` signature, and
+        - define your parse action using the full ``(s, loc, toks)`` signature, and
           reference the input string using the parse action's ``s`` argument
         - explictly expand the tabs in your input string before calling
           ``parseString``
@@ -1809,17 +1920,17 @@ class ParserElement(object):
         ParserElement.resetCache()
         if not self.streamlined:
             self.streamline()
-            #~ self.saveAsList = True
+            # ~ self.saveAsList = True
         for e in self.ignoreExprs:
             e.streamline()
         if not self.keepTabs:
             instring = instring.expandtabs()
         try:
-            loc, tokens = self._parse( instring, 0 )
+            loc, tokens = self._parse(instring, 0)
             if parseAll:
-                loc = self.preParse( instring, loc )
+                loc = self.preParse(instring, loc)
                 se = Empty() + StringEnd()
-                se._parse( instring, loc )
+                se._parse(instring, loc)
         except ParseBaseException as exc:
             if ParserElement.verbose_stacktrace:
                 raise
@@ -1829,7 +1940,7 @@ class ParserElement(object):
         else:
             return tokens
 
-    def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ):
+    def scanString(self, instring, maxMatches=_MAX_INT, overlap=False):
         """
         Scan the input string for expression matches.  Each match will return the
         matching tokens, start location, and end location.  May be called with optional
@@ -1844,7 +1955,7 @@ class ParserElement(object):
 
             source = "sldjf123lsdjjkf345sldkjf879lkjsfd987"
             print(source)
-            for tokens,start,end in Word(alphas).scanString(source):
+            for tokens, start, end in Word(alphas).scanString(source):
                 print(' '*start + '^'*(end-start))
                 print(' '*start + tokens[0])
 
@@ -1876,16 +1987,16 @@ class ParserElement(object):
         try:
             while loc <= instrlen and matches < maxMatches:
                 try:
-                    preloc = preparseFn( instring, loc )
-                    nextLoc,tokens = parseFn( instring, preloc, callPreParse=False )
+                    preloc = preparseFn(instring, loc)
+                    nextLoc, tokens = parseFn(instring, preloc, callPreParse=False)
                 except ParseException:
-                    loc = preloc+1
+                    loc = preloc + 1
                 else:
                     if nextLoc > loc:
                         matches += 1
                         yield tokens, preloc, nextLoc
                         if overlap:
-                            nextloc = preparseFn( instring, loc )
+                            nextloc = preparseFn(instring, loc)
                             if nextloc > loc:
                                 loc = nextLoc
                             else:
@@ -1893,7 +2004,7 @@ class ParserElement(object):
                         else:
                             loc = nextLoc
                     else:
-                        loc = preloc+1
+                        loc = preloc + 1
         except ParseBaseException as exc:
             if ParserElement.verbose_stacktrace:
                 raise
@@ -1901,7 +2012,7 @@ class ParserElement(object):
                 # catch and re-raise exception from here, clears out pyparsing internal stack trace
                 raise exc
 
-    def transformString( self, instring ):
+    def transformString(self, instring):
         """
         Extension to :class:`scanString`, to modify matching text with modified tokens that may
         be returned from a parse action.  To use ``transformString``, define a grammar and
@@ -1927,19 +2038,19 @@ class ParserElement(object):
         # keep string locs straight between transformString and scanString
         self.keepTabs = True
         try:
-            for t,s,e in self.scanString( instring ):
-                out.append( instring[lastE:s] )
+            for t, s, e in self.scanString(instring):
+                out.append(instring[lastE:s])
                 if t:
-                    if isinstance(t,ParseResults):
+                    if isinstance(t, ParseResults):
                         out += t.asList()
-                    elif isinstance(t,list):
+                    elif isinstance(t, list):
                         out += t
                     else:
                         out.append(t)
                 lastE = e
             out.append(instring[lastE:])
             out = [o for o in out if o]
-            return "".join(map(_ustr,_flatten(out)))
+            return "".join(map(_ustr, _flatten(out)))
         except ParseBaseException as exc:
             if ParserElement.verbose_stacktrace:
                 raise
@@ -1947,7 +2058,7 @@ class ParserElement(object):
                 # catch and re-raise exception from here, clears out pyparsing internal stack trace
                 raise exc
 
-    def searchString( self, instring, maxMatches=_MAX_INT ):
+    def searchString(self, instring, maxMatches=_MAX_INT):
         """
         Another extension to :class:`scanString`, simplifying the access to the tokens found
         to match the given parse expression.  May be called with optional
@@ -1969,7 +2080,7 @@ class ParserElement(object):
             ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']
         """
         try:
-            return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ])
+            return ParseResults([t for t, s, e in self.scanString(instring, maxMatches)])
         except ParseBaseException as exc:
             if ParserElement.verbose_stacktrace:
                 raise
@@ -1995,14 +2106,14 @@ class ParserElement(object):
         """
         splits = 0
         last = 0
-        for t,s,e in self.scanString(instring, maxMatches=maxsplit):
+        for t, s, e in self.scanString(instring, maxMatches=maxsplit):
             yield instring[last:s]
             if includeSeparators:
                 yield t[0]
             last = e
         yield instring[last:]
 
-    def __add__(self, other ):
+    def __add__(self, other):
         """
         Implementation of + operator - returns :class:`And`. Adding strings to a ParserElement
         converts them to :class:`Literal`s by default.
@@ -2016,24 +2127,42 @@ class ParserElement(object):
         prints::
 
             Hello, World! -> ['Hello', ',', 'World', '!']
+
+        ``...`` may be used as a parse expression as a short form of :class:`SkipTo`.
+
+            Literal('start') + ... + Literal('end')
+
+        is equivalent to:
+
+            Literal('start') + SkipTo('end')("_skipped*") + Literal('end')
+
+        Note that the skipped text is returned with '_skipped' as a results name,
+        and to support having multiple skips in the same parser, the value returned is
+        a list of all skipped text.
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if other is Ellipsis:
+            return _PendingSkip(self)
+
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
-        return And( [ self, other ] )
+        return And([self, other])
 
-    def __radd__(self, other ):
+    def __radd__(self, other):
         """
         Implementation of + operator when left operand is not a :class:`ParserElement`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if other is Ellipsis:
+            return SkipTo(self)("_skipped*") + self
+
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
         return other + self
 
@@ -2041,64 +2170,67 @@ class ParserElement(object):
         """
         Implementation of - operator, returns :class:`And` with error stop
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
         return self + And._ErrorStop() + other
 
-    def __rsub__(self, other ):
+    def __rsub__(self, other):
         """
         Implementation of - operator when left operand is not a :class:`ParserElement`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
         return other - self
 
-    def __mul__(self,other):
+    def __mul__(self, other):
         """
         Implementation of * operator, allows use of ``expr * 3`` in place of
         ``expr + expr + expr``.  Expressions may also me multiplied by a 2-integer
-        tuple, similar to ``{min,max}`` multipliers in regular expressions.  Tuples
+        tuple, similar to ``{min, max}`` multipliers in regular expressions.  Tuples
         may also include ``None`` as in:
-         - ``expr*(n,None)`` or ``expr*(n,)`` is equivalent
+         - ``expr*(n, None)`` or ``expr*(n, )`` is equivalent
               to ``expr*n + ZeroOrMore(expr)``
               (read as "at least n instances of ``expr``")
-         - ``expr*(None,n)`` is equivalent to ``expr*(0,n)``
+         - ``expr*(None, n)`` is equivalent to ``expr*(0, n)``
               (read as "0 to n instances of ``expr``")
-         - ``expr*(None,None)`` is equivalent to ``ZeroOrMore(expr)``
-         - ``expr*(1,None)`` is equivalent to ``OneOrMore(expr)``
+         - ``expr*(None, None)`` is equivalent to ``ZeroOrMore(expr)``
+         - ``expr*(1, None)`` is equivalent to ``OneOrMore(expr)``
 
-        Note that ``expr*(None,n)`` does not raise an exception if
+        Note that ``expr*(None, n)`` does not raise an exception if
         more than n exprs exist in the input stream; that is,
-        ``expr*(None,n)`` does not enforce a maximum number of expr
+        ``expr*(None, n)`` does not enforce a maximum number of expr
         occurrences.  If this behavior is desired, then write
-        ``expr*(None,n) + ~expr``
+        ``expr*(None, n) + ~expr``
         """
-        if isinstance(other,int):
-            minElements, optElements = other,0
-        elif isinstance(other,tuple):
+        if other is Ellipsis or other == (Ellipsis,):
+            other = (1, None)
+        if isinstance(other, int):
+            minElements, optElements = other, 0
+        elif isinstance(other, tuple):
+            other = tuple(o if o is not Ellipsis else None for o in other)
             other = (other + (None, None))[:2]
             if other[0] is None:
                 other = (0, other[1])
-            if isinstance(other[0],int) and other[1] is None:
+            if isinstance(other[0], int) and other[1] is None:
                 if other[0] == 0:
                     return ZeroOrMore(self)
                 if other[0] == 1:
                     return OneOrMore(self)
                 else:
-                    return self*other[0] + ZeroOrMore(self)
-            elif isinstance(other[0],int) and isinstance(other[1],int):
+                    return self * other[0] + ZeroOrMore(self)
+            elif isinstance(other[0], int) and isinstance(other[1], int):
                 minElements, optElements = other
                 optElements -= minElements
             else:
-                raise TypeError("cannot multiply 'ParserElement' and ('%s','%s') objects", type(other[0]),type(other[1]))
+                raise TypeError("cannot multiply 'ParserElement' and ('%s', '%s') objects", type(other[0]), type(other[1]))
         else:
             raise TypeError("cannot multiply 'ParserElement' and '%s' objects", type(other))
 
@@ -2107,108 +2239,148 @@ class ParserElement(object):
         if optElements < 0:
             raise ValueError("second tuple value must be greater or equal to first tuple value")
         if minElements == optElements == 0:
-            raise ValueError("cannot multiply ParserElement by 0 or (0,0)")
+            raise ValueError("cannot multiply ParserElement by 0 or (0, 0)")
 
-        if (optElements):
+        if optElements:
             def makeOptionalList(n):
-                if n>1:
-                    return Optional(self + makeOptionalList(n-1))
+                if n > 1:
+                    return Optional(self + makeOptionalList(n - 1))
                 else:
                     return Optional(self)
             if minElements:
                 if minElements == 1:
                     ret = self + makeOptionalList(optElements)
                 else:
-                    ret = And([self]*minElements) + makeOptionalList(optElements)
+                    ret = And([self] * minElements) + makeOptionalList(optElements)
             else:
                 ret = makeOptionalList(optElements)
         else:
             if minElements == 1:
                 ret = self
             else:
-                ret = And([self]*minElements)
+                ret = And([self] * minElements)
         return ret
 
     def __rmul__(self, other):
         return self.__mul__(other)
 
-    def __or__(self, other ):
+    def __or__(self, other):
         """
         Implementation of | operator - returns :class:`MatchFirst`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if other is Ellipsis:
+            return _PendingSkip(self, must_skip=True)
+
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
-        return MatchFirst( [ self, other ] )
+        return MatchFirst([self, other])
 
-    def __ror__(self, other ):
+    def __ror__(self, other):
         """
         Implementation of | operator when left operand is not a :class:`ParserElement`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
         return other | self
 
-    def __xor__(self, other ):
+    def __xor__(self, other):
         """
         Implementation of ^ operator - returns :class:`Or`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
-        return Or( [ self, other ] )
+        return Or([self, other])
 
-    def __rxor__(self, other ):
+    def __rxor__(self, other):
         """
         Implementation of ^ operator when left operand is not a :class:`ParserElement`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
         return other ^ self
 
-    def __and__(self, other ):
+    def __and__(self, other):
         """
         Implementation of & operator - returns :class:`Each`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
-        return Each( [ self, other ] )
+        return Each([self, other])
 
-    def __rand__(self, other ):
+    def __rand__(self, other):
         """
         Implementation of & operator when left operand is not a :class:`ParserElement`
         """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        if not isinstance(other, ParserElement):
             warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             return None
         return other & self
 
-    def __invert__( self ):
+    def __invert__(self):
         """
         Implementation of ~ operator - returns :class:`NotAny`
         """
-        return NotAny( self )
+        return NotAny(self)
+
+    def __getitem__(self, key):
+        """
+        use ``[]`` indexing notation as a short form for expression repetition:
+         - ``expr[n]`` is equivalent to ``expr*n``
+         - ``expr[m, n]`` is equivalent to ``expr*(m, n)``
+         - ``expr[n, ...]`` or ``expr[n,]`` is equivalent
+              to ``expr*n + ZeroOrMore(expr)``
+              (read as "at least n instances of ``expr``")
+         - ``expr[..., n]`` is equivalent to ``expr*(0, n)``
+              (read as "0 to n instances of ``expr``")
+         - ``expr[0, ...]`` is equivalent to ``ZeroOrMore(expr)``
+         - ``expr[1, ...]`` is equivalent to ``OneOrMore(expr)``
+         - ``expr[...]`` is equivalent to ``OneOrMore(expr)``
+         ``None`` may be used in place of ``...``.
+
+        Note that ``expr[..., n]`` and ``expr[m, n]``do not raise an exception
+        if more than ``n`` ``expr``s exist in the input stream.  If this behavior is
+        desired, then write ``expr[..., n] + ~expr``.
+       """
+
+        # convert single arg keys to tuples
+        try:
+            if isinstance(key, str):
+                key = (key,)
+            iter(key)
+        except TypeError:
+            key = (key,)
+
+        if len(key) > 2:
+            warnings.warn("only 1 or 2 index arguments supported ({0}{1})".format(key[:5],
+                                                                                '... [{0}]'.format(len(key))
+                                                                                if len(key) > 5 else ''))
+
+        # clip to 2 elements
+        ret = self * tuple(key[:2])
+        return ret
 
     def __call__(self, name=None):
         """
@@ -2222,22 +2394,22 @@ class ParserElement(object):
         Example::
 
             # these are equivalent
-            userdata = Word(alphas).setResultsName("name") + Word(nums+"-").setResultsName("socsecno")
-            userdata = Word(alphas)("name") + Word(nums+"-")("socsecno")
+            userdata = Word(alphas).setResultsName("name") + Word(nums + "-").setResultsName("socsecno")
+            userdata = Word(alphas)("name") + Word(nums + "-")("socsecno")
         """
         if name is not None:
-            return self.setResultsName(name)
+            return self._setResultsName(name)
         else:
             return self.copy()
 
-    def suppress( self ):
+    def suppress(self):
         """
         Suppresses the output of this :class:`ParserElement`; useful to keep punctuation from
         cluttering up returned output.
         """
-        return Suppress( self )
+        return Suppress(self)
 
-    def leaveWhitespace( self ):
+    def leaveWhitespace(self):
         """
         Disables the skipping of whitespace before matching the characters in the
         :class:`ParserElement`'s defined pattern.  This is normally only used internally by
@@ -2246,7 +2418,7 @@ class ParserElement(object):
         self.skipWhitespace = False
         return self
 
-    def setWhitespaceChars( self, chars ):
+    def setWhitespaceChars(self, chars):
         """
         Overrides the default whitespace chars
         """
@@ -2255,7 +2427,7 @@ class ParserElement(object):
         self.copyDefaultWhiteChars = False
         return self
 
-    def parseWithTabs( self ):
+    def parseWithTabs(self):
         """
         Overrides default behavior to expand ``<TAB>``s to spaces before parsing the input string.
         Must be called before ``parseString`` when the input grammar contains elements that
@@ -2264,7 +2436,7 @@ class ParserElement(object):
         self.keepTabs = True
         return self
 
-    def ignore( self, other ):
+    def ignore(self, other):
         """
         Define expression to be ignored (e.g., comments) while doing pattern
         matching; may be called repeatedly, to define multiple comment or other
@@ -2281,14 +2453,14 @@ class ParserElement(object):
         if isinstance(other, basestring):
             other = Suppress(other)
 
-        if isinstance( other, Suppress ):
+        if isinstance(other, Suppress):
             if other not in self.ignoreExprs:
                 self.ignoreExprs.append(other)
         else:
-            self.ignoreExprs.append( Suppress( other.copy() ) )
+            self.ignoreExprs.append(Suppress(other.copy()))
         return self
 
-    def setDebugActions( self, startAction, successAction, exceptionAction ):
+    def setDebugActions(self, startAction, successAction, exceptionAction):
         """
         Enable display of debugging messages while doing pattern matching.
         """
@@ -2298,7 +2470,7 @@ class ParserElement(object):
         self.debug = True
         return self
 
-    def setDebug( self, flag=True ):
+    def setDebug(self, flag=True):
         """
         Enable display of debugging messages while doing pattern matching.
         Set ``flag`` to True to enable, False to disable.
@@ -2336,32 +2508,32 @@ class ParserElement(object):
         name created for the :class:`Word` expression without calling ``setName`` is ``"W:(ABCD...)"``.
         """
         if flag:
-            self.setDebugActions( _defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction )
+            self.setDebugActions(_defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction)
         else:
             self.debug = False
         return self
 
-    def __str__( self ):
+    def __str__(self):
         return self.name
 
-    def __repr__( self ):
+    def __repr__(self):
         return _ustr(self)
 
-    def streamline( self ):
+    def streamline(self):
         self.streamlined = True
         self.strRepr = None
         return self
 
-    def checkRecursion( self, parseElementList ):
+    def checkRecursion(self, parseElementList):
         pass
 
-    def validate( self, validateTrace=[] ):
+    def validate(self, validateTrace=None):
         """
         Check defined expressions for valid structure, check for infinite recursive definitions.
         """
-        self.checkRecursion( [] )
+        self.checkRecursion([])
 
-    def parseFile( self, file_or_filename, parseAll=False ):
+    def parseFile(self, file_or_filename, parseAll=False):
         """
         Execute the parse expression on the given file or filename.
         If a filename is specified (instead of a file object),
@@ -2381,24 +2553,27 @@ class ParserElement(object):
                 # catch and re-raise exception from here, clears out pyparsing internal stack trace
                 raise exc
 
-    def __eq__(self,other):
+    def __eq__(self, other):
         if isinstance(other, ParserElement):
-            return self is other or vars(self) == vars(other)
+            if PY_3:
+                self is other or super(ParserElement, self).__eq__(other)
+            else:
+                return self is other or vars(self) == vars(other)
         elif isinstance(other, basestring):
             return self.matches(other)
         else:
-            return super(ParserElement,self)==other
+            return super(ParserElement, self) == other
 
-    def __ne__(self,other):
+    def __ne__(self, other):
         return not (self == other)
 
     def __hash__(self):
-        return hash(id(self))
+        return id(self)
 
-    def __req__(self,other):
+    def __req__(self, other):
         return self == other
 
-    def __rne__(self,other):
+    def __rne__(self, other):
         return not (self == other)
 
     def matches(self, testString, parseAll=True):
@@ -2422,7 +2597,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
@@ -2439,6 +2615,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
@@ -2518,9 +2696,15 @@ 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
+        NL = Literal(r'\n').addParseAction(replaceWith('\n')).ignore(quotedString)
+        BOM = u'\ufeff'
         for t in tests:
             if comment is not None and comment.matches(t, False) or comments and not t:
                 comments.append(t)
@@ -2531,26 +2715,15 @@ class ParserElement(object):
             comments = []
             try:
                 # convert newline marks to actual newlines, and strip leading BOM if present
-                NL = Literal(r'\n').addParseAction(replaceWith('\n')).ignore(quotedString)
-                BOM = '\ufeff'
                 t = NL.transformString(t.lstrip(BOM))
                 result = self.parseString(t, parseAll=parseAll)
-                out.append(result.dump(full=fullDump))
-                success = success and not failureTests
-                if postParse is not None:
-                    try:
-                        pp_value = postParse(t, result)
-                        if pp_value is not None:
-                            out.append(str(pp_value))
-                    except Exception as e:
-                        out.append("{0} failed: {1}: {2}".format(postParse.__name__, type(e).__name__, e))
             except ParseBaseException as pe:
                 fatal = "(FATAL)" if isinstance(pe, ParseFatalException) else ""
                 if '\n' in t:
                     out.append(line(pe.loc, t))
-                    out.append(' '*(col(pe.loc,t)-1) + '^' + fatal)
+                    out.append(' ' * (col(pe.loc, t) - 1) + '^' + fatal)
                 else:
-                    out.append(' '*pe.loc + '^' + fatal)
+                    out.append(' ' * pe.loc + '^' + fatal)
                 out.append("FAIL: " + str(pe))
                 success = success and failureTests
                 result = pe
@@ -2558,30 +2731,80 @@ class ParserElement(object):
                 out.append("FAIL-EXCEPTION: " + str(exc))
                 success = success and failureTests
                 result = exc
+            else:
+                success = success and not failureTests
+                if postParse is not None:
+                    try:
+                        pp_value = postParse(t, result)
+                        if pp_value is not None:
+                            if isinstance(pp_value, ParseResults):
+                                out.append(pp_value.dump())
+                            else:
+                                out.append(str(pp_value))
+                        else:
+                            out.append(result.dump())
+                    except Exception as e:
+                        out.append(result.dump(full=fullDump))
+                        out.append("{0} failed: {1}: {2}".format(postParse.__name__, type(e).__name__, e))
+                else:
+                    out.append(result.dump(full=fullDump))
 
             if printResults:
                 if fullDump:
                     out.append('')
-                print('\n'.join(out))
+                print_('\n'.join(out))
 
             allResults.append((t, result))
 
         return success, allResults
 
 
+class _PendingSkip(ParserElement):
+    # internal placeholder class to hold a place were '...' is added to a parser element,
+    # once another ParserElement is added, this placeholder will be replaced with a SkipTo
+    def __init__(self, expr, must_skip=False):
+        super(_PendingSkip, self).__init__()
+        self.strRepr = str(expr + Empty()).replace('Empty', '...')
+        self.name = self.strRepr
+        self.anchor = expr
+        self.must_skip = must_skip
+
+    def __add__(self, other):
+        skipper = SkipTo(other).setName("...")("_skipped*")
+        if self.must_skip:
+            def must_skip(t):
+                if not t._skipped or t._skipped.asList() == ['']:
+                    del t[0]
+                    t.pop("_skipped", None)
+            def show_skip(t):
+                if t._skipped.asList()[-1:] == ['']:
+                    skipped = t.pop('_skipped')
+                    t['_skipped'] = 'missing <' + repr(self.anchor) + '>'
+            return (self.anchor + skipper().addParseAction(must_skip)
+                    | skipper().addParseAction(show_skip)) + other
+
+        return self.anchor + skipper + other
+
+    def __repr__(self):
+        return self.strRepr
+
+    def parseImpl(self, *args):
+        raise Exception("use of `...` expression without following SkipTo target expression")
+
+
 class Token(ParserElement):
     """Abstract :class:`ParserElement` subclass, for defining atomic
     matching patterns.
     """
-    def __init__( self ):
-        super(Token,self).__init__( savelist=False )
+    def __init__(self):
+        super(Token, self).__init__(savelist=False)
 
 
 class Empty(Token):
     """An empty token, will always match.
     """
-    def __init__( self ):
-        super(Empty,self).__init__()
+    def __init__(self):
+        super(Empty, self).__init__()
         self.name = "Empty"
         self.mayReturnEmpty = True
         self.mayIndexError = False
@@ -2590,14 +2813,14 @@ class Empty(Token):
 class NoMatch(Token):
     """A token that will never match.
     """
-    def __init__( self ):
-        super(NoMatch,self).__init__()
+    def __init__(self):
+        super(NoMatch, self).__init__()
         self.name = "NoMatch"
         self.mayReturnEmpty = True
         self.mayIndexError = False
         self.errmsg = "Unmatchable token"
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         raise ParseException(instring, loc, self.errmsg, self)
 
 
@@ -2615,8 +2838,8 @@ class Literal(Token):
     For keyword matching (force word break before and after the matched string),
     use :class:`Keyword` or :class:`CaselessKeyword`.
     """
-    def __init__( self, matchString ):
-        super(Literal,self).__init__()
+    def __init__(self, matchString):
+        super(Literal, self).__init__()
         self.match = matchString
         self.matchLen = len(matchString)
         try:
@@ -2630,15 +2853,22 @@ class Literal(Token):
         self.mayReturnEmpty = False
         self.mayIndexError = False
 
-    # Performance tuning: this routine gets called a *lot*
-    # if this is a single character match string  and the first character matches,
-    # short-circuit as quickly as possible, and avoid calling startswith
-    #~ @profile
-    def parseImpl( self, instring, loc, doActions=True ):
-        if (instring[loc] == self.firstMatchChar and
-            (self.matchLen==1 or instring.startswith(self.match,loc)) ):
-            return loc+self.matchLen, self.match
+        # Performance tuning: modify __class__ to select
+        # a parseImpl optimized for single-character check
+        if self.matchLen == 1 and type(self) is Literal:
+            self.__class__ = _SingleCharLiteral
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if instring[loc] == self.firstMatchChar and instring.startswith(self.match, loc):
+            return loc + self.matchLen, self.match
+        raise ParseException(instring, loc, self.errmsg, self)
+
+class _SingleCharLiteral(Literal):
+    def parseImpl(self, instring, loc, doActions=True):
+        if instring[loc] == self.firstMatchChar:
+            return loc + 1, self.match
         raise ParseException(instring, loc, self.errmsg, self)
+
 _L = Literal
 ParserElement._literalStringClass = Literal
 
@@ -2667,10 +2897,10 @@ class Keyword(Token):
 
     For case-insensitive matching, use :class:`CaselessKeyword`.
     """
-    DEFAULT_KEYWORD_CHARS = alphanums+"_$"
+    DEFAULT_KEYWORD_CHARS = alphanums + "_$"
 
-    def __init__( self, matchString, identChars=None, caseless=False ):
-        super(Keyword,self).__init__()
+    def __init__(self, matchString, identChars=None, caseless=False):
+        super(Keyword, self).__init__()
         if identChars is None:
             identChars = Keyword.DEFAULT_KEYWORD_CHARS
         self.match = matchString
@@ -2679,7 +2909,7 @@ class Keyword(Token):
             self.firstMatchChar = matchString[0]
         except IndexError:
             warnings.warn("null string passed to Keyword; use Empty() instead",
-                            SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
         self.name = '"%s"' % self.match
         self.errmsg = "Expected " + self.name
         self.mayReturnEmpty = False
@@ -2690,27 +2920,32 @@ class Keyword(Token):
             identChars = identChars.upper()
         self.identChars = set(identChars)
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if self.caseless:
-            if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and
-                 (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and
-                 (loc == 0 or instring[loc-1].upper() not in self.identChars) ):
-                return loc+self.matchLen, self.match
+            if ((instring[loc:loc + self.matchLen].upper() == self.caselessmatch)
+                    and (loc >= len(instring) - self.matchLen
+                         or instring[loc + self.matchLen].upper() not in self.identChars)
+                    and (loc == 0
+                         or instring[loc - 1].upper() not in self.identChars)):
+                return loc + self.matchLen, self.match
+
         else:
-            if (instring[loc] == self.firstMatchChar and
-                (self.matchLen==1 or instring.startswith(self.match,loc)) and
-                (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and
-                (loc == 0 or instring[loc-1] not in self.identChars) ):
-                return loc+self.matchLen, self.match
+            if instring[loc] == self.firstMatchChar:
+                if ((self.matchLen == 1 or instring.startswith(self.match, loc))
+                        and (loc >= len(instring) - self.matchLen
+                             or instring[loc + self.matchLen] not in self.identChars)
+                        and (loc == 0 or instring[loc - 1] not in self.identChars)):
+                    return loc + self.matchLen, self.match
+
         raise ParseException(instring, loc, self.errmsg, self)
 
     def copy(self):
-        c = super(Keyword,self).copy()
+        c = super(Keyword, self).copy()
         c.identChars = Keyword.DEFAULT_KEYWORD_CHARS
         return c
 
     @staticmethod
-    def setDefaultKeywordChars( chars ):
+    def setDefaultKeywordChars(chars):
         """Overrides the default Keyword chars
         """
         Keyword.DEFAULT_KEYWORD_CHARS = chars
@@ -2726,16 +2961,16 @@ class CaselessLiteral(Literal):
 
     (Contrast with example for :class:`CaselessKeyword`.)
     """
-    def __init__( self, matchString ):
-        super(CaselessLiteral,self).__init__( matchString.upper() )
+    def __init__(self, matchString):
+        super(CaselessLiteral, self).__init__(matchString.upper())
         # Preserve the defining literal.
         self.returnString = matchString
         self.name = "'%s'" % self.returnString
         self.errmsg = "Expected " + self.name
 
-    def parseImpl( self, instring, loc, doActions=True ):
-        if instring[ loc:loc+self.matchLen ].upper() == self.match:
-            return loc+self.matchLen, self.returnString
+    def parseImpl(self, instring, loc, doActions=True):
+        if instring[loc:loc + self.matchLen].upper() == self.match:
+            return loc + self.matchLen, self.returnString
         raise ParseException(instring, loc, self.errmsg, self)
 
 class CaselessKeyword(Keyword):
@@ -2748,8 +2983,8 @@ class CaselessKeyword(Keyword):
 
     (Contrast with example for :class:`CaselessLiteral`.)
     """
-    def __init__( self, matchString, identChars=None ):
-        super(CaselessKeyword,self).__init__( matchString, identChars, caseless=True )
+    def __init__(self, matchString, identChars=None):
+        super(CaselessKeyword, self).__init__(matchString, identChars, caseless=True)
 
 class CloseMatch(Token):
     """A variation on :class:`Literal` which matches "close" matches,
@@ -2785,7 +3020,7 @@ class CloseMatch(Token):
         patt.parseString("ATCAXCGAAXGGA") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']})
     """
     def __init__(self, match_string, maxMismatches=1):
-        super(CloseMatch,self).__init__()
+        super(CloseMatch, self).__init__()
         self.name = match_string
         self.match_string = match_string
         self.maxMismatches = maxMismatches
@@ -2793,7 +3028,7 @@ class CloseMatch(Token):
         self.mayIndexError = False
         self.mayReturnEmpty = False
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         start = loc
         instrlen = len(instring)
         maxloc = start + len(self.match_string)
@@ -2804,8 +3039,8 @@ class CloseMatch(Token):
             mismatches = []
             maxMismatches = self.maxMismatches
 
-            for match_stringloc,s_m in enumerate(zip(instring[loc:maxloc], self.match_string)):
-                src,mat = s_m
+            for match_stringloc, s_m in enumerate(zip(instring[loc:maxloc], match_string)):
+                src, mat = s_m
                 if src != mat:
                     mismatches.append(match_stringloc)
                     if len(mismatches) > maxMismatches:
@@ -2813,7 +3048,7 @@ class CloseMatch(Token):
             else:
                 loc = match_stringloc + 1
                 results = ParseResults([instring[start:loc]])
-                results['original'] = self.match_string
+                results['original'] = match_string
                 results['mismatches'] = mismatches
                 return loc, results
 
@@ -2865,7 +3100,7 @@ class Word(Token):
         capital_word = Word(alphas.upper(), alphas.lower())
 
         # hostnames are alphanumeric, with leading alpha, and '-'
-        hostname = Word(alphas, alphanums+'-')
+        hostname = Word(alphas, alphanums + '-')
 
         # roman numeral (not a strict parser, accepts invalid mix of characters)
         roman = Word("IVXLCDM")
@@ -2873,8 +3108,8 @@ class Word(Token):
         # any string of non-whitespace characters, except for ','
         csv_value = Word(printables, excludeChars=",")
     """
-    def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ):
-        super(Word,self).__init__()
+    def __init__(self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None):
+        super(Word, self).__init__()
         if excludeChars:
             excludeChars = set(excludeChars)
             initChars = ''.join(c for c in initChars if c not in excludeChars)
@@ -2882,7 +3117,7 @@ class Word(Token):
                 bodyChars = ''.join(c for c in bodyChars if c not in excludeChars)
         self.initCharsOrig = initChars
         self.initChars = set(initChars)
-        if bodyChars :
+        if bodyChars:
             self.bodyCharsOrig = bodyChars
             self.bodyChars = set(bodyChars)
         else:
@@ -2910,33 +3145,27 @@ class Word(Token):
         self.mayIndexError = False
         self.asKeyword = asKeyword
 
-        if ' ' not in self.initCharsOrig+self.bodyCharsOrig and (min==1 and max==0 and exact==0):
+        if ' ' not in self.initCharsOrig + self.bodyCharsOrig and (min == 1 and max == 0 and exact == 0):
             if self.bodyCharsOrig == self.initCharsOrig:
                 self.reString = "[%s]+" % _escapeRegexRangeChars(self.initCharsOrig)
             elif len(self.initCharsOrig) == 1:
-                self.reString = "%s[%s]*" % \
-                                      (re.escape(self.initCharsOrig),
-                                      _escapeRegexRangeChars(self.bodyCharsOrig),)
+                self.reString = "%s[%s]*" % (re.escape(self.initCharsOrig),
+                                             _escapeRegexRangeChars(self.bodyCharsOrig),)
             else:
-                self.reString = "[%s][%s]*" % \
-                                      (_escapeRegexRangeChars(self.initCharsOrig),
-                                      _escapeRegexRangeChars(self.bodyCharsOrig),)
+                self.reString = "[%s][%s]*" % (_escapeRegexRangeChars(self.initCharsOrig),
+                                               _escapeRegexRangeChars(self.bodyCharsOrig),)
             if self.asKeyword:
-                self.reString = r"\b"+self.reString+r"\b"
+                self.reString = r"\b" + self.reString + r"\b"
+
             try:
-                self.re = re.compile( self.reString )
+                self.re = re.compile(self.reString)
             except Exception:
                 self.re = None
+            else:
+                self.re_match = self.re.match
+                self.__class__ = _WordRegex
 
-    def parseImpl( self, instring, loc, doActions=True ):
-        if self.re:
-            result = self.re.match(instring,loc)
-            if not result:
-                raise ParseException(instring, loc, self.errmsg, self)
-
-            loc = result.end()
-            return loc, result.group()
-
+    def parseImpl(self, instring, loc, doActions=True):
         if instring[loc] not in self.initChars:
             raise ParseException(instring, loc, self.errmsg, self)
 
@@ -2945,7 +3174,7 @@ class Word(Token):
         instrlen = len(instring)
         bodychars = self.bodyChars
         maxloc = start + self.maxLen
-        maxloc = min( maxloc, instrlen )
+        maxloc = min(maxloc, instrlen)
         while loc < maxloc and instring[loc] in bodychars:
             loc += 1
 
@@ -2955,7 +3184,8 @@ class Word(Token):
         elif self.maxSpecified and loc < instrlen and instring[loc] in bodychars:
             throwException = True
         elif self.asKeyword:
-            if (start>0 and instring[start-1] in bodychars) or (loc<instrlen and instring[loc] in bodychars):
+            if (start > 0 and instring[start - 1] in bodychars
+                    or loc < instrlen and instring[loc] in bodychars):
                 throwException = True
 
         if throwException:
@@ -2963,38 +3193,49 @@ class Word(Token):
 
         return loc, instring[start:loc]
 
-    def __str__( self ):
+    def __str__(self):
         try:
-            return super(Word,self).__str__()
+            return super(Word, self).__str__()
         except Exception:
             pass
 
-
         if self.strRepr is None:
 
             def charsAsStr(s):
-                if len(s)>4:
-                    return s[:4]+"..."
+                if len(s) > 4:
+                    return s[:4] + "..."
                 else:
                     return s
 
-            if ( self.initCharsOrig != self.bodyCharsOrig ):
-                self.strRepr = "W:(%s,%s)" % ( charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig) )
+            if self.initCharsOrig != self.bodyCharsOrig:
+                self.strRepr = "W:(%s, %s)" % (charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig))
             else:
                 self.strRepr = "W:(%s)" % charsAsStr(self.initCharsOrig)
 
         return self.strRepr
 
+class _WordRegex(Word):
+    def parseImpl(self, instring, loc, doActions=True):
+        result = self.re_match(instring, loc)
+        if not result:
+            raise ParseException(instring, loc, self.errmsg, self)
 
-class Char(Word):
+        loc = result.end()
+        return loc, result.group()
+
+
+class Char(_WordRegex):
     """A short-cut class for defining ``Word(characters, exact=1)``,
     when defining a match of any single character in a string of
     characters.
     """
     def __init__(self, charset, asKeyword=False, excludeChars=None):
         super(Char, self).__init__(charset, exact=1, asKeyword=asKeyword, excludeChars=excludeChars)
-        self.reString = "[%s]" % _escapeRegexRangeChars(self.initCharsOrig)
-        self.re = re.compile( self.reString )
+        self.reString = "[%s]" % _escapeRegexRangeChars(''.join(self.initChars))
+        if asKeyword:
+            self.reString = r"\b%s\b" % self.reString
+        self.re = re.compile(self.reString)
+        self.re_match = self.re.match
 
 
 class Regex(Token):
@@ -3012,18 +3253,18 @@ class Regex(Token):
         roman = Regex(r"M{0,4}(CM|CD|D?{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})")
     """
     compiledREtype = type(re.compile("[A-Z]"))
-    def __init__( self, pattern, flags=0, asGroupList=False, asMatch=False):
+    def __init__(self, pattern, flags=0, asGroupList=False, asMatch=False):
         """The parameters ``pattern`` and ``flags`` are passed
         to the ``re.compile()`` function as-is. See the Python
         `re module <https://docs.python.org/3/library/re.html>`_ module for an
         explanation of the acceptable patterns and flags.
         """
-        super(Regex,self).__init__()
+        super(Regex, self).__init__()
 
         if isinstance(pattern, basestring):
             if not pattern:
                 warnings.warn("null string passed to Regex; use Empty() instead",
-                        SyntaxWarning, stacklevel=2)
+                              SyntaxWarning, stacklevel=2)
 
             self.pattern = pattern
             self.flags = flags
@@ -3033,18 +3274,19 @@ class Regex(Token):
                 self.reString = self.pattern
             except sre_constants.error:
                 warnings.warn("invalid pattern (%s) passed to Regex" % pattern,
-                    SyntaxWarning, stacklevel=2)
+                              SyntaxWarning, stacklevel=2)
                 raise
 
         elif isinstance(pattern, Regex.compiledREtype):
             self.re = pattern
-            self.pattern = \
-            self.reString = str(pattern)
+            self.pattern = self.reString = str(pattern)
             self.flags = flags
 
         else:
             raise ValueError("Regex may only be constructed with a string or a compiled RE object")
 
+        self.re_match = self.re.match
+
         self.name = _ustr(self)
         self.errmsg = "Expected " + self.name
         self.mayIndexError = False
@@ -3057,7 +3299,7 @@ class Regex(Token):
             self.parseImpl = self.parseImplAsMatch
 
     def parseImpl(self, instring, loc, doActions=True):
-        result = self.re.match(instring,loc)
+        result = self.re_match(instring, loc)
         if not result:
             raise ParseException(instring, loc, self.errmsg, self)
 
@@ -3070,7 +3312,7 @@ class Regex(Token):
         return loc, ret
 
     def parseImplAsGroupList(self, instring, loc, doActions=True):
-        result = self.re.match(instring,loc)
+        result = self.re_match(instring, loc)
         if not result:
             raise ParseException(instring, loc, self.errmsg, self)
 
@@ -3079,7 +3321,7 @@ class Regex(Token):
         return loc, ret
 
     def parseImplAsMatch(self, instring, loc, doActions=True):
-        result = self.re.match(instring,loc)
+        result = self.re_match(instring, loc)
         if not result:
             raise ParseException(instring, loc, self.errmsg, self)
 
@@ -3087,9 +3329,9 @@ class Regex(Token):
         ret = result
         return loc, ret
 
-    def __str__( self ):
+    def __str__(self):
         try:
-            return super(Regex,self).__str__()
+            return super(Regex, self).__str__()
         except Exception:
             pass
 
@@ -3111,12 +3353,12 @@ class Regex(Token):
         """
         if self.asGroupList:
             warnings.warn("cannot use sub() with Regex(asGroupList=True)",
-                           SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             raise SyntaxError()
 
         if self.asMatch and callable(repl):
             warnings.warn("cannot use sub() with a callable with Regex(asMatch=True)",
-                           SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             raise SyntaxError()
 
         if self.asMatch:
@@ -3136,20 +3378,20 @@ class QuotedString(Token):
         - quoteChar - string of one or more characters defining the
           quote delimiting string
         - escChar - character to escape quotes, typically backslash
-          (default= ``None`` )
+          (default= ``None``)
         - escQuote - special quote sequence to escape an embedded quote
           string (such as SQL's ``""`` to escape an embedded ``"``)
-          (default= ``None`` )
+          (default= ``None``)
         - multiline - boolean indicating whether quotes can span
-          multiple lines (default= ``False`` )
+          multiple lines (default= ``False``)
         - unquoteResults - boolean indicating whether the matched text
-          should be unquoted (default= ``True`` )
+          should be unquoted (default= ``True``)
         - endQuoteChar - string of one or more characters defining the
           end of the quote delimited string (default= ``None``  => same as
           quoteChar)
         - convertWhitespaceEscapes - convert escaped whitespace
           (``'\t'``, ``'\n'``, etc.) to actual whitespace
-          (default= ``True`` )
+          (default= ``True``)
 
     Example::
 
@@ -3166,13 +3408,14 @@ class QuotedString(Token):
         [['This is the "quote"']]
         [['This is the quote with "embedded" quotes']]
     """
-    def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True):
-        super(QuotedString,self).__init__()
+    def __init__(self, quoteChar, escChar=None, escQuote=None, multiline=False,
+                 unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True):
+        super(QuotedString, self).__init__()
 
         # remove white space from quote chars - wont work anyway
         quoteChar = quoteChar.strip()
         if not quoteChar:
-            warnings.warn("quoteChar cannot be the empty string",SyntaxWarning,stacklevel=2)
+            warnings.warn("quoteChar cannot be the empty string", SyntaxWarning, stacklevel=2)
             raise SyntaxError()
 
         if endQuoteChar is None:
@@ -3180,7 +3423,7 @@ class QuotedString(Token):
         else:
             endQuoteChar = endQuoteChar.strip()
             if not endQuoteChar:
-                warnings.warn("endQuoteChar cannot be the empty string",SyntaxWarning,stacklevel=2)
+                warnings.warn("endQuoteChar cannot be the empty string", SyntaxWarning, stacklevel=2)
                 raise SyntaxError()
 
         self.quoteChar = quoteChar
@@ -3195,35 +3438,34 @@ class QuotedString(Token):
 
         if multiline:
             self.flags = re.MULTILINE | re.DOTALL
-            self.pattern = r'%s(?:[^%s%s]' % \
-                ( re.escape(self.quoteChar),
-                  _escapeRegexRangeChars(self.endQuoteChar[0]),
-                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )
+            self.pattern = r'%s(?:[^%s%s]' % (re.escape(self.quoteChar),
+                                              _escapeRegexRangeChars(self.endQuoteChar[0]),
+                                              (escChar is not None and _escapeRegexRangeChars(escChar) or ''))
         else:
             self.flags = 0
-            self.pattern = r'%s(?:[^%s\n\r%s]' % \
-                ( re.escape(self.quoteChar),
-                  _escapeRegexRangeChars(self.endQuoteChar[0]),
-                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )
+            self.pattern = r'%s(?:[^%s\n\r%s]' % (re.escape(self.quoteChar),
+                                                  _escapeRegexRangeChars(self.endQuoteChar[0]),
+                                                  (escChar is not None and _escapeRegexRangeChars(escChar) or ''))
         if len(self.endQuoteChar) > 1:
             self.pattern += (
                 '|(?:' + ')|(?:'.join("%s[^%s]" % (re.escape(self.endQuoteChar[:i]),
-                                               _escapeRegexRangeChars(self.endQuoteChar[i]))
-                                    for i in range(len(self.endQuoteChar)-1,0,-1)) + ')'
-                )
+                                                   _escapeRegexRangeChars(self.endQuoteChar[i]))
+                                      for i in range(len(self.endQuoteChar) - 1, 0, -1)) + ')')
+
         if escQuote:
             self.pattern += (r'|(?:%s)' % re.escape(escQuote))
         if escChar:
             self.pattern += (r'|(?:%s.)' % re.escape(escChar))
-            self.escCharReplacePattern = re.escape(self.escChar)+"(.)"
+            self.escCharReplacePattern = re.escape(self.escChar) + "(.)"
         self.pattern += (r')*%s' % re.escape(self.endQuoteChar))
 
         try:
             self.re = re.compile(self.pattern, self.flags)
             self.reString = self.pattern
+            self.re_match = self.re.match
         except sre_constants.error:
             warnings.warn("invalid pattern (%s) passed to Regex" % self.pattern,
-                SyntaxWarning, stacklevel=2)
+                          SyntaxWarning, stacklevel=2)
             raise
 
         self.name = _ustr(self)
@@ -3231,8 +3473,8 @@ class QuotedString(Token):
         self.mayIndexError = False
         self.mayReturnEmpty = True
 
-    def parseImpl( self, instring, loc, doActions=True ):
-        result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None
+    def parseImpl(self, instring, loc, doActions=True):
+        result = instring[loc] == self.firstQuoteChar and self.re_match(instring, loc) or None
         if not result:
             raise ParseException(instring, loc, self.errmsg, self)
 
@@ -3242,18 +3484,18 @@ class QuotedString(Token):
         if self.unquoteResults:
 
             # strip off quotes
-            ret = ret[self.quoteCharLen:-self.endQuoteCharLen]
+            ret = ret[self.quoteCharLen: -self.endQuoteCharLen]
 
-            if isinstance(ret,basestring):
+            if isinstance(ret, basestring):
                 # replace escaped whitespace
                 if '\\' in ret and self.convertWhitespaceEscapes:
                     ws_map = {
-                        r'\t' : '\t',
-                        r'\n' : '\n',
-                        r'\f' : '\f',
-                        r'\r' : '\r',
+                        r'\t': '\t',
+                        r'\n': '\n',
+                        r'\f': '\f',
+                        r'\r': '\r',
                     }
-                    for wslit,wschar in ws_map.items():
+                    for wslit, wschar in ws_map.items():
                         ret = ret.replace(wslit, wschar)
 
                 # replace escaped characters
@@ -3266,9 +3508,9 @@ class QuotedString(Token):
 
         return loc, ret
 
-    def __str__( self ):
+    def __str__(self):
         try:
-            return super(QuotedString,self).__str__()
+            return super(QuotedString, self).__str__()
         except Exception:
             pass
 
@@ -3298,15 +3540,14 @@ class CharsNotIn(Token):
 
         ['dkls', 'lsdkjf', 's12 34', '@!#', '213']
     """
-    def __init__( self, notChars, min=1, max=0, exact=0 ):
-        super(CharsNotIn,self).__init__()
+    def __init__(self, notChars, min=1, max=0, exact=0):
+        super(CharsNotIn, self).__init__()
         self.skipWhitespace = False
         self.notChars = notChars
 
         if min < 1:
-            raise ValueError(
-                "cannot specify a minimum length < 1; use " +
-                "Optional(CharsNotIn()) if zero-length char group is permitted")
+            raise ValueError("cannot specify a minimum length < 1; use "
+                             "Optional(CharsNotIn()) if zero-length char group is permitted")
 
         self.minLen = min
 
@@ -3321,19 +3562,18 @@ class CharsNotIn(Token):
 
         self.name = _ustr(self)
         self.errmsg = "Expected " + self.name
-        self.mayReturnEmpty = ( self.minLen == 0 )
+        self.mayReturnEmpty = (self.minLen == 0)
         self.mayIndexError = False
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if instring[loc] in self.notChars:
             raise ParseException(instring, loc, self.errmsg, self)
 
         start = loc
         loc += 1
         notchars = self.notChars
-        maxlen = min( start+self.maxLen, len(instring) )
-        while loc < maxlen and \
-              (instring[loc] not in notchars):
+        maxlen = min(start + self.maxLen, len(instring))
+        while loc < maxlen and instring[loc] not in notchars:
             loc += 1
 
         if loc - start < self.minLen:
@@ -3341,7 +3581,7 @@ class CharsNotIn(Token):
 
         return loc, instring[start:loc]
 
-    def __str__( self ):
+    def __str__(self):
         try:
             return super(CharsNotIn, self).__str__()
         except Exception:
@@ -3390,10 +3630,10 @@ class White(Token):
         'u\3000': '<IDEOGRAPHIC_SPACE>',
         }
     def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0):
-        super(White,self).__init__()
+        super(White, self).__init__()
         self.matchWhite = ws
-        self.setWhitespaceChars( "".join(c for c in self.whiteChars if c not in self.matchWhite) )
-        #~ self.leaveWhitespace()
+        self.setWhitespaceChars("".join(c for c in self.whiteChars if c not in self.matchWhite))
+        # ~ self.leaveWhitespace()
         self.name = ("".join(White.whiteStrs[c] for c in self.matchWhite))
         self.mayReturnEmpty = True
         self.errmsg = "Expected " + self.name
@@ -3409,13 +3649,13 @@ class White(Token):
             self.maxLen = exact
             self.minLen = exact
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if instring[loc] not in self.matchWhite:
             raise ParseException(instring, loc, self.errmsg, self)
         start = loc
         loc += 1
         maxloc = start + self.maxLen
-        maxloc = min( maxloc, len(instring) )
+        maxloc = min(maxloc, len(instring))
         while loc < maxloc and instring[loc] in self.matchWhite:
             loc += 1
 
@@ -3426,9 +3666,9 @@ class White(Token):
 
 
 class _PositionToken(Token):
-    def __init__( self ):
-        super(_PositionToken,self).__init__()
-        self.name=self.__class__.__name__
+    def __init__(self):
+        super(_PositionToken, self).__init__()
+        self.name = self.__class__.__name__
         self.mayReturnEmpty = True
         self.mayIndexError = False
 
@@ -3436,25 +3676,25 @@ class GoToColumn(_PositionToken):
     """Token to advance to a specific column of input text; useful for
     tabular report scraping.
     """
-    def __init__( self, colno ):
-        super(GoToColumn,self).__init__()
+    def __init__(self, colno):
+        super(GoToColumn, self).__init__()
         self.col = colno
 
-    def preParse( self, instring, loc ):
-        if col(loc,instring) != self.col:
+    def preParse(self, instring, loc):
+        if col(loc, instring) != self.col:
             instrlen = len(instring)
             if self.ignoreExprs:
-                loc = self._skipIgnorables( instring, loc )
-            while loc < instrlen and instring[loc].isspace() and col( loc, instring ) != self.col :
+                loc = self._skipIgnorables(instring, loc)
+            while loc < instrlen and instring[loc].isspace() and col(loc, instring) != self.col:
                 loc += 1
         return loc
 
-    def parseImpl( self, instring, loc, doActions=True ):
-        thiscol = col( loc, instring )
+    def parseImpl(self, instring, loc, doActions=True):
+        thiscol = col(loc, instring)
         if thiscol > self.col:
-            raise ParseException( instring, loc, "Text not in expected column", self )
+            raise ParseException(instring, loc, "Text not in expected column", self)
         newloc = loc + self.col - thiscol
-        ret = instring[ loc: newloc ]
+        ret = instring[loc: newloc]
         return newloc, ret
 
 
@@ -3480,11 +3720,11 @@ class LineStart(_PositionToken):
         ['AAA', ' and this line']
 
     """
-    def __init__( self ):
-        super(LineStart,self).__init__()
+    def __init__(self):
+        super(LineStart, self).__init__()
         self.errmsg = "Expected start of line"
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if col(loc, instring) == 1:
             return loc, []
         raise ParseException(instring, loc, self.errmsg, self)
@@ -3493,19 +3733,19 @@ class LineEnd(_PositionToken):
     """Matches if current position is at the end of a line within the
     parse string
     """
-    def __init__( self ):
-        super(LineEnd,self).__init__()
-        self.setWhitespaceChars( ParserElement.DEFAULT_WHITE_CHARS.replace("\n","") )
+    def __init__(self):
+        super(LineEnd, self).__init__()
+        self.setWhitespaceChars(ParserElement.DEFAULT_WHITE_CHARS.replace("\n", ""))
         self.errmsg = "Expected end of line"
 
-    def parseImpl( self, instring, loc, doActions=True ):
-        if loc<len(instring):
+    def parseImpl(self, instring, loc, doActions=True):
+        if loc < len(instring):
             if instring[loc] == "\n":
-                return loc+1, "\n"
+                return loc + 1, "\n"
             else:
                 raise ParseException(instring, loc, self.errmsg, self)
         elif loc == len(instring):
-            return loc+1, []
+            return loc + 1, []
         else:
             raise ParseException(instring, loc, self.errmsg, self)
 
@@ -3513,29 +3753,29 @@ class StringStart(_PositionToken):
     """Matches if current position is at the beginning of the parse
     string
     """
-    def __init__( self ):
-        super(StringStart,self).__init__()
+    def __init__(self):
+        super(StringStart, self).__init__()
         self.errmsg = "Expected start of text"
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if loc != 0:
             # see if entire string up to here is just whitespace and ignoreables
-            if loc != self.preParse( instring, 0 ):
+            if loc != self.preParse(instring, 0):
                 raise ParseException(instring, loc, self.errmsg, self)
         return loc, []
 
 class StringEnd(_PositionToken):
     """Matches if current position is at the end of the parse string
     """
-    def __init__( self ):
-        super(StringEnd,self).__init__()
+    def __init__(self):
+        super(StringEnd, self).__init__()
         self.errmsg = "Expected end of text"
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if loc < len(instring):
             raise ParseException(instring, loc, self.errmsg, self)
         elif loc == len(instring):
-            return loc+1, []
+            return loc + 1, []
         elif loc > len(instring):
             return loc, []
         else:
@@ -3550,15 +3790,15 @@ class WordStart(_PositionToken):
     the beginning of the string being parsed, or at the beginning of
     a line.
     """
-    def __init__(self, wordChars = printables):
-        super(WordStart,self).__init__()
+    def __init__(self, wordChars=printables):
+        super(WordStart, self).__init__()
         self.wordChars = set(wordChars)
         self.errmsg = "Not at the start of a word"
 
-    def parseImpl(self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if loc != 0:
-            if (instring[loc-1] in self.wordChars or
-                instring[loc] not in self.wordChars):
+            if (instring[loc - 1] in self.wordChars
+                    or instring[loc] not in self.wordChars):
                 raise ParseException(instring, loc, self.errmsg, self)
         return loc, []
 
@@ -3570,17 +3810,17 @@ class WordEnd(_PositionToken):
     will also match at the end of the string being parsed, or at the end
     of a line.
     """
-    def __init__(self, wordChars = printables):
-        super(WordEnd,self).__init__()
+    def __init__(self, wordChars=printables):
+        super(WordEnd, self).__init__()
         self.wordChars = set(wordChars)
         self.skipWhitespace = False
         self.errmsg = "Not at the end of a word"
 
-    def parseImpl(self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         instrlen = len(instring)
-        if instrlen>0 and loc<instrlen:
+        if instrlen > 0 and loc < instrlen:
             if (instring[loc] in self.wordChars or
-                instring[loc-1] not in self.wordChars):
+                    instring[loc - 1] not in self.wordChars):
                 raise ParseException(instring, loc, self.errmsg, self)
         return loc, []
 
@@ -3589,90 +3829,87 @@ class ParseExpression(ParserElement):
     """Abstract subclass of ParserElement, for combining and
     post-processing parsed tokens.
     """
-    def __init__( self, exprs, savelist = False ):
-        super(ParseExpression,self).__init__(savelist)
-        if isinstance( exprs, _generatorType ):
+    def __init__(self, exprs, savelist=False):
+        super(ParseExpression, self).__init__(savelist)
+        if isinstance(exprs, _generatorType):
             exprs = list(exprs)
 
-        if isinstance( exprs, basestring ):
-            self.exprs = [ ParserElement._literalStringClass( exprs ) ]
-        elif isinstance( exprs, Iterable ):
+        if isinstance(exprs, basestring):
+            self.exprs = [self._literalStringClass(exprs)]
+        elif isinstance(exprs, Iterable):
             exprs = list(exprs)
             # if sequence of strings provided, wrap with Literal
-            if all(isinstance(expr, basestring) for expr in exprs):
-                exprs = map(ParserElement._literalStringClass, exprs)
+            if any(isinstance(expr, basestring) for expr in exprs):
+                exprs = (self._literalStringClass(e) if isinstance(e, basestring) else e for e in exprs)
             self.exprs = list(exprs)
         else:
             try:
-                self.exprs = list( exprs )
+                self.exprs = list(exprs)
             except TypeError:
-                self.exprs = [ exprs ]
+                self.exprs = [exprs]
         self.callPreparse = False
 
-    def __getitem__( self, i ):
-        return self.exprs[i]
-
-    def append( self, other ):
-        self.exprs.append( other )
+    def append(self, other):
+        self.exprs.append(other)
         self.strRepr = None
         return self
 
-    def leaveWhitespace( self ):
+    def leaveWhitespace(self):
         """Extends ``leaveWhitespace`` defined in base class, and also invokes ``leaveWhitespace`` on
            all contained expressions."""
         self.skipWhitespace = False
-        self.exprs = [ e.copy() for e in self.exprs ]
+        self.exprs = [e.copy() for e in self.exprs]
         for e in self.exprs:
             e.leaveWhitespace()
         return self
 
-    def ignore( self, other ):
-        if isinstance( other, Suppress ):
+    def ignore(self, other):
+        if isinstance(other, Suppress):
             if other not in self.ignoreExprs:
-                super( ParseExpression, self).ignore( other )
+                super(ParseExpression, self).ignore(other)
                 for e in self.exprs:
-                    e.ignore( self.ignoreExprs[-1] )
+                    e.ignore(self.ignoreExprs[-1])
         else:
-            super( ParseExpression, self).ignore( other )
+            super(ParseExpression, self).ignore(other)
             for e in self.exprs:
-                e.ignore( self.ignoreExprs[-1] )
+                e.ignore(self.ignoreExprs[-1])
         return self
 
-    def __str__( self ):
+    def __str__(self):
         try:
-            return super(ParseExpression,self).__str__()
+            return super(ParseExpression, self).__str__()
         except Exception:
             pass
 
         if self.strRepr is None:
-            self.strRepr = "%s:(%s)" % ( self.__class__.__name__, _ustr(self.exprs) )
+            self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.exprs))
         return self.strRepr
 
-    def streamline( self ):
-        super(ParseExpression,self).streamline()
+    def streamline(self):
+        super(ParseExpression, self).streamline()
 
         for e in self.exprs:
             e.streamline()
 
-        # collapse nested And's of the form And( And( And( a,b), c), d) to And( a,b,c,d )
+        # collapse nested And's of the form And(And(And(a, b), c), d) to And(a, b, c, d)
         # but only if there are no parse actions or resultsNames on the nested And's
         # (likewise for Or's and MatchFirst's)
-        if ( len(self.exprs) == 2 ):
+        if len(self.exprs) == 2:
             other = self.exprs[0]
-            if ( isinstance( other, self.__class__ ) and
-                  not(other.parseAction) and
-                  other.resultsName is None and
-                  not other.debug ):
-                self.exprs = other.exprs[:] + [ self.exprs[1] ]
+            if (isinstance(other, self.__class__)
+                    and not other.parseAction
+                    and other.resultsName is None
+                    and not other.debug):
+                self.exprs = other.exprs[:] + [self.exprs[1]]
                 self.strRepr = None
                 self.mayReturnEmpty |= other.mayReturnEmpty
                 self.mayIndexError  |= other.mayIndexError
 
             other = self.exprs[-1]
-            if ( isinstance( other, self.__class__ ) and
-                  not(other.parseAction) and
-                  other.resultsName is None and
-                  not other.debug ):
+            if (isinstance(other, self.__class__)
+                    and not other.parseAction
+                    and other.resultsName is None
+                    and not other.debug):
                 self.exprs = self.exprs[:-1] + other.exprs[:]
                 self.strRepr = None
                 self.mayReturnEmpty |= other.mayReturnEmpty
@@ -3682,17 +3919,31 @@ class ParseExpression(ParserElement):
 
         return self
 
-    def validate( self, validateTrace=[] ):
-        tmp = validateTrace[:]+[self]
+    def validate(self, validateTrace=None):
+        tmp = (validateTrace if validateTrace is not None else [])[:] + [self]
         for e in self.exprs:
             e.validate(tmp)
-        self.checkRecursion( [] )
+        self.checkRecursion([])
 
     def copy(self):
-        ret = super(ParseExpression,self).copy()
+        ret = super(ParseExpression, self).copy()
         ret.exprs = [e.copy() for e in self.exprs]
         return ret
 
+    def _setResultsName(self, name, listAllMatches=False):
+        if __diag__.warn_ungrouped_named_tokens_in_collection:
+            for e in self.exprs:
+                if isinstance(e, ParserElement) and e.resultsName:
+                    warnings.warn("{0}: setting results name {1!r} on {2} expression "
+                                  "collides with {3!r} on contained expression".format("warn_ungrouped_named_tokens_in_collection",
+                                                                                       name,
+                                                                                       type(self).__name__,
+                                                                                       e.resultsName),
+                                  stacklevel=3)
+
+        return super(ParseExpression, self)._setResultsName(name, listAllMatches)
+
+
 class And(ParseExpression):
     """
     Requires all given :class:`ParseExpression` s to be found in the given order.
@@ -3706,33 +3957,56 @@ class And(ParseExpression):
         integer = Word(nums)
         name_expr = OneOrMore(Word(alphas))
 
-        expr = And([integer("id"),name_expr("name"),integer("age")])
+        expr = And([integer("id"), name_expr("name"), integer("age")])
         # more easily written as:
         expr = integer("id") + name_expr("name") + integer("age")
     """
 
     class _ErrorStop(Empty):
         def __init__(self, *args, **kwargs):
-            super(And._ErrorStop,self).__init__(*args, **kwargs)
+            super(And._ErrorStop, self).__init__(*args, **kwargs)
             self.name = '-'
             self.leaveWhitespace()
 
-    def __init__( self, exprs, savelist = True ):
-        super(And,self).__init__(exprs, savelist)
+    def __init__(self, exprs, savelist=True):
+        if exprs and Ellipsis in exprs:
+            tmp = []
+            for i, expr in enumerate(exprs):
+                if expr is Ellipsis:
+                    if i < len(exprs) - 1:
+                        skipto_arg = (Empty() + exprs[i + 1]).exprs[-1]
+                        tmp.append(SkipTo(skipto_arg)("_skipped*"))
+                    else:
+                        raise Exception("cannot construct And with sequence ending in ...")
+                else:
+                    tmp.append(expr)
+            exprs[:] = tmp
+        super(And, self).__init__(exprs, savelist)
         self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
-        self.setWhitespaceChars( self.exprs[0].whiteChars )
+        self.setWhitespaceChars(self.exprs[0].whiteChars)
         self.skipWhitespace = self.exprs[0].skipWhitespace
         self.callPreparse = True
 
     def streamline(self):
+        # collapse any _PendingSkip's
+        if any(isinstance(e, ParseExpression) and isinstance(e.exprs[-1], _PendingSkip) for e in self.exprs[:-1]):
+            for i, e in enumerate(self.exprs[:-1]):
+                if e is None:
+                    continue
+                if (isinstance(e, ParseExpression)
+                        and isinstance(e.exprs[-1], _PendingSkip)):
+                    e.exprs[-1] = e.exprs[-1] + self.exprs[i + 1]
+                    self.exprs[i + 1] = None
+            self.exprs = [e for e in self.exprs if e is not None]
+
         super(And, self).streamline()
         self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
         return self
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         # pass False as last arg to _parse for first element, since we already
         # pre-parsed the string as part of our And pre-parsing
-        loc, resultlist = self.exprs[0]._parse( instring, loc, doActions, callPreParse=False )
+        loc, resultlist = self.exprs[0]._parse(instring, loc, doActions, callPreParse=False)
         errorStop = False
         for e in self.exprs[1:]:
             if isinstance(e, And._ErrorStop):
@@ -3740,7 +4014,7 @@ class And(ParseExpression):
                 continue
             if errorStop:
                 try:
-                    loc, exprtokens = e._parse( instring, loc, doActions )
+                    loc, exprtokens = e._parse(instring, loc, doActions)
                 except ParseSyntaxException:
                     raise
                 except ParseBaseException as pe:
@@ -3749,25 +4023,25 @@ class And(ParseExpression):
                 except IndexError:
                     raise ParseSyntaxException(instring, len(instring), self.errmsg, self)
             else:
-                loc, exprtokens = e._parse( instring, loc, doActions )
+                loc, exprtokens = e._parse(instring, loc, doActions)
             if exprtokens or exprtokens.haskeys():
                 resultlist += exprtokens
         return loc, resultlist
 
-    def __iadd__(self, other ):
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        return self.append( other ) #And( [ self, other ] )
+    def __iadd__(self, other):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        return self.append(other)  # And([self, other])
 
-    def checkRecursion( self, parseElementList ):
-        subRecCheckList = parseElementList[:] + [ self ]
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [self]
         for e in self.exprs:
-            e.checkRecursion( subRecCheckList )
+            e.checkRecursion(subRecCheckList)
             if not e.mayReturnEmpty:
                 break
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -3793,8 +4067,8 @@ class Or(ParseExpression):
 
         [['123'], ['3.1416'], ['789']]
     """
-    def __init__( self, exprs, savelist = False ):
-        super(Or,self).__init__(exprs, savelist)
+    def __init__(self, exprs, savelist=False):
+        super(Or, self).__init__(exprs, savelist)
         if self.exprs:
             self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
         else:
@@ -3806,13 +4080,13 @@ class Or(ParseExpression):
             self.saveAsList = any(e.saveAsList for e in self.exprs)
         return self
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         maxExcLoc = -1
         maxException = None
         matches = []
         for e in self.exprs:
             try:
-                loc2 = e.tryParse( instring, loc )
+                loc2 = e.tryParse(instring, loc)
             except ParseException as err:
                 err.__traceback__ = None
                 if err.loc > maxExcLoc:
@@ -3820,22 +4094,39 @@ class Or(ParseExpression):
                     maxExcLoc = err.loc
             except IndexError:
                 if len(instring) > maxExcLoc:
-                    maxException = ParseException(instring,len(instring),e.errmsg,self)
+                    maxException = ParseException(instring, len(instring), e.errmsg, self)
                     maxExcLoc = len(instring)
             else:
                 # save match among all matches, to retry longest to shortest
                 matches.append((loc2, e))
 
         if matches:
-            matches.sort(key=lambda x: -x[0])
-            for _,e in matches:
+            # re-evaluate all matches in descending order of length of match, in case attached actions
+            # might change whether or how much they match of the input.
+            matches.sort(key=itemgetter(0), reverse=True)
+
+            longest = -1, None
+            for loc1, expr1 in matches:
+                if loc1 <= longest[0]:
+                    # already have a longer match than this one will deliver, we are done
+                    return longest
+
                 try:
-                    return e._parse( instring, loc, doActions )
+                    loc2, toks = expr1._parse(instring, loc, doActions)
                 except ParseException as err:
                     err.__traceback__ = None
                     if err.loc > maxExcLoc:
                         maxException = err
                         maxExcLoc = err.loc
+                else:
+                    if loc2 >= loc1:
+                        return loc2, toks
+                    # didn't match as much as before
+                    elif loc2 > longest[0]:
+                        longest = loc2, toks
+
+            if longest != (-1, None):
+                return longest
 
         if maxException is not None:
             maxException.msg = self.errmsg
@@ -3844,13 +4135,13 @@ class Or(ParseExpression):
             raise ParseException(instring, loc, "no defined alternatives to match", self)
 
 
-    def __ixor__(self, other ):
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        return self.append( other ) #Or( [ self, other ] )
+    def __ixor__(self, other):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        return self.append(other)  # Or([self, other])
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -3858,10 +4149,22 @@ class Or(ParseExpression):
 
         return self.strRepr
 
-    def checkRecursion( self, parseElementList ):
-        subRecCheckList = parseElementList[:] + [ self ]
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [self]
         for e in self.exprs:
-            e.checkRecursion( subRecCheckList )
+            e.checkRecursion(subRecCheckList)
+
+    def _setResultsName(self, name, listAllMatches=False):
+        if (not __compat__.collect_all_And_tokens
+                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, "
+                              "in future will return the full list of tokens".format(
+                    "warn_multiple_tokens_in_named_alternation", name, type(self).__name__),
+                    stacklevel=3)
+
+        return super(Or, self)._setResultsName(name, listAllMatches)
 
 
 class MatchFirst(ParseExpression):
@@ -3881,8 +4184,8 @@ class MatchFirst(ParseExpression):
         number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums)
         print(number.searchString("123 3.1416 789")) #  Better -> [['123'], ['3.1416'], ['789']]
     """
-    def __init__( self, exprs, savelist = False ):
-        super(MatchFirst,self).__init__(exprs, savelist)
+    def __init__(self, exprs, savelist=False):
+        super(MatchFirst, self).__init__(exprs, savelist)
         if self.exprs:
             self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
         else:
@@ -3894,12 +4197,12 @@ class MatchFirst(ParseExpression):
             self.saveAsList = any(e.saveAsList for e in self.exprs)
         return self
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         maxExcLoc = -1
         maxException = None
         for e in self.exprs:
             try:
-                ret = e._parse( instring, loc, doActions )
+                ret = e._parse(instring, loc, doActions)
                 return ret
             except ParseException as err:
                 if err.loc > maxExcLoc:
@@ -3907,7 +4210,7 @@ class MatchFirst(ParseExpression):
                     maxExcLoc = err.loc
             except IndexError:
                 if len(instring) > maxExcLoc:
-                    maxException = ParseException(instring,len(instring),e.errmsg,self)
+                    maxException = ParseException(instring, len(instring), e.errmsg, self)
                     maxExcLoc = len(instring)
 
         # only got here if no expression matched, raise exception for match that made it the furthest
@@ -3918,13 +4221,13 @@ class MatchFirst(ParseExpression):
             else:
                 raise ParseException(instring, loc, "no defined alternatives to match", self)
 
-    def __ior__(self, other ):
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        return self.append( other ) #MatchFirst( [ self, other ] )
+    def __ior__(self, other):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
+        return self.append(other)  # MatchFirst([self, other])
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -3932,10 +4235,22 @@ class MatchFirst(ParseExpression):
 
         return self.strRepr
 
-    def checkRecursion( self, parseElementList ):
-        subRecCheckList = parseElementList[:] + [ self ]
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [self]
         for e in self.exprs:
-            e.checkRecursion( subRecCheckList )
+            e.checkRecursion(subRecCheckList)
+
+    def _setResultsName(self, name, listAllMatches=False):
+        if (not __compat__.collect_all_And_tokens
+                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, "
+                              "in future will return the full list of tokens".format(
+                    "warn_multiple_tokens_in_named_alternation", name, type(self).__name__),
+                    stacklevel=3)
+
+        return super(MatchFirst, self)._setResultsName(name, listAllMatches)
 
 
 class Each(ParseExpression):
@@ -3995,8 +4310,8 @@ class Each(ParseExpression):
         - shape: TRIANGLE
         - size: 20
     """
-    def __init__( self, exprs, savelist = True ):
-        super(Each,self).__init__(exprs, savelist)
+    def __init__(self, exprs, savelist=True):
+        super(Each, self).__init__(exprs, savelist)
         self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
         self.skipWhitespace = True
         self.initExprGroups = True
@@ -4007,15 +4322,15 @@ class Each(ParseExpression):
         self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
         return self
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if self.initExprGroups:
-            self.opt1map = dict((id(e.expr),e) for e in self.exprs if isinstance(e,Optional))
-            opt1 = [ e.expr for e in self.exprs if isinstance(e,Optional) ]
-            opt2 = [ e for e in self.exprs if e.mayReturnEmpty and not isinstance(e,Optional)]
+            self.opt1map = dict((id(e.expr), e) for e in self.exprs if isinstance(e, Optional))
+            opt1 = [e.expr for e in self.exprs if isinstance(e, Optional)]
+            opt2 = [e for e in self.exprs if e.mayReturnEmpty and not isinstance(e, Optional)]
             self.optionals = opt1 + opt2
-            self.multioptionals = [ e.expr for e in self.exprs if isinstance(e,ZeroOrMore) ]
-            self.multirequired = [ e.expr for e in self.exprs if isinstance(e,OneOrMore) ]
-            self.required = [ e for e in self.exprs if not isinstance(e,(Optional,ZeroOrMore,OneOrMore)) ]
+            self.multioptionals = [e.expr for e in self.exprs if isinstance(e, ZeroOrMore)]
+            self.multirequired = [e.expr for e in self.exprs if isinstance(e, OneOrMore)]
+            self.required = [e for e in self.exprs if not isinstance(e, (Optional, ZeroOrMore, OneOrMore))]
             self.required += self.multirequired
             self.initExprGroups = False
         tmpLoc = loc
@@ -4029,11 +4344,11 @@ class Each(ParseExpression):
             failed = []
             for e in tmpExprs:
                 try:
-                    tmpLoc = e.tryParse( instring, tmpLoc )
+                    tmpLoc = e.tryParse(instring, tmpLoc)
                 except ParseException:
                     failed.append(e)
                 else:
-                    matchOrder.append(self.opt1map.get(id(e),e))
+                    matchOrder.append(self.opt1map.get(id(e), e))
                     if e in tmpReqd:
                         tmpReqd.remove(e)
                     elif e in tmpOpt:
@@ -4043,21 +4358,21 @@ class Each(ParseExpression):
 
         if tmpReqd:
             missing = ", ".join(_ustr(e) for e in tmpReqd)
-            raise ParseException(instring,loc,"Missing one or more required elements (%s)" % missing )
+            raise ParseException(instring, loc, "Missing one or more required elements (%s)" % missing)
 
         # add any unmatched Optionals, in case they have default values defined
-        matchOrder += [e for e in self.exprs if isinstance(e,Optional) and e.expr in tmpOpt]
+        matchOrder += [e for e in self.exprs if isinstance(e, Optional) and e.expr in tmpOpt]
 
         resultlist = []
         for e in matchOrder:
-            loc,results = e._parse(instring,loc,doActions)
+            loc, results = e._parse(instring, loc, doActions)
             resultlist.append(results)
 
         finalResults = sum(resultlist, ParseResults([]))
         return loc, finalResults
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -4065,86 +4380,88 @@ class Each(ParseExpression):
 
         return self.strRepr
 
-    def checkRecursion( self, parseElementList ):
-        subRecCheckList = parseElementList[:] + [ self ]
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [self]
         for e in self.exprs:
-            e.checkRecursion( subRecCheckList )
+            e.checkRecursion(subRecCheckList)
 
 
 class ParseElementEnhance(ParserElement):
     """Abstract subclass of :class:`ParserElement`, for combining and
     post-processing parsed tokens.
     """
-    def __init__( self, expr, savelist=False ):
-        super(ParseElementEnhance,self).__init__(savelist)
-        if isinstance( expr, basestring ):
-            if issubclass(ParserElement._literalStringClass, Token):
-                expr = ParserElement._literalStringClass(expr)
+    def __init__(self, expr, savelist=False):
+        super(ParseElementEnhance, self).__init__(savelist)
+        if isinstance(expr, basestring):
+            if issubclass(self._literalStringClass, Token):
+                expr = self._literalStringClass(expr)
             else:
-                expr = ParserElement._literalStringClass(Literal(expr))
+                expr = self._literalStringClass(Literal(expr))
         self.expr = expr
         self.strRepr = None
         if expr is not None:
             self.mayIndexError = expr.mayIndexError
             self.mayReturnEmpty = expr.mayReturnEmpty
-            self.setWhitespaceChars( expr.whiteChars )
+            self.setWhitespaceChars(expr.whiteChars)
             self.skipWhitespace = expr.skipWhitespace
             self.saveAsList = expr.saveAsList
             self.callPreparse = expr.callPreparse
             self.ignoreExprs.extend(expr.ignoreExprs)
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if self.expr is not None:
-            return self.expr._parse( instring, loc, doActions, callPreParse=False )
+            return self.expr._parse(instring, loc, doActions, callPreParse=False)
         else:
-            raise ParseException("",loc,self.errmsg,self)
+            raise ParseException("", loc, self.errmsg, self)
 
-    def leaveWhitespace( self ):
+    def leaveWhitespace(self):
         self.skipWhitespace = False
         self.expr = self.expr.copy()
         if self.expr is not None:
             self.expr.leaveWhitespace()
         return self
 
-    def ignore( self, other ):
-        if isinstance( other, Suppress ):
+    def ignore(self, other):
+        if isinstance(other, Suppress):
             if other not in self.ignoreExprs:
-                super( ParseElementEnhance, self).ignore( other )
+                super(ParseElementEnhance, self).ignore(other)
                 if self.expr is not None:
-                    self.expr.ignore( self.ignoreExprs[-1] )
+                    self.expr.ignore(self.ignoreExprs[-1])
         else:
-            super( ParseElementEnhance, self).ignore( other )
+            super(ParseElementEnhance, self).ignore(other)
             if self.expr is not None:
-                self.expr.ignore( self.ignoreExprs[-1] )
+                self.expr.ignore(self.ignoreExprs[-1])
         return self
 
-    def streamline( self ):
-        super(ParseElementEnhance,self).streamline()
+    def streamline(self):
+        super(ParseElementEnhance, self).streamline()
         if self.expr is not None:
             self.expr.streamline()
         return self
 
-    def checkRecursion( self, parseElementList ):
+    def checkRecursion(self, parseElementList):
         if self in parseElementList:
-            raise RecursiveGrammarException( parseElementList+[self] )
-        subRecCheckList = parseElementList[:] + [ self ]
+            raise RecursiveGrammarException(parseElementList + [self])
+        subRecCheckList = parseElementList[:] + [self]
         if self.expr is not None:
-            self.expr.checkRecursion( subRecCheckList )
+            self.expr.checkRecursion(subRecCheckList)
 
-    def validate( self, validateTrace=[] ):
-        tmp = validateTrace[:]+[self]
+    def validate(self, validateTrace=None):
+        if validateTrace is None:
+            validateTrace = []
+        tmp = validateTrace[:] + [self]
         if self.expr is not None:
             self.expr.validate(tmp)
-        self.checkRecursion( [] )
+        self.checkRecursion([])
 
-    def __str__( self ):
+    def __str__(self):
         try:
-            return super(ParseElementEnhance,self).__str__()
+            return super(ParseElementEnhance, self).__str__()
         except Exception:
             pass
 
         if self.strRepr is None and self.expr is not None:
-            self.strRepr = "%s:(%s)" % ( self.__class__.__name__, _ustr(self.expr) )
+            self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.expr))
         return self.strRepr
 
 
@@ -4170,13 +4487,16 @@ class FollowedBy(ParseElementEnhance):
 
         [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']]
     """
-    def __init__( self, expr ):
-        super(FollowedBy,self).__init__(expr)
+    def __init__(self, expr):
+        super(FollowedBy, self).__init__(expr)
         self.mayReturnEmpty = True
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
+        # by using self._expr.parse and deleting the contents of the returned ParseResults list
+        # we keep any named results that were defined in the FollowedBy expression
         _, ret = self.expr._parse(instring, loc, doActions=doActions)
         del ret[:]
+
         return loc, ret
 
 
@@ -4241,9 +4561,9 @@ class PrecededBy(ParseElementEnhance):
             test_expr = self.expr + StringEnd()
             instring_slice = instring[:loc]
             last_expr = ParseException(instring, loc, self.errmsg)
-            for offset in range(1, min(loc, self.retreat+1)):
+            for offset in range(1, min(loc, self.retreat + 1)):
                 try:
-                    _, ret = test_expr._parse(instring_slice, loc-offset)
+                    _, ret = test_expr._parse(instring_slice, loc - offset)
                 except ParseBaseException as pbe:
                     last_expr = pbe
                 else:
@@ -4278,20 +4598,20 @@ class NotAny(ParseElementEnhance):
         # integers that are followed by "." are actually floats
         integer = Word(nums) + ~Char(".")
     """
-    def __init__( self, expr ):
-        super(NotAny,self).__init__(expr)
-        #~ self.leaveWhitespace()
+    def __init__(self, expr):
+        super(NotAny, self).__init__(expr)
+        # ~ self.leaveWhitespace()
         self.skipWhitespace = False  # do NOT use self.leaveWhitespace(), don't want to propagate to exprs
         self.mayReturnEmpty = True
-        self.errmsg = "Found unwanted token, "+_ustr(self.expr)
+        self.errmsg = "Found unwanted token, " + _ustr(self.expr)
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         if self.expr.canParseNext(instring, loc):
             raise ParseException(instring, loc, self.errmsg, self)
         return loc, []
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -4300,15 +4620,21 @@ class NotAny(ParseElementEnhance):
         return self.strRepr
 
 class _MultipleMatch(ParseElementEnhance):
-    def __init__( self, expr, stopOn=None):
+    def __init__(self, expr, stopOn=None):
         super(_MultipleMatch, self).__init__(expr)
         self.saveAsList = True
         ender = stopOn
         if isinstance(ender, basestring):
-            ender = ParserElement._literalStringClass(ender)
+            ender = self._literalStringClass(ender)
+        self.stopOn(ender)
+
+    def stopOn(self, ender):
+        if isinstance(ender, basestring):
+            ender = self._literalStringClass(ender)
         self.not_ender = ~ender if ender is not None else None
+        return self
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         self_expr_parse = self.expr._parse
         self_skip_ignorables = self._skipIgnorables
         check_ender = self.not_ender is not None
@@ -4319,24 +4645,38 @@ class _MultipleMatch(ParseElementEnhance):
         # if so, fail)
         if check_ender:
             try_not_ender(instring, loc)
-        loc, tokens = self_expr_parse( instring, loc, doActions, callPreParse=False )
+        loc, tokens = self_expr_parse(instring, loc, doActions, callPreParse=False)
         try:
             hasIgnoreExprs = (not not self.ignoreExprs)
             while 1:
                 if check_ender:
                     try_not_ender(instring, loc)
                 if hasIgnoreExprs:
-                    preloc = self_skip_ignorables( instring, loc )
+                    preloc = self_skip_ignorables(instring, loc)
                 else:
                     preloc = loc
-                loc, tmptokens = self_expr_parse( instring, preloc, doActions )
+                loc, tmptokens = self_expr_parse(instring, preloc, doActions)
                 if tmptokens or tmptokens.haskeys():
                     tokens += tmptokens
-        except (ParseException,IndexError):
+        except (ParseException, IndexError):
             pass
 
         return loc, tokens
 
+    def _setResultsName(self, name, listAllMatches=False):
+        if __diag__.warn_ungrouped_named_tokens_in_collection:
+            for e in [self.expr] + getattr(self.expr, 'exprs', []):
+                if isinstance(e, ParserElement) and e.resultsName:
+                    warnings.warn("{0}: setting results name {1!r} on {2} expression "
+                                  "collides with {3!r} on contained expression".format("warn_ungrouped_named_tokens_in_collection",
+                                                                                       name,
+                                                                                       type(self).__name__,
+                                                                                       e.resultsName),
+                                  stacklevel=3)
+
+        return super(_MultipleMatch, self)._setResultsName(name, listAllMatches)
+
+
 class OneOrMore(_MultipleMatch):
     """Repetition of one or more of the given expression.
 
@@ -4363,8 +4703,8 @@ class OneOrMore(_MultipleMatch):
         (attr_expr * (1,)).parseString(text).pprint()
     """
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -4383,18 +4723,18 @@ class ZeroOrMore(_MultipleMatch):
 
     Example: similar to :class:`OneOrMore`
     """
-    def __init__( self, expr, stopOn=None):
-        super(ZeroOrMore,self).__init__(expr, stopOn=stopOn)
+    def __init__(self, expr, stopOn=None):
+        super(ZeroOrMore, self).__init__(expr, stopOn=stopOn)
         self.mayReturnEmpty = True
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         try:
             return super(ZeroOrMore, self).parseImpl(instring, loc, doActions)
-        except (ParseException,IndexError):
+        except (ParseException, IndexError):
             return loc, []
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -4402,6 +4742,7 @@ class ZeroOrMore(_MultipleMatch):
 
         return self.strRepr
 
+
 class _NullToken(object):
     def __bool__(self):
         return False
@@ -4409,7 +4750,6 @@ class _NullToken(object):
     def __str__(self):
         return ""
 
-_optionalNotMatched = _NullToken()
 class Optional(ParseElementEnhance):
     """Optional matching of the given expression.
 
@@ -4447,28 +4787,30 @@ class Optional(ParseElementEnhance):
              ^
         FAIL: Expected end of text (at char 5), (line:1, col:6)
     """
-    def __init__( self, expr, default=_optionalNotMatched ):
-        super(Optional,self).__init__( expr, savelist=False )
+    __optionalNotMatched = _NullToken()
+
+    def __init__(self, expr, default=__optionalNotMatched):
+        super(Optional, self).__init__(expr, savelist=False)
         self.saveAsList = self.expr.saveAsList
         self.defaultValue = default
         self.mayReturnEmpty = True
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         try:
-            loc, tokens = self.expr._parse( instring, loc, doActions, callPreParse=False )
-        except (ParseException,IndexError):
-            if self.defaultValue is not _optionalNotMatched:
+            loc, tokens = self.expr._parse(instring, loc, doActions, callPreParse=False)
+        except (ParseException, IndexError):
+            if self.defaultValue is not self.__optionalNotMatched:
                 if self.expr.resultsName:
-                    tokens = ParseResults([ self.defaultValue ])
+                    tokens = ParseResults([self.defaultValue])
                     tokens[self.expr.resultsName] = self.defaultValue
                 else:
-                    tokens = [ self.defaultValue ]
+                    tokens = [self.defaultValue]
             else:
                 tokens = []
         return loc, tokens
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
 
         if self.strRepr is None:
@@ -4534,20 +4876,20 @@ class SkipTo(ParseElementEnhance):
         - issue_num: 79
         - sev: Minor
     """
-    def __init__( self, other, include=False, ignore=None, failOn=None ):
-        super( SkipTo, self ).__init__( other )
+    def __init__(self, other, include=False, ignore=None, failOn=None):
+        super(SkipTo, self).__init__(other)
         self.ignoreExpr = ignore
         self.mayReturnEmpty = True
         self.mayIndexError = False
         self.includeMatch = include
         self.saveAsList = False
         if isinstance(failOn, basestring):
-            self.failOn = ParserElement._literalStringClass(failOn)
+            self.failOn = self._literalStringClass(failOn)
         else:
             self.failOn = failOn
-        self.errmsg = "No match found for "+_ustr(self.expr)
+        self.errmsg = "No match found for " + _ustr(self.expr)
 
-    def parseImpl( self, instring, loc, doActions=True ):
+    def parseImpl(self, instring, loc, doActions=True):
         startloc = loc
         instrlen = len(instring)
         expr = self.expr
@@ -4589,7 +4931,7 @@ class SkipTo(ParseElementEnhance):
         skipresult = ParseResults(skiptext)
 
         if self.includeMatch:
-            loc, mat = expr_parse(instring,loc,doActions,callPreParse=False)
+            loc, mat = expr_parse(instring, loc, doActions, callPreParse=False)
             skipresult += mat
 
         return loc, skipresult
@@ -4621,17 +4963,17 @@ class Forward(ParseElementEnhance):
     See :class:`ParseResults.pprint` for an example of a recursive
     parser created using ``Forward``.
     """
-    def __init__( self, other=None ):
-        super(Forward,self).__init__( other, savelist=False )
+    def __init__(self, other=None):
+        super(Forward, self).__init__(other, savelist=False)
 
-    def __lshift__( self, other ):
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass(other)
+    def __lshift__(self, other):
+        if isinstance(other, basestring):
+            other = self._literalStringClass(other)
         self.expr = other
         self.strRepr = None
         self.mayIndexError = self.expr.mayIndexError
         self.mayReturnEmpty = self.expr.mayReturnEmpty
-        self.setWhitespaceChars( self.expr.whiteChars )
+        self.setWhitespaceChars(self.expr.whiteChars)
         self.skipWhitespace = self.expr.skipWhitespace
         self.saveAsList = self.expr.saveAsList
         self.ignoreExprs.extend(self.expr.ignoreExprs)
@@ -4640,55 +4982,72 @@ class Forward(ParseElementEnhance):
     def __ilshift__(self, other):
         return self << other
 
-    def leaveWhitespace( self ):
+    def leaveWhitespace(self):
         self.skipWhitespace = False
         return self
 
-    def streamline( self ):
+    def streamline(self):
         if not self.streamlined:
             self.streamlined = True
             if self.expr is not None:
                 self.expr.streamline()
         return self
 
-    def validate( self, validateTrace=[] ):
+    def validate(self, validateTrace=None):
+        if validateTrace is None:
+            validateTrace = []
+
         if self not in validateTrace:
-            tmp = validateTrace[:]+[self]
+            tmp = validateTrace[:] + [self]
             if self.expr is not None:
                 self.expr.validate(tmp)
         self.checkRecursion([])
 
-    def __str__( self ):
-        if hasattr(self,"name"):
+    def __str__(self):
+        if hasattr(self, "name"):
             return self.name
+        if self.strRepr is not None:
+            return self.strRepr
 
-        # Avoid infinite recursion by setting a temporary name
-        self.name = self.__class__.__name__ + ": ..."
+        # Avoid infinite recursion by setting a temporary strRepr
+        self.strRepr = ": ..."
 
         # Use the string representation of main expression.
+        retString = '...'
         try:
             if self.expr is not None:
-                retString = _ustr(self.expr)
+                retString = _ustr(self.expr)[:1000]
             else:
                 retString = "None"
         finally:
-            del self.name
-        return self.__class__.__name__ + ": " + retString
+            self.strRepr = self.__class__.__name__ + ": " + retString
+        return self.strRepr
 
     def copy(self):
         if self.expr is not None:
-            return super(Forward,self).copy()
+            return super(Forward, self).copy()
         else:
             ret = Forward()
             ret <<= self
             return ret
 
+    def _setResultsName(self, name, listAllMatches=False):
+        if __diag__.warn_name_set_on_empty_Forward:
+            if self.expr is None:
+                warnings.warn("{0}: setting results name {0!r} on {1} expression "
+                              "that has no contained expression".format("warn_name_set_on_empty_Forward",
+                                                                        name,
+                                                                        type(self).__name__),
+                              stacklevel=3)
+
+        return super(Forward, self)._setResultsName(name, listAllMatches)
+
 class TokenConverter(ParseElementEnhance):
     """
     Abstract subclass of :class:`ParseExpression`, for converting parsed results.
     """
-    def __init__( self, expr, savelist=False ):
-        super(TokenConverter,self).__init__( expr )#, savelist )
+    def __init__(self, expr, savelist=False):
+        super(TokenConverter, self).__init__(expr)  # , savelist)
         self.saveAsList = False
 
 class Combine(TokenConverter):
@@ -4709,8 +5068,8 @@ class Combine(TokenConverter):
         # no match when there are internal spaces
         print(real.parseString('3. 1416')) # -> Exception: Expected W:(0123...)
     """
-    def __init__( self, expr, joinString="", adjacent=True ):
-        super(Combine,self).__init__( expr )
+    def __init__(self, expr, joinString="", adjacent=True):
+        super(Combine, self).__init__(expr)
         # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself
         if adjacent:
             self.leaveWhitespace()
@@ -4719,20 +5078,20 @@ class Combine(TokenConverter):
         self.joinString = joinString
         self.callPreparse = True
 
-    def ignore( self, other ):
+    def ignore(self, other):
         if self.adjacent:
             ParserElement.ignore(self, other)
         else:
-            super( Combine, self).ignore( other )
+            super(Combine, self).ignore(other)
         return self
 
-    def postParse( self, instring, loc, tokenlist ):
+    def postParse(self, instring, loc, tokenlist):
         retToks = tokenlist.copy()
         del retToks[:]
-        retToks += ParseResults([ "".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults)
+        retToks += ParseResults(["".join(tokenlist._asStringList(self.joinString))], modal=self.modalResults)
 
         if self.resultsName and retToks.haskeys():
-            return [ retToks ]
+            return [retToks]
         else:
             return retToks
 
@@ -4746,17 +5105,17 @@ class Group(TokenConverter):
         num = Word(nums)
         term = ident | num
         func = ident + Optional(delimitedList(term))
-        print(func.parseString("fn a,b,100"))  # -> ['fn', 'a', 'b', '100']
+        print(func.parseString("fn a, b, 100"))  # -> ['fn', 'a', 'b', '100']
 
         func = ident + Group(Optional(delimitedList(term)))
-        print(func.parseString("fn a,b,100"))  # -> ['fn', ['a', 'b', '100']]
+        print(func.parseString("fn a, b, 100"))  # -> ['fn', ['a', 'b', '100']]
     """
-    def __init__( self, expr ):
-        super(Group,self).__init__( expr )
+    def __init__(self, expr):
+        super(Group, self).__init__(expr)
         self.saveAsList = True
 
-    def postParse( self, instring, loc, tokenlist ):
-        return [ tokenlist ]
+    def postParse(self, instring, loc, tokenlist):
+        return [tokenlist]
 
 class Dict(TokenConverter):
     """Converter to return a repetitive expression as a list, but also
@@ -4797,31 +5156,31 @@ class Dict(TokenConverter):
 
     See more examples at :class:`ParseResults` of accessing fields by results name.
     """
-    def __init__( self, expr ):
-        super(Dict,self).__init__( expr )
+    def __init__(self, expr):
+        super(Dict, self).__init__(expr)
         self.saveAsList = True
 
-    def postParse( self, instring, loc, tokenlist ):
-        for i,tok in enumerate(tokenlist):
+    def postParse(self, instring, loc, tokenlist):
+        for i, tok in enumerate(tokenlist):
             if len(tok) == 0:
                 continue
             ikey = tok[0]
-            if isinstance(ikey,int):
+            if isinstance(ikey, int):
                 ikey = _ustr(tok[0]).strip()
-            if len(tok)==1:
-                tokenlist[ikey] = _ParseResultsWithOffset("",i)
-            elif len(tok)==2 and not isinstance(tok[1],ParseResults):
-                tokenlist[ikey] = _ParseResultsWithOffset(tok[1],i)
+            if len(tok) == 1:
+                tokenlist[ikey] = _ParseResultsWithOffset("", i)
+            elif len(tok) == 2 and not isinstance(tok[1], ParseResults):
+                tokenlist[ikey] = _ParseResultsWithOffset(tok[1], i)
             else:
-                dictvalue = tok.copy() #ParseResults(i)
+                dictvalue = tok.copy()  # ParseResults(i)
                 del dictvalue[0]
-                if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.haskeys()):
-                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i)
+                if len(dictvalue) != 1 or (isinstance(dictvalue, ParseResults) and dictvalue.haskeys()):
+                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue, i)
                 else:
-                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i)
+                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0], i)
 
         if self.resultsName:
-            return [ tokenlist ]
+            return [tokenlist]
         else:
             return tokenlist
 
@@ -4848,10 +5207,10 @@ class Suppress(TokenConverter):
 
     (See also :class:`delimitedList`.)
     """
-    def postParse( self, instring, loc, tokenlist ):
+    def postParse(self, instring, loc, tokenlist):
         return []
 
-    def suppress( self ):
+    def suppress(self):
         return self
 
 
@@ -4861,12 +5220,12 @@ class OnlyOnce(object):
     def __init__(self, methodCall):
         self.callable = _trim_arity(methodCall)
         self.called = False
-    def __call__(self,s,l,t):
+    def __call__(self, s, l, t):
         if not self.called:
-            results = self.callable(s,l,t)
+            results = self.callable(s, l, t)
             self.called = True
             return results
-        raise ParseException(s,l,"")
+        raise ParseException(s, l, "")
     def reset(self):
         self.called = False
 
@@ -4898,16 +5257,16 @@ def traceParseAction(f):
     f = _trim_arity(f)
     def z(*paArgs):
         thisFunc = f.__name__
-        s,l,t = paArgs[-3:]
-        if len(paArgs)>3:
+        s, l, t = paArgs[-3:]
+        if len(paArgs) > 3:
             thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc
-        sys.stderr.write( ">>entering %s(line: '%s', %d, %r)\n" % (thisFunc,line(l,s),l,t) )
+        sys.stderr.write(">>entering %s(line: '%s', %d, %r)\n" % (thisFunc, line(l, s), l, t))
         try:
             ret = f(*paArgs)
         except Exception as exc:
-            sys.stderr.write( "<<leaving %s (exception: %s)\n" % (thisFunc,exc) )
+            sys.stderr.write("<<leaving %s (exception: %s)\n" % (thisFunc, exc))
             raise
-        sys.stderr.write( "<<leaving %s (ret: %r)\n" % (thisFunc,ret) )
+        sys.stderr.write("<<leaving %s (ret: %r)\n" % (thisFunc, ret))
         return ret
     try:
         z.__name__ = f.__name__
@@ -4918,7 +5277,7 @@ def traceParseAction(f):
 #
 # global helpers
 #
-def delimitedList( expr, delim=",", combine=False ):
+def delimitedList(expr, delim=",", combine=False):
     """Helper to define a delimited list of expressions - the delimiter
     defaults to ','. By default, the list elements and delimiters can
     have intervening whitespace, and comments, but this can be
@@ -4933,13 +5292,13 @@ def delimitedList( expr, delim=",", combine=False ):
         delimitedList(Word(alphas)).parseString("aa,bb,cc") # -> ['aa', 'bb', 'cc']
         delimitedList(Word(hexnums), delim=':', combine=True).parseString("AA:BB:CC:DD:EE") # -> ['AA:BB:CC:DD:EE']
     """
-    dlName = _ustr(expr)+" ["+_ustr(delim)+" "+_ustr(expr)+"]..."
+    dlName = _ustr(expr) + " [" + _ustr(delim) + " " + _ustr(expr) + "]..."
     if combine:
-        return Combine( expr + ZeroOrMore( delim + expr ) ).setName(dlName)
+        return Combine(expr + ZeroOrMore(delim + expr)).setName(dlName)
     else:
-        return ( expr + ZeroOrMore( Suppress( delim ) + expr ) ).setName(dlName)
+        return (expr + ZeroOrMore(Suppress(delim) + expr)).setName(dlName)
 
-def countedArray( expr, intExpr=None ):
+def countedArray(expr, intExpr=None):
     """Helper to define a counted list of expressions.
 
     This helper defines a pattern of the form::
@@ -4963,22 +5322,22 @@ def countedArray( expr, intExpr=None ):
         countedArray(Word(alphas), intExpr=binaryConstant).parseString('10 ab cd ef')  # -> ['ab', 'cd']
     """
     arrayExpr = Forward()
-    def countFieldParseAction(s,l,t):
+    def countFieldParseAction(s, l, t):
         n = t[0]
-        arrayExpr << (n and Group(And([expr]*n)) or Group(empty))
+        arrayExpr << (n and Group(And([expr] * n)) or Group(empty))
         return []
     if intExpr is None:
-        intExpr = Word(nums).setParseAction(lambda t:int(t[0]))
+        intExpr = Word(nums).setParseAction(lambda t: int(t[0]))
     else:
         intExpr = intExpr.copy()
     intExpr.setName("arrayLen")
     intExpr.addParseAction(countFieldParseAction, callDuringTry=True)
-    return ( intExpr + arrayExpr ).setName('(len) ' + _ustr(expr) + '...')
+    return (intExpr + arrayExpr).setName('(len) ' + _ustr(expr) + '...')
 
 def _flatten(L):
     ret = []
     for i in L:
-        if isinstance(i,list):
+        if isinstance(i, list):
             ret.extend(_flatten(i))
         else:
             ret.append(i)
@@ -5000,7 +5359,7 @@ def matchPreviousLiteral(expr):
     enabled.
     """
     rep = Forward()
-    def copyTokenToRepeater(s,l,t):
+    def copyTokenToRepeater(s, l, t):
         if t:
             if len(t) == 1:
                 rep << t[0]
@@ -5032,26 +5391,26 @@ def matchPreviousExpr(expr):
     rep = Forward()
     e2 = expr.copy()
     rep <<= e2
-    def copyTokenToRepeater(s,l,t):
+    def copyTokenToRepeater(s, l, t):
         matchTokens = _flatten(t.asList())
-        def mustMatchTheseTokens(s,l,t):
+        def mustMatchTheseTokens(s, l, t):
             theseTokens = _flatten(t.asList())
-            if  theseTokens != matchTokens:
-                raise ParseException("",0,"")
-        rep.setParseAction( mustMatchTheseTokens, callDuringTry=True )
+            if theseTokens != matchTokens:
+                raise ParseException('', 0, '')
+        rep.setParseAction(mustMatchTheseTokens, callDuringTry=True)
     expr.addParseAction(copyTokenToRepeater, callDuringTry=True)
     rep.setName('(prev) ' + _ustr(expr))
     return rep
 
 def _escapeRegexRangeChars(s):
-    #~  escape these chars: ^-]
+    # ~  escape these chars: ^-]
     for c in r"\^-]":
-        s = s.replace(c,_bslash+c)
-    s = s.replace("\n",r"\n")
-    s = s.replace("\t",r"\t")
+        s = s.replace(c, _bslash + c)
+    s = s.replace("\n", r"\n")
+    s = s.replace("\t", r"\t")
     return _ustr(s)
 
-def oneOf( strs, caseless=False, useRegex=True ):
+def oneOf(strs, caseless=False, useRegex=True, asKeyword=False):
     """Helper to quickly define a set of alternative Literals, and makes
     sure to do longest-first testing when there is a conflict,
     regardless of the input order, but returns
@@ -5065,8 +5424,10 @@ def oneOf( strs, caseless=False, useRegex=True ):
        caseless
      - useRegex - (default= ``True``) - as an optimization, will
        generate a Regex object; otherwise, will generate
-       a :class:`MatchFirst` object (if ``caseless=True``, or if
+       a :class:`MatchFirst` object (if ``caseless=True`` or ``asKeyword=True``, or if
        creating a :class:`Regex` raises an exception)
+     - asKeyword - (default=``False``) - enforce Keyword-style matching on the
+       generated expressions
 
     Example::
 
@@ -5081,57 +5442,62 @@ def oneOf( strs, caseless=False, useRegex=True ):
 
         [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]
     """
+    if isinstance(caseless, basestring):
+        warnings.warn("More than one string argument passed to oneOf, pass "
+                      "choices as a list or space-delimited string", stacklevel=2)
+
     if caseless:
-        isequal = ( lambda a,b: a.upper() == b.upper() )
-        masks = ( lambda a,b: b.upper().startswith(a.upper()) )
-        parseElementClass = CaselessLiteral
+        isequal = (lambda a, b: a.upper() == b.upper())
+        masks = (lambda a, b: b.upper().startswith(a.upper()))
+        parseElementClass = CaselessKeyword if asKeyword else CaselessLiteral
     else:
-        isequal = ( lambda a,b: a == b )
-        masks = ( lambda a,b: b.startswith(a) )
-        parseElementClass = Literal
+        isequal = (lambda a, b: a == b)
+        masks = (lambda a, b: b.startswith(a))
+        parseElementClass = Keyword if asKeyword else Literal
 
     symbols = []
-    if isinstance(strs,basestring):
+    if isinstance(strs, basestring):
         symbols = strs.split()
     elif isinstance(strs, Iterable):
         symbols = list(strs)
     else:
         warnings.warn("Invalid argument to oneOf, expected string or iterable",
-                SyntaxWarning, stacklevel=2)
+                      SyntaxWarning, stacklevel=2)
     if not symbols:
         return NoMatch()
 
-    i = 0
-    while i < len(symbols)-1:
-        cur = symbols[i]
-        for j,other in enumerate(symbols[i+1:]):
-            if ( isequal(other, cur) ):
-                del symbols[i+j+1]
-                break
-            elif ( masks(cur, other) ):
-                del symbols[i+j+1]
-                symbols.insert(i,other)
-                cur = other
-                break
-        else:
-            i += 1
+    if not asKeyword:
+        # if not producing keywords, need to reorder to take care to avoid masking
+        # longer choices with shorter ones
+        i = 0
+        while i < len(symbols) - 1:
+            cur = symbols[i]
+            for j, other in enumerate(symbols[i + 1:]):
+                if isequal(other, cur):
+                    del symbols[i + j + 1]
+                    break
+                elif masks(cur, other):
+                    del symbols[i + j + 1]
+                    symbols.insert(i, other)
+                    break
+            else:
+                i += 1
 
-    if not caseless and useRegex:
-        #~ print (strs,"->", "|".join( [ _escapeRegexChars(sym) for sym in symbols] ))
+    if not (caseless or asKeyword) and useRegex:
+        # ~ print (strs, "->", "|".join([_escapeRegexChars(sym) for sym in symbols]))
         try:
-            if len(symbols)==len("".join(symbols)):
-                return Regex( "[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols) ).setName(' | '.join(symbols))
+            if len(symbols) == len("".join(symbols)):
+                return Regex("[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols)).setName(' | '.join(symbols))
             else:
-                return Regex( "|".join(re.escape(sym) for sym in symbols) ).setName(' | '.join(symbols))
+                return Regex("|".join(re.escape(sym) for sym in symbols)).setName(' | '.join(symbols))
         except Exception:
             warnings.warn("Exception creating Regex for oneOf, building MatchFirst",
                     SyntaxWarning, stacklevel=2)
 
-
     # last resort, just use MatchFirst
     return MatchFirst(parseElementClass(sym) for sym in symbols).setName(' | '.join(symbols))
 
-def dictOf( key, value ):
+def dictOf(key, value):
     """Helper to easily and clearly define a dictionary by specifying
     the respective patterns for the key and value.  Takes care of
     defining the :class:`Dict`, :class:`ZeroOrMore`, and
@@ -5189,8 +5555,8 @@ def originalTextFor(expr, asString=True):
     Example::
 
         src = "this is test <b> bold <i>text</i> </b> normal text "
-        for tag in ("b","i"):
-            opener,closer = makeHTMLTags(tag)
+        for tag in ("b", "i"):
+            opener, closer = makeHTMLTags(tag)
             patt = originalTextFor(opener + SkipTo(closer) + closer)
             print(patt.searchString(src)[0])
 
@@ -5199,14 +5565,14 @@ def originalTextFor(expr, asString=True):
         ['<b> bold <i>text</i> </b>']
         ['<i>text</i>']
     """
-    locMarker = Empty().setParseAction(lambda s,loc,t: loc)
+    locMarker = Empty().setParseAction(lambda s, loc, t: loc)
     endlocMarker = locMarker.copy()
     endlocMarker.callPreparse = False
     matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")
     if asString:
-        extractText = lambda s,l,t: s[t._original_start:t._original_end]
+        extractText = lambda s, l, t: s[t._original_start: t._original_end]
     else:
-        def extractText(s,l,t):
+        def extractText(s, l, t):
             t[:] = [s[t.pop('_original_start'):t.pop('_original_end')]]
     matchExpr.setParseAction(extractText)
     matchExpr.ignoreExprs = expr.ignoreExprs
@@ -5216,7 +5582,7 @@ def ungroup(expr):
     """Helper to undo pyparsing's default grouping of And expressions,
     even if all but one are non-empty.
     """
-    return TokenConverter(expr).addParseAction(lambda t:t[0])
+    return TokenConverter(expr).addParseAction(lambda t: t[0])
 
 def locatedExpr(expr):
     """Helper to decorate a returned token with its starting and ending
@@ -5243,7 +5609,7 @@ def locatedExpr(expr):
         [[8, 'lksdjjf', 15]]
         [[18, 'lkkjj', 23]]
     """
-    locator = Empty().setParseAction(lambda s,l,t: l)
+    locator = Empty().setParseAction(lambda s, l, t: l)
     return Group(locator("locn_start") + expr("value") + locator.copy().leaveWhitespace()("locn_end"))
 
 
@@ -5254,12 +5620,12 @@ lineEnd     = LineEnd().setName("lineEnd")
 stringStart = StringStart().setName("stringStart")
 stringEnd   = StringEnd().setName("stringEnd")
 
-_escapedPunc = Word( _bslash, r"\[]-*.$+^?()~ ", exact=2 ).setParseAction(lambda s,l,t:t[0][1])
-_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\0x'),16)))
-_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8)))
+_escapedPunc = Word(_bslash, r"\[]-*.$+^?()~ ", exact=2).setParseAction(lambda s, l, t: t[0][1])
+_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s, l, t: unichr(int(t[0].lstrip(r'\0x'), 16)))
+_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s, l, t: unichr(int(t[0][1:], 8)))
 _singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r'\]', exact=1)
 _charRange = Group(_singleChar + Suppress("-") + _singleChar)
-_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName("body") + "]"
+_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group(OneOrMore(_charRange | _singleChar)).setResultsName("body") + "]"
 
 def srange(s):
     r"""Helper to easily define string ranges for use in Word
@@ -5287,7 +5653,7 @@ def srange(s):
      - any combination of the above (``'aeiouy'``,
        ``'a-zA-Z0-9_$'``, etc.)
     """
-    _expanded = lambda p: p if not isinstance(p,ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1))
+    _expanded = lambda p: p if not isinstance(p, ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]), ord(p[1]) + 1))
     try:
         return "".join(_expanded(part) for part in _reBracketExpr.parseString(s).body)
     except Exception:
@@ -5297,9 +5663,9 @@ def matchOnlyAtCol(n):
     """Helper method for defining parse actions that require matching at
     a specific column in the input text.
     """
-    def verifyCol(strg,locn,toks):
-        if col(locn,strg) != n:
-            raise ParseException(strg,locn,"matched token not at column %d" % n)
+    def verifyCol(strg, locn, toks):
+        if col(locn, strg) != n:
+            raise ParseException(strg, locn, "matched token not at column %d" % n)
     return verifyCol
 
 def replaceWith(replStr):
@@ -5315,9 +5681,9 @@ def replaceWith(replStr):
 
         OneOrMore(term).parseString("324 234 N/A 234") # -> [324, 234, nan, 234]
     """
-    return lambda s,l,t: [replStr]
+    return lambda s, l, t: [replStr]
 
-def removeQuotes(s,l,t):
+def removeQuotes(s, l, t):
     """Helper parse action for removing quotation marks from parsed
     quoted strings.
 
@@ -5368,7 +5734,7 @@ def tokenMap(func, *args):
         now is the winter of our discontent made glorious summer by this sun of york
         ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York']
     """
-    def pa(s,l,t):
+    def pa(s, l, t):
         return [func(tokn, *args) for tokn in t]
 
     try:
@@ -5392,34 +5758,34 @@ def _makeTags(tagStr, xml,
               suppress_LT=Suppress("<"),
               suppress_GT=Suppress(">")):
     """Internal helper to construct opening and closing tag expressions, given a tag name"""
-    if isinstance(tagStr,basestring):
+    if isinstance(tagStr, basestring):
         resname = tagStr
         tagStr = Keyword(tagStr, caseless=not xml)
     else:
         resname = tagStr.name
 
-    tagAttrName = Word(alphas,alphanums+"_-:")
-    if (xml):
-        tagAttrValue = dblQuotedString.copy().setParseAction( removeQuotes )
+    tagAttrName = Word(alphas, alphanums + "_-:")
+    if xml:
+        tagAttrValue = dblQuotedString.copy().setParseAction(removeQuotes)
         openTag = (suppress_LT
                    + tagStr("tag")
-                   + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue )))
-                   + Optional("/", default=[False])("empty").setParseAction(lambda s,l,t:t[0]=='/')
+                   + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue)))
+                   + Optional("/", default=[False])("empty").setParseAction(lambda s, l, t: t[0] == '/')
                    + suppress_GT)
     else:
-        tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printables, excludeChars=">")
+        tagAttrValue = quotedString.copy().setParseAction(removeQuotes) | Word(printables, excludeChars=">")
         openTag = (suppress_LT
                    + tagStr("tag")
                    + Dict(ZeroOrMore(Group(tagAttrName.setParseAction(downcaseTokens)
                                            + Optional(Suppress("=") + tagAttrValue))))
-                   + Optional("/",default=[False])("empty").setParseAction(lambda s,l,t:t[0]=='/')
+                   + Optional("/", default=[False])("empty").setParseAction(lambda s, l, t: t[0] == '/')
                    + suppress_GT)
     closeTag = Combine(_L("</") + tagStr + ">", adjacent=False)
 
     openTag.setName("<%s>" % resname)
     # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels
-    openTag.addParseAction(lambda t: t.__setitem__("start"+"".join(resname.replace(":"," ").title().split()), t.copy()))
-    closeTag = closeTag("end"+"".join(resname.replace(":"," ").title().split())).setName("</%s>" % resname)
+    openTag.addParseAction(lambda t: t.__setitem__("start" + "".join(resname.replace(":", " ").title().split()), t.copy()))
+    closeTag = closeTag("end" + "".join(resname.replace(":", " ").title().split())).setName("</%s>" % resname)
     openTag.tag = resname
     closeTag.tag = resname
     openTag.tag_body = SkipTo(closeTag())
@@ -5435,7 +5801,7 @@ def makeHTMLTags(tagStr):
         text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
         # makeHTMLTags returns pyparsing expressions for the opening and
         # closing tags as a 2-tuple
-        a,a_end = makeHTMLTags("A")
+        a, a_end = makeHTMLTags("A")
         link_expr = a + SkipTo(a_end)("link_text") + a_end
 
         for link in link_expr.searchString(text):
@@ -5447,7 +5813,7 @@ def makeHTMLTags(tagStr):
 
         pyparsing -> https://github.com/pyparsing/pyparsing/wiki
     """
-    return _makeTags( tagStr, False )
+    return _makeTags(tagStr, False)
 
 def makeXMLTags(tagStr):
     """Helper to construct opening and closing tag expressions for XML,
@@ -5455,9 +5821,9 @@ def makeXMLTags(tagStr):
 
     Example: similar to :class:`makeHTMLTags`
     """
-    return _makeTags( tagStr, True )
+    return _makeTags(tagStr, True)
 
-def withAttribute(*args,**attrDict):
+def withAttribute(*args, **attrDict):
     """Helper to create a validating parse action to be used with start
     tags created with :class:`makeXMLTags` or
     :class:`makeHTMLTags`. Use ``withAttribute`` to qualify
@@ -5470,7 +5836,7 @@ def withAttribute(*args,**attrDict):
      - keyword arguments, as in ``(align="right")``, or
      - as an explicit dict with ``**`` operator, when an attribute
        name is also a Python reserved word, as in ``**{"class":"Customer", "align":"right"}``
-     - a list of name-value tuples, as in ``(("ns1:class", "Customer"), ("ns2:align","right"))``
+     - a list of name-value tuples, as in ``(("ns1:class", "Customer"), ("ns2:align", "right"))``
 
     For attribute names with a namespace prefix, you must use the second
     form.  Attribute names are matched insensitive to upper/lower case.
@@ -5517,13 +5883,13 @@ def withAttribute(*args,**attrDict):
         attrs = args[:]
     else:
         attrs = attrDict.items()
-    attrs = [(k,v) for k,v in attrs]
-    def pa(s,l,tokens):
-        for attrName,attrValue in attrs:
+    attrs = [(k, v) for k, v in attrs]
+    def pa(s, l, tokens):
+        for attrName, attrValue in attrs:
             if attrName not in tokens:
-                raise ParseException(s,l,"no matching attribute " + attrName)
+                raise ParseException(s, l, "no matching attribute " + attrName)
             if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue:
-                raise ParseException(s,l,"attribute '%s' has value '%s', must be '%s'" %
+                raise ParseException(s, l, "attribute '%s' has value '%s', must be '%s'" %
                                             (attrName, tokens[attrName], attrValue))
     return pa
 withAttribute.ANY_VALUE = object()
@@ -5564,13 +5930,13 @@ def withClass(classname, namespace=''):
         1,3 2,3 1,1
     """
     classattr = "%s:class" % namespace if namespace else "class"
-    return withAttribute(**{classattr : classname})
+    return withAttribute(**{classattr: classname})
 
 opAssoc = SimpleNamespace()
 opAssoc.LEFT = object()
 opAssoc.RIGHT = object()
 
-def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):
+def infixNotation(baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')')):
     """Helper method for constructing grammars of expressions made up of
     operators working in a precedence hierarchy.  Operators may be unary
     or binary, left- or right-associative.  Parse actions can also be
@@ -5648,9 +6014,9 @@ def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):
             return loc, []
 
     ret = Forward()
-    lastExpr = baseExpr | ( lpar + ret + rpar )
-    for i,operDef in enumerate(opList):
-        opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4]
+    lastExpr = baseExpr | (lpar + ret + rpar)
+    for i, operDef in enumerate(opList):
+        opExpr, arity, rightLeftAssoc, pa = (operDef + (None, ))[:4]
         termName = "%s term" % opExpr if arity < 3 else "%s%s term" % opExpr
         if arity == 3:
             if opExpr is None or len(opExpr) != 2:
@@ -5660,15 +6026,15 @@ def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):
         thisExpr = Forward().setName(termName)
         if rightLeftAssoc == opAssoc.LEFT:
             if arity == 1:
-                matchExpr = _FB(lastExpr + opExpr) + Group( lastExpr + OneOrMore( opExpr ) )
+                matchExpr = _FB(lastExpr + opExpr) + Group(lastExpr + OneOrMore(opExpr))
             elif arity == 2:
                 if opExpr is not None:
-                    matchExpr = _FB(lastExpr + opExpr + lastExpr) + Group( lastExpr + OneOrMore( opExpr + lastExpr ) )
+                    matchExpr = _FB(lastExpr + opExpr + lastExpr) + Group(lastExpr + OneOrMore(opExpr + lastExpr))
                 else:
-                    matchExpr = _FB(lastExpr+lastExpr) + Group( lastExpr + OneOrMore(lastExpr) )
+                    matchExpr = _FB(lastExpr + lastExpr) + Group(lastExpr + OneOrMore(lastExpr))
             elif arity == 3:
-                matchExpr = _FB(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + \
-                            Group( lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr )
+                matchExpr = (_FB(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr)
+                             + Group(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr))
             else:
                 raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
         elif rightLeftAssoc == opAssoc.RIGHT:
@@ -5676,15 +6042,15 @@ def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):
                 # try to avoid LR with this extra test
                 if not isinstance(opExpr, Optional):
                     opExpr = Optional(opExpr)
-                matchExpr = _FB(opExpr.expr + thisExpr) + Group( opExpr + thisExpr )
+                matchExpr = _FB(opExpr.expr + thisExpr) + Group(opExpr + thisExpr)
             elif arity == 2:
                 if opExpr is not None:
-                    matchExpr = _FB(lastExpr + opExpr + thisExpr) + Group( lastExpr + OneOrMore( opExpr + thisExpr ) )
+                    matchExpr = _FB(lastExpr + opExpr + thisExpr) + Group(lastExpr + OneOrMore(opExpr + thisExpr))
                 else:
-                    matchExpr = _FB(lastExpr + thisExpr) + Group( lastExpr + OneOrMore( thisExpr ) )
+                    matchExpr = _FB(lastExpr + thisExpr) + Group(lastExpr + OneOrMore(thisExpr))
             elif arity == 3:
-                matchExpr = _FB(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + \
-                            Group( lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr )
+                matchExpr = (_FB(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)
+                             + Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr))
             else:
                 raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
         else:
@@ -5694,7 +6060,7 @@ def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):
                 matchExpr.setParseAction(*pa)
             else:
                 matchExpr.setParseAction(pa)
-        thisExpr <<= ( matchExpr.setName(termName) | lastExpr )
+        thisExpr <<= (matchExpr.setName(termName) | lastExpr)
         lastExpr = thisExpr
     ret <<= lastExpr
     return ret
@@ -5703,10 +6069,10 @@ operatorPrecedence = infixNotation
 """(Deprecated) Former name of :class:`infixNotation`, will be
 dropped in a future release."""
 
-dblQuotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"').setName("string enclosed in double quotes")
-sglQuotedString = Combine(Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("string enclosed in single quotes")
-quotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"'|
-                       Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("quotedString using single or double quotes")
+dblQuotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"').setName("string enclosed in double quotes")
+sglQuotedString = Combine(Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'").setName("string enclosed in single quotes")
+quotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"'
+                       | Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'").setName("quotedString using single or double quotes")
 unicodeString = Combine(_L('u') + quotedString.copy()).setName("unicode string literal")
 
 def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.copy()):
@@ -5742,7 +6108,7 @@ def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.cop
         ident = Word(alphas+'_', alphanums+'_')
         number = pyparsing_common.number
         arg = Group(decl_data_type + ident)
-        LPAR,RPAR = map(Suppress, "()")
+        LPAR, RPAR = map(Suppress, "()")
 
         code_body = nestedExpr('{', '}', ignoreExpr=(quotedString | cStyleComment))
 
@@ -5777,33 +6143,40 @@ def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.cop
     if opener == closer:
         raise ValueError("opening and closing strings cannot be the same")
     if content is None:
-        if isinstance(opener,basestring) and isinstance(closer,basestring):
-            if len(opener) == 1 and len(closer)==1:
+        if isinstance(opener, basestring) and isinstance(closer, basestring):
+            if len(opener) == 1 and len(closer) == 1:
                 if ignoreExpr is not None:
-                    content = (Combine(OneOrMore(~ignoreExpr +
-                                    CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS,exact=1))
-                                ).setParseAction(lambda t:t[0].strip()))
+                    content = (Combine(OneOrMore(~ignoreExpr
+                                                 + CharsNotIn(opener
+                                                              + closer
+                                                              + ParserElement.DEFAULT_WHITE_CHARS, exact=1)
+                                                 )
+                                       ).setParseAction(lambda t: t[0].strip()))
                 else:
-                    content = (empty.copy()+CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS
-                                ).setParseAction(lambda t:t[0].strip()))
+                    content = (empty.copy() + CharsNotIn(opener
+                                                         + closer
+                                                         + ParserElement.DEFAULT_WHITE_CHARS
+                                                         ).setParseAction(lambda t: t[0].strip()))
             else:
                 if ignoreExpr is not None:
-                    content = (Combine(OneOrMore(~ignoreExpr +
-                                    ~Literal(opener) + ~Literal(closer) +
-                                    CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))
-                                ).setParseAction(lambda t:t[0].strip()))
+                    content = (Combine(OneOrMore(~ignoreExpr
+                                                 + ~Literal(opener)
+                                                 + ~Literal(closer)
+                                                 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1))
+                                       ).setParseAction(lambda t: t[0].strip()))
                 else:
-                    content = (Combine(OneOrMore(~Literal(opener) + ~Literal(closer) +
-                                    CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))
-                                ).setParseAction(lambda t:t[0].strip()))
+                    content = (Combine(OneOrMore(~Literal(opener)
+                                                 + ~Literal(closer)
+                                                 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1))
+                                       ).setParseAction(lambda t: t[0].strip()))
         else:
             raise ValueError("opening and closing arguments must be strings if no content expression is given")
     ret = Forward()
     if ignoreExpr is not None:
-        ret <<= Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) )
+        ret <<= Group(Suppress(opener) + ZeroOrMore(ignoreExpr | ret | content) + Suppress(closer))
     else:
-        ret <<= Group( Suppress(opener) + ZeroOrMore( ret | content )  + Suppress(closer) )
-    ret.setName('nested %s%s expression' % (opener,closer))
+        ret <<= Group(Suppress(opener) + ZeroOrMore(ret | content)  + Suppress(closer))
+    ret.setName('nested %s%s expression' % (opener, closer))
     return ret
 
 def indentedBlock(blockStatementExpr, indentStack, indent=True):
@@ -5818,7 +6191,7 @@ def indentedBlock(blockStatementExpr, indentStack, indent=True):
        (multiple statementWithIndentedBlock expressions within a single
        grammar should share a common indentStack)
      - indent - boolean indicating whether block must be indented beyond
-       the the current level; set to False for block of left-most
+       the current level; set to False for block of left-most
        statements (default= ``True``)
 
     A valid block must contain at least one ``blockStatement``.
@@ -5851,15 +6224,15 @@ def indentedBlock(blockStatementExpr, indentStack, indent=True):
         stmt = Forward()
 
         identifier = Word(alphas, alphanums)
-        funcDecl = ("def" + identifier + Group( "(" + Optional( delimitedList(identifier) ) + ")" ) + ":")
+        funcDecl = ("def" + identifier + Group("(" + Optional(delimitedList(identifier)) + ")") + ":")
         func_body = indentedBlock(stmt, indentStack)
-        funcDef = Group( funcDecl + func_body )
+        funcDef = Group(funcDecl + func_body)
 
         rvalue = Forward()
         funcCall = Group(identifier + "(" + Optional(delimitedList(rvalue)) + ")")
         rvalue << (funcCall | identifier | Word(nums))
         assignment = Group(identifier + "=" + rvalue)
-        stmt << ( funcDef | assignment | identifier )
+        stmt << (funcDef | assignment | identifier)
 
         module_body = OneOrMore(stmt)
 
@@ -5892,39 +6265,42 @@ def indentedBlock(blockStatementExpr, indentStack, indent=True):
     def reset_stack():
         indentStack[:] = backup_stack
 
-    def checkPeerIndent(s,l,t):
+    def checkPeerIndent(s, l, t):
         if l >= len(s): return
-        curCol = col(l,s)
+        curCol = col(l, s)
         if curCol != indentStack[-1]:
             if curCol > indentStack[-1]:
-                raise ParseException(s,l,"illegal nesting")
-            raise ParseException(s,l,"not a peer entry")
+                raise ParseException(s, l, "illegal nesting")
+            raise ParseException(s, l, "not a peer entry")
 
-    def checkSubIndent(s,l,t):
-        curCol = col(l,s)
+    def checkSubIndent(s, l, t):
+        curCol = col(l, s)
         if curCol > indentStack[-1]:
-            indentStack.append( curCol )
+            indentStack.append(curCol)
         else:
-            raise ParseException(s,l,"not a subentry")
+            raise ParseException(s, l, "not a subentry")
 
-    def checkUnindent(s,l,t):
+    def checkUnindent(s, l, t):
         if l >= len(s): return
-        curCol = col(l,s)
-        if not(indentStack and curCol < indentStack[-1] and curCol <= indentStack[-2]):
-            raise ParseException(s,l,"not an unindent")
-        indentStack.pop()
+        curCol = col(l, s)
+        if not(indentStack and curCol in indentStack):
+            raise ParseException(s, l, "not an unindent")
+        if curCol < indentStack[-1]:
+            indentStack.pop()
 
     NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress())
     INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT')
     PEER   = Empty().setParseAction(checkPeerIndent).setName('')
     UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT')
     if indent:
-        smExpr = Group( Optional(NL) +
-            #~ FollowedBy(blockStatementExpr) +
-            INDENT + (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) + UNDENT)
+        smExpr = Group(Optional(NL)
+                       + INDENT
+                       + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL))
+                       + UNDENT)
     else:
-        smExpr = Group( Optional(NL) +
-            (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) )
+        smExpr = Group(Optional(NL)
+                       + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL))
+                       + UNDENT)
     smExpr.setFailAction(lambda a, b, c, d: reset_stack())
     blockStatementExpr.ignore(_bslash + LineEnd())
     return smExpr.setName('indented block')
@@ -5932,8 +6308,8 @@ def indentedBlock(blockStatementExpr, indentStack, indent=True):
 alphas8bit = srange(r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]")
 punc8bit = srange(r"[\0xa1-\0xbf\0xd7\0xf7]")
 
-anyOpenTag,anyCloseTag = makeHTMLTags(Word(alphas,alphanums+"_:").setName('any tag'))
-_htmlEntityMap = dict(zip("gt lt amp nbsp quot apos".split(),'><& "\''))
+anyOpenTag, anyCloseTag = makeHTMLTags(Word(alphas, alphanums + "_:").setName('any tag'))
+_htmlEntityMap = dict(zip("gt lt amp nbsp quot apos".split(), '><& "\''))
 commonHTMLEntity = Regex('&(?P<entity>' + '|'.join(_htmlEntityMap.keys()) +");").setName("common HTML entity")
 def replaceHTMLEntity(t):
     """Helper parser action to replace common HTML entities with their special characters"""
@@ -5950,7 +6326,7 @@ restOfLine = Regex(r".*").leaveWhitespace().setName("rest of line")
 dblSlashComment = Regex(r"//(?:\\\n|[^\n])*").setName("// comment")
 "Comment of the form ``// ... (to end of line)``"
 
-cppStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/'| dblSlashComment).setName("C++ style comment")
+cppStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/' | dblSlashComment).setName("C++ style comment")
 "Comment of either form :class:`cStyleComment` or :class:`dblSlashComment`"
 
 javaStyleComment = cppStyleComment
@@ -5959,10 +6335,10 @@ javaStyleComment = cppStyleComment
 pythonStyleComment = Regex(r"#.*").setName("Python style comment")
 "Comment of the form ``# ... (to end of line)``"
 
-_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') +
-                                  Optional( Word(" \t") +
-                                            ~Literal(",") + ~LineEnd() ) ) ).streamline().setName("commaItem")
-commaSeparatedList = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("commaSeparatedList")
+_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',')
+                                  + Optional(Word(" \t")
+                                             + ~Literal(",") + ~LineEnd()))).streamline().setName("commaItem")
+commaSeparatedList = delimitedList(Optional(quotedString.copy() | _commasepitem, default="")).setName("commaSeparatedList")
 """(Deprecated) Predefined expression of 1 or more printable words or
 quoted strings, separated by commas.
 
@@ -6128,7 +6504,7 @@ class pyparsing_common:
     integer = Word(nums).setName("integer").setParseAction(convertToInteger)
     """expression that parses an unsigned integer, returns an int"""
 
-    hex_integer = Word(hexnums).setName("hex integer").setParseAction(tokenMap(int,16))
+    hex_integer = Word(hexnums).setName("hex integer").setParseAction(tokenMap(int, 16))
     """expression that parses a hexadecimal integer, returns an int"""
 
     signed_integer = Regex(r'[+-]?\d+').setName("signed integer").setParseAction(convertToInteger)
@@ -6142,10 +6518,10 @@ class pyparsing_common:
     """mixed integer of the form 'integer - fraction', with optional leading integer, returns float"""
     mixed_integer.addParseAction(sum)
 
-    real = Regex(r'[+-]?\d+\.\d*').setName("real number").setParseAction(convertToFloat)
+    real = Regex(r'[+-]?(:?\d+\.\d*|\.\d+)').setName("real number").setParseAction(convertToFloat)
     """expression that parses a floating point number and returns a float"""
 
-    sci_real = Regex(r'[+-]?\d+([eE][+-]?\d+|\.\d*([eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat)
+    sci_real = Regex(r'[+-]?(:?\d+(:?[eE][+-]?\d+)|(:?\d+\.\d*|\.\d+)(:?[eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat)
     """expression that parses a floating point number with optional
     scientific notation and returns a float"""
 
@@ -6156,15 +6532,18 @@ class pyparsing_common:
     fnumber = Regex(r'[+-]?\d+\.?\d*([eE][+-]?\d+)?').setName("fnumber").setParseAction(convertToFloat)
     """any int or real number, returned as float"""
 
-    identifier = Word(alphas+'_', alphanums+'_').setName("identifier")
+    identifier = Word(alphas + '_', alphanums + '_').setName("identifier")
     """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')"""
 
     ipv4_address = Regex(r'(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}').setName("IPv4 address")
     "IPv4 address (``0.0.0.0 - 255.255.255.255``)"
 
     _ipv6_part = Regex(r'[0-9a-fA-F]{1,4}').setName("hex_integer")
-    _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part)*7).setName("full IPv6 address")
-    _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part)*(0,6)) + "::" + Optional(_ipv6_part + (':' + _ipv6_part)*(0,6))).setName("short IPv6 address")
+    _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part) * 7).setName("full IPv6 address")
+    _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part) * (0, 6))
+                           + "::"
+                           + Optional(_ipv6_part + (':' + _ipv6_part) * (0, 6))
+                           ).setName("short IPv6 address")
     _short_ipv6_address.addCondition(lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8)
     _mixed_ipv6_address = ("::ffff:" + ipv4_address).setName("mixed IPv6 address")
     ipv6_address = Combine((_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).setName("IPv6 address")).setName("IPv6 address")
@@ -6191,7 +6570,7 @@ class pyparsing_common:
 
             [datetime.date(1999, 12, 31)]
         """
-        def cvt_fn(s,l,t):
+        def cvt_fn(s, l, t):
             try:
                 return datetime.strptime(t[0], fmt).date()
             except ValueError as ve:
@@ -6216,7 +6595,7 @@ class pyparsing_common:
 
             [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)]
         """
-        def cvt_fn(s,l,t):
+        def cvt_fn(s, l, t):
             try:
                 return datetime.strptime(t[0], fmt)
             except ValueError as ve:
@@ -6241,7 +6620,7 @@ class pyparsing_common:
 
             # strip HTML links from normal text
             text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
-            td,td_end = makeHTMLTags("TD")
+            td, td_end = makeHTMLTags("TD")
             table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)("body") + td_end
             print(table_text.parseString(text).body)
 
@@ -6251,9 +6630,13 @@ class pyparsing_common:
         """
         return pyparsing_common._html_stripper.transformString(tokens[0])
 
-    _commasepitem = Combine(OneOrMore(~Literal(",") + ~LineEnd() + Word(printables, excludeChars=',')
-                                        + Optional( White(" \t") ) ) ).streamline().setName("commaItem")
-    comma_separated_list = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("comma separated list")
+    _commasepitem = Combine(OneOrMore(~Literal(",")
+                                      + ~LineEnd()
+                                      + Word(printables, excludeChars=',')
+                                      + Optional(White(" \t")))).streamline().setName("commaItem")
+    comma_separated_list = delimitedList(Optional(quotedString.copy()
+                                                  | _commasepitem, default='')
+                                         ).setName("comma separated list")
     """Predefined expression of 1 or more printable words or quoted strings, separated by commas."""
 
     upcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).upper()))
@@ -6272,7 +6655,8 @@ class _lazyclassproperty(object):
     def __get__(self, obj, cls):
         if cls is None:
             cls = type(obj)
-        if not hasattr(cls, '_intern') or any(cls._intern is getattr(superclass, '_intern', []) for superclass in cls.__mro__[1:]):
+        if not hasattr(cls, '_intern') or any(cls._intern is getattr(superclass, '_intern', [])
+                                              for superclass in cls.__mro__[1:]):
             cls._intern = {}
         attrname = self.fn.__name__
         if attrname not in cls._intern:
@@ -6303,7 +6687,7 @@ class unicode_set(object):
             if cc is unicode_set:
                 break
             for rr in cc._ranges:
-                ret.extend(range(rr[0], rr[-1]+1))
+                ret.extend(range(rr[0], rr[-1] + 1))
         return [unichr(c) for c in sorted(set(ret))]
 
     @_lazyclassproperty
@@ -6359,27 +6743,27 @@ class pyparsing_unicode(unicode_set):
 
     class Chinese(unicode_set):
         "Unicode set for Chinese Unicode Character Range"
-        _ranges = [(0x4e00, 0x9fff), (0x3000, 0x303f), ]
+        _ranges = [(0x4e00, 0x9fff), (0x3000, 0x303f),]
 
     class Japanese(unicode_set):
         "Unicode set for Japanese Unicode Character Range, combining Kanji, Hiragana, and Katakana ranges"
-        _ranges = [ ]
+        _ranges = []
 
         class Kanji(unicode_set):
             "Unicode set for Kanji Unicode Character Range"
-            _ranges = [(0x4E00, 0x9Fbf), (0x3000, 0x303f), ]
+            _ranges = [(0x4E00, 0x9Fbf), (0x3000, 0x303f),]
 
         class Hiragana(unicode_set):
             "Unicode set for Hiragana Unicode Character Range"
-            _ranges = [(0x3040, 0x309f), ]
+            _ranges = [(0x3040, 0x309f),]
 
         class Katakana(unicode_set):
             "Unicode set for Katakana  Unicode Character Range"
-            _ranges = [(0x30a0, 0x30ff), ]
+            _ranges = [(0x30a0, 0x30ff),]
 
     class Korean(unicode_set):
         "Unicode set for Korean Unicode Character Range"
-        _ranges = [(0xac00, 0xd7af), (0x1100, 0x11ff), (0x3130, 0x318f), (0xa960, 0xa97f), (0xd7b0, 0xd7ff), (0x3000, 0x303f), ]
+        _ranges = [(0xac00, 0xd7af), (0x1100, 0x11ff), (0x3130, 0x318f), (0xa960, 0xa97f), (0xd7b0, 0xd7ff), (0x3000, 0x303f),]
 
     class CJK(Chinese, Japanese, Korean):
         "Unicode set for combined Chinese, Japanese, and Korean (CJK) Unicode Character Range"
@@ -6387,15 +6771,15 @@ class pyparsing_unicode(unicode_set):
 
     class Thai(unicode_set):
         "Unicode set for Thai Unicode Character Range"
-        _ranges = [(0x0e01, 0x0e3a), (0x0e3f, 0x0e5b), ]
+        _ranges = [(0x0e01, 0x0e3a), (0x0e3f, 0x0e5b),]
 
     class Arabic(unicode_set):
         "Unicode set for Arabic Unicode Character Range"
-        _ranges = [(0x0600, 0x061b), (0x061e, 0x06ff), (0x0700, 0x077f), ]
+        _ranges = [(0x0600, 0x061b), (0x061e, 0x06ff), (0x0700, 0x077f),]
 
     class Hebrew(unicode_set):
         "Unicode set for Hebrew Unicode Character Range"
-        _ranges = [(0x0590, 0x05ff), ]
+        _ranges = [(0x0590, 0x05ff),]
 
     class Devanagari(unicode_set):
         "Unicode set for Devanagari Unicode Character Range"
@@ -6407,18 +6791,18 @@ pyparsing_unicode.Japanese._ranges = (pyparsing_unicode.Japanese.Kanji._ranges
 
 # define ranges in language character sets
 if PY_3:
-    setattr(pyparsing_unicode, "العربية", pyparsing_unicode.Arabic)
-    setattr(pyparsing_unicode, "中文", pyparsing_unicode.Chinese)
-    setattr(pyparsing_unicode, "кириллица", pyparsing_unicode.Cyrillic)
-    setattr(pyparsing_unicode, "Ελληνικά", pyparsing_unicode.Greek)
-    setattr(pyparsing_unicode, "עִברִית", pyparsing_unicode.Hebrew)
-    setattr(pyparsing_unicode, "日本語", pyparsing_unicode.Japanese)
-    setattr(pyparsing_unicode.Japanese, "漢字", pyparsing_unicode.Japanese.Kanji)
-    setattr(pyparsing_unicode.Japanese, "カタカナ", pyparsing_unicode.Japanese.Katakana)
-    setattr(pyparsing_unicode.Japanese, "ひらがな", pyparsing_unicode.Japanese.Hiragana)
-    setattr(pyparsing_unicode, "한국어", pyparsing_unicode.Korean)
-    setattr(pyparsing_unicode, "ไทย", pyparsing_unicode.Thai)
-    setattr(pyparsing_unicode, "देवनागरी", pyparsing_unicode.Devanagari)
+    setattr(pyparsing_unicode, u"العربية", pyparsing_unicode.Arabic)
+    setattr(pyparsing_unicode, u"中文", pyparsing_unicode.Chinese)
+    setattr(pyparsing_unicode, u"кириллица", pyparsing_unicode.Cyrillic)
+    setattr(pyparsing_unicode, u"Ελληνικά", pyparsing_unicode.Greek)
+    setattr(pyparsing_unicode, u"עִברִית", pyparsing_unicode.Hebrew)
+    setattr(pyparsing_unicode, u"日本語", pyparsing_unicode.Japanese)
+    setattr(pyparsing_unicode.Japanese, u"漢字", pyparsing_unicode.Japanese.Kanji)
+    setattr(pyparsing_unicode.Japanese, u"カタカナ", pyparsing_unicode.Japanese.Katakana)
+    setattr(pyparsing_unicode.Japanese, u"ひらがな", pyparsing_unicode.Japanese.Hiragana)
+    setattr(pyparsing_unicode, u"한국어", pyparsing_unicode.Korean)
+    setattr(pyparsing_unicode, u"ไทย", pyparsing_unicode.Thai)
+    setattr(pyparsing_unicode, u"देवनागरी", pyparsing_unicode.Devanagari)
 
 
 if __name__ == "__main__":
index 7e42003850f86b7a27c8a4c978c8e824ebd1d6ad..1af7474a6668f8bdf1862c5e44239cc9d0a0a777 100644 (file)
@@ -140,7 +140,9 @@ class TestCaselessLiteral(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc = "Match colors, converting to consistent case",
-            expr = pp.OneOrMore(pp.CaselessLiteral("RED") | pp.CaselessLiteral("GREEN") | pp.CaselessLiteral("BLUE")),
+            expr = (pp.CaselessLiteral("RED")
+                    | pp.CaselessLiteral("GREEN")
+                    | pp.CaselessLiteral("BLUE"))[...],
             text = "red Green BluE blue GREEN green rEd",
             expected_list = ['RED', 'GREEN', 'BLUE', 'BLUE', 'GREEN', 'GREEN', 'RED'],
         ),
@@ -172,13 +174,13 @@ class TestCombine(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc="Parsing real numbers - fail, parsed numbers are in pieces",
-            expr=pp.OneOrMore(pp.Word(pp.nums) + '.' + pp.Word(pp.nums)),
+            expr=(pp.Word(pp.nums) + '.' + pp.Word(pp.nums))[...],
             text="1.2 2.3 3.1416 98.6",
             expected_list=['1', '.', '2', '2', '.', '3', '3', '.', '1416', '98', '.', '6'],
         ),
         PpTestSpec(
             desc="Parsing real numbers - better, use Combine to combine multiple tokens into one",
-            expr=pp.OneOrMore(pp.Combine(pp.Word(pp.nums) + '.' + pp.Word(pp.nums))),
+            expr=pp.Combine(pp.Word(pp.nums) + '.' + pp.Word(pp.nums))[...],
             text="1.2 2.3 3.1416 98.6",
             expected_list=['1.2', '2.3', '3.1416', '98.6'],
         ),
@@ -188,19 +190,26 @@ class TestRepetition(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc = "Match several words",
-            expr = pp.OneOrMore(pp.Word("x") | pp.Word("y")),
+            expr = (pp.Word("x") | pp.Word("y"))[...],
             text = "xxyxxyyxxyxyxxxy",
             expected_list = ['xx', 'y', 'xx', 'yy', 'xx', 'y', 'x', 'y', 'xxx', 'y'],
         ),
         PpTestSpec(
             desc = "Match several words, skipping whitespace",
+            expr = (pp.Word("x") | pp.Word("y"))[...],
+            text = "x x  y xxy yxx y xyx  xxy",
+            expected_list = ['x', 'x', 'y', 'xx', 'y', 'y', 'xx', 'y', 'x', 'y', 'x', 'xx', 'y'],
+        ),
+        PpTestSpec(
+            desc = "Match several words, skipping whitespace (old style)",
             expr = pp.OneOrMore(pp.Word("x") | pp.Word("y")),
             text = "x x  y xxy yxx y xyx  xxy",
             expected_list = ['x', 'x', 'y', 'xx', 'y', 'y', 'xx', 'y', 'x', 'y', 'x', 'xx', 'y'],
         ),
         PpTestSpec(
             desc = "Match words and numbers - show use of results names to collect types of tokens",
-            expr = pp.OneOrMore(pp.Word(pp.alphas)("alpha*") | pp.pyparsing_common.integer("int*")),
+            expr = (pp.Word(pp.alphas)("alpha*")
+                    | pp.pyparsing_common.integer("int*"))[...],
             text = "sdlfj23084ksdfs08234kjsdlfkjd0934",
             expected_list = ['sdlfj', 23084, 'ksdfs', 8234, 'kjsdlfkjd', 934],
             expected_dict = { 'alpha': ['sdlfj', 'ksdfs', 'kjsdlfkjd'], 'int': [23084, 8234, 934] }
@@ -249,27 +258,28 @@ class TestGroups(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc = "Define multiple results names in groups",
-            expr = pp.OneOrMore(pp.Group(pp.Word(pp.alphas)("key")
-                                          + EQ
-                                          + pp.pyparsing_common.number("value"))),
+            expr = pp.Group(pp.Word(pp.alphas)("key")
+                            + EQ
+                            + pp.pyparsing_common.number("value"))[...],
             text = "range=5280 long=-138.52 lat=46.91",
             expected_list = [['range', 5280], ['long', -138.52], ['lat', 46.91]],
         ),
         PpTestSpec(
             desc = "Define multiple results names in groups - use Dict to define results names using parsed keys",
-            expr = pp.Dict(pp.OneOrMore(pp.Group(pp.Word(pp.alphas)
-                                          + EQ
-                                          + pp.pyparsing_common.number))),
+            expr = pp.Dict(pp.Group(pp.Word(pp.alphas)
+                                    + EQ
+                                    + pp.pyparsing_common.number)[...]),
             text = "range=5280 long=-138.52 lat=46.91",
             expected_list = [['range', 5280], ['long', -138.52], ['lat', 46.91]],
             expected_dict = {'lat': 46.91, 'long': -138.52, 'range': 5280}
         ),
         PpTestSpec(
             desc = "Define multiple value types",
-            expr = pp.Dict(pp.OneOrMore(pp.Group(pp.Word(pp.alphas)
+            expr = pp.Dict(pp.Group(pp.Word(pp.alphas)
                                           + EQ
                                           + (pp.pyparsing_common.number | pp.oneOf("True False") | pp.QuotedString("'"))
-                                        ))),
+                                        )[...]
+                           ),
             text = "long=-122.47 lat=37.82 public=True name='Golden Gate Bridge'",
             expected_list = [['long', -122.47], ['lat', 37.82], ['public', 'True'], ['name', 'Golden Gate Bridge']],
             expected_dict = {'long': -122.47, 'lat': 37.82, 'public': 'True', 'name': 'Golden Gate Bridge'}
@@ -280,7 +290,7 @@ class TestParseAction(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc="Parsing real numbers - use parse action to convert to float at parse time",
-            expr=pp.OneOrMore(pp.Combine(pp.Word(pp.nums) + '.' + pp.Word(pp.nums)).addParseAction(lambda t: float(t[0]))),
+            expr=pp.Combine(pp.Word(pp.nums) + '.' + pp.Word(pp.nums)).addParseAction(lambda t: float(t[0]))[...],
             text="1.2 2.3 3.1416 98.6",
             expected_list= [1.2, 2.3, 3.1416, 98.6], # note, these are now floats, not strs
         ),
@@ -306,13 +316,13 @@ class TestParseAction(PyparsingExpressionTestCase):
         ),
         PpTestSpec(
             desc = "Using a built-in function that takes a sequence of strs as a parse action",
-            expr = pp.OneOrMore(pp.Word(pp.hexnums, exact=2)).addParseAction(':'.join),
+            expr = pp.Word(pp.hexnums, exact=2)[...].addParseAction(':'.join),
             text = "0A4B7321FE76",
             expected_list = ['0A:4B:73:21:FE:76'],
         ),
         PpTestSpec(
             desc = "Using a built-in function that takes a sequence of strs as a parse action",
-            expr = pp.OneOrMore(pp.Word(pp.hexnums, exact=2)).addParseAction(sorted),
+            expr = pp.Word(pp.hexnums, exact=2)[...].addParseAction(sorted),
             text = "0A4B7321FE76",
             expected_list = ['0A', '21', '4B', '73', '76', 'FE'],
         ),
@@ -331,7 +341,7 @@ class TestResultsModifyingParseAction(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc = "A parse action that adds new key-values",
-            expr = pp.OneOrMore(pp.pyparsing_common.integer).addParseAction(compute_stats_parse_action),
+            expr = pp.pyparsing_common.integer[...].addParseAction(compute_stats_parse_action),
             text = "27 1 14 22 89",
             expected_list = [27, 1, 14, 22, 89],
             expected_dict = {'ave': 30.6, 'max': 89, 'min': 1, 'sum': 153}
@@ -342,7 +352,7 @@ class TestRegex(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc="Parsing real numbers - using Regex instead of Combine",
-            expr=pp.OneOrMore(pp.Regex(r'\d+\.\d+').addParseAction(lambda t: float(t[0]))),
+            expr=pp.Regex(r'\d+\.\d+').addParseAction(lambda t: float(t[0]))[...],
             text="1.2 2.3 3.1416 98.6",
             expected_list=[1.2, 2.3, 3.1416, 98.6],  # note, these are now floats, not strs
         ),
@@ -352,14 +362,14 @@ class TestParseCondition(PyparsingExpressionTestCase):
     tests = [
         PpTestSpec(
             desc = "Define a condition to only match numeric values that are multiples of 7",
-            expr = pp.OneOrMore(pp.Word(pp.nums).addCondition(lambda t: int(t[0]) % 7 == 0)),
+            expr = pp.Word(pp.nums).addCondition(lambda t: int(t[0]) % 7 == 0)[...],
             text = "14 35 77 12 28",
             expected_list = ['14', '35', '77'],
         ),
         PpTestSpec(
             desc = "Separate conversion to int and condition into separate parse action/conditions",
-            expr = pp.OneOrMore(pp.Word(pp.nums).addParseAction(lambda t: int(t[0]))
-                                                 .addCondition(lambda t: t[0] % 7 == 0)),
+            expr = pp.Word(pp.nums).addParseAction(lambda t: int(t[0]))
+                                   .addCondition(lambda t: t[0] % 7 == 0)[...],
             text = "14 35 77 12 28",
             expected_list = [14, 35, 77],
         ),
@@ -396,7 +406,7 @@ class TestCommonHelperExpressions(PyparsingExpressionTestCase):
         ),
         PpTestSpec(
             desc = "A counted array of words",
-            expr = pp.OneOrMore(pp.countedArray(pp.Word('ab'))),
+            expr = pp.countedArray(pp.Word('ab'))[...],
             text = "2 aaa bbb 0 3 abab bbaa abbab",
             expected_list = [['aaa', 'bbb'], [], ['abab', 'bbaa', 'abbab']],
         ),
@@ -421,7 +431,7 @@ class TestCommonHelperExpressions(PyparsingExpressionTestCase):
         ),
         PpTestSpec(
             desc = "using oneOf (shortcut for Literal('a') | Literal('b') | Literal('c'))",
-            expr = pp.OneOrMore(pp.oneOf("a b c")),
+            expr = pp.oneOf("a b c")[...],
             text = "a b a b b a c c a b b",
             expected_list = ['a', 'b', 'a', 'b', 'b', 'a', 'c', 'c', 'a', 'b', 'b'],
         ),
@@ -464,6 +474,11 @@ suite = unittest.TestSuite(cls() for cls in test_case_classes)
 # ============ MAIN ================
 
 if __name__ == '__main__':
+    import sys
+    if sys.version_info[0] < 3:
+        print("simple_unit_tests.py requires Python 3.x - exiting...")
+        exit(0)
+
     result = unittest.TextTestRunner().run(suite)
 
     exit(0 if result.wasSuccessful() else 1)
index 5db9ffeb282a7ce4477c45c34d51049bb70e4022..7bfbe5206074e101244398feafad4a45d55a0f43 100644 (file)
@@ -117,50 +117,12 @@ class ParseTestCase(TestCase):
 
 class PyparsingTestInit(ParseTestCase):
     def setUp(self):
-        from pyparsing import __version__ as pyparsingVersion
-        print_("Beginning test of pyparsing, version", pyparsingVersion)
+        from pyparsing import __version__ as pyparsingVersion, __versionTime__ as pyparsingVersionTime
+        print_("Beginning test of pyparsing, version", pyparsingVersion, pyparsingVersionTime)
         print_("Python version", sys.version)
     def tearDown(self):
         pass
 
-if 0:
-    class ParseASMLTest(ParseTestCase):
-        def runTest(self):
-            import parseASML
-            files = [ ("A52759.txt", 2150, True, True, 0.38, 25, "21:47:17", "22:07:32", 235),
-                      ("24141506_P5107RM59_399A1457N1_PHS04", 373,True, True, 0.5, 1, "11:35:25", "11:37:05", 183),
-                      ("24141506_P5107RM59_399A1457N1_PHS04B", 373, True, True, 0.5, 1, "01:02:54", "01:04:49", 186),
-                      ("24157800_P5107RM74_399A1828M1_PHS04", 1141, True, False, 0.5, 13, "00:00:54", "23:59:48", 154) ]
-            for testFile,numToks,trkInpUsed,trkOutpUsed,maxDelta,numWafers,minProcBeg,maxProcEnd,maxLevStatsIV in files:
-                print_("Parsing",testFile,"...", end=' ')
-                #~ text = "\n".join( [ line for line in file(testFile) ] )
-                #~ results = parseASML.BNF().parseString( text )
-                results = parseASML.BNF().parseFile( testFile )
-                #~ pprint.pprint( results.asList() )
-                #~ pprint.pprint( results.batchData.asList() )
-                #~ print results.batchData.keys()
-
-                allToks = flatten( results.asList() )
-                self.assertTrue(len(allToks) == numToks,
-                    "wrong number of tokens parsed (%s), got %d, expected %d" % (testFile, len(allToks),numToks))
-                self.assertTrue(results.batchData.trackInputUsed == trkInpUsed, "error evaluating results.batchData.trackInputUsed")
-                self.assertTrue(results.batchData.trackOutputUsed == trkOutpUsed, "error evaluating results.batchData.trackOutputUsed")
-                self.assertTrue(results.batchData.maxDelta == maxDelta,"error evaluating results.batchData.maxDelta")
-                self.assertTrue(len(results.waferData) == numWafers, "did not read correct number of wafers")
-                self.assertTrue(min([wd.procBegin for wd in results.waferData]) == minProcBeg, "error reading waferData.procBegin")
-                self.assertTrue(max([results.waferData[k].procEnd for k in range(len(results.waferData))]) == maxProcEnd, "error reading waferData.procEnd")
-                self.assertTrue(sum(results.levelStatsIV['MAX']) == maxLevStatsIV, "error reading levelStatsIV")
-                self.assertTrue(sum(results.levelStatsIV.MAX) == maxLevStatsIV, "error reading levelStatsIV")
-                print_("OK")
-                print_(testFile,len(allToks))
-                #~ print "results.batchData.trackInputUsed =",results.batchData.trackInputUsed
-                #~ print "results.batchData.trackOutputUsed =",results.batchData.trackOutputUsed
-                #~ print "results.batchData.maxDelta =",results.batchData.maxDelta
-                #~ print len(results.waferData)," wafers"
-                #~ print min([wd.procBegin for wd in results.waferData])
-                #~ print max([results.waferData[k].procEnd for k in range(len(results.waferData))])
-                #~ print sum(results.levelStatsIV['MAX.'])
-
 
 class ParseFourFnTest(ParseTestCase):
     def runTest(self):
@@ -237,12 +199,7 @@ class ParseConfigFileTest(ParseTestCase):
                 iniFileLines = "\n".join(infile.read().splitlines())
             iniData = configParse.inifile_BNF().parseString( iniFileLines )
             print_(len(flatten(iniData.asList())))
-            #~ pprint.pprint( iniData.asList() )
-            #~ pprint.pprint( repr(iniData) )
-            #~ print len(iniData), len(flatten(iniData.asList()))
             print_(list(iniData.keys()))
-            #~ print iniData.users.keys()
-            #~ print
             self.assertEqual(len(flatten(iniData.asList())), numToks, "file %s not parsed correctly" % fnam)
             for chk in resCheckList:
                 var = iniData
@@ -269,23 +226,178 @@ class ParseJSONDataTest(ParseTestCase):
     def runTest(self):
         from examples.jsonParser import jsonObject
         from test.jsonParserTests import test1,test2,test3,test4,test5
-        from test.jsonParserTests import test1,test2,test3,test4,test5
 
         expected = [
-            [],
-            [],
-            [],
-            [],
-            [],
+            [['glossary',
+             [['title', 'example glossary'],
+              ['GlossDiv',
+               [['title', 'S'],
+                ['GlossList',
+                 [[['ID', 'SGML'],
+                   ['SortAs', 'SGML'],
+                   ['GlossTerm', 'Standard Generalized Markup Language'],
+                   ['Acronym', 'SGML'],
+                   ['LargestPrimeLessThan100', 97],
+                   ['AvogadroNumber', 6.02e+23],
+                   ['EvenPrimesGreaterThan2', None],
+                   ['PrimesLessThan10', [2, 3, 5, 7]],
+                   ['WMDsFound', False],
+                   ['IraqAlQaedaConnections', None],
+                   ['Abbrev', 'ISO 8879:1986'],
+                   ['GlossDef',
+                    'A meta-markup language, used to create markup languages such as '
+                    'DocBook.'],
+                   ['GlossSeeAlso', ['GML', 'XML', 'markup']],
+                   ['EmptyDict', []],
+                   ['EmptyList', [[]]]]]]]]]
+             ]]
+            ,
+            [['menu',
+             [['id', 'file'],
+              ['value', 'File:'],
+              ['popup',
+               [['menuitem',
+                 [[['value', 'New'], ['onclick', 'CreateNewDoc()']],
+                  [['value', 'Open'], ['onclick', 'OpenDoc()']],
+                  [['value', 'Close'], ['onclick', 'CloseDoc()']]]]]]]]]
+            ,
+            [['widget',
+             [['debug', 'on'],
+              ['window',
+               [['title', 'Sample Konfabulator Widget'],
+                ['name', 'main_window'],
+                ['width', 500],
+                ['height', 500]]],
+              ['image',
+               [['src', 'Images/Sun.png'],
+                ['name', 'sun1'],
+                ['hOffset', 250],
+                ['vOffset', 250],
+                ['alignment', 'center']]],
+              ['text',
+               [['data', 'Click Here'],
+                ['size', 36],
+                ['style', 'bold'],
+                ['name', 'text1'],
+                ['hOffset', 250],
+                ['vOffset', 100],
+                ['alignment', 'center'],
+                ['onMouseUp', 'sun1.opacity = (sun1.opacity / 100) * 90;']]]]]]
+            ,
+            [['web-app',
+             [['servlet',
+               [[['servlet-name', 'cofaxCDS'],
+                 ['servlet-class', 'org.cofax.cds.CDSServlet'],
+                 ['init-param',
+                  [['configGlossary:installationAt', 'Philadelphia, PA'],
+                   ['configGlossary:adminEmail', 'ksm@pobox.com'],
+                   ['configGlossary:poweredBy', 'Cofax'],
+                   ['configGlossary:poweredByIcon', '/images/cofax.gif'],
+                   ['configGlossary:staticPath', '/content/static'],
+                   ['templateProcessorClass', 'org.cofax.WysiwygTemplate'],
+                   ['templateLoaderClass', 'org.cofax.FilesTemplateLoader'],
+                   ['templatePath', 'templates'],
+                   ['templateOverridePath', ''],
+                   ['defaultListTemplate', 'listTemplate.htm'],
+                   ['defaultFileTemplate', 'articleTemplate.htm'],
+                   ['useJSP', False],
+                   ['jspListTemplate', 'listTemplate.jsp'],
+                   ['jspFileTemplate', 'articleTemplate.jsp'],
+                   ['cachePackageTagsTrack', 200],
+                   ['cachePackageTagsStore', 200],
+                   ['cachePackageTagsRefresh', 60],
+                   ['cacheTemplatesTrack', 100],
+                   ['cacheTemplatesStore', 50],
+                   ['cacheTemplatesRefresh', 15],
+                   ['cachePagesTrack', 200],
+                   ['cachePagesStore', 100],
+                   ['cachePagesRefresh', 10],
+                   ['cachePagesDirtyRead', 10],
+                   ['searchEngineListTemplate', 'forSearchEnginesList.htm'],
+                   ['searchEngineFileTemplate', 'forSearchEngines.htm'],
+                   ['searchEngineRobotsDb', 'WEB-INF/robots.db'],
+                   ['useDataStore', True],
+                   ['dataStoreClass', 'org.cofax.SqlDataStore'],
+                   ['redirectionClass', 'org.cofax.SqlRedirection'],
+                   ['dataStoreName', 'cofax'],
+                   ['dataStoreDriver', 'com.microsoft.jdbc.sqlserver.SQLServerDriver'],
+                   ['dataStoreUrl',
+                    'jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon'],
+                   ['dataStoreUser', 'sa'],
+                   ['dataStorePassword', 'dataStoreTestQuery'],
+                   ['dataStoreTestQuery', "SET NOCOUNT ON;select test='test';"],
+                   ['dataStoreLogFile', '/usr/local/tomcat/logs/datastore.log'],
+                   ['dataStoreInitConns', 10],
+                   ['dataStoreMaxConns', 100],
+                   ['dataStoreConnUsageLimit', 100],
+                   ['dataStoreLogLevel', 'debug'],
+                   ['maxUrlLength', 500]]]],
+                [['servlet-name', 'cofaxEmail'],
+                 ['servlet-class', 'org.cofax.cds.EmailServlet'],
+                 ['init-param', [['mailHost', 'mail1'], ['mailHostOverride', 'mail2']]]],
+                [['servlet-name', 'cofaxAdmin'],
+                 ['servlet-class', 'org.cofax.cds.AdminServlet']],
+                [['servlet-name', 'fileServlet'],
+                 ['servlet-class', 'org.cofax.cds.FileServlet']],
+                [['servlet-name', 'cofaxTools'],
+                 ['servlet-class', 'org.cofax.cms.CofaxToolsServlet'],
+                 ['init-param',
+                  [['templatePath', 'toolstemplates/'],
+                   ['log', 1],
+                   ['logLocation', '/usr/local/tomcat/logs/CofaxTools.log'],
+                   ['logMaxSize', ''],
+                   ['dataLog', 1],
+                   ['dataLogLocation', '/usr/local/tomcat/logs/dataLog.log'],
+                   ['dataLogMaxSize', ''],
+                   ['removePageCache', '/content/admin/remove?cache=pages&id='],
+                   ['removeTemplateCache', '/content/admin/remove?cache=templates&id='],
+                   ['fileTransferFolder',
+                    '/usr/local/tomcat/webapps/content/fileTransferFolder'],
+                   ['lookInContext', 1],
+                   ['adminGroupID', 4],
+                   ['betaServer', True]]]]]],
+              ['servlet-mapping',
+               [['cofaxCDS', '/'],
+                ['cofaxEmail', '/cofaxutil/aemail/*'],
+                ['cofaxAdmin', '/admin/*'],
+                ['fileServlet', '/static/*'],
+                ['cofaxTools', '/tools/*']]],
+              ['taglib',
+               [['taglib-uri', 'cofax.tld'],
+                ['taglib-location', '/WEB-INF/tlds/cofax.tld']]]]]]
+            ,
+            [['menu',
+              [['header', 'SVG Viewer'],
+               ['items',
+                [[['id', 'Open']],
+                 [['id', 'OpenNew'], ['label', 'Open New']],
+                 None,
+                 [['id', 'ZoomIn'], ['label', 'Zoom In']],
+                 [['id', 'ZoomOut'], ['label', 'Zoom Out']],
+                 [['id', 'OriginalView'], ['label', 'Original View']],
+                 None,
+                 [['id', 'Quality']],
+                 [['id', 'Pause']],
+                 [['id', 'Mute']],
+                 None,
+                 [['id', 'Find'], ['label', 'Find...']],
+                 [['id', 'FindAgain'], ['label', 'Find Again']],
+                 [['id', 'Copy']],
+                 [['id', 'CopyAgain'], ['label', 'Copy Again']],
+                 [['id', 'CopySVG'], ['label', 'Copy SVG']],
+                 [['id', 'ViewSVG'], ['label', 'View SVG']],
+                 [['id', 'ViewSource'], ['label', 'View Source']],
+                 [['id', 'SaveAs'], ['label', 'Save As']],
+                 None,
+                 [['id', 'Help']],
+                 [['id', 'About'], ['label', 'About Adobe CVG Viewer...']]]]]]]
+            ,
             ]
 
-        for t,exp in zip((test1,test2,test3,test4,test5),expected):
+        for t, exp in zip((test1,test2,test3,test4,test5), expected):
             result = jsonObject.parseString(t)
-##            print result.dump()
             result.pprint()
-            print_()
-##            if result.asList() != exp:
-##                print "Expected %s, parsed results as %s" % (exp, result.asList())
+            self.assertEqual(result.asList(), exp, "failed test {0}".format(t))
 
 class ParseCommaSeparatedValuesTest(ParseTestCase):
     def runTest(self):
@@ -352,8 +464,7 @@ class ParseEBNFTest(ParseTestCase):
         print_('Parsing EBNF grammar with EBNF parser...')
         parsers = ebnf.parse(grammar, table)
         ebnf_parser = parsers['syntax']
-        #~ print ",\n ".join( str(parsers.keys()).split(", ") )
-        print_("-","\n- ".join( list(parsers.keys()) ))
+        print_("-","\n- ".join(parsers.keys()))
         self.assertEqual(len(list(parsers.keys())), 13, "failed to construct syntax grammar")
 
         print_('Parsing EBNF grammar with generated EBNF parser...')
@@ -476,10 +587,6 @@ class ParseVerilogTest(ParseTestCase):
     def runTest(self):
         pass
 
-class RunExamplesTest(ParseTestCase):
-    def runTest(self):
-        pass
-
 class ScanStringTest(ParseTestCase):
     def runTest(self):
         from pyparsing import Word, Combine, Suppress, CharsNotIn, nums, StringEnd
@@ -818,25 +925,18 @@ class ParseExpressionResultsTest(ParseTestCase):
     def runTest(self):
         from pyparsing import Word,alphas,OneOrMore,Optional,Group
 
-        a = Word("a",alphas).setName("A")
-        b = Word("b",alphas).setName("B")
-        c = Word("c",alphas).setName("C")
+        a = Word("a", alphas).setName("A")
+        b = Word("b", alphas).setName("B")
+        c = Word("c", alphas).setName("C")
         ab = (a + b).setName("AB")
         abc = (ab + c).setName("ABC")
         word = Word(alphas).setName("word")
 
-        #~ words = OneOrMore(word).setName("words")
         words = Group(OneOrMore(~a + word)).setName("words")
 
-        #~ phrase = words.setResultsName("Head") + \
-                    #~ ( abc ^ ab ^ a ).setResultsName("ABC") + \
-                    #~ words.setResultsName("Tail")
-        #~ phrase = words.setResultsName("Head") + \
-                    #~ ( abc | ab | a ).setResultsName("ABC") + \
-                    #~ words.setResultsName("Tail")
-        phrase = words("Head") + \
-                    Group( a + Optional(b + Optional(c)) )("ABC") + \
-                    words("Tail")
+        phrase = (words("Head")
+                  + Group(a + Optional(b + Optional(c)))("ABC")
+                  + words("Tail"))
 
         results = phrase.parseString("xavier yeti alpha beta charlie will beaver")
         print_(results,results.Head, results.ABC,results.Tail)
@@ -993,7 +1093,7 @@ class ReStringRangeTest(ParseTestCase):
 class SkipToParserTests(ParseTestCase):
     def runTest(self):
 
-        from pyparsing import Literal, SkipTo, cStyleComment, ParseBaseException
+        from pyparsing import Literal, SkipTo, cStyleComment, ParseBaseException, And, Word, alphas, nums, Optional, NotAny
 
         thingToFind = Literal('working')
         testExpr = SkipTo(Literal(';'), include=True, ignore=cStyleComment) + thingToFind
@@ -1025,6 +1125,79 @@ class SkipToParserTests(ParseTestCase):
         result = expr.parseString(text)
         self.assertTrue(isinstance(result.prefix, str), "SkipTo created with wrong saveAsList attribute")
 
+        if PY_3:
+            def define_expr(s):
+                from pyparsing import Literal, And, Word, alphas, nums, Optional, NotAny
+                alpha_word = (~Literal("end") + Word(alphas, asKeyword=True)).setName("alpha")
+                num_word = Word(nums, asKeyword=True).setName("int")
+
+                ret = eval(s)
+                ret.streamline()
+                print_(ret)
+                return ret
+
+            def test(expr, test_string, expected_list, expected_dict):
+                try:
+                    result = expr.parseString(test_string)
+                except Exception as pe:
+                    if any(expected is not None for expected in (expected_list, expected_dict)):
+                        self.assertTrue(False, "{} failed to parse {!r}".format(expr, test_string))
+                else:
+                    self.assertEqual(result.asList(), expected_list)
+                    self.assertEqual(result.asDict(), expected_dict)
+
+            # ellipses for SkipTo
+            # (use eval() to avoid syntax problems when running in Py2)
+            e = define_expr('... + Literal("end")')
+            test(e, "start 123 end", ['start 123 ', 'end'], {'_skipped': ['start 123 ']})
+
+            e = define_expr('Literal("start") + ... + Literal("end")')
+            test(e, "start 123 end", ['start', '123 ', 'end'], {'_skipped': ['123 ']})
+
+            e = define_expr('Literal("start") + ...')
+            test(e, "start 123 end", None, None)
+
+            e = define_expr('And(["start", ..., "end"])')
+            test(e, "start 123 end", ['start', '123 ', 'end'], {'_skipped': ['123 ']})
+
+            e = define_expr('And([..., "end"])')
+            test(e, "start 123 end", ['start 123 ', 'end'], {'_skipped': ['start 123 ']})
+
+            e = define_expr('"start" + (num_word | ...) + "end"')
+            test(e, "start 456 end", ['start', '456', 'end'], {})
+            test(e, "start 123 456 end", ['start', '123', '456 ', 'end'], {'_skipped': ['456 ']})
+            test(e, "start end", ['start', '', 'end'], {'_skipped': ['missing <int>']})
+
+            # e = define_expr('"start" + (num_word | ...)("inner") + "end"')
+            # test(e, "start 456 end", ['start', '456', 'end'], {'inner': '456'})
+
+            e = define_expr('"start" + (alpha_word[0, ...] & num_word[0, ...] | ...) + "end"')
+            test(e, "start 456 red end", ['start', '456', 'red', 'end'], {})
+            test(e, "start red 456 end", ['start', 'red', '456', 'end'], {})
+            test(e, "start 456 red + end", ['start', '456', 'red', '+ ', 'end'], {'_skipped': ['+ ']})
+            test(e, "start red end", ['start', 'red', 'end'], {})
+            test(e, "start 456 end", ['start', '456', 'end'], {})
+            test(e, "start end", ['start', 'end'], {})
+            test(e, "start 456 + end", ['start', '456', '+ ', 'end'], {'_skipped': ['+ ']})
+
+            e = define_expr('"start" + (alpha_word[...] & num_word[...] | ...) + "end"')
+            test(e, "start 456 red end", ['start', '456', 'red', 'end'], {})
+            test(e, "start red 456 end", ['start', 'red', '456', 'end'], {})
+            test(e, "start 456 red + end", ['start', '456', 'red', '+ ', 'end'], {'_skipped': ['+ ']})
+            test(e, "start red end", ['start', 'red ', 'end'], {'_skipped': ['red ']})
+            test(e, "start 456 end", ['start', '456 ', 'end'], {'_skipped': ['456 ']})
+            test(e, "start end", ['start', '', 'end'], {'_skipped': ['missing <{{alpha}... & {int}...}>']})
+            test(e, "start 456 + end", ['start', '456 + ', 'end'], {'_skipped': ['456 + ']})
+
+            e = define_expr('"start" + (alpha_word | ...) + (num_word | ...) + "end"')
+            test(e, "start red 456 end", ['start', 'red', '456', 'end'], {})
+            test(e, "start red end", ['start', 'red', '', 'end'], {'_skipped': ['missing <int>']})
+            test(e, "start end", ['start', '', '', 'end'], {'_skipped': ['missing <alpha>', 'missing <int>']})
+
+            e = define_expr('Literal("start") + ... + "+" + ... + "end"')
+            test(e, "start red + 456 end", ['start', 'red ', '+', '456 ', 'end'], {'_skipped': ['red ', '456 ']})
+
+
 class CustomQuotesTest(ParseTestCase):
     def runTest(self):
         from pyparsing import QuotedString
@@ -1139,28 +1312,9 @@ class RepeaterTest(ParseTestCase):
             ( "abc12abc:abc12abcdef", False ),
             ]
 
-        #~ for tst,result in tests:
-            #~ print tst,
-            #~ try:
-                #~ compoundSeq.parseString(tst)
-                #~ print "MATCH"
-                #~ assert result, "matched when shouldn't have matched"
-            #~ except ParseException:
-                #~ print "NO MATCH"
-                #~ assert not result, "didnt match but should have"
-
-        #~ for tst,result in tests:
-            #~ print tst,
-            #~ if compoundSeq == tst:
-                #~ print "MATCH"
-                #~ assert result, "matched when shouldn't have matched"
-            #~ else:
-                #~ print "NO MATCH"
-                #~ assert not result, "didnt match but should have"
-
-        for tst,result in tests:
+        for tst, result in tests:
             found = False
-            for tokens,start,end in compoundSeq.scanString(tst):
+            for tokens, start, end in compoundSeq.scanString(tst):
                 print_("match:", tokens.asList())
                 found = True
                 break
@@ -1181,8 +1335,6 @@ class RepeaterTest(ParseTestCase):
         for tst,result in tests:
             found = False
             for tokens,start,end in eSeq.scanString(tst):
-                #~ f,b,s = tokens
-                #~ print f,b,s
                 print_(tokens.asList())
                 found = True
             if not found:
@@ -1222,8 +1374,8 @@ class InfixNotationGrammarTest1(ParseTestCase):
         factop = Literal('!')
 
         expr = infixNotation( operand,
-            [("!", 1, opAssoc.LEFT),
-             ("^", 2, opAssoc.RIGHT),
+            [(factop, 1, opAssoc.LEFT),
+             (expop, 2, opAssoc.RIGHT),
              (signop, 1, opAssoc.RIGHT),
              (multop, 2, opAssoc.LEFT),
              (plusop, 2, opAssoc.LEFT),]
@@ -2156,14 +2308,6 @@ class VariableParseActionArgsTest(ParseTestCase):
             def __str__(self):
                 return ''.join(self)
 
-        #~ def ClassAsPANew(object):
-            #~ def __new__(cls, t):
-                #~ return object.__new__(cls, t)
-            #~ def __init__(self,t):
-                #~ self.t = t
-            #~ def __str__(self):
-                #~ return self.t
-
         from pyparsing import Literal,OneOrMore
 
         A = Literal("A").setParseAction(pa0)
@@ -2241,15 +2385,12 @@ class OriginalTextForTest(ParseTestCase):
         makeHTMLStartTag = lambda tag: originalTextFor(makeHTMLTags(tag)[0], asString=False)
 
         # use the lambda, Luke
-        #~ start, imge = makeHTMLTags('IMG')
         start = makeHTMLStartTag('IMG')
 
         # don't replace our fancy parse action with rfn,
         # append rfn to the list of parse actions
-        #~ start.setParseAction(rfn)
         start.addParseAction(rfn)
 
-        #start.setParseAction(lambda s,l,t:t.src)
         text = '''_<img src="images/cal.png"
             alt="cal image" width="16" height="15">_'''
         s = start.transformString(text)
@@ -2860,7 +3001,23 @@ class PatientOrTest(ParseTestCase):
             failed = True
         else:
             failed = False
-            self.assertFalse(failed, "invalid logic in Or, fails on longest match with exception in parse action")
+        self.assertFalse(failed, "invalid logic in Or, fails on longest match with exception in parse action")
+
+        # from issue #93
+        word = pp.Word(pp.alphas).setName('word')
+        word_1 = pp.Word(pp.alphas).setName('word_1').addCondition(lambda t: len(t[0]) == 1)
+
+        a = word + (word_1 + word ^ word)
+        b = word * 3
+        c = a ^ b
+        c.streamline()
+        print_(c)
+        test_string = 'foo bar temp'
+        result = c.parseString(test_string)
+        print_(test_string, '->', result.asList())
+
+        self.assertEqual(result.asList(), test_string.split(), "failed to match longest choice")
+
 
 class EachWithOptionalWithResultsNameTest(ParseTestCase):
     def runTest(self):
@@ -2932,7 +3089,7 @@ class SetNameTest(ParseTestCase):
             + | - term
             Forward: ?: term
             ?: term
-            Forward: {a | b | c [{d | e | f Forward: ...}]...}
+            Forward: {a | b | c [{d | e | f : ...}]...}
             int [, int]...
             (len) int...
             nested () expression
@@ -2943,6 +3100,7 @@ class SetNameTest(ParseTestCase):
 
         for t,e in zip(tests, expected):
             tname = str(t)
+            print_(tname)
             self.assertEqual(tname, e, "expression name mismatch, expected {0} got {1}".format(e, tname))
 
 class TrimArityExceptionMaskingTest(ParseTestCase):
@@ -3009,6 +3167,24 @@ class TrimArityExceptionMaskingTest2(ParseTestCase):
 
         K()
 
+
+class ClearParseActionsTest(ParseTestCase):
+    def runTest(self):
+        import pyparsing as pp
+        ppc = pp.pyparsing_common
+
+        realnum = ppc.real()
+        self.assertEqual(realnum.parseString("3.14159")[0], 3.14159, "failed basic real number parsing")
+
+        # clear parse action that converts to float
+        realnum.setParseAction(None)
+        self.assertEqual(realnum.parseString("3.14159")[0], "3.14159", "failed clearing parse action")
+
+        # add a new parse action that tests if a '.' is prsent
+        realnum.addParseAction(lambda t: '.' in t[0])
+        self.assertEqual(realnum.parseString("3.14159")[0], True,
+                         "failed setting new parse action after clearing parse action")
+
 class OneOrMoreStopTest(ParseTestCase):
     def runTest(self):
         from pyparsing import (Word, OneOrMore, alphas, Keyword, CaselessKeyword,
@@ -3021,6 +3197,10 @@ class OneOrMoreStopTest(ParseTestCase):
             expr = BEGIN + OneOrMore(body_word, stopOn=ender) + END
             self.assertEqual(test, expr, "Did not successfully stop on ending expression %r" % ender)
 
+            if PY_3:
+                expr = eval('BEGIN + body_word[...].stopOn(ender) + END')
+                self.assertEqual(test, expr, "Did not successfully stop on ending expression %r" % ender)
+
         number = Word(nums+',.()').setName("number with optional commas")
         parser= (OneOrMore(Word(alphanums+'-/.'), stopOn=number)('id').setParseAction(' '.join)
                     + number('data'))
@@ -3039,6 +3219,10 @@ class ZeroOrMoreStopTest(ParseTestCase):
             expr = BEGIN + ZeroOrMore(body_word, stopOn=ender) + END
             self.assertEqual(test, expr, "Did not successfully stop on ending expression %r" % ender)
 
+            if PY_3:
+                expr = eval('BEGIN + body_word[0, ...].stopOn(ender) + END')
+                self.assertEqual(test, expr, "Did not successfully stop on ending expression %r" % ender)
+
 class NestedAsDictTest(ParseTestCase):
     def runTest(self):
         from pyparsing import Literal, Forward, alphanums, Group, delimitedList, Dict, Word, Optional
@@ -3310,6 +3494,111 @@ class CommonExpressionsTest(ParseTestCase):
             self.assertEqual(type(result[0]), type(expected), "numeric parse failed (wrong type) (%s should be %s)" % (type(result[0]), type(expected)))
 
 
+class NumericExpressionsTest(ParseTestCase):
+    def runTest(self):
+        import pyparsing as pp
+        ppc = pp.pyparsing_common
+
+        # disable parse actions that do type conversion so we don't accidentally trigger
+        # conversion exceptions when what we want to check is the parsing expression
+        real = ppc.real().setParseAction(None)
+        sci_real = ppc.sci_real().setParseAction(None)
+        signed_integer = ppc.signed_integer().setParseAction(None)
+
+        from itertools import product
+
+        def make_tests():
+            leading_sign = ['+', '-', '']
+            leading_digit = ['0', '']
+            dot = ['.', '']
+            decimal_digit = ['1', '']
+            e = ['e', 'E', '']
+            e_sign = ['+', '-', '']
+            e_int = ['22', '']
+            stray = ['9', '.', '']
+
+            seen = set()
+            seen.add('')
+            for parts in product(leading_sign, stray, leading_digit, dot, decimal_digit, stray, e, e_sign, e_int,
+                                 stray):
+                parts_str = ''.join(parts).strip()
+                if parts_str in seen:
+                    continue
+                seen.add(parts_str)
+                yield parts_str
+
+            print_(len(seen)-1, "tests produced")
+
+        # collect tests into valid/invalid sets, depending on whether they evaluate to valid Python floats or ints
+        valid_ints = set()
+        valid_reals = set()
+        valid_sci_reals = set()
+        invalid_ints = set()
+        invalid_reals = set()
+        invalid_sci_reals = set()
+
+        # check which strings parse as valid floats or ints, and store in related valid or invalid test sets
+        for test_str in make_tests():
+            if '.' in test_str or 'e' in test_str.lower():
+                try:
+                    float(test_str)
+                except ValueError:
+                    invalid_sci_reals.add(test_str)
+                    if 'e' not in test_str.lower():
+                        invalid_reals.add(test_str)
+                else:
+                    valid_sci_reals.add(test_str)
+                    if 'e' not in test_str.lower():
+                        valid_reals.add(test_str)
+
+            try:
+                int(test_str)
+            except ValueError:
+                invalid_ints.add(test_str)
+            else:
+                valid_ints.add(test_str)
+
+        # now try all the test sets against their respective expressions
+        all_pass = True
+        suppress_results = {'printResults': False}
+        for expr, tests, is_fail, fn in zip([real, sci_real, signed_integer]*2,
+                                            [valid_reals, valid_sci_reals, valid_ints,
+                                             invalid_reals, invalid_sci_reals, invalid_ints],
+                                            [False, False, False, True, True, True],
+                                            [float, float, int]*2):
+            #
+            # success, test_results = expr.runTests(sorted(tests, key=len), failureTests=is_fail, **suppress_results)
+            # filter_result_fn = (lambda r: isinstance(r, Exception),
+            #                     lambda r: not isinstance(r, Exception))[is_fail]
+            # print_(expr, ('FAIL', 'PASS')[success], "{1}valid tests ({0})".format(len(tests),
+            #                                                                       'in' if is_fail else ''))
+            # if not success:
+            #     all_pass = False
+            #     for test_string, result in test_results:
+            #         if filter_result_fn(result):
+            #             try:
+            #                 test_value = fn(test_string)
+            #             except ValueError as ve:
+            #                 test_value = str(ve)
+            #             print_("{0!r}: {1} {2} {3}".format(test_string, result,
+            #                                                expr.matches(test_string, parseAll=True), test_value))
+
+            success = True
+            for t in tests:
+                if expr.matches(t, parseAll=True):
+                    if is_fail:
+                        print_(t, "should fail but did not")
+                        success = False
+                else:
+                    if not is_fail:
+                        print_(t, "should not fail but did")
+                        success = False
+            print_(expr, ('FAIL', 'PASS')[success], "{1}valid tests ({0})".format(len(tests),
+                                                                                  'in' if is_fail else ''))
+            all_pass = all_pass and success
+
+        self.assertTrue(all_pass, "failed one or more numeric tests")
+
 class TokenMapTest(ParseTestCase):
     def runTest(self):
         from pyparsing import tokenMap, Word, hexnums, OneOrMore
@@ -3817,12 +4106,78 @@ class UnicodeTests(ParseTestCase):
         self.assertEqual(result.asDict(), {u'şehir': u'İzmir', u'ülke': u'Türkiye', u'nüfus': 4279677},
                          "Failed to parse Turkish key-value pairs")
 
+
+class IndentedBlockExampleTest(ParseTestCase):
+    # Make sure example in indentedBlock docstring actually works!
+    def runTest(self):
+        from textwrap import dedent
+        from pyparsing import (Word, alphas, alphanums, indentedBlock, Optional, delimitedList, Group, Forward,
+                               nums, OneOrMore)
+        data = dedent('''
+        def A(z):
+          A1
+          B = 100
+          G = A2
+          A2
+          A3
+        B
+        def BB(a,b,c):
+          BB1
+          def BBA():
+            bba1
+            bba2
+            bba3
+        C
+        D
+        def spam(x,y):
+             def eggs(z):
+                 pass
+        ''')
+
+        indentStack = [1]
+        stmt = Forward()
+
+        identifier = Word(alphas, alphanums)
+        funcDecl = ("def" + identifier + Group("(" + Optional(delimitedList(identifier)) + ")") + ":")
+        func_body = indentedBlock(stmt, indentStack)
+        funcDef = Group(funcDecl + func_body)
+
+        rvalue = Forward()
+        funcCall = Group(identifier + "(" + Optional(delimitedList(rvalue)) + ")")
+        rvalue << (funcCall | identifier | Word(nums))
+        assignment = Group(identifier + "=" + rvalue)
+        stmt << (funcDef | assignment | identifier)
+
+        module_body = OneOrMore(stmt)
+
+        parseTree = module_body.parseString(data)
+        parseTree.pprint()
+        self.assertEqual(parseTree.asList(),
+                         [['def',
+                           'A',
+                           ['(', 'z', ')'],
+                           ':',
+                           [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],
+                          'B',
+                          ['def',
+                           'BB',
+                           ['(', 'a', 'b', 'c', ')'],
+                           ':',
+                           [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],
+                          'C',
+                          'D',
+                          ['def',
+                           'spam',
+                           ['(', 'x', 'y', ')'],
+                           ':',
+                           [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]],
+                         "Failed indentedBlock example"
+                         )
+
+
 class IndentedBlockTest(ParseTestCase):
     # parse pseudo-yaml indented text
     def runTest(self):
-        if pp.ParserElement.packrat_cache:
-            print_("cannot test indentedBlock with packrat enabled")
-            return
         import textwrap
 
         EQ = pp.Suppress('=')
@@ -3834,7 +4189,7 @@ class IndentedBlockTest(ParseTestCase):
         value <<= pp.pyparsing_common.integer | pp.QuotedString("'") | compound_value
         parser = pp.Dict(pp.OneOrMore(pp.Group(key_value)))
 
-        text = """\
+        text = """
             a = 100
             b = 101
             c =
@@ -3854,6 +4209,57 @@ class IndentedBlockTest(ParseTestCase):
         self.assertEqual(result.c.c2.c21, 999, "invalid indented block result")
 
 
+class IndentedBlockTest2(ParseTestCase):
+    # exercise indentedBlock with example posted in issue #87
+    def runTest(self):
+        from textwrap import dedent
+        from pyparsing import Word, alphas, alphanums, Suppress, Forward, indentedBlock, Literal, OneOrMore
+
+        indent_stack = [1]
+
+        key = Word(alphas, alphanums) + Suppress(":")
+        stmt = Forward()
+
+        suite = indentedBlock(stmt, indent_stack)
+        body = key + suite
+
+        pattern = (Word(alphas) + Suppress("(") + Word(alphas) + Suppress(")"))
+        stmt << pattern
+
+        def key_parse_action(toks):
+            print_("Parsing '%s'..." % toks[0])
+
+        key.setParseAction(key_parse_action)
+        header = Suppress("[") + Literal("test") + Suppress("]")
+        content = (header + OneOrMore(indentedBlock(body, indent_stack, False)))
+
+        contents = Forward()
+        suites = indentedBlock(content, indent_stack)
+
+        extra = Literal("extra") + Suppress(":") + suites
+        contents << (content | extra)
+
+        parser = OneOrMore(contents)
+
+        sample = dedent("""
+        extra:
+            [test]
+            one0: 
+                two (three)
+            four0:
+                five (seven)
+        extra:
+            [test]
+            one1: 
+                two (three)
+            four1:
+                five (seven)
+        """)
+
+        success, _ = parser.runTests([sample])
+        self.assertTrue(success, "Failed indentedBlock test for issue #87")
+
+
 class IndentedBlockScanTest(ParseTestCase):
     def get_parser(self):
         """
@@ -3945,10 +4351,15 @@ class ParseResultsWithNameMatchFirst(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')
-            expr.runTests("""\
+            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
             """)
@@ -3980,9 +4391,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
@@ -4087,6 +4503,126 @@ class CaselessKeywordVsKeywordCaselessTest(ParseTestCase):
         self.assertEqual(flist, clist, "CaselessKeyword not working the same as Keyword(caseless=True)")
 
 
+class OneOfKeywordsTest(ParseTestCase):
+    def runTest(self):
+        import pyparsing as pp
+
+        literal_expr = pp.oneOf("a b c")
+        success, _ = literal_expr[...].runTests("""
+            # literal oneOf tests
+            a b c
+            a a a
+            abc
+        """)
+        self.assertTrue(success, "failed literal oneOf matching")
+
+        keyword_expr = pp.oneOf("a b c", asKeyword=True)
+        success, _ = keyword_expr[...].runTests("""
+            # keyword oneOf tests
+            a b c
+            a a a
+        """)
+        self.assertTrue(success, "failed keyword oneOf matching")
+
+        success, _ = keyword_expr[...].runTests("""
+            # keyword oneOf failure tests
+            abc
+        """, failureTests=True)
+        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):
 
@@ -4333,16 +4869,17 @@ suite = makeTestSuite()
 
 if __name__ == '__main__':
 
-    testRunner = TextTestRunner()
-
     # run specific tests by including them in this list, otherwise
     # all tests will be run
     testclasses = [
         ]
 
     if not testclasses:
+        testRunner = TextTestRunner()
         result = testRunner.run(suite)
     else:
+        # disable chaser '.' display
+        testRunner = TextTestRunner(verbosity=0)
         BUFFER_OUTPUT = False
         result = testRunner.run(makeTestSuiteTemp(testclasses))