Aluode/PerceptionLabPortable
0
1# helpers.py2import html.entities3import operator4import re5import sys6import typing7 8from . import __diag__9from .core import *10from .util import (11 _bslash,12 _flatten,13 _escape_regex_range_chars,14 make_compressed_re,15 replaced_by_pep8,16)17 18 19def _suppression(expr: Union[ParserElement, str]) -> ParserElement:20 # internal helper to avoid wrapping Suppress inside another Suppress21 if isinstance(expr, Suppress):22 return expr23 return Suppress(expr)24 25 26#27# global helpers28#29def counted_array(30 expr: ParserElement, int_expr: typing.Optional[ParserElement] = None, **kwargs31) -> ParserElement:32 """Helper to define a counted list of expressions.33 34 This helper defines a pattern of the form::35 36 integer expr expr expr...37 38 where the leading integer tells how many expr expressions follow.39 The matched tokens returns the array of expr tokens as a list - the40 leading count token is suppressed.41 42 If ``int_expr`` is specified, it should be a pyparsing expression43 that produces an integer value.44 45 Examples:46 47 .. doctest::48 49 >>> counted_array(Word(alphas)).parse_string('2 ab cd ef')50 ParseResults(['ab', 'cd'], {})51 52 - In this parser, the leading integer value is given in binary,53 '10' indicating that 2 values are in the array:54 55 .. doctest::56 57 >>> binary_constant = Word('01').set_parse_action(lambda t: int(t[0], 2))58 >>> counted_array(Word(alphas), int_expr=binary_constant59 ... ).parse_string('10 ab cd ef')60 ParseResults(['ab', 'cd'], {})61 62 - If other fields must be parsed after the count but before the63 list items, give the fields results names and they will64 be preserved in the returned ParseResults:65 66 .. doctest::67 68 >>> ppc = pyparsing.common69 >>> count_with_metadata = ppc.integer + Word(alphas)("type")70 >>> typed_array = counted_array(Word(alphanums),71 ... int_expr=count_with_metadata)("items")72 >>> result = typed_array.parse_string("3 bool True True False")73 >>> print(result.dump())74 ['True', 'True', 'False']75 - items: ['True', 'True', 'False']76 - type: 'bool'77 """78 intExpr: typing.Optional[ParserElement] = deprecate_argument(79 kwargs, "intExpr", None80 )81 82 intExpr = intExpr or int_expr83 array_expr = Forward()84 85 def count_field_parse_action(s, l, t):86 nonlocal array_expr87 n = t[0]88 array_expr <<= (expr * n) if n else Empty()89 # clear list contents, but keep any named results90 del t[:]91 92 if intExpr is None:93 intExpr = Word(nums).set_parse_action(lambda t: int(t[0]))94 else:95 intExpr = intExpr.copy()96 intExpr.set_name("arrayLen")97 intExpr.add_parse_action(count_field_parse_action, call_during_try=True)98 return (intExpr + array_expr).set_name(f"(len) {expr}...")99 100 101def match_previous_literal(expr: ParserElement) -> ParserElement:102 """Helper to define an expression that is indirectly defined from103 the tokens matched in a previous expression, that is, it looks for104 a 'repeat' of a previous expression. For example::105 106 .. testcode::107 108 first = Word(nums)109 second = match_previous_literal(first)110 match_expr = first + ":" + second111 112 will match ``"1:1"``, but not ``"1:2"``. Because this113 matches a previous literal, will also match the leading114 ``"1:1"`` in ``"1:10"``. If this is not desired, use115 :class:`match_previous_expr`. Do *not* use with packrat parsing116 enabled.117 """118 rep = Forward()119 120 def copy_token_to_repeater(s, l, t):121 if not t:122 rep << Empty()123 return124 125 if len(t) == 1:126 rep << t[0]127 return128 129 # flatten t tokens130 tflat = _flatten(t.as_list())131 rep << And(Literal(tt) for tt in tflat)132 133 expr.add_parse_action(copy_token_to_repeater, call_during_try=True)134 rep.set_name("(prev) " + str(expr))135 return rep136 137 138def match_previous_expr(expr: ParserElement) -> ParserElement:139 """Helper to define an expression that is indirectly defined from140 the tokens matched in a previous expression, that is, it looks for141 a 'repeat' of a previous expression. For example:142 143 .. testcode::144 145 first = Word(nums)146 second = match_previous_expr(first)147 match_expr = first + ":" + second148 149 will match ``"1:1"``, but not ``"1:2"``. Because this150 matches by expressions, will *not* match the leading ``"1:1"``151 in ``"1:10"``; the expressions are evaluated first, and then152 compared, so ``"1"`` is compared with ``"10"``. Do *not* use153 with packrat parsing enabled.154 """155 rep = Forward()156 e2 = expr.copy()157 rep <<= e2158 159 def copy_token_to_repeater(s, l, t):160 matchTokens = _flatten(t.as_list())161 162 def must_match_these_tokens(s, l, t):163 theseTokens = _flatten(t.as_list())164 if theseTokens != matchTokens:165 raise ParseException(166 s, l, f"Expected {matchTokens}, found{theseTokens}"167 )168 169 rep.set_parse_action(must_match_these_tokens, call_during_try=True)170 171 expr.add_parse_action(copy_token_to_repeater, call_during_try=True)172 rep.set_name("(prev) " + str(expr))173 return rep174 175 176def one_of(177 strs: Union[typing.Iterable[str], str],178 caseless: bool = False,179 use_regex: bool = True,180 as_keyword: bool = False,181 **kwargs,182) -> ParserElement:183 """Helper to quickly define a set of alternative :class:`Literal` s,184 and makes sure to do longest-first testing when there is a conflict,185 regardless of the input order, but returns186 a :class:`MatchFirst` for best performance.187 188 :param strs: a string of space-delimited literals, or a collection of189 string literals190 :param caseless: treat all literals as caseless191 :param use_regex: bool - as an optimization, will192 generate a :class:`Regex` object; otherwise, will generate193 a :class:`MatchFirst` object (if ``caseless=True`` or194 ``as_keyword=True``, or if creating a :class:`Regex` raises an exception)195 :param as_keyword: bool - enforce :class:`Keyword`-style matching on the196 generated expressions197 198 Parameters ``asKeyword`` and ``useRegex`` are retained for pre-PEP8199 compatibility, but will be removed in a future release.200 201 Example:202 203 .. testcode::204 205 comp_oper = one_of("< = > <= >= !=")206 var = Word(alphas)207 number = Word(nums)208 term = var | number209 comparison_expr = term + comp_oper + term210 print(comparison_expr.search_string("B = 12 AA=23 B<=AA AA>12"))211 212 prints:213 214 .. testoutput::215 216 [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]217 """218 useRegex: bool = deprecate_argument(kwargs, "useRegex", True)219 asKeyword: bool = deprecate_argument(kwargs, "asKeyword", False)220 221 asKeyword = asKeyword or as_keyword222 useRegex = useRegex and use_regex223 224 if (225 isinstance(caseless, str_type)226 and __diag__.warn_on_multiple_string_args_to_oneof227 ):228 warnings.warn(229 "warn_on_multiple_string_args_to_oneof:"230 " More than one string argument passed to one_of, pass"231 " choices as a list or space-delimited string",232 stacklevel=2,233 )234 235 if caseless:236 is_equal = lambda a, b: a.upper() == b.upper()237 masks = lambda a, b: b.upper().startswith(a.upper())238 else:239 is_equal = operator.eq240 masks = lambda a, b: b.startswith(a)241 242 symbols: list[str]243 if isinstance(strs, str_type):244 strs = typing.cast(str, strs)245 symbols = strs.split()246 elif isinstance(strs, Iterable):247 symbols = list(strs)248 else:249 raise TypeError("Invalid argument to one_of, expected string or iterable")250 if not symbols:251 return NoMatch()252 253 # reorder given symbols to take care to avoid masking longer choices with shorter ones254 # (but only if the given symbols are not just single characters)255 i = 0256 while i < len(symbols) - 1:257 cur = symbols[i]258 for j, other in enumerate(symbols[i + 1 :]):259 if is_equal(other, cur):260 del symbols[i + j + 1]261 break262 if len(other) > len(cur) and masks(cur, other):263 del symbols[i + j + 1]264 symbols.insert(i, other)265 break266 else:267 i += 1268 269 if useRegex:270 re_flags: int = re.IGNORECASE if caseless else 0271 272 try:273 if all(len(sym) == 1 for sym in symbols):274 # symbols are just single characters, create range regex pattern275 patt = f"[{''.join(_escape_regex_range_chars(sym) for sym in symbols)}]"276 else:277 patt = "|".join(re.escape(sym) for sym in symbols)278 279 # wrap with \b word break markers if defining as keywords280 if asKeyword:281 patt = rf"\b(?:{patt})\b"282 283 ret = Regex(patt, flags=re_flags)284 ret.set_name(" | ".join(repr(s) for s in symbols))285 286 if caseless:287 # add parse action to return symbols as specified, not in random288 # casing as found in input string289 symbol_map = {sym.lower(): sym for sym in symbols}290 ret.add_parse_action(lambda s, l, t: symbol_map[t[0].lower()])291 292 return ret293 294 except re.error:295 warnings.warn(296 "Exception creating Regex for one_of, building MatchFirst", stacklevel=2297 )298 299 # last resort, just use MatchFirst of Token class corresponding to caseless300 # and asKeyword settings301 CASELESS = KEYWORD = True302 parse_element_class = {303 (CASELESS, KEYWORD): CaselessKeyword,304 (CASELESS, not KEYWORD): CaselessLiteral,305 (not CASELESS, KEYWORD): Keyword,306 (not CASELESS, not KEYWORD): Literal,307 }[(caseless, asKeyword)]308 return MatchFirst(parse_element_class(sym) for sym in symbols).set_name(309 " | ".join(symbols)310 )311 312 313def dict_of(key: ParserElement, value: ParserElement) -> Dict:314 """Helper to easily and clearly define a dictionary by specifying315 the respective patterns for the key and value. Takes care of316 defining the :class:`Dict`, :class:`ZeroOrMore`, and317 :class:`Group` tokens in the proper order. The key pattern318 can include delimiting markers or punctuation, as long as they are319 suppressed, thereby leaving the significant key text. The value320 pattern can include named results, so that the :class:`Dict` results321 can include named token fields.322 323 Example:324 325 .. doctest::326 327 >>> text = "shape: SQUARE posn: upper left color: light blue texture: burlap"328 329 >>> data_word = Word(alphas)330 >>> label = data_word + FollowedBy(':')331 >>> attr_expr = (332 ... label333 ... + Suppress(':')334 ... + OneOrMore(data_word, stop_on=label)335 ... .set_parse_action(' '.join))336 >>> print(attr_expr[1, ...].parse_string(text).dump())337 ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']338 339 >>> attr_label = label340 >>> attr_value = Suppress(':') + OneOrMore(data_word, stop_on=label341 ... ).set_parse_action(' '.join)342 343 # similar to Dict, but simpler call format344 >>> result = dict_of(attr_label, attr_value).parse_string(text)345 >>> print(result.dump())346 [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]347 - color: 'light blue'348 - posn: 'upper left'349 - shape: 'SQUARE'350 - texture: 'burlap'351 [0]:352 ['shape', 'SQUARE']353 [1]:354 ['posn', 'upper left']355 [2]:356 ['color', 'light blue']357 [3]:358 ['texture', 'burlap']359 360 >>> print(result['shape'])361 SQUARE362 >>> print(result.shape) # object attribute access works too363 SQUARE364 >>> print(result.as_dict())365 {'shape': 'SQUARE', 'posn': 'upper left', 'color': 'light blue', 'texture': 'burlap'}366 """367 return Dict(OneOrMore(Group(key + value)))368 369 370def original_text_for(371 expr: ParserElement, as_string: bool = True, **kwargs372) -> ParserElement:373 """Helper to return the original, untokenized text for a given374 expression. Useful to restore the parsed fields of an HTML start375 tag into the raw tag text itself, or to revert separate tokens with376 intervening whitespace back to the original matching input text. By377 default, returns a string containing the original parsed text.378 379 If the optional ``as_string`` argument is passed as380 ``False``, then the return value is381 a :class:`ParseResults` containing any results names that382 were originally matched, and a single token containing the original383 matched text from the input string. So if the expression passed to384 :class:`original_text_for` contains expressions with defined385 results names, you must set ``as_string`` to ``False`` if you386 want to preserve those results name values.387 388 The ``asString`` pre-PEP8 argument is retained for compatibility,389 but will be removed in a future release.390 391 Example:392 393 .. testcode::394 395 src = "this is test <b> bold <i>text</i> </b> normal text "396 for tag in ("b", "i"):397 opener, closer = make_html_tags(tag)398 patt = original_text_for(opener + ... + closer)399 print(patt.search_string(src)[0])400 401 prints:402 403 .. testoutput::404 405 ['<b> bold <i>text</i> </b>']406 ['<i>text</i>']407 """408 asString: bool = deprecate_argument(kwargs, "asString", True)409 410 asString = asString and as_string411 412 locMarker = Empty().set_parse_action(lambda s, loc, t: loc)413 endlocMarker = locMarker.copy()414 endlocMarker.callPreparse = False415 matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")416 if asString:417 extractText = lambda s, l, t: s[t._original_start : t._original_end]418 else:419 420 def extractText(s, l, t):421 t[:] = [s[t.pop("_original_start") : t.pop("_original_end")]]422 423 matchExpr.set_parse_action(extractText)424 matchExpr.ignoreExprs = expr.ignoreExprs425 matchExpr.suppress_warning(Diagnostics.warn_ungrouped_named_tokens_in_collection)426 return matchExpr427 428 429def ungroup(expr: ParserElement) -> ParserElement:430 """Helper to undo pyparsing's default grouping of And expressions,431 even if all but one are non-empty.432 """433 return TokenConverter(expr).add_parse_action(lambda t: t[0])434 435 436def locatedExpr(expr: ParserElement) -> ParserElement:437 """438 .. deprecated:: 3.0.0439 Use the :class:`Located` class instead. Note that `Located`440 returns results with one less grouping level.441 442 Helper to decorate a returned token with its starting and ending443 locations in the input string.444 445 This helper adds the following results names:446 447 - ``locn_start`` - location where matched expression begins448 - ``locn_end`` - location where matched expression ends449 - ``value`` - the actual parsed results450 451 Be careful if the input text contains ``<TAB>`` characters, you452 may want to call :meth:`ParserElement.parse_with_tabs`453 """454 warnings.warn(455 f"{'locatedExpr'!r} deprecated - use {'Located'!r}",456 DeprecationWarning,457 stacklevel=2,458 )459 460 locator = Empty().set_parse_action(lambda ss, ll, tt: ll)461 return Group(462 locator("locn_start")463 + expr("value")464 + locator.copy().leave_whitespace()("locn_end")465 )466 467 468# define special default value to permit None as a significant value for469# ignore_expr470_NO_IGNORE_EXPR_GIVEN = NoMatch()471 472 473def nested_expr(474 opener: Union[str, ParserElement] = "(",475 closer: Union[str, ParserElement] = ")",476 content: typing.Optional[ParserElement] = None,477 ignore_expr: typing.Optional[ParserElement] = _NO_IGNORE_EXPR_GIVEN,478 **kwargs,479) -> ParserElement:480 """Helper method for defining nested lists enclosed in opening and481 closing delimiters (``"("`` and ``")"`` are the default).482 483 :param opener: str - opening character for a nested list484 (default= ``"("``); can also be a pyparsing expression485 486 :param closer: str - closing character for a nested list487 (default= ``")"``); can also be a pyparsing expression488 489 :param content: expression for items within the nested lists490 491 :param ignore_expr: expression for ignoring opening and closing delimiters492 (default = :class:`quoted_string`)493 494 Parameter ``ignoreExpr`` is retained for compatibility495 but will be removed in a future release.496 497 If an expression is not provided for the content argument, the498 nested expression will capture all whitespace-delimited content499 between delimiters as a list of separate values.500 501 Use the ``ignore_expr`` argument to define expressions that may502 contain opening or closing characters that should not be treated as503 opening or closing characters for nesting, such as quoted_string or504 a comment expression. Specify multiple expressions using an505 :class:`Or` or :class:`MatchFirst`. The default is506 :class:`quoted_string`, but if no expressions are to be ignored, then507 pass ``None`` for this argument.508 509 Example:510 511 .. testcode::512 513 data_type = one_of("void int short long char float double")514 decl_data_type = Combine(data_type + Opt(Word('*')))515 ident = Word(alphas+'_', alphanums+'_')516 number = pyparsing_common.number517 arg = Group(decl_data_type + ident)518 LPAR, RPAR = map(Suppress, "()")519 520 code_body = nested_expr('{', '}', ignore_expr=(quoted_string | c_style_comment))521 522 c_function = (decl_data_type("type")523 + ident("name")524 + LPAR + Opt(DelimitedList(arg), [])("args") + RPAR525 + code_body("body"))526 c_function.ignore(c_style_comment)527 528 source_code = '''529 int is_odd(int x) {530 return (x%2);531 }532 533 int dec_to_hex(char hchar) {534 if (hchar >= '0' && hchar <= '9') {535 return (ord(hchar)-ord('0'));536 } else {537 return (10+ord(hchar)-ord('A'));538 }539 }540 '''541 for func in c_function.search_string(source_code):542 print(f"{func.name} ({func.type}) args: {func.args}")543 544 545 prints:546 547 .. testoutput::548 549 is_odd (int) args: [['int', 'x']]550 dec_to_hex (int) args: [['char', 'hchar']]551 """552 ignoreExpr: ParserElement = deprecate_argument(553 kwargs, "ignoreExpr", _NO_IGNORE_EXPR_GIVEN554 )555 556 if ignoreExpr != ignore_expr:557 ignoreExpr = ignore_expr if ignoreExpr is _NO_IGNORE_EXPR_GIVEN else ignoreExpr # type: ignore [assignment]558 559 if ignoreExpr is _NO_IGNORE_EXPR_GIVEN:560 ignoreExpr = quoted_string()561 562 if opener == closer:563 raise ValueError("opening and closing strings cannot be the same")564 565 if content is None:566 if isinstance(opener, str_type) and isinstance(closer, str_type):567 opener = typing.cast(str, opener)568 closer = typing.cast(str, closer)569 if len(opener) == 1 and len(closer) == 1:570 if ignoreExpr is not None:571 content = Combine(572 OneOrMore(573 ~ignoreExpr574 + CharsNotIn(575 opener + closer + ParserElement.DEFAULT_WHITE_CHARS,576 exact=1,577 )578 )579 )580 else:581 content = Combine(582 Empty()583 + CharsNotIn(584 opener + closer + ParserElement.DEFAULT_WHITE_CHARS585 )586 )587 else:588 if ignoreExpr is not None:589 content = Combine(590 OneOrMore(591 ~ignoreExpr592 + ~Literal(opener)593 + ~Literal(closer)594 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)595 )596 )597 else:598 content = Combine(599 OneOrMore(600 ~Literal(opener)601 + ~Literal(closer)602 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)603 )604 )605 else:606 raise ValueError(607 "opening and closing arguments must be strings if no content expression is given"608 )609 610 # for these internally-created context expressions, simulate whitespace-skipping611 if ParserElement.DEFAULT_WHITE_CHARS:612 content.set_parse_action(613 lambda t: t[0].strip(ParserElement.DEFAULT_WHITE_CHARS)614 )615 616 ret = Forward()617 if ignoreExpr is not None:618 ret <<= Group(619 _suppression(opener)620 + ZeroOrMore(ignoreExpr | ret | content)621 + _suppression(closer)622 )623 else:624 ret <<= Group(625 _suppression(opener) + ZeroOrMore(ret | content) + _suppression(closer)626 )627 628 ret.set_name(f"nested {opener}{closer} expression")629 630 # don't override error message from content expressions631 ret.errmsg = None632 return ret633 634 635def _makeTags(tagStr, xml, suppress_LT=Suppress("<"), suppress_GT=Suppress(">")):636 """Internal helper to construct opening and closing tag expressions,637 given a tag name"""638 if isinstance(tagStr, str_type):639 resname = tagStr640 tagStr = Keyword(tagStr, caseless=not xml)641 else:642 resname = tagStr.name643 644 tagAttrName = Word(alphas, alphanums + "_-:")645 if xml:646 tagAttrValue = dbl_quoted_string.copy().set_parse_action(remove_quotes)647 openTag = (648 suppress_LT649 + tagStr("tag")650 + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue)))651 + Opt("/", default=[False])("empty").set_parse_action(652 lambda s, l, t: t[0] == "/"653 )654 + suppress_GT655 )656 else:657 tagAttrValue = quoted_string.copy().set_parse_action(remove_quotes) | Word(658 printables, exclude_chars=">"659 )660 openTag = (661 suppress_LT662 + tagStr("tag")663 + Dict(664 ZeroOrMore(665 Group(666 tagAttrName.set_parse_action(lambda t: t[0].lower())667 + Opt(Suppress("=") + tagAttrValue)668 )669 )670 )671 + Opt("/", default=[False])("empty").set_parse_action(672 lambda s, l, t: t[0] == "/"673 )674 + suppress_GT675 )676 closeTag = Combine(Literal("</") + tagStr + ">", adjacent=False)677 678 openTag.set_name(f"<{resname}>")679 # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels680 openTag.add_parse_action(681 lambda t: t.__setitem__(682 "start" + "".join(resname.replace(":", " ").title().split()), t.copy()683 )684 )685 closeTag = closeTag(686 "end" + "".join(resname.replace(":", " ").title().split())687 ).set_name(f"</{resname}>")688 openTag.tag = resname689 closeTag.tag = resname690 openTag.tag_body = SkipTo(closeTag())691 return openTag, closeTag692 693 694def make_html_tags(695 tag_str: Union[str, ParserElement],696) -> tuple[ParserElement, ParserElement]:697 """Helper to construct opening and closing tag expressions for HTML,698 given a tag name. Matches tags in either upper or lower case,699 attributes with namespaces and with quoted or unquoted values.700 701 Example:702 703 .. testcode::704 705 text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'706 # make_html_tags returns pyparsing expressions for the opening and707 # closing tags as a 2-tuple708 a, a_end = make_html_tags("A")709 link_expr = a + SkipTo(a_end)("link_text") + a_end710 711 for link in link_expr.search_string(text):712 # attributes in the <A> tag (like "href" shown here) are713 # also accessible as named results714 print(link.link_text, '->', link.href)715 716 prints:717 718 .. testoutput::719 720 pyparsing -> https://github.com/pyparsing/pyparsing/wiki721 """722 return _makeTags(tag_str, False)723 724 725def make_xml_tags(726 tag_str: Union[str, ParserElement],727) -> tuple[ParserElement, ParserElement]:728 """Helper to construct opening and closing tag expressions for XML,729 given a tag name. Matches tags only in the given upper/lower case.730 731 Example: similar to :class:`make_html_tags`732 """733 return _makeTags(tag_str, True)734 735 736any_open_tag: ParserElement737any_close_tag: ParserElement738any_open_tag, any_close_tag = make_html_tags(739 Word(alphas, alphanums + "_:").set_name("any tag")740)741 742_htmlEntityMap = {k.rstrip(";"): v for k, v in html.entities.html5.items()}743_most_common_entities = "nbsp lt gt amp quot apos cent pound euro copy".replace(744 " ", "|"745)746common_html_entity = Regex(747 lambda: f"&(?P<entity>{_most_common_entities}|{make_compressed_re(_htmlEntityMap)});"748).set_name("common HTML entity")749 750 751def replace_html_entity(s, l, t):752 """Helper parser action to replace common HTML entities with their special characters"""753 return _htmlEntityMap.get(t.entity)754 755 756class OpAssoc(Enum):757 """Enumeration of operator associativity758 - used in constructing InfixNotationOperatorSpec for :class:`infix_notation`"""759 760 LEFT = 1761 RIGHT = 2762 763 764InfixNotationOperatorArgType = Union[765 ParserElement, str, tuple[Union[ParserElement, str], Union[ParserElement, str]]766]767InfixNotationOperatorSpec = Union[768 tuple[769 InfixNotationOperatorArgType,770 int,771 OpAssoc,772 typing.Optional[ParseAction],773 ],774 tuple[775 InfixNotationOperatorArgType,776 int,777 OpAssoc,778 ],779]780 781 782def infix_notation(783 base_expr: ParserElement,784 op_list: list[InfixNotationOperatorSpec],785 lpar: Union[str, ParserElement] = Suppress("("),786 rpar: Union[str, ParserElement] = Suppress(")"),787) -> Forward:788 """Helper method for constructing grammars of expressions made up of789 operators working in a precedence hierarchy. Operators may be unary790 or binary, left- or right-associative. Parse actions can also be791 attached to operator expressions. The generated parser will also792 recognize the use of parentheses to override operator precedences793 (see example below).794 795 Note: if you define a deep operator list, you may see performance796 issues when using infix_notation. See797 :class:`ParserElement.enable_packrat` for a mechanism to potentially798 improve your parser performance.799 800 Parameters:801 802 :param base_expr: expression representing the most basic operand to803 be used in the expression804 :param op_list: list of tuples, one for each operator precedence level805 in the expression grammar; each tuple is of the form ``(op_expr,806 num_operands, right_left_assoc, (optional)parse_action)``, where:807 808 - ``op_expr`` is the pyparsing expression for the operator; may also809 be a string, which will be converted to a Literal; if ``num_operands``810 is 3, ``op_expr`` is a tuple of two expressions, for the two811 operators separating the 3 terms812 - ``num_operands`` is the number of terms for this operator (must be 1,813 2, or 3)814 - ``right_left_assoc`` is the indicator whether the operator is right815 or left associative, using the pyparsing-defined constants816 ``OpAssoc.RIGHT`` and ``OpAssoc.LEFT``.817 - ``parse_action`` is the parse action to be associated with818 expressions matching this operator expression (the parse action819 tuple member may be omitted); if the parse action is passed820 a tuple or list of functions, this is equivalent to calling821 ``set_parse_action(*fn)``822 (:class:`ParserElement.set_parse_action`)823 824 :param lpar: expression for matching left-parentheses; if passed as a825 str, then will be parsed as ``Suppress(lpar)``. If lpar is passed as826 an expression (such as ``Literal('(')``), then it will be kept in827 the parsed results, and grouped with them. (default= ``Suppress('(')``)828 :param rpar: expression for matching right-parentheses; if passed as a829 str, then will be parsed as ``Suppress(rpar)``. If rpar is passed as830 an expression (such as ``Literal(')')``), then it will be kept in831 the parsed results, and grouped with them. (default= ``Suppress(')')``)832 833 Example:834 835 .. testcode::836 837 # simple example of four-function arithmetic with ints and838 # variable names839 integer = pyparsing_common.signed_integer840 varname = pyparsing_common.identifier841 842 arith_expr = infix_notation(integer | varname,843 [844 ('-', 1, OpAssoc.RIGHT),845 (one_of('* /'), 2, OpAssoc.LEFT),846 (one_of('+ -'), 2, OpAssoc.LEFT),847 ])848 849 arith_expr.run_tests('''850 5+3*6851 (5+3)*6852 (5+x)*y853 -2--11854 ''', full_dump=False)855 856 prints:857 858 .. testoutput::859 :options: +NORMALIZE_WHITESPACE860 861 862 5+3*6863 [[5, '+', [3, '*', 6]]]864 865 (5+3)*6866 [[[5, '+', 3], '*', 6]]867 868 (5+x)*y869 [[[5, '+', 'x'], '*', 'y']]870 871 -2--11872 [[['-', 2], '-', ['-', 11]]]873 """874 875 # captive version of FollowedBy that does not do parse actions or capture results names876 class _FB(FollowedBy):877 def parseImpl(self, instring, loc, doActions=True):878 self.expr.try_parse(instring, loc)879 return loc, []880 881 _FB.__name__ = "FollowedBy>"882 883 ret = Forward()884 ret.set_name(f"{base_expr.name}_expression")885 if isinstance(lpar, str):886 lpar = Suppress(lpar)887 if isinstance(rpar, str):888 rpar = Suppress(rpar)889 890 nested_expr = (lpar + ret + rpar).set_name(f"nested_{base_expr.name}_expression")891 892 # if lpar and rpar are not suppressed, wrap in group893 if not (isinstance(lpar, Suppress) and isinstance(rpar, Suppress)):894 lastExpr = base_expr | Group(nested_expr)895 else:896 lastExpr = base_expr | nested_expr897 898 arity: int899 rightLeftAssoc: opAssoc900 pa: typing.Optional[ParseAction]901 opExpr1: ParserElement902 opExpr2: ParserElement903 matchExpr: ParserElement904 match_lookahead: ParserElement905 for operDef in op_list:906 opExpr, arity, rightLeftAssoc, pa = (operDef + (None,))[:4] # type: ignore[assignment]907 if isinstance(opExpr, str_type):908 opExpr = ParserElement._literalStringClass(opExpr)909 opExpr = typing.cast(ParserElement, opExpr)910 if arity == 3:911 if not isinstance(opExpr, (tuple, list)) or len(opExpr) != 2:912 raise ValueError(913 "if numterms=3, opExpr must be a tuple or list of two expressions"914 )915 opExpr1, opExpr2 = opExpr916 term_name = f"{opExpr1}{opExpr2} operations"917 else:918 term_name = f"{opExpr} operations"919 920 if not 1 <= arity <= 3:921 raise ValueError("operator must be unary (1), binary (2), or ternary (3)")922 923 if rightLeftAssoc not in (OpAssoc.LEFT, OpAssoc.RIGHT):924 raise ValueError("operator must indicate right or left associativity")925 926 thisExpr: ParserElement = Forward().set_name(term_name)927 thisExpr = typing.cast(Forward, thisExpr)928 match_lookahead = And([])929 if rightLeftAssoc is OpAssoc.LEFT:930 if arity == 1:931 match_lookahead = _FB(lastExpr + opExpr)932 matchExpr = Group(lastExpr + opExpr[1, ...])933 elif arity == 2:934 if opExpr is not None:935 match_lookahead = _FB(lastExpr + opExpr + lastExpr)936 matchExpr = Group(lastExpr + (opExpr + lastExpr)[1, ...])937 else:938 match_lookahead = _FB(lastExpr + lastExpr)939 matchExpr = Group(lastExpr[2, ...])940 elif arity == 3:941 match_lookahead = _FB(942 lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr943 )944 matchExpr = Group(945 lastExpr + (opExpr1 + lastExpr + opExpr2 + lastExpr)[1, ...]946 )947 elif rightLeftAssoc is OpAssoc.RIGHT:948 if arity == 1:949 # try to avoid LR with this extra test950 if not isinstance(opExpr, Opt):951 opExpr = Opt(opExpr)952 match_lookahead = _FB(opExpr.expr + thisExpr)953 matchExpr = Group(opExpr + thisExpr)954 elif arity == 2:955 if opExpr is not None:956 match_lookahead = _FB(lastExpr + opExpr + thisExpr)957 matchExpr = Group(lastExpr + (opExpr + thisExpr)[1, ...])958 else:959 match_lookahead = _FB(lastExpr + thisExpr)960 matchExpr = Group(lastExpr + thisExpr[1, ...])961 elif arity == 3:962 match_lookahead = _FB(963 lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr964 )965 matchExpr = Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)966 967 # suppress lookahead expr from railroad diagrams968 match_lookahead.show_in_diagram = False969 970 # TODO - determine why this statement can't be included in the following971 # if pa block972 matchExpr = match_lookahead + matchExpr973 974 if pa:975 if isinstance(pa, (tuple, list)):976 matchExpr.set_parse_action(*pa)977 else:978 matchExpr.set_parse_action(pa)979 980 thisExpr <<= (matchExpr | lastExpr).set_name(term_name)981 lastExpr = thisExpr982 983 ret <<= lastExpr984 return ret985 986 987def indentedBlock(blockStatementExpr, indentStack, indent=True, backup_stacks=[]):988 """989 .. deprecated:: 3.0.0990 Use the :class:`IndentedBlock` class instead. Note that `IndentedBlock`991 has a difference method signature.992 993 Helper method for defining space-delimited indentation blocks,994 such as those used to define block statements in Python source code.995 996 :param blockStatementExpr: expression defining syntax of statement that997 is repeated within the indented block998 999 :param indentStack: list created by caller to manage indentation stack1000 (multiple ``statementWithIndentedBlock`` expressions within a single1001 grammar should share a common ``indentStack``)1002 1003 :param indent: boolean indicating whether block must be indented beyond1004 the current level; set to ``False`` for block of left-most statements1005 1006 A valid block must contain at least one ``blockStatement``.1007 1008 (Note that indentedBlock uses internal parse actions which make it1009 incompatible with packrat parsing.)1010 1011 Example:1012 1013 .. testcode::1014 1015 data = '''1016 def A(z):1017 A11018 B = 1001019 G = A21020 A21021 A31022 B1023 def BB(a,b,c):1024 BB11025 def BBA():1026 bba11027 bba21028 bba31029 C1030 D1031 def spam(x,y):1032 def eggs(z):1033 pass1034 '''1035 1036 indentStack = [1]1037 stmt = Forward()1038 1039 identifier = Word(alphas, alphanums)1040 funcDecl = ("def" + identifier + Group("(" + Opt(delimitedList(identifier)) + ")") + ":")1041 func_body = indentedBlock(stmt, indentStack)1042 funcDef = Group(funcDecl + func_body)1043 1044 rvalue = Forward()1045 funcCall = Group(identifier + "(" + Opt(delimitedList(rvalue)) + ")")1046 rvalue << (funcCall | identifier | Word(nums))1047 assignment = Group(identifier + "=" + rvalue)1048 stmt << (funcDef | assignment | identifier)1049 1050 module_body = stmt[1, ...]1051 1052 parseTree = module_body.parseString(data)1053 parseTree.pprint()1054 1055 prints:1056 1057 .. testoutput::1058 1059 [['def',1060 'A',1061 ['(', 'z', ')'],1062 ':',1063 [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],1064 'B',1065 ['def',1066 'BB',1067 ['(', 'a', 'b', 'c', ')'],1068 ':',1069 [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],1070 'C',1071 'D',1072 ['def',1073 'spam',1074 ['(', 'x', 'y', ')'],1075 ':',1076 [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]]1077 """1078 warnings.warn(1079 f"{'indentedBlock'!r} deprecated - use {'IndentedBlock'!r}",1080 DeprecationWarning,1081 stacklevel=2,1082 )1083 1084 backup_stacks.append(indentStack[:])1085 1086 def reset_stack():1087 indentStack[:] = backup_stacks[-1]1088 1089 def checkPeerIndent(s, l, t):1090 if l >= len(s):1091 return1092 curCol = col(l, s)1093 if curCol != indentStack[-1]:1094 if curCol > indentStack[-1]:1095 raise ParseException(s, l, "illegal nesting")1096 raise ParseException(s, l, "not a peer entry")1097 1098 def checkSubIndent(s, l, t):1099 curCol = col(l, s)1100 if curCol > indentStack[-1]:1101 indentStack.append(curCol)1102 else:1103 raise ParseException(s, l, "not a subentry")1104 1105 def checkUnindent(s, l, t):1106 if l >= len(s):1107 return1108 curCol = col(l, s)1109 if not (indentStack and curCol in indentStack):1110 raise ParseException(s, l, "not an unindent")1111 if curCol < indentStack[-1]:1112 indentStack.pop()1113 1114 NL = OneOrMore(LineEnd().set_whitespace_chars("\t ").suppress())1115 INDENT = (Empty() + Empty().set_parse_action(checkSubIndent)).set_name("INDENT")1116 PEER = Empty().set_parse_action(checkPeerIndent).set_name("")1117 UNDENT = Empty().set_parse_action(checkUnindent).set_name("UNINDENT")1118 if indent:1119 smExpr = Group(1120 Opt(NL)1121 + INDENT1122 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))1123 + UNDENT1124 )1125 else:1126 smExpr = Group(1127 Opt(NL)1128 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))1129 + Opt(UNDENT)1130 )1131 1132 # add a parse action to remove backup_stack from list of backups1133 smExpr.add_parse_action(1134 lambda: backup_stacks.pop(-1) and None if backup_stacks else None1135 )1136 smExpr.set_fail_action(lambda a, b, c, d: reset_stack())1137 blockStatementExpr.ignore(_bslash + LineEnd())1138 return smExpr.set_name("indented block")1139 1140 1141# it's easy to get these comment structures wrong - they're very common,1142# so may as well make them available1143c_style_comment = Regex(r"/\*(?:[^*]|\*(?!/))*\*\/").set_name("C style comment")1144"Comment of the form ``/* ... */``"1145 1146html_comment = Regex(r"<!--[\s\S]*?-->").set_name("HTML comment")1147"Comment of the form ``<!-- ... -->``"1148 1149rest_of_line = Regex(r".*").leave_whitespace().set_name("rest of line")1150dbl_slash_comment = Regex(r"//(?:\\\n|[^\n])*").set_name("// comment")1151"Comment of the form ``// ... (to end of line)``"1152 1153cpp_style_comment = Regex(1154 r"(?:/\*(?:[^*]|\*(?!/))*\*\/)|(?://(?:\\\n|[^\n])*)"1155).set_name("C++ style comment")1156"Comment of either form :class:`c_style_comment` or :class:`dbl_slash_comment`"1157 1158java_style_comment = cpp_style_comment1159"Same as :class:`cpp_style_comment`"1160 1161python_style_comment = Regex(r"#.*").set_name("Python style comment")1162"Comment of the form ``# ... (to end of line)``"1163 1164 1165# build list of built-in expressions, for future reference if a global default value1166# gets updated1167_builtin_exprs: list[ParserElement] = [1168 v for v in vars().values() if isinstance(v, ParserElement)1169]1170 1171 1172# compatibility function, superseded by DelimitedList class1173def delimited_list(1174 expr: Union[str, ParserElement],1175 delim: Union[str, ParserElement] = ",",1176 combine: bool = False,1177 min: typing.Optional[int] = None,1178 max: typing.Optional[int] = None,1179 *,1180 allow_trailing_delim: bool = False,1181) -> ParserElement:1182 """1183 .. deprecated:: 3.1.01184 Use the :class:`DelimitedList` class instead.1185 """1186 return DelimitedList(1187 expr, delim, combine, min, max, allow_trailing_delim=allow_trailing_delim1188 )1189 1190 1191# Compatibility synonyms1192# fmt: off1193opAssoc = OpAssoc1194anyOpenTag = any_open_tag1195anyCloseTag = any_close_tag1196commonHTMLEntity = common_html_entity1197cStyleComment = c_style_comment1198htmlComment = html_comment1199restOfLine = rest_of_line1200dblSlashComment = dbl_slash_comment