codekingpro/portable-devtools
114k
1# helpers.py2import html.entities3import re4import sys5import typing6 7from . import __diag__8from .core import *9from .util import (10 _bslash,11 _flatten,12 _escape_regex_range_chars,13 replaced_by_pep8,14)15 16 17#18# global helpers19#20def counted_array(21 expr: ParserElement,22 int_expr: typing.Optional[ParserElement] = None,23 *,24 intExpr: typing.Optional[ParserElement] = None,25) -> ParserElement:26 """Helper to define a counted list of expressions.27 28 This helper defines a pattern of the form::29 30 integer expr expr expr...31 32 where the leading integer tells how many expr expressions follow.33 The matched tokens returns the array of expr tokens as a list - the34 leading count token is suppressed.35 36 If ``int_expr`` is specified, it should be a pyparsing expression37 that produces an integer value.38 39 Example::40 41 counted_array(Word(alphas)).parse_string('2 ab cd ef') # -> ['ab', 'cd']42 43 # in this parser, the leading integer value is given in binary,44 # '10' indicating that 2 values are in the array45 binary_constant = Word('01').set_parse_action(lambda t: int(t[0], 2))46 counted_array(Word(alphas), int_expr=binary_constant).parse_string('10 ab cd ef') # -> ['ab', 'cd']47 48 # if other fields must be parsed after the count but before the49 # list items, give the fields results names and they will50 # be preserved in the returned ParseResults:51 count_with_metadata = integer + Word(alphas)("type")52 typed_array = counted_array(Word(alphanums), int_expr=count_with_metadata)("items")53 result = typed_array.parse_string("3 bool True True False")54 print(result.dump())55 56 # prints57 # ['True', 'True', 'False']58 # - items: ['True', 'True', 'False']59 # - type: 'bool'60 """61 intExpr = intExpr or int_expr62 array_expr = Forward()63 64 def count_field_parse_action(s, l, t):65 nonlocal array_expr66 n = t[0]67 array_expr <<= (expr * n) if n else Empty()68 # clear list contents, but keep any named results69 del t[:]70 71 if intExpr is None:72 intExpr = Word(nums).set_parse_action(lambda t: int(t[0]))73 else:74 intExpr = intExpr.copy()75 intExpr.set_name("arrayLen")76 intExpr.add_parse_action(count_field_parse_action, call_during_try=True)77 return (intExpr + array_expr).set_name(f"(len) {expr}...")78 79 80def match_previous_literal(expr: ParserElement) -> ParserElement:81 """Helper to define an expression that is indirectly defined from82 the tokens matched in a previous expression, that is, it looks for83 a 'repeat' of a previous expression. For example::84 85 first = Word(nums)86 second = match_previous_literal(first)87 match_expr = first + ":" + second88 89 will match ``"1:1"``, but not ``"1:2"``. Because this90 matches a previous literal, will also match the leading91 ``"1:1"`` in ``"1:10"``. If this is not desired, use92 :class:`match_previous_expr`. Do *not* use with packrat parsing93 enabled.94 """95 rep = Forward()96 97 def copy_token_to_repeater(s, l, t):98 if not t:99 rep << Empty()100 return101 102 if len(t) == 1:103 rep << t[0]104 return105 106 # flatten t tokens107 tflat = _flatten(t.as_list())108 rep << And(Literal(tt) for tt in tflat)109 110 expr.add_parse_action(copy_token_to_repeater, callDuringTry=True)111 rep.set_name("(prev) " + str(expr))112 return rep113 114 115def match_previous_expr(expr: ParserElement) -> ParserElement:116 """Helper to define an expression that is indirectly defined from117 the tokens matched in a previous expression, that is, it looks for118 a 'repeat' of a previous expression. For example::119 120 first = Word(nums)121 second = match_previous_expr(first)122 match_expr = first + ":" + second123 124 will match ``"1:1"``, but not ``"1:2"``. Because this125 matches by expressions, will *not* match the leading ``"1:1"``126 in ``"1:10"``; the expressions are evaluated first, and then127 compared, so ``"1"`` is compared with ``"10"``. Do *not* use128 with packrat parsing enabled.129 """130 rep = Forward()131 e2 = expr.copy()132 rep <<= e2133 134 def copy_token_to_repeater(s, l, t):135 matchTokens = _flatten(t.as_list())136 137 def must_match_these_tokens(s, l, t):138 theseTokens = _flatten(t.as_list())139 if theseTokens != matchTokens:140 raise ParseException(141 s, l, f"Expected {matchTokens}, found{theseTokens}"142 )143 144 rep.set_parse_action(must_match_these_tokens, callDuringTry=True)145 146 expr.add_parse_action(copy_token_to_repeater, callDuringTry=True)147 rep.set_name("(prev) " + str(expr))148 return rep149 150 151def one_of(152 strs: Union[typing.Iterable[str], str],153 caseless: bool = False,154 use_regex: bool = True,155 as_keyword: bool = False,156 *,157 useRegex: bool = True,158 asKeyword: bool = False,159) -> ParserElement:160 """Helper to quickly define a set of alternative :class:`Literal` s,161 and makes sure to do longest-first testing when there is a conflict,162 regardless of the input order, but returns163 a :class:`MatchFirst` for best performance.164 165 Parameters:166 167 - ``strs`` - a string of space-delimited literals, or a collection of168 string literals169 - ``caseless`` - treat all literals as caseless - (default= ``False``)170 - ``use_regex`` - as an optimization, will171 generate a :class:`Regex` object; otherwise, will generate172 a :class:`MatchFirst` object (if ``caseless=True`` or ``as_keyword=True``, or if173 creating a :class:`Regex` raises an exception) - (default= ``True``)174 - ``as_keyword`` - enforce :class:`Keyword`-style matching on the175 generated expressions - (default= ``False``)176 - ``asKeyword`` and ``useRegex`` are retained for pre-PEP8 compatibility,177 but will be removed in a future release178 179 Example::180 181 comp_oper = one_of("< = > <= >= !=")182 var = Word(alphas)183 number = Word(nums)184 term = var | number185 comparison_expr = term + comp_oper + term186 print(comparison_expr.search_string("B = 12 AA=23 B<=AA AA>12"))187 188 prints::189 190 [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]191 """192 asKeyword = asKeyword or as_keyword193 useRegex = useRegex and use_regex194 195 if (196 isinstance(caseless, str_type)197 and __diag__.warn_on_multiple_string_args_to_oneof198 ):199 warnings.warn(200 "More than one string argument passed to one_of, pass"201 " choices as a list or space-delimited string",202 stacklevel=2,203 )204 205 if caseless:206 isequal = lambda a, b: a.upper() == b.upper()207 masks = lambda a, b: b.upper().startswith(a.upper())208 parseElementClass = CaselessKeyword if asKeyword else CaselessLiteral209 else:210 isequal = lambda a, b: a == b211 masks = lambda a, b: b.startswith(a)212 parseElementClass = Keyword if asKeyword else Literal213 214 symbols: List[str] = []215 if isinstance(strs, str_type):216 strs = typing.cast(str, strs)217 symbols = strs.split()218 elif isinstance(strs, Iterable):219 symbols = list(strs)220 else:221 raise TypeError("Invalid argument to one_of, expected string or iterable")222 if not symbols:223 return NoMatch()224 225 # reorder given symbols to take care to avoid masking longer choices with shorter ones226 # (but only if the given symbols are not just single characters)227 if any(len(sym) > 1 for sym in symbols):228 i = 0229 while i < len(symbols) - 1:230 cur = symbols[i]231 for j, other in enumerate(symbols[i + 1 :]):232 if isequal(other, cur):233 del symbols[i + j + 1]234 break235 if masks(cur, other):236 del symbols[i + j + 1]237 symbols.insert(i, other)238 break239 else:240 i += 1241 242 if useRegex:243 re_flags: int = re.IGNORECASE if caseless else 0244 245 try:246 if all(len(sym) == 1 for sym in symbols):247 # symbols are just single characters, create range regex pattern248 patt = f"[{''.join(_escape_regex_range_chars(sym) for sym in symbols)}]"249 else:250 patt = "|".join(re.escape(sym) for sym in symbols)251 252 # wrap with \b word break markers if defining as keywords253 if asKeyword:254 patt = rf"\b(?:{patt})\b"255 256 ret = Regex(patt, flags=re_flags).set_name(" | ".join(symbols))257 258 if caseless:259 # add parse action to return symbols as specified, not in random260 # casing as found in input string261 symbol_map = {sym.lower(): sym for sym in symbols}262 ret.add_parse_action(lambda s, l, t: symbol_map[t[0].lower()])263 264 return ret265 266 except re.error:267 warnings.warn(268 "Exception creating Regex for one_of, building MatchFirst", stacklevel=2269 )270 271 # last resort, just use MatchFirst272 return MatchFirst(parseElementClass(sym) for sym in symbols).set_name(273 " | ".join(symbols)274 )275 276 277def dict_of(key: ParserElement, value: ParserElement) -> ParserElement:278 """Helper to easily and clearly define a dictionary by specifying279 the respective patterns for the key and value. Takes care of280 defining the :class:`Dict`, :class:`ZeroOrMore`, and281 :class:`Group` tokens in the proper order. The key pattern282 can include delimiting markers or punctuation, as long as they are283 suppressed, thereby leaving the significant key text. The value284 pattern can include named results, so that the :class:`Dict` results285 can include named token fields.286 287 Example::288 289 text = "shape: SQUARE posn: upper left color: light blue texture: burlap"290 attr_expr = (label + Suppress(':') + OneOrMore(data_word, stop_on=label).set_parse_action(' '.join))291 print(attr_expr[1, ...].parse_string(text).dump())292 293 attr_label = label294 attr_value = Suppress(':') + OneOrMore(data_word, stop_on=label).set_parse_action(' '.join)295 296 # similar to Dict, but simpler call format297 result = dict_of(attr_label, attr_value).parse_string(text)298 print(result.dump())299 print(result['shape'])300 print(result.shape) # object attribute access works too301 print(result.as_dict())302 303 prints::304 305 [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]306 - color: 'light blue'307 - posn: 'upper left'308 - shape: 'SQUARE'309 - texture: 'burlap'310 SQUARE311 SQUARE312 {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'}313 """314 return Dict(OneOrMore(Group(key + value)))315 316 317def original_text_for(318 expr: ParserElement, as_string: bool = True, *, asString: bool = True319) -> ParserElement:320 """Helper to return the original, untokenized text for a given321 expression. Useful to restore the parsed fields of an HTML start322 tag into the raw tag text itself, or to revert separate tokens with323 intervening whitespace back to the original matching input text. By324 default, returns a string containing the original parsed text.325 326 If the optional ``as_string`` argument is passed as327 ``False``, then the return value is328 a :class:`ParseResults` containing any results names that329 were originally matched, and a single token containing the original330 matched text from the input string. So if the expression passed to331 :class:`original_text_for` contains expressions with defined332 results names, you must set ``as_string`` to ``False`` if you333 want to preserve those results name values.334 335 The ``asString`` pre-PEP8 argument is retained for compatibility,336 but will be removed in a future release.337 338 Example::339 340 src = "this is test <b> bold <i>text</i> </b> normal text "341 for tag in ("b", "i"):342 opener, closer = make_html_tags(tag)343 patt = original_text_for(opener + ... + closer)344 print(patt.search_string(src)[0])345 346 prints::347 348 ['<b> bold <i>text</i> </b>']349 ['<i>text</i>']350 """351 asString = asString and as_string352 353 locMarker = Empty().set_parse_action(lambda s, loc, t: loc)354 endlocMarker = locMarker.copy()355 endlocMarker.callPreparse = False356 matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")357 if asString:358 extractText = lambda s, l, t: s[t._original_start : t._original_end]359 else:360 361 def extractText(s, l, t):362 t[:] = [s[t.pop("_original_start") : t.pop("_original_end")]]363 364 matchExpr.set_parse_action(extractText)365 matchExpr.ignoreExprs = expr.ignoreExprs366 matchExpr.suppress_warning(Diagnostics.warn_ungrouped_named_tokens_in_collection)367 return matchExpr368 369 370def ungroup(expr: ParserElement) -> ParserElement:371 """Helper to undo pyparsing's default grouping of And expressions,372 even if all but one are non-empty.373 """374 return TokenConverter(expr).add_parse_action(lambda t: t[0])375 376 377def locatedExpr(expr: ParserElement) -> ParserElement:378 """379 (DEPRECATED - future code should use the :class:`Located` class)380 Helper to decorate a returned token with its starting and ending381 locations in the input string.382 383 This helper adds the following results names:384 385 - ``locn_start`` - location where matched expression begins386 - ``locn_end`` - location where matched expression ends387 - ``value`` - the actual parsed results388 389 Be careful if the input text contains ``<TAB>`` characters, you390 may want to call :class:`ParserElement.parse_with_tabs`391 392 Example::393 394 wd = Word(alphas)395 for match in locatedExpr(wd).search_string("ljsdf123lksdjjf123lkkjj1222"):396 print(match)397 398 prints::399 400 [[0, 'ljsdf', 5]]401 [[8, 'lksdjjf', 15]]402 [[18, 'lkkjj', 23]]403 """404 locator = Empty().set_parse_action(lambda ss, ll, tt: ll)405 return Group(406 locator("locn_start")407 + expr("value")408 + locator.copy().leaveWhitespace()("locn_end")409 )410 411 412def nested_expr(413 opener: Union[str, ParserElement] = "(",414 closer: Union[str, ParserElement] = ")",415 content: typing.Optional[ParserElement] = None,416 ignore_expr: ParserElement = quoted_string(),417 *,418 ignoreExpr: ParserElement = quoted_string(),419) -> ParserElement:420 """Helper method for defining nested lists enclosed in opening and421 closing delimiters (``"("`` and ``")"`` are the default).422 423 Parameters:424 425 - ``opener`` - opening character for a nested list426 (default= ``"("``); can also be a pyparsing expression427 - ``closer`` - closing character for a nested list428 (default= ``")"``); can also be a pyparsing expression429 - ``content`` - expression for items within the nested lists430 (default= ``None``)431 - ``ignore_expr`` - expression for ignoring opening and closing delimiters432 (default= :class:`quoted_string`)433 - ``ignoreExpr`` - this pre-PEP8 argument is retained for compatibility434 but will be removed in a future release435 436 If an expression is not provided for the content argument, the437 nested expression will capture all whitespace-delimited content438 between delimiters as a list of separate values.439 440 Use the ``ignore_expr`` argument to define expressions that may441 contain opening or closing characters that should not be treated as442 opening or closing characters for nesting, such as quoted_string or443 a comment expression. Specify multiple expressions using an444 :class:`Or` or :class:`MatchFirst`. The default is445 :class:`quoted_string`, but if no expressions are to be ignored, then446 pass ``None`` for this argument.447 448 Example::449 450 data_type = one_of("void int short long char float double")451 decl_data_type = Combine(data_type + Opt(Word('*')))452 ident = Word(alphas+'_', alphanums+'_')453 number = pyparsing_common.number454 arg = Group(decl_data_type + ident)455 LPAR, RPAR = map(Suppress, "()")456 457 code_body = nested_expr('{', '}', ignore_expr=(quoted_string | c_style_comment))458 459 c_function = (decl_data_type("type")460 + ident("name")461 + LPAR + Opt(DelimitedList(arg), [])("args") + RPAR462 + code_body("body"))463 c_function.ignore(c_style_comment)464 465 source_code = '''466 int is_odd(int x) {467 return (x%2);468 }469 470 int dec_to_hex(char hchar) {471 if (hchar >= '0' && hchar <= '9') {472 return (ord(hchar)-ord('0'));473 } else {474 return (10+ord(hchar)-ord('A'));475 }476 }477 '''478 for func in c_function.search_string(source_code):479 print("%(name)s (%(type)s) args: %(args)s" % func)480 481 482 prints::483 484 is_odd (int) args: [['int', 'x']]485 dec_to_hex (int) args: [['char', 'hchar']]486 """487 if ignoreExpr != ignore_expr:488 ignoreExpr = ignore_expr if ignoreExpr == quoted_string() else ignoreExpr489 if opener == closer:490 raise ValueError("opening and closing strings cannot be the same")491 if content is None:492 if isinstance(opener, str_type) and isinstance(closer, str_type):493 opener = typing.cast(str, opener)494 closer = typing.cast(str, closer)495 if len(opener) == 1 and len(closer) == 1:496 if ignoreExpr is not None:497 content = Combine(498 OneOrMore(499 ~ignoreExpr500 + CharsNotIn(501 opener + closer + ParserElement.DEFAULT_WHITE_CHARS,502 exact=1,503 )504 )505 ).set_parse_action(lambda t: t[0].strip())506 else:507 content = empty.copy() + CharsNotIn(508 opener + closer + ParserElement.DEFAULT_WHITE_CHARS509 ).set_parse_action(lambda t: t[0].strip())510 else:511 if ignoreExpr is not None:512 content = Combine(513 OneOrMore(514 ~ignoreExpr515 + ~Literal(opener)516 + ~Literal(closer)517 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)518 )519 ).set_parse_action(lambda t: t[0].strip())520 else:521 content = Combine(522 OneOrMore(523 ~Literal(opener)524 + ~Literal(closer)525 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)526 )527 ).set_parse_action(lambda t: t[0].strip())528 else:529 raise ValueError(530 "opening and closing arguments must be strings if no content expression is given"531 )532 ret = Forward()533 if ignoreExpr is not None:534 ret <<= Group(535 Suppress(opener) + ZeroOrMore(ignoreExpr | ret | content) + Suppress(closer)536 )537 else:538 ret <<= Group(Suppress(opener) + ZeroOrMore(ret | content) + Suppress(closer))539 ret.set_name(f"nested {opener}{closer} expression")540 # don't override error message from content expressions541 ret.errmsg = None542 return ret543 544 545def _makeTags(tagStr, xml, suppress_LT=Suppress("<"), suppress_GT=Suppress(">")):546 """Internal helper to construct opening and closing tag expressions, given a tag name"""547 if isinstance(tagStr, str_type):548 resname = tagStr549 tagStr = Keyword(tagStr, caseless=not xml)550 else:551 resname = tagStr.name552 553 tagAttrName = Word(alphas, alphanums + "_-:")554 if xml:555 tagAttrValue = dbl_quoted_string.copy().set_parse_action(remove_quotes)556 openTag = (557 suppress_LT558 + tagStr("tag")559 + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue)))560 + Opt("/", default=[False])("empty").set_parse_action(561 lambda s, l, t: t[0] == "/"562 )563 + suppress_GT564 )565 else:566 tagAttrValue = quoted_string.copy().set_parse_action(remove_quotes) | Word(567 printables, exclude_chars=">"568 )569 openTag = (570 suppress_LT571 + tagStr("tag")572 + Dict(573 ZeroOrMore(574 Group(575 tagAttrName.set_parse_action(lambda t: t[0].lower())576 + Opt(Suppress("=") + tagAttrValue)577 )578 )579 )580 + Opt("/", default=[False])("empty").set_parse_action(581 lambda s, l, t: t[0] == "/"582 )583 + suppress_GT584 )585 closeTag = Combine(Literal("</") + tagStr + ">", adjacent=False)586 587 openTag.set_name(f"<{resname}>")588 # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels589 openTag.add_parse_action(590 lambda t: t.__setitem__(591 "start" + "".join(resname.replace(":", " ").title().split()), t.copy()592 )593 )594 closeTag = closeTag(595 "end" + "".join(resname.replace(":", " ").title().split())596 ).set_name(f"</{resname}>")597 openTag.tag = resname598 closeTag.tag = resname599 openTag.tag_body = SkipTo(closeTag())600 return openTag, closeTag601 602 603def make_html_tags(604 tag_str: Union[str, ParserElement]605) -> Tuple[ParserElement, ParserElement]:606 """Helper to construct opening and closing tag expressions for HTML,607 given a tag name. Matches tags in either upper or lower case,608 attributes with namespaces and with quoted or unquoted values.609 610 Example::611 612 text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'613 # make_html_tags returns pyparsing expressions for the opening and614 # closing tags as a 2-tuple615 a, a_end = make_html_tags("A")616 link_expr = a + SkipTo(a_end)("link_text") + a_end617 618 for link in link_expr.search_string(text):619 # attributes in the <A> tag (like "href" shown here) are620 # also accessible as named results621 print(link.link_text, '->', link.href)622 623 prints::624 625 pyparsing -> https://github.com/pyparsing/pyparsing/wiki626 """627 return _makeTags(tag_str, False)628 629 630def make_xml_tags(631 tag_str: Union[str, ParserElement]632) -> Tuple[ParserElement, ParserElement]:633 """Helper to construct opening and closing tag expressions for XML,634 given a tag name. Matches tags only in the given upper/lower case.635 636 Example: similar to :class:`make_html_tags`637 """638 return _makeTags(tag_str, True)639 640 641any_open_tag: ParserElement642any_close_tag: ParserElement643any_open_tag, any_close_tag = make_html_tags(644 Word(alphas, alphanums + "_:").set_name("any tag")645)646 647_htmlEntityMap = {k.rstrip(";"): v for k, v in html.entities.html5.items()}648common_html_entity = Regex("&(?P<entity>" + "|".join(_htmlEntityMap) + ");").set_name(649 "common HTML entity"650)651 652 653def replace_html_entity(s, l, t):654 """Helper parser action to replace common HTML entities with their special characters"""655 return _htmlEntityMap.get(t.entity)656 657 658class OpAssoc(Enum):659 """Enumeration of operator associativity660 - used in constructing InfixNotationOperatorSpec for :class:`infix_notation`"""661 662 LEFT = 1663 RIGHT = 2664 665 666InfixNotationOperatorArgType = Union[667 ParserElement, str, Tuple[Union[ParserElement, str], Union[ParserElement, str]]668]669InfixNotationOperatorSpec = Union[670 Tuple[671 InfixNotationOperatorArgType,672 int,673 OpAssoc,674 typing.Optional[ParseAction],675 ],676 Tuple[677 InfixNotationOperatorArgType,678 int,679 OpAssoc,680 ],681]682 683 684def infix_notation(685 base_expr: ParserElement,686 op_list: List[InfixNotationOperatorSpec],687 lpar: Union[str, ParserElement] = Suppress("("),688 rpar: Union[str, ParserElement] = Suppress(")"),689) -> ParserElement:690 """Helper method for constructing grammars of expressions made up of691 operators working in a precedence hierarchy. Operators may be unary692 or binary, left- or right-associative. Parse actions can also be693 attached to operator expressions. The generated parser will also694 recognize the use of parentheses to override operator precedences695 (see example below).696 697 Note: if you define a deep operator list, you may see performance698 issues when using infix_notation. See699 :class:`ParserElement.enable_packrat` for a mechanism to potentially700 improve your parser performance.701 702 Parameters:703 704 - ``base_expr`` - expression representing the most basic operand to705 be used in the expression706 - ``op_list`` - list of tuples, one for each operator precedence level707 in the expression grammar; each tuple is of the form ``(op_expr,708 num_operands, right_left_assoc, (optional)parse_action)``, where:709 710 - ``op_expr`` is the pyparsing expression for the operator; may also711 be a string, which will be converted to a Literal; if ``num_operands``712 is 3, ``op_expr`` is a tuple of two expressions, for the two713 operators separating the 3 terms714 - ``num_operands`` is the number of terms for this operator (must be 1,715 2, or 3)716 - ``right_left_assoc`` is the indicator whether the operator is right717 or left associative, using the pyparsing-defined constants718 ``OpAssoc.RIGHT`` and ``OpAssoc.LEFT``.719 - ``parse_action`` is the parse action to be associated with720 expressions matching this operator expression (the parse action721 tuple member may be omitted); if the parse action is passed722 a tuple or list of functions, this is equivalent to calling723 ``set_parse_action(*fn)``724 (:class:`ParserElement.set_parse_action`)725 - ``lpar`` - expression for matching left-parentheses; if passed as a726 str, then will be parsed as ``Suppress(lpar)``. If lpar is passed as727 an expression (such as ``Literal('(')``), then it will be kept in728 the parsed results, and grouped with them. (default= ``Suppress('(')``)729 - ``rpar`` - expression for matching right-parentheses; if passed as a730 str, then will be parsed as ``Suppress(rpar)``. If rpar is passed as731 an expression (such as ``Literal(')')``), then it will be kept in732 the parsed results, and grouped with them. (default= ``Suppress(')')``)733 734 Example::735 736 # simple example of four-function arithmetic with ints and737 # variable names738 integer = pyparsing_common.signed_integer739 varname = pyparsing_common.identifier740 741 arith_expr = infix_notation(integer | varname,742 [743 ('-', 1, OpAssoc.RIGHT),744 (one_of('* /'), 2, OpAssoc.LEFT),745 (one_of('+ -'), 2, OpAssoc.LEFT),746 ])747 748 arith_expr.run_tests('''749 5+3*6750 (5+3)*6751 -2--11752 ''', full_dump=False)753 754 prints::755 756 5+3*6757 [[5, '+', [3, '*', 6]]]758 759 (5+3)*6760 [[[5, '+', 3], '*', 6]]761 762 (5+x)*y763 [[[5, '+', 'x'], '*', 'y']]764 765 -2--11766 [[['-', 2], '-', ['-', 11]]]767 """768 769 # captive version of FollowedBy that does not do parse actions or capture results names770 class _FB(FollowedBy):771 def parseImpl(self, instring, loc, doActions=True):772 self.expr.try_parse(instring, loc)773 return loc, []774 775 _FB.__name__ = "FollowedBy>"776 777 ret = Forward()778 if isinstance(lpar, str):779 lpar = Suppress(lpar)780 if isinstance(rpar, str):781 rpar = Suppress(rpar)782 783 # if lpar and rpar are not suppressed, wrap in group784 if not (isinstance(lpar, Suppress) and isinstance(rpar, Suppress)):785 lastExpr = base_expr | Group(lpar + ret + rpar)786 else:787 lastExpr = base_expr | (lpar + ret + rpar)788 789 arity: int790 rightLeftAssoc: opAssoc791 pa: typing.Optional[ParseAction]792 opExpr1: ParserElement793 opExpr2: ParserElement794 for operDef in op_list:795 opExpr, arity, rightLeftAssoc, pa = (operDef + (None,))[:4] # type: ignore[assignment]796 if isinstance(opExpr, str_type):797 opExpr = ParserElement._literalStringClass(opExpr)798 opExpr = typing.cast(ParserElement, opExpr)799 if arity == 3:800 if not isinstance(opExpr, (tuple, list)) or len(opExpr) != 2:801 raise ValueError(802 "if numterms=3, opExpr must be a tuple or list of two expressions"803 )804 opExpr1, opExpr2 = opExpr805 term_name = f"{opExpr1}{opExpr2} term"806 else:807 term_name = f"{opExpr} term"808 809 if not 1 <= arity <= 3:810 raise ValueError("operator must be unary (1), binary (2), or ternary (3)")811 812 if rightLeftAssoc not in (OpAssoc.LEFT, OpAssoc.RIGHT):813 raise ValueError("operator must indicate right or left associativity")814 815 thisExpr: ParserElement = Forward().set_name(term_name)816 thisExpr = typing.cast(Forward, thisExpr)817 if rightLeftAssoc is OpAssoc.LEFT:818 if arity == 1:819 matchExpr = _FB(lastExpr + opExpr) + Group(lastExpr + opExpr[1, ...])820 elif arity == 2:821 if opExpr is not None:822 matchExpr = _FB(lastExpr + opExpr + lastExpr) + Group(823 lastExpr + (opExpr + lastExpr)[1, ...]824 )825 else:826 matchExpr = _FB(lastExpr + lastExpr) + Group(lastExpr[2, ...])827 elif arity == 3:828 matchExpr = _FB(829 lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr830 ) + Group(lastExpr + OneOrMore(opExpr1 + lastExpr + opExpr2 + lastExpr))831 elif rightLeftAssoc is OpAssoc.RIGHT:832 if arity == 1:833 # try to avoid LR with this extra test834 if not isinstance(opExpr, Opt):835 opExpr = Opt(opExpr)836 matchExpr = _FB(opExpr.expr + thisExpr) + Group(opExpr + thisExpr)837 elif arity == 2:838 if opExpr is not None:839 matchExpr = _FB(lastExpr + opExpr + thisExpr) + Group(840 lastExpr + (opExpr + thisExpr)[1, ...]841 )842 else:843 matchExpr = _FB(lastExpr + thisExpr) + Group(844 lastExpr + thisExpr[1, ...]845 )846 elif arity == 3:847 matchExpr = _FB(848 lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr849 ) + Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)850 if pa:851 if isinstance(pa, (tuple, list)):852 matchExpr.set_parse_action(*pa)853 else:854 matchExpr.set_parse_action(pa)855 thisExpr <<= (matchExpr | lastExpr).setName(term_name)856 lastExpr = thisExpr857 ret <<= lastExpr858 return ret859 860 861def indentedBlock(blockStatementExpr, indentStack, indent=True, backup_stacks=[]):862 """863 (DEPRECATED - use :class:`IndentedBlock` class instead)864 Helper method for defining space-delimited indentation blocks,865 such as those used to define block statements in Python source code.866 867 Parameters:868 869 - ``blockStatementExpr`` - expression defining syntax of statement that870 is repeated within the indented block871 - ``indentStack`` - list created by caller to manage indentation stack872 (multiple ``statementWithIndentedBlock`` expressions within a single873 grammar should share a common ``indentStack``)874 - ``indent`` - boolean indicating whether block must be indented beyond875 the current level; set to ``False`` for block of left-most statements876 (default= ``True``)877 878 A valid block must contain at least one ``blockStatement``.879 880 (Note that indentedBlock uses internal parse actions which make it881 incompatible with packrat parsing.)882 883 Example::884 885 data = '''886 def A(z):887 A1888 B = 100889 G = A2890 A2891 A3892 B893 def BB(a,b,c):894 BB1895 def BBA():896 bba1897 bba2898 bba3899 C900 D901 def spam(x,y):902 def eggs(z):903 pass904 '''905 906 907 indentStack = [1]908 stmt = Forward()909 910 identifier = Word(alphas, alphanums)911 funcDecl = ("def" + identifier + Group("(" + Opt(delimitedList(identifier)) + ")") + ":")912 func_body = indentedBlock(stmt, indentStack)913 funcDef = Group(funcDecl + func_body)914 915 rvalue = Forward()916 funcCall = Group(identifier + "(" + Opt(delimitedList(rvalue)) + ")")917 rvalue << (funcCall | identifier | Word(nums))918 assignment = Group(identifier + "=" + rvalue)919 stmt << (funcDef | assignment | identifier)920 921 module_body = stmt[1, ...]922 923 parseTree = module_body.parseString(data)924 parseTree.pprint()925 926 prints::927 928 [['def',929 'A',930 ['(', 'z', ')'],931 ':',932 [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],933 'B',934 ['def',935 'BB',936 ['(', 'a', 'b', 'c', ')'],937 ':',938 [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],939 'C',940 'D',941 ['def',942 'spam',943 ['(', 'x', 'y', ')'],944 ':',945 [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]]946 """947 backup_stacks.append(indentStack[:])948 949 def reset_stack():950 indentStack[:] = backup_stacks[-1]951 952 def checkPeerIndent(s, l, t):953 if l >= len(s):954 return955 curCol = col(l, s)956 if curCol != indentStack[-1]:957 if curCol > indentStack[-1]:958 raise ParseException(s, l, "illegal nesting")959 raise ParseException(s, l, "not a peer entry")960 961 def checkSubIndent(s, l, t):962 curCol = col(l, s)963 if curCol > indentStack[-1]:964 indentStack.append(curCol)965 else:966 raise ParseException(s, l, "not a subentry")967 968 def checkUnindent(s, l, t):969 if l >= len(s):970 return971 curCol = col(l, s)972 if not (indentStack and curCol in indentStack):973 raise ParseException(s, l, "not an unindent")974 if curCol < indentStack[-1]:975 indentStack.pop()976 977 NL = OneOrMore(LineEnd().set_whitespace_chars("\t ").suppress())978 INDENT = (Empty() + Empty().set_parse_action(checkSubIndent)).set_name("INDENT")979 PEER = Empty().set_parse_action(checkPeerIndent).set_name("")980 UNDENT = Empty().set_parse_action(checkUnindent).set_name("UNINDENT")981 if indent:982 smExpr = Group(983 Opt(NL)984 + INDENT985 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))986 + UNDENT987 )988 else:989 smExpr = Group(990 Opt(NL)991 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))992 + Opt(UNDENT)993 )994 995 # add a parse action to remove backup_stack from list of backups996 smExpr.add_parse_action(997 lambda: backup_stacks.pop(-1) and None if backup_stacks else None998 )999 smExpr.set_fail_action(lambda a, b, c, d: reset_stack())1000 blockStatementExpr.ignore(_bslash + LineEnd())1001 return smExpr.set_name("indented block")1002 1003 1004# it's easy to get these comment structures wrong - they're very common, so may as well make them available1005c_style_comment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + "*/").set_name(1006 "C style comment"1007)1008"Comment of the form ``/* ... */``"1009 1010html_comment = Regex(r"<!--[\s\S]*?-->").set_name("HTML comment")1011"Comment of the form ``<!-- ... -->``"1012 1013rest_of_line = Regex(r".*").leave_whitespace().set_name("rest of line")1014dbl_slash_comment = Regex(r"//(?:\\\n|[^\n])*").set_name("// comment")1015"Comment of the form ``// ... (to end of line)``"1016 1017cpp_style_comment = Combine(1018 Regex(r"/\*(?:[^*]|\*(?!/))*") + "*/" | dbl_slash_comment1019).set_name("C++ style comment")1020"Comment of either form :class:`c_style_comment` or :class:`dbl_slash_comment`"1021 1022java_style_comment = cpp_style_comment1023"Same as :class:`cpp_style_comment`"1024 1025python_style_comment = Regex(r"#.*").set_name("Python style comment")1026"Comment of the form ``# ... (to end of line)``"1027 1028 1029# build list of built-in expressions, for future reference if a global default value1030# gets updated1031_builtin_exprs: List[ParserElement] = [1032 v for v in vars().values() if isinstance(v, ParserElement)1033]1034 1035 1036# compatibility function, superseded by DelimitedList class1037def delimited_list(1038 expr: Union[str, ParserElement],1039 delim: Union[str, ParserElement] = ",",1040 combine: bool = False,1041 min: typing.Optional[int] = None,1042 max: typing.Optional[int] = None,1043 *,1044 allow_trailing_delim: bool = False,1045) -> ParserElement:1046 """(DEPRECATED - use :class:`DelimitedList` class)"""1047 return DelimitedList(1048 expr, delim, combine, min, max, allow_trailing_delim=allow_trailing_delim1049 )1050 1051 1052# pre-PEP8 compatible names1053# fmt: off1054opAssoc = OpAssoc1055anyOpenTag = any_open_tag1056anyCloseTag = any_close_tag1057commonHTMLEntity = common_html_entity1058cStyleComment = c_style_comment1059htmlComment = html_comment1060restOfLine = rest_of_line1061dblSlashComment = dbl_slash_comment1062cppStyleComment = cpp_style_comment1063javaStyleComment = java_style_comment1064pythonStyleComment = python_style_comment1065delimitedList = replaced_by_pep8("delimitedList", DelimitedList)1066delimited_list = replaced_by_pep8("delimited_list", DelimitedList)1067countedArray = replaced_by_pep8("countedArray", counted_array)1068matchPreviousLiteral = replaced_by_pep8("matchPreviousLiteral", match_previous_literal)1069matchPreviousExpr = replaced_by_pep8("matchPreviousExpr", match_previous_expr)1070oneOf = replaced_by_pep8("oneOf", one_of)1071dictOf = replaced_by_pep8("dictOf", dict_of)1072originalTextFor = replaced_by_pep8("originalTextFor", original_text_for)1073nestedExpr = replaced_by_pep8("nestedExpr", nested_expr)1074makeHTMLTags = replaced_by_pep8("makeHTMLTags", make_html_tags)1075makeXMLTags = replaced_by_pep8("makeXMLTags", make_xml_tags)1076replaceHTMLEntity = replaced_by_pep8("replaceHTMLEntity", replace_html_entity)1077infixNotation = replaced_by_pep8("infixNotation", infix_notation)1078# fmt: on1079 