Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pyparsing/helpers.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# helpers.py
2import html.entities
3import operator
4import re
5import sys
6import typing
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)
19def _suppression(expr: Union[ParserElement, str]) -> ParserElement:
20 # internal helper to avoid wrapping Suppress inside another Suppress
21 if isinstance(expr, Suppress):
22 return expr
23 return Suppress(expr)
26#
27# global helpers
28#
29def counted_array(
30 expr: ParserElement, int_expr: typing.Optional[ParserElement] = None, **kwargs
31) -> ParserElement:
32 """Helper to define a counted list of expressions.
34 This helper defines a pattern of the form::
36 integer expr expr expr...
38 where the leading integer tells how many expr expressions follow.
39 The matched tokens returns the array of expr tokens as a list - the
40 leading count token is suppressed.
42 If ``int_expr`` is specified, it should be a pyparsing expression
43 that produces an integer value.
45 Examples:
47 .. doctest::
49 >>> counted_array(Word(alphas)).parse_string('2 ab cd ef')
50 ParseResults(['ab', 'cd'], {})
52 - In this parser, the leading integer value is given in binary,
53 '10' indicating that 2 values are in the array:
55 .. doctest::
57 >>> binary_constant = Word('01').set_parse_action(lambda t: int(t[0], 2))
58 >>> counted_array(Word(alphas), int_expr=binary_constant
59 ... ).parse_string('10 ab cd ef')
60 ParseResults(['ab', 'cd'], {})
62 - If other fields must be parsed after the count but before the
63 list items, give the fields results names and they will
64 be preserved in the returned ParseResults:
66 .. doctest::
68 >>> ppc = pyparsing.common
69 >>> 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", None
80 )
82 intExpr = intExpr or int_expr
83 array_expr = Forward()
85 def count_field_parse_action(s, l, t):
86 nonlocal array_expr
87 n = t[0]
88 array_expr <<= (expr * n) if n else Empty()
89 # clear list contents, but keep any named results
90 del t[:]
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}...")
101def match_previous_literal(expr: ParserElement) -> ParserElement:
102 """Helper to define an expression that is indirectly defined from
103 the tokens matched in a previous expression, that is, it looks for
104 a 'repeat' of a previous expression. For example::
106 .. testcode::
108 first = Word(nums)
109 second = match_previous_literal(first)
110 match_expr = first + ":" + second
112 will match ``"1:1"``, but not ``"1:2"``. Because this
113 matches a previous literal, will also match the leading
114 ``"1:1"`` in ``"1:10"``. If this is not desired, use
115 :class:`match_previous_expr`. Do *not* use with packrat parsing
116 enabled.
117 """
118 rep = Forward()
120 def copy_token_to_repeater(s, l, t):
121 if not t:
122 rep << Empty()
123 return
125 if len(t) == 1:
126 rep << t[0]
127 return
129 # flatten t tokens
130 tflat = _flatten(t.as_list())
131 rep << And(Literal(tt) for tt in tflat)
133 expr.add_parse_action(copy_token_to_repeater, call_during_try=True)
134 rep.set_name(f"(prev) {expr}")
135 return rep
138def match_previous_expr(expr: ParserElement) -> ParserElement:
139 """Helper to define an expression that is indirectly defined from
140 the tokens matched in a previous expression, that is, it looks for
141 a 'repeat' of a previous expression. For example:
143 .. testcode::
145 first = Word(nums)
146 second = match_previous_expr(first)
147 match_expr = first + ":" + second
149 will match ``"1:1"``, but not ``"1:2"``. Because this
150 matches by expressions, will *not* match the leading ``"1:1"``
151 in ``"1:10"``; the expressions are evaluated first, and then
152 compared, so ``"1"`` is compared with ``"10"``. Do *not* use
153 with packrat parsing enabled.
155 Matches may be nested, so that an inner match is paired with the
156 nearest enclosing occurrence of ``expr``:
158 .. testcode::
160 tag_name = Word(alphas)
161 open_tag = "<" + tag_name + ">"
162 close_tag = "</" + match_previous_expr(tag_name) + ">"
164 will match ``"<a><b></b></a>"``, but not ``"<a><b></a></b>"``.
165 """
166 rep = Forward()
167 e2 = expr.copy()
168 rep <<= e2
170 # stack of (location, tokens) for matches of expr not yet paired with a
171 # match of rep, innermost last
172 match_stack: list[tuple[int, list]] = []
173 last_match: typing.Optional[tuple[int, list]] = None
175 def copy_token_to_repeater(s, l, t):
176 nonlocal last_match
178 # entries at or after this location belong to a branch that was
179 # abandoned on backtracking, and can never be paired
180 while match_stack and match_stack[-1][0] >= l:
181 match_stack.pop()
182 match_stack.append((l, _flatten(t.as_list())))
183 last_match = None
185 def must_match_these_tokens(s, l, t):
186 nonlocal last_match
188 these_tokens = _flatten(t.as_list())
190 # a memoizing parser may run this action more than once for the
191 # same match, which must not consume a second stack entry
192 if last_match == (l, these_tokens):
193 return
195 if not match_stack:
196 raise ParseException(s, l, "no previous expression to match")
198 match_tokens = match_stack[-1][1]
199 if these_tokens != match_tokens:
200 raise ParseException(s, l, f"Expected {match_tokens}, found{these_tokens}")
201 match_stack.pop()
202 last_match = (l, these_tokens)
204 e2.add_parse_action(must_match_these_tokens, call_during_try=True)
205 expr.add_parse_action(copy_token_to_repeater, call_during_try=True)
206 rep.set_name(f"(prev) {expr}")
207 return rep
210def one_of(
211 strs: Union[typing.Iterable[str], str],
212 caseless: bool = False,
213 use_regex: bool = True,
214 as_keyword: bool = False,
215 **kwargs,
216) -> ParserElement:
217 """Helper to quickly define a set of alternative :class:`Literal` s,
218 and makes sure to do longest-first testing when there is a conflict,
219 regardless of the input order, but returns
220 a :class:`MatchFirst` for best performance.
222 :param strs: a string of space-delimited literals, or a collection of
223 string literals
224 :param caseless: treat all literals as caseless
225 :param use_regex: bool - as an optimization, will
226 generate a :class:`Regex` object; otherwise, will generate
227 a :class:`MatchFirst` object (if ``caseless=True`` or
228 ``as_keyword=True``, or if creating a :class:`Regex` raises an exception)
229 :param as_keyword: bool - enforce :class:`Keyword`-style matching on the
230 generated expressions
232 Parameters ``asKeyword`` and ``useRegex`` are retained for pre-PEP8
233 compatibility, but will be removed in a future release.
235 Example:
237 .. testcode::
239 comp_oper = one_of("< = > <= >= !=")
240 var = Word(alphas)
241 number = Word(nums)
242 term = var | number
243 comparison_expr = term + comp_oper + term
244 print(comparison_expr.search_string("B = 12 AA=23 B<=AA AA>12"))
246 prints:
248 .. testoutput::
250 [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]
251 """
252 useRegex: bool = deprecate_argument(kwargs, "useRegex", True)
253 asKeyword: bool = deprecate_argument(kwargs, "asKeyword", False)
255 asKeyword = asKeyword or as_keyword
256 useRegex = useRegex and use_regex
258 if (
259 isinstance(caseless, str_type)
260 and __diag__.warn_on_multiple_string_args_to_oneof
261 ):
262 warnings.warn(
263 "warn_on_multiple_string_args_to_oneof:"
264 " More than one string argument passed to one_of, pass"
265 " choices as a list or space-delimited string",
266 PyparsingDiagnosticWarning,
267 stacklevel=2,
268 )
270 if caseless:
271 is_equal = lambda a, b: a.upper() == b.upper()
272 masks = lambda a, b: b.upper().startswith(a.upper())
273 else:
274 is_equal = operator.eq
275 masks = lambda a, b: b.startswith(a)
277 symbols: list[str]
278 if isinstance(strs, str_type):
279 strs = typing.cast(str, strs)
280 symbols = strs.split()
281 elif isinstance(strs, Iterable):
282 symbols = list(strs)
283 else:
284 raise TypeError("Invalid argument to one_of, expected string or iterable")
285 if not symbols:
286 return NoMatch()
288 # reorder given symbols to take care to avoid masking longer choices with shorter ones
289 # (but only if the given symbols are not just single characters)
290 i = 0
291 while i < len(symbols) - 1:
292 cur = symbols[i]
293 for j, other in enumerate(symbols[i + 1 :]):
294 if is_equal(other, cur):
295 del symbols[i + j + 1]
296 break
297 if len(other) > len(cur) and masks(cur, other):
298 del symbols[i + j + 1]
299 symbols.insert(i, other)
300 break
301 else:
302 i += 1
304 if useRegex:
305 re_flags: int = re.IGNORECASE if caseless else 0
307 try:
308 if all(len(sym) == 1 for sym in symbols):
309 # symbols are just single characters, create range regex pattern
310 patt = f"[{''.join(_escape_regex_range_chars(sym) for sym in symbols)}]"
311 else:
312 patt = "|".join(re.escape(sym) for sym in symbols)
314 # wrap with \b word break markers if defining as keywords
315 if asKeyword:
316 patt = rf"\b(?:{patt})\b"
318 ret = Regex(patt, flags=re_flags)
319 ret.set_name(" | ".join(repr(s) for s in symbols))
321 if caseless:
322 # add parse action to return symbols as specified, not in random
323 # casing as found in input string
324 symbol_map = {sym.lower(): sym for sym in symbols}
325 ret.add_parse_action(lambda s, l, t: symbol_map[t[0].lower()])
327 return ret
329 except re.error:
330 warnings.warn(
331 "Exception creating Regex for one_of, building MatchFirst",
332 PyparsingDiagnosticWarning,
333 stacklevel=2,
334 )
336 # last resort, just use MatchFirst of Token class corresponding to caseless
337 # and asKeyword settings
338 CASELESS = KEYWORD = True
339 parse_element_class = {
340 (CASELESS, KEYWORD): CaselessKeyword,
341 (CASELESS, not KEYWORD): CaselessLiteral,
342 (not CASELESS, KEYWORD): Keyword,
343 (not CASELESS, not KEYWORD): Literal,
344 }[(caseless, asKeyword)]
345 return MatchFirst(parse_element_class(sym) for sym in symbols).set_name(
346 " | ".join(symbols)
347 )
350def dict_of(key: ParserElement, value: ParserElement) -> Dict:
351 """Helper to easily and clearly define a dictionary by specifying
352 the respective patterns for the key and value. Takes care of
353 defining the :class:`Dict`, :class:`ZeroOrMore`, and
354 :class:`Group` tokens in the proper order. The key pattern
355 can include delimiting markers or punctuation, as long as they are
356 suppressed, thereby leaving the significant key text. The value
357 pattern can include named results, so that the :class:`Dict` results
358 can include named token fields.
360 Example:
362 .. doctest::
364 >>> text = "shape: SQUARE posn: upper left color: light blue texture: burlap"
366 >>> data_word = Word(alphas)
367 >>> label = data_word + FollowedBy(':')
368 >>> attr_expr = (
369 ... label
370 ... + Suppress(':')
371 ... + OneOrMore(data_word, stop_on=label)
372 ... .set_parse_action(' '.join))
373 >>> print(attr_expr[1, ...].parse_string(text).dump())
374 ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']
376 >>> attr_label = label
377 >>> attr_value = Suppress(':') + OneOrMore(data_word, stop_on=label
378 ... ).set_parse_action(' '.join)
380 # similar to Dict, but simpler call format
381 >>> result = dict_of(attr_label, attr_value).parse_string(text)
382 >>> print(result.dump())
383 [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
384 - color: 'light blue'
385 - posn: 'upper left'
386 - shape: 'SQUARE'
387 - texture: 'burlap'
388 [0]:
389 ['shape', 'SQUARE']
390 [1]:
391 ['posn', 'upper left']
392 [2]:
393 ['color', 'light blue']
394 [3]:
395 ['texture', 'burlap']
397 >>> print(result['shape'])
398 SQUARE
399 >>> print(result.shape) # object attribute access works too
400 SQUARE
401 >>> print(result.as_dict())
402 {'shape': 'SQUARE', 'posn': 'upper left', 'color': 'light blue', 'texture': 'burlap'}
403 """
404 return Dict(OneOrMore(Group(key + value)))
407def original_text_for(
408 expr: ParserElement, as_string: bool = True, **kwargs
409) -> ParserElement:
410 """Helper to return the original, untokenized text for a given
411 expression. Useful to restore the parsed fields of an HTML start
412 tag into the raw tag text itself, or to revert separate tokens with
413 intervening whitespace back to the original matching input text. By
414 default, returns a string containing the original parsed text.
416 If the optional ``as_string`` argument is passed as
417 ``False``, then the return value is
418 a :class:`ParseResults` containing any results names that
419 were originally matched, and a single token containing the original
420 matched text from the input string. So if the expression passed to
421 :class:`original_text_for` contains expressions with defined
422 results names, you must set ``as_string`` to ``False`` if you
423 want to preserve those results name values.
425 The ``asString`` pre-PEP8 argument is retained for compatibility,
426 but will be removed in a future release.
428 Example:
430 .. testcode::
432 src = "this is test <b> bold <i>text</i> </b> normal text "
433 for tag in ("b", "i"):
434 opener, closer = make_html_tags(tag)
435 patt = original_text_for(opener + ... + closer)
436 print(patt.search_string(src)[0])
438 prints:
440 .. testoutput::
442 ['<b> bold <i>text</i> </b>']
443 ['<i>text</i>']
444 """
445 asString: bool = deprecate_argument(kwargs, "asString", True)
447 asString = asString and as_string
449 locMarker = Empty().set_parse_action(lambda s, loc, t: loc)
450 endlocMarker = locMarker.copy()
451 endlocMarker.callPreparse = False
452 matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")
453 if asString:
454 extractText = lambda s, l, t: s[t._original_start : t._original_end]
455 else:
457 def extractText(s, l, t):
458 t[:] = [s[t.pop("_original_start") : t.pop("_original_end")]]
460 matchExpr.set_parse_action(extractText)
461 matchExpr.ignoreExprs = expr.ignoreExprs
462 matchExpr.suppress_warning(Diagnostics.warn_ungrouped_named_tokens_in_collection)
463 return matchExpr
466def ungroup(expr: ParserElement) -> ParserElement:
467 """Helper to undo pyparsing's default grouping of And expressions,
468 even if all but one are non-empty.
469 """
470 return TokenConverter(expr).add_parse_action(lambda t: t[0])
473def locatedExpr(expr: ParserElement) -> ParserElement:
474 """
475 .. deprecated:: 3.0.0
476 Use the :class:`Located` class instead. Note that `Located`
477 returns results with one less grouping level.
479 Helper to decorate a returned token with its starting and ending
480 locations in the input string.
482 This helper adds the following results names:
484 - ``locn_start`` - location where matched expression begins
485 - ``locn_end`` - location where matched expression ends
486 - ``value`` - the actual parsed results
488 Be careful if the input text contains ``<TAB>`` characters, you
489 may want to call :meth:`ParserElement.parse_with_tabs`
490 """
491 warnings.warn(
492 f"{'locatedExpr'!r} deprecated - use {'Located'!r}",
493 PyparsingDeprecationWarning,
494 stacklevel=2,
495 )
497 locator = Empty().set_parse_action(lambda ss, ll, tt: ll)
498 return Group(
499 locator("locn_start")
500 + expr("value")
501 + locator.copy().leave_whitespace()("locn_end")
502 )
505# define special default value to permit None as a significant value for
506# ignore_expr
507_NO_IGNORE_EXPR_GIVEN = NoMatch()
510def nested_expr(
511 opener: Union[str, ParserElement] = "(",
512 closer: Union[str, ParserElement] = ")",
513 content: typing.Optional[ParserElement] = None,
514 ignore_expr: typing.Optional[ParserElement] = _NO_IGNORE_EXPR_GIVEN,
515 **kwargs,
516) -> ParserElement:
517 """Helper method for defining nested lists enclosed in opening and
518 closing delimiters (``"("`` and ``")"`` are the default).
520 :param opener: str - opening character for a nested list
521 (default= ``"("``); can also be a pyparsing expression
523 :param closer: str - closing character for a nested list
524 (default= ``")"``); can also be a pyparsing expression
526 :param content: expression for items within the nested lists
528 :param ignore_expr: expression for ignoring opening and closing delimiters
529 (default = :class:`quoted_string`)
531 Parameter ``ignoreExpr`` is retained for compatibility
532 but will be removed in a future release.
534 If an expression is not provided for the content argument, the
535 nested expression will capture all whitespace-delimited content
536 between delimiters as a list of separate values.
538 Use the ``ignore_expr`` argument to define expressions that may
539 contain opening or closing characters that should not be treated as
540 opening or closing characters for nesting, such as quoted_string or
541 a comment expression. Specify multiple expressions using an
542 :class:`Or` or :class:`MatchFirst`. The default is
543 :class:`quoted_string`, but if no expressions are to be ignored, then
544 pass ``None`` for this argument.
546 Example:
548 .. testcode::
550 data_type = one_of("void int short long char float double")
551 decl_data_type = Combine(data_type + Opt(Word('*')))
552 ident = Word(alphas+'_', alphanums+'_')
553 number = pyparsing_common.number
554 arg = Group(decl_data_type + ident)
555 LPAR, RPAR = map(Suppress, "()")
557 code_body = nested_expr('{', '}', ignore_expr=(quoted_string | c_style_comment))
559 c_function = (decl_data_type("type")
560 + ident("name")
561 + LPAR + Opt(DelimitedList(arg), [])("args") + RPAR
562 + code_body("body"))
563 c_function.ignore(c_style_comment)
565 source_code = '''
566 int is_odd(int x) {
567 return (x%2);
568 }
570 int dec_to_hex(char hchar) {
571 if (hchar >= '0' && hchar <= '9') {
572 return (ord(hchar)-ord('0'));
573 } else {
574 return (10+ord(hchar)-ord('A'));
575 }
576 }
577 '''
578 for func in c_function.search_string(source_code):
579 print(f"{func.name} ({func.type}) args: {func.args}")
582 prints:
584 .. testoutput::
586 is_odd (int) args: [['int', 'x']]
587 dec_to_hex (int) args: [['char', 'hchar']]
588 """
589 ignoreExpr: ParserElement = deprecate_argument(
590 kwargs, "ignoreExpr", _NO_IGNORE_EXPR_GIVEN
591 )
593 if ignoreExpr != ignore_expr:
594 ignoreExpr = ignore_expr if ignoreExpr is _NO_IGNORE_EXPR_GIVEN else ignoreExpr # type: ignore [assignment]
596 if ignoreExpr is _NO_IGNORE_EXPR_GIVEN:
597 ignoreExpr = quoted_string()
599 if opener == closer:
600 raise ValueError("opening and closing strings cannot be the same")
602 if content is None:
603 if isinstance(opener, str_type) and isinstance(closer, str_type):
604 opener = typing.cast(str, opener)
605 closer = typing.cast(str, closer)
606 if len(opener) == 1 and len(closer) == 1:
607 if ignoreExpr is not None:
608 content = Combine(
609 OneOrMore(
610 ~ignoreExpr
611 + CharsNotIn(
612 opener + closer + ParserElement.DEFAULT_WHITE_CHARS,
613 exact=1,
614 )
615 )
616 )
617 else:
618 content = Combine(
619 Empty()
620 + CharsNotIn(
621 opener + closer + ParserElement.DEFAULT_WHITE_CHARS
622 )
623 )
624 else:
625 if ignoreExpr is not None:
626 content = Combine(
627 OneOrMore(
628 ~ignoreExpr
629 + ~Literal(opener)
630 + ~Literal(closer)
631 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)
632 )
633 )
634 else:
635 content = Combine(
636 OneOrMore(
637 ~Literal(opener)
638 + ~Literal(closer)
639 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)
640 )
641 )
642 else:
643 raise ValueError(
644 "opening and closing arguments must be strings if no content expression is given"
645 )
647 # for these internally-created context expressions, simulate whitespace-skipping
648 if ParserElement.DEFAULT_WHITE_CHARS:
649 content.set_parse_action(
650 lambda t: t[0].strip(ParserElement.DEFAULT_WHITE_CHARS)
651 )
653 ret = Forward()
654 if ignoreExpr is not None:
655 ret <<= Group(
656 _suppression(opener)
657 + ZeroOrMore(ignoreExpr | ret | content)
658 + _suppression(closer)
659 )
660 else:
661 ret <<= Group(
662 _suppression(opener) + ZeroOrMore(ret | content) + _suppression(closer)
663 )
665 ret.set_name(f"nested {opener}{closer} expression")
667 # don't override error message from content expressions
668 ret.errmsg = None
669 return ret
672def _makeTags(tagStr, xml, suppress_LT=Suppress("<"), suppress_GT=Suppress(">")):
673 """Internal helper to construct opening and closing tag expressions,
674 given a tag name"""
675 if isinstance(tagStr, str_type):
676 resname = tagStr
677 tagStr = Keyword(tagStr, caseless=not xml)
678 else:
679 resname = tagStr.name
681 tagAttrName = Word(alphas, alphanums + "_-:")
682 if xml:
683 tagAttrValue = dbl_quoted_string.copy().set_parse_action(remove_quotes)
684 openTag = (
685 suppress_LT
686 + tagStr("tag")
687 + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue)))
688 + Opt("/", default=[False])("empty").set_parse_action(
689 lambda s, l, t: t[0] == "/"
690 )
691 + suppress_GT
692 )
693 else:
694 tagAttrValue = quoted_string.copy().set_parse_action(remove_quotes) | Word(
695 printables, exclude_chars=">"
696 )
697 openTag = (
698 suppress_LT
699 + tagStr("tag")
700 + Dict(
701 ZeroOrMore(
702 Group(
703 tagAttrName.set_parse_action(lambda t: t[0].lower())
704 + Opt(Suppress("=") + tagAttrValue)
705 )
706 )
707 )
708 + Opt("/", default=[False])("empty").set_parse_action(
709 lambda s, l, t: t[0] == "/"
710 )
711 + suppress_GT
712 )
713 closeTag = Combine(Literal("</") + tagStr + ">", adjacent=False)
715 openTag.set_name(f"<{resname}>")
716 # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels
717 openTag.add_parse_action(
718 lambda t: t.__setitem__(
719 "start" + "".join(resname.replace(":", " ").title().split()), t.copy()
720 )
721 )
722 closeTag = closeTag(
723 "end" + "".join(resname.replace(":", " ").title().split())
724 ).set_name(f"</{resname}>")
725 openTag.tag = resname
726 closeTag.tag = resname
727 openTag.tag_body = SkipTo(closeTag())
728 return openTag, closeTag
731def make_html_tags(
732 tag_str: Union[str, ParserElement],
733) -> tuple[ParserElement, ParserElement]:
734 """Helper to construct opening and closing tag expressions for HTML,
735 given a tag name. Matches tags in either upper or lower case,
736 attributes with namespaces and with quoted or unquoted values.
738 Example:
740 .. testcode::
742 text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
743 # make_html_tags returns pyparsing expressions for the opening and
744 # closing tags as a 2-tuple
745 a, a_end = make_html_tags("A")
746 link_expr = a + SkipTo(a_end)("link_text") + a_end
748 for link in link_expr.search_string(text):
749 # attributes in the <A> tag (like "href" shown here) are
750 # also accessible as named results
751 print(link.link_text, '->', link.href)
753 prints:
755 .. testoutput::
757 pyparsing -> https://github.com/pyparsing/pyparsing/wiki
758 """
759 return _makeTags(tag_str, False)
762def make_xml_tags(
763 tag_str: Union[str, ParserElement],
764) -> tuple[ParserElement, ParserElement]:
765 """Helper to construct opening and closing tag expressions for XML,
766 given a tag name. Matches tags only in the given upper/lower case.
768 Example: similar to :class:`make_html_tags`
769 """
770 return _makeTags(tag_str, True)
773any_open_tag: ParserElement
774any_close_tag: ParserElement
775any_open_tag, any_close_tag = make_html_tags(
776 Word(alphas, alphanums + "_:").set_name("any tag")
777)
779_htmlEntityMap = {k.rstrip(";"): v for k, v in html.entities.html5.items()}
780_most_common_entities = "nbsp lt gt amp quot apos cent pound euro copy".replace(
781 " ", "|"
782)
783common_html_entity = Regex(
784 lambda: f"&(?P<entity>{_most_common_entities}|{make_compressed_re(_htmlEntityMap)});"
785).set_name("common HTML entity")
788def replace_html_entity(s, l, t):
789 """Helper parser action to replace common HTML entities with their special characters"""
790 return _htmlEntityMap.get(t.entity)
793class OpAssoc(Enum):
794 """Enumeration of operator associativity
795 - used in constructing InfixNotationOperatorSpec for :class:`infix_notation`"""
797 LEFT = 1
798 RIGHT = 2
801InfixNotationOperatorArgType = Union[
802 ParserElement, str, tuple[Union[ParserElement, str], Union[ParserElement, str]]
803]
804InfixNotationOperatorSpec = Union[
805 tuple[
806 InfixNotationOperatorArgType,
807 int,
808 OpAssoc,
809 typing.Optional[ParseAction],
810 ],
811 tuple[
812 InfixNotationOperatorArgType,
813 int,
814 OpAssoc,
815 ],
816]
819def infix_notation(
820 base_expr: ParserElement,
821 op_list: list[InfixNotationOperatorSpec],
822 lpar: Union[str, ParserElement] = Suppress("("),
823 rpar: Union[str, ParserElement] = Suppress(")"),
824) -> Forward:
825 """Helper method for constructing grammars of expressions made up of
826 operators working in a precedence hierarchy. Operators may be unary
827 or binary, left- or right-associative. Parse actions can also be
828 attached to operator expressions. The generated parser will also
829 recognize the use of parentheses to override operator precedences
830 (see example below).
832 Note: if you define a deep operator list, you may see performance
833 issues when using infix_notation. See
834 :class:`ParserElement.enable_packrat` for a mechanism to potentially
835 improve your parser performance.
837 Parameters:
839 :param base_expr: expression representing the most basic operand to
840 be used in the expression
841 :param op_list: list of tuples, one for each operator precedence level
842 in the expression grammar; each tuple is of the form ``(op_expr,
843 num_operands, right_left_assoc, (optional)parse_action)``, where:
845 - ``op_expr`` is the pyparsing expression for the operator; may also
846 be a string, which will be converted to a Literal; if ``num_operands``
847 is 3, ``op_expr`` is a tuple of two expressions, for the two
848 operators separating the 3 terms
849 - ``num_operands`` is the number of terms for this operator (must be 1,
850 2, or 3)
851 - ``right_left_assoc`` is the indicator whether the operator is right
852 or left associative, using the pyparsing-defined constants
853 ``OpAssoc.RIGHT`` and ``OpAssoc.LEFT``.
854 - ``parse_action`` is the parse action to be associated with
855 expressions matching this operator expression (the parse action
856 tuple member may be omitted); if the parse action is passed
857 a tuple or list of functions, this is equivalent to calling
858 ``set_parse_action(*fn)``
859 (:class:`ParserElement.set_parse_action`)
861 :param lpar: expression for matching left-parentheses; if passed as a
862 str, then will be parsed as ``Suppress(lpar)``. If lpar is passed as
863 an expression (such as ``Literal('(')``), then it will be kept in
864 the parsed results, and grouped with them. (default= ``Suppress('(')``)
865 :param rpar: expression for matching right-parentheses; if passed as a
866 str, then will be parsed as ``Suppress(rpar)``. If rpar is passed as
867 an expression (such as ``Literal(')')``), then it will be kept in
868 the parsed results, and grouped with them. (default= ``Suppress(')')``)
870 Example:
872 .. testcode::
874 # simple example of four-function arithmetic with ints and
875 # variable names
876 integer = pyparsing_common.signed_integer
877 varname = pyparsing_common.identifier
879 arith_expr = infix_notation(integer | varname,
880 [
881 ('-', 1, OpAssoc.RIGHT),
882 (one_of('* /'), 2, OpAssoc.LEFT),
883 (one_of('+ -'), 2, OpAssoc.LEFT),
884 ])
886 arith_expr.run_tests('''
887 5+3*6
888 (5+3)*6
889 (5+x)*y
890 -2--11
891 ''', full_dump=False)
893 prints:
895 .. testoutput::
896 :options: +NORMALIZE_WHITESPACE
899 5+3*6
900 [[5, '+', [3, '*', 6]]]
902 (5+3)*6
903 [[[5, '+', 3], '*', 6]]
905 (5+x)*y
906 [[[5, '+', 'x'], '*', 'y']]
908 -2--11
909 [[['-', 2], '-', ['-', 11]]]
910 """
912 # captive version of FollowedBy that does not do parse actions or capture results names
913 class _FB(FollowedBy):
914 def parseImpl(self, instring, loc, doActions=True):
915 self.expr.try_parse(instring, loc)
916 return loc, []
918 _FB.__name__ = "FollowedBy>"
920 ret = Forward()
921 ret.set_name(f"{base_expr.name}_expression")
922 if isinstance(lpar, str):
923 lpar = Suppress(lpar)
924 if isinstance(rpar, str):
925 rpar = Suppress(rpar)
927 nested_expr = (lpar + ret + rpar).set_name(f"nested_{base_expr.name}_expression")
929 # if lpar and rpar are not suppressed, wrap in group
930 if not (isinstance(lpar, Suppress) and isinstance(rpar, Suppress)):
931 lastExpr = base_expr | Group(nested_expr)
932 else:
933 lastExpr = base_expr | nested_expr
935 arity: int
936 rightLeftAssoc: opAssoc
937 pa: typing.Optional[ParseAction]
938 opExpr1: ParserElement
939 opExpr2: ParserElement
940 matchExpr: ParserElement
941 match_lookahead: ParserElement
942 for operDef in op_list:
943 opExpr, arity, rightLeftAssoc, pa = (operDef + (None,))[:4] # type: ignore[assignment]
944 if isinstance(opExpr, str_type):
945 opExpr = ParserElement._literalStringClass(opExpr)
946 opExpr = typing.cast(ParserElement, opExpr)
947 if arity == 3:
948 if not isinstance(opExpr, (tuple, list)) or len(opExpr) != 2:
949 raise ValueError(
950 "if numterms=3, opExpr must be a tuple or list of two expressions"
951 )
952 opExpr1, opExpr2 = opExpr
953 term_name = f"{opExpr1}{opExpr2} operations"
954 else:
955 term_name = f"{opExpr} operations"
957 if not 1 <= arity <= 3:
958 raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
960 if rightLeftAssoc not in (OpAssoc.LEFT, OpAssoc.RIGHT):
961 raise ValueError("operator must indicate right or left associativity")
963 thisExpr: ParserElement = Forward().set_name(term_name)
964 thisExpr = typing.cast(Forward, thisExpr)
965 match_lookahead = And([])
966 if rightLeftAssoc is OpAssoc.LEFT:
967 if arity == 1:
968 match_lookahead = _FB(lastExpr + opExpr)
969 matchExpr = Group(lastExpr + opExpr[1, ...])
970 elif arity == 2:
971 if opExpr is not None:
972 match_lookahead = _FB(lastExpr + opExpr + lastExpr)
973 matchExpr = Group(lastExpr + (opExpr + lastExpr)[1, ...])
974 else:
975 match_lookahead = _FB(lastExpr + lastExpr)
976 matchExpr = Group(lastExpr[2, ...])
977 elif arity == 3:
978 match_lookahead = _FB(
979 lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr
980 )
981 matchExpr = Group(
982 lastExpr + (opExpr1 + lastExpr + opExpr2 + lastExpr)[1, ...]
983 )
984 elif rightLeftAssoc is OpAssoc.RIGHT:
985 if arity == 1:
986 # try to avoid LR with this extra test
987 if not isinstance(opExpr, Opt):
988 opExpr = Opt(opExpr)
989 match_lookahead = _FB(opExpr.expr + thisExpr)
990 matchExpr = Group(opExpr + thisExpr)
991 elif arity == 2:
992 if opExpr is not None:
993 match_lookahead = _FB(lastExpr + opExpr + thisExpr)
994 matchExpr = Group(lastExpr + (opExpr + thisExpr)[1, ...])
995 else:
996 match_lookahead = _FB(lastExpr + thisExpr)
997 matchExpr = Group(lastExpr + thisExpr[1, ...])
998 elif arity == 3:
999 match_lookahead = _FB(
1000 lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr
1001 )
1002 matchExpr = Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)
1004 # suppress lookahead expr from railroad diagrams
1005 match_lookahead.show_in_diagram = False
1007 # TODO - determine why this statement can't be included in the following
1008 # if pa block
1009 matchExpr = match_lookahead + matchExpr
1011 if pa:
1012 if isinstance(pa, (tuple, list)):
1013 matchExpr.set_parse_action(*pa)
1014 else:
1015 matchExpr.set_parse_action(pa)
1017 thisExpr <<= (matchExpr | lastExpr).set_name(term_name)
1018 lastExpr = thisExpr
1020 ret <<= lastExpr
1021 return ret
1024def indentedBlock(blockStatementExpr, indentStack, indent=True, backup_stacks=[]):
1025 """
1026 .. deprecated:: 3.0.0
1027 Use the :class:`IndentedBlock` class instead. Note that `IndentedBlock`
1028 has a difference method signature.
1030 Helper method for defining space-delimited indentation blocks,
1031 such as those used to define block statements in Python source code.
1033 :param blockStatementExpr: expression defining syntax of statement that
1034 is repeated within the indented block
1036 :param indentStack: list created by caller to manage indentation stack
1037 (multiple ``statementWithIndentedBlock`` expressions within a single
1038 grammar should share a common ``indentStack``)
1040 :param indent: boolean indicating whether block must be indented beyond
1041 the current level; set to ``False`` for block of left-most statements
1043 A valid block must contain at least one ``blockStatement``.
1045 (Note that indentedBlock uses internal parse actions which make it
1046 incompatible with packrat parsing.)
1048 Example:
1050 .. testcode::
1052 data = '''
1053 def A(z):
1054 A1
1055 B = 100
1056 G = A2
1057 A2
1058 A3
1059 B
1060 def BB(a,b,c):
1061 BB1
1062 def BBA():
1063 bba1
1064 bba2
1065 bba3
1066 C
1067 D
1068 def spam(x,y):
1069 def eggs(z):
1070 pass
1071 '''
1073 indentStack = [1]
1074 stmt = Forward()
1076 identifier = Word(alphas, alphanums)
1077 funcDecl = ("def" + identifier + Group("(" + Opt(delimitedList(identifier)) + ")") + ":")
1078 func_body = indentedBlock(stmt, indentStack)
1079 funcDef = Group(funcDecl + func_body)
1081 rvalue = Forward()
1082 funcCall = Group(identifier + "(" + Opt(delimitedList(rvalue)) + ")")
1083 rvalue << (funcCall | identifier | Word(nums))
1084 assignment = Group(identifier + "=" + rvalue)
1085 stmt << (funcDef | assignment | identifier)
1087 module_body = stmt[1, ...]
1089 parseTree = module_body.parseString(data)
1090 parseTree.pprint()
1092 prints:
1094 .. testoutput::
1096 [['def',
1097 'A',
1098 ['(', 'z', ')'],
1099 ':',
1100 [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],
1101 'B',
1102 ['def',
1103 'BB',
1104 ['(', 'a', 'b', 'c', ')'],
1105 ':',
1106 [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],
1107 'C',
1108 'D',
1109 ['def',
1110 'spam',
1111 ['(', 'x', 'y', ')'],
1112 ':',
1113 [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]]
1114 """
1115 warnings.warn(
1116 f"{'indentedBlock'!r} deprecated - use {'IndentedBlock'!r}",
1117 PyparsingDeprecationWarning,
1118 stacklevel=2,
1119 )
1121 backup_stacks.append(indentStack[:])
1123 def reset_stack():
1124 indentStack[:] = backup_stacks[-1]
1126 def checkPeerIndent(s, l, t):
1127 if l >= len(s):
1128 return
1129 curCol = col(l, s)
1130 if curCol != indentStack[-1]:
1131 if curCol > indentStack[-1]:
1132 raise ParseException(s, l, "illegal nesting")
1133 raise ParseException(s, l, "not a peer entry")
1135 def checkSubIndent(s, l, t):
1136 curCol = col(l, s)
1137 if curCol > indentStack[-1]:
1138 indentStack.append(curCol)
1139 else:
1140 raise ParseException(s, l, "not a subentry")
1142 def checkUnindent(s, l, t):
1143 if l >= len(s):
1144 return
1145 curCol = col(l, s)
1146 if not (indentStack and curCol in indentStack):
1147 raise ParseException(s, l, "not an unindent")
1148 if curCol < indentStack[-1]:
1149 indentStack.pop()
1151 NL = OneOrMore(LineEnd().set_whitespace_chars("\t ").suppress())
1152 INDENT = (Empty() + Empty().set_parse_action(checkSubIndent)).set_name("INDENT")
1153 PEER = Empty().set_parse_action(checkPeerIndent).set_name("")
1154 UNDENT = Empty().set_parse_action(checkUnindent).set_name("UNINDENT")
1155 if indent:
1156 smExpr = Group(
1157 Opt(NL)
1158 + INDENT
1159 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))
1160 + UNDENT
1161 )
1162 else:
1163 smExpr = Group(
1164 Opt(NL)
1165 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))
1166 + Opt(UNDENT)
1167 )
1169 # add a parse action to remove backup_stack from list of backups
1170 smExpr.add_parse_action(
1171 lambda: backup_stacks.pop(-1) and None if backup_stacks else None
1172 )
1173 smExpr.set_fail_action(lambda a, b, c, d: reset_stack())
1174 blockStatementExpr.ignore(_bslash + LineEnd())
1175 return smExpr.set_name("indented block")
1178# it's easy to get these comment structures wrong - they're very common,
1179# so may as well make them available
1180c_style_comment = Regex(r"/\*(?:[^*]|\*(?!/))*\*\/").set_name("C style comment")
1181"Comment of the form ``/* ... */``"
1183html_comment = Regex(r"<!--[\s\S]*?-->").set_name("HTML comment")
1184"Comment of the form ``<!-- ... -->``"
1186rest_of_line = Regex(r".*").leave_whitespace().set_name("rest of line")
1187dbl_slash_comment = Regex(r"//(?:\\\n|[^\n])*").set_name("// comment")
1188"Comment of the form ``// ... (to end of line)``"
1190cpp_style_comment = Regex(
1191 r"(?:/\*(?:[^*]|\*(?!/))*\*\/)|(?://(?:\\\n|[^\n])*)"
1192).set_name("C++ style comment")
1193"Comment of either form :class:`c_style_comment` or :class:`dbl_slash_comment`"
1195java_style_comment = cpp_style_comment
1196"Same as :class:`cpp_style_comment`"
1198python_style_comment = Regex(r"#.*").set_name("Python style comment")
1199"Comment of the form ``# ... (to end of line)``"
1202# build list of built-in expressions, for future reference if a global default value
1203# gets updated
1204_builtin_exprs: list[ParserElement] = [
1205 v for v in vars().values() if isinstance(v, ParserElement)
1206]
1209# compatibility function, superseded by DelimitedList class
1210def delimited_list(
1211 expr: Union[str, ParserElement],
1212 delim: Union[str, ParserElement] = ",",
1213 combine: bool = False,
1214 min: typing.Optional[int] = None,
1215 max: typing.Optional[int] = None,
1216 *,
1217 allow_trailing_delim: bool = False,
1218) -> ParserElement:
1219 """
1220 .. deprecated:: 3.1.0
1221 Use the :class:`DelimitedList` class instead.
1222 """
1223 return DelimitedList(
1224 expr, delim, combine, min, max, allow_trailing_delim=allow_trailing_delim
1225 )
1228# Compatibility synonyms
1229# fmt: off
1230opAssoc = OpAssoc
1231anyOpenTag = any_open_tag
1232anyCloseTag = any_close_tag
1233commonHTMLEntity = common_html_entity
1234cStyleComment = c_style_comment
1235htmlComment = html_comment
1236restOfLine = rest_of_line
1237dblSlashComment = dbl_slash_comment
1238cppStyleComment = cpp_style_comment
1239javaStyleComment = java_style_comment
1240pythonStyleComment = python_style_comment
1241delimitedList = replaced_by_pep8("delimitedList", DelimitedList)
1242delimited_list = replaced_by_pep8("delimited_list", DelimitedList)
1243countedArray = replaced_by_pep8("countedArray", counted_array)
1244matchPreviousLiteral = replaced_by_pep8("matchPreviousLiteral", match_previous_literal)
1245matchPreviousExpr = replaced_by_pep8("matchPreviousExpr", match_previous_expr)
1246oneOf = replaced_by_pep8("oneOf", one_of)
1247dictOf = replaced_by_pep8("dictOf", dict_of)
1248originalTextFor = replaced_by_pep8("originalTextFor", original_text_for)
1249nestedExpr = replaced_by_pep8("nestedExpr", nested_expr)
1250makeHTMLTags = replaced_by_pep8("makeHTMLTags", make_html_tags)
1251makeXMLTags = replaced_by_pep8("makeXMLTags", make_xml_tags)
1252replaceHTMLEntity = replaced_by_pep8("replaceHTMLEntity", replace_html_entity)
1253infixNotation = replaced_by_pep8("infixNotation", infix_notation)
1254# fmt: on