codekingpro/portable-devtools
114k
1# results.py2 3from __future__ import annotations4 5import collections6from collections.abc import (7 MutableMapping,8 Mapping,9 MutableSequence,10 Iterator,11 Iterable,12)13import pprint14from typing import Any15 16from .util import deprecate_argument, _is_iterable, _flatten17 18 19str_type: tuple[type, ...] = (str, bytes)20_generator_type = type((_ for _ in ()))21NULL_SLICE: slice = slice(None)22 23 24class _ParseResultsWithOffset:25 tup: tuple[ParseResults, int]26 __slots__ = ["tup"]27 28 def __init__(self, p1: ParseResults, p2: int) -> None:29 self.tup: tuple[ParseResults, int] = (p1, p2)30 31 def __getitem__(self, i):32 return self.tup[i]33 34 def __getstate__(self):35 return self.tup36 37 def __setstate__(self, *args):38 self.tup = args[0]39 40 41class ParseResults:42 """Structured parse results, to provide multiple means of access to43 the parsed data:44 45 - as a list (``len(results)``)46 - by list index (``results[0], results[1]``, etc.)47 - by attribute (``results.<results_name>`` - see :class:`ParserElement.set_results_name`)48 49 Example:50 51 .. testcode::52 53 integer = Word(nums)54 date_str = (integer.set_results_name("year") + '/'55 + integer.set_results_name("month") + '/'56 + integer.set_results_name("day"))57 # equivalent form:58 # date_str = (integer("year") + '/'59 # + integer("month") + '/'60 # + integer("day"))61 62 # parse_string returns a ParseResults object63 result = date_str.parse_string("1999/12/31")64 65 def test(s, fn=repr):66 print(f"{s} -> {fn(eval(s))}")67 68 test("list(result)")69 test("result[0]")70 test("result['month']")71 test("result.day")72 test("'month' in result")73 test("'minutes' in result")74 test("result.dump()", str)75 76 prints:77 78 .. testoutput::79 80 list(result) -> ['1999', '/', '12', '/', '31']81 result[0] -> '1999'82 result['month'] -> '12'83 result.day -> '31'84 'month' in result -> True85 'minutes' in result -> False86 result.dump() -> ['1999', '/', '12', '/', '31']87 - day: '31'88 - month: '12'89 - year: '1999'90 91 """92 93 _null_values: tuple[Any, ...] = (None, [], ())94 95 _name: str96 _parent: ParseResults97 _all_names: set[str]98 _modal: bool99 _toklist: list[Any]100 _tokdict: dict[str, Any]101 102 __slots__ = (103 "_name",104 "_parent",105 "_all_names",106 "_modal",107 "_toklist",108 "_tokdict",109 )110 111 class List(list):112 """113 Simple wrapper class to distinguish parsed list results that should be preserved114 as actual Python lists, instead of being converted to :class:`ParseResults`:115 116 .. testcode::117 118 import pyparsing as pp119 ppc = pp.common120 121 LBRACK, RBRACK, LPAR, RPAR = pp.Suppress.using_each("[]()")122 element = pp.Forward()123 item = ppc.integer124 item_list = pp.DelimitedList(element)125 element_list = LBRACK + item_list + RBRACK | LPAR + item_list + RPAR126 element <<= item | element_list127 128 # add parse action to convert from ParseResults129 # to actual Python collection types130 @element_list.add_parse_action131 def as_python_list(t):132 return pp.ParseResults.List(t.as_list())133 134 element.run_tests('''135 100136 [2,3,4]137 [[2, 1],3,4]138 [(2, 1),3,4]139 (2,3,4)140 ([2, 3], 4)141 ''', post_parse=lambda s, r: (r[0], type(r[0]))142 )143 144 prints:145 146 .. testoutput::147 :options: +NORMALIZE_WHITESPACE148 149 150 100151 (100, <class 'int'>)152 153 [2,3,4]154 ([2, 3, 4], <class 'list'>)155 156 [[2, 1],3,4]157 ([[2, 1], 3, 4], <class 'list'>)158 159 [(2, 1),3,4]160 ([[2, 1], 3, 4], <class 'list'>)161 162 (2,3,4)163 ([2, 3, 4], <class 'list'>)164 165 ([2, 3], 4)166 ([[2, 3], 4], <class 'list'>)167 168 (Used internally by :class:`Group` when `aslist=True`.)169 """170 171 def __new__(cls, contained=None):172 if contained is None:173 contained = []174 175 if not isinstance(contained, list):176 raise TypeError(177 f"{cls.__name__} may only be constructed with a list, not {type(contained).__name__}"178 )179 180 return list.__new__(cls)181 182 def __new__(cls, toklist=None, name=None, **kwargs):183 if isinstance(toklist, ParseResults):184 return toklist185 self = object.__new__(cls)186 self._name = None187 self._parent = None188 self._all_names = set()189 190 if toklist is None:191 self._toklist = []192 elif isinstance(toklist, (list, _generator_type)):193 self._toklist = (194 [toklist[:]]195 if isinstance(toklist, ParseResults.List)196 else list(toklist)197 )198 else:199 self._toklist = [toklist]200 self._tokdict = dict()201 return self202 203 # Performance tuning: we construct a *lot* of these, so keep this204 # constructor as small and fast as possible205 def __init__(206 self,207 toklist=None,208 name=None,209 aslist=True,210 modal=True,211 isinstance=isinstance,212 **kwargs,213 ) -> None:214 asList = deprecate_argument(kwargs, "asList", True, new_name="aslist")215 216 asList = asList and aslist217 self._tokdict: dict[str, _ParseResultsWithOffset]218 self._modal = modal219 220 if name is None or name == "":221 return222 223 if isinstance(name, int):224 name = str(name)225 226 if not modal:227 self._all_names = {name}228 229 self._name = name230 231 if toklist in self._null_values:232 return233 234 if isinstance(toklist, (str_type, type)):235 toklist = [toklist]236 237 if asList:238 if isinstance(toklist, ParseResults):239 self[name] = _ParseResultsWithOffset(ParseResults(toklist._toklist), 0)240 else:241 self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]), 0)242 self[name]._name = name243 return244 245 try:246 self[name] = toklist[0]247 except (KeyError, TypeError, IndexError):248 if toklist is not self:249 self[name] = toklist250 else:251 self._name = name252 253 def __getitem__(self, i):254 if isinstance(i, (int, slice)):255 return self._toklist[i]256 257 if i not in self._all_names:258 return self._tokdict[i][-1][0]259 260 return ParseResults([v[0] for v in self._tokdict[i]])261 262 def __setitem__(self, k, v, isinstance=isinstance):263 if isinstance(v, _ParseResultsWithOffset):264 self._tokdict[k] = self._tokdict.get(k, list()) + [v]265 sub = v[0]266 elif isinstance(k, (int, slice)):267 self._toklist[k] = v268 sub = v269 else:270 self._tokdict[k] = self._tokdict.get(k, []) + [271 _ParseResultsWithOffset(v, 0)272 ]273 sub = v274 if isinstance(sub, ParseResults):275 sub._parent = self276 277 def __delitem__(self, i):278 if not isinstance(i, (int, slice)):279 del self._tokdict[i]280 return281 282 # slight optimization if del results[:]283 if i == NULL_SLICE:284 self._toklist.clear()285 return286 287 mylen = len(self._toklist)288 del self._toklist[i]289 290 # convert int to slice291 if isinstance(i, int):292 if i < 0:293 i += mylen294 i = slice(i, i + 1)295 # get removed indices296 removed = list(range(*i.indices(mylen)))297 removed.reverse()298 # fixup indices in token dictionary299 for occurrences in self._tokdict.values():300 for j in removed:301 for k, (value, position) in enumerate(occurrences):302 occurrences[k] = _ParseResultsWithOffset(303 value, position - (position > j)304 )305 306 def __contains__(self, k) -> bool:307 return k in self._tokdict308 309 def __len__(self) -> int:310 return len(self._toklist)311 312 def __bool__(self) -> bool:313 return not not (self._toklist or self._tokdict)314 315 def __iter__(self) -> Iterator:316 return iter(self._toklist)317 318 def __reversed__(self) -> Iterator:319 return iter(self._toklist[::-1])320 321 def keys(self):322 return iter(self._tokdict)323 324 def values(self):325 return (self[k] for k in self.keys())326 327 def items(self):328 return ((k, self[k]) for k in self.keys())329 330 def haskeys(self) -> bool:331 """332 Since ``keys()`` returns an iterator, this method is helpful in bypassing333 code that looks for the existence of any defined results names."""334 return not not self._tokdict335 336 def pop(self, *args, **kwargs):337 """338 Removes and returns item at specified index (default= ``last``).339 Supports both ``list`` and ``dict`` semantics for ``pop()``. If340 passed no argument or an integer argument, it will use ``list``341 semantics and pop tokens from the list of parsed tokens. If passed342 a non-integer argument (most likely a string), it will use ``dict``343 semantics and pop the corresponding value from any defined results344 names. A second default return value argument is supported, just as in345 ``dict.pop()``.346 347 Example:348 349 .. doctest::350 351 >>> numlist = Word(nums)[...]352 >>> print(numlist.parse_string("0 123 321"))353 ['0', '123', '321']354 355 >>> def remove_first(tokens):356 ... tokens.pop(0)357 ...358 >>> numlist.add_parse_action(remove_first)359 [W:(0-9)]...360 >>> print(numlist.parse_string("0 123 321"))361 ['123', '321']362 363 >>> label = Word(alphas)364 >>> patt = label("LABEL") + Word(nums)[1, ...]365 >>> print(patt.parse_string("AAB 123 321").dump())366 ['AAB', '123', '321']367 - LABEL: 'AAB'368 369 >>> # Use pop() in a parse action to remove named result370 >>> # (note that corresponding value is not371 >>> # removed from list form of results)372 >>> def remove_LABEL(tokens):373 ... tokens.pop("LABEL")374 ... return tokens375 ...376 >>> patt.add_parse_action(remove_LABEL)377 {W:(A-Za-z) {W:(0-9)}...}378 >>> print(patt.parse_string("AAB 123 321").dump())379 ['AAB', '123', '321']380 381 """382 if not args:383 args = [-1]384 for k, v in kwargs.items():385 if k == "default":386 args = (args[0], v)387 else:388 raise TypeError(f"pop() got an unexpected keyword argument {k!r}")389 if isinstance(args[0], int) or len(args) == 1 or args[0] in self:390 index = args[0]391 ret = self[index]392 del self[index]393 return ret394 else:395 defaultvalue = args[1]396 return defaultvalue397 398 def get(self, key, default_value=None):399 """400 Returns named result matching the given key, or if there is no401 such name, then returns the given ``default_value`` or ``None`` if no402 ``default_value`` is specified.403 404 Similar to ``dict.get()``.405 406 Example:407 408 .. doctest::409 410 >>> integer = Word(nums)411 >>> date_str = integer("year") + '/' + integer("month") + '/' + integer("day")412 413 >>> result = date_str.parse_string("1999/12/31")414 >>> result.get("year")415 '1999'416 >>> result.get("hour", "not specified")417 'not specified'418 >>> result.get("hour")419 420 """421 if key in self:422 return self[key]423 else:424 return default_value425 426 def insert(self, index, ins_string):427 """428 Inserts new element at location index in the list of parsed tokens.429 430 Similar to ``list.insert()``.431 432 Example:433 434 .. doctest::435 436 >>> numlist = Word(nums)[...]437 >>> print(numlist.parse_string("0 123 321"))438 ['0', '123', '321']439 440 >>> # use a parse action to insert the parse location441 >>> # in the front of the parsed results442 >>> def insert_locn(locn, tokens):443 ... tokens.insert(0, locn)444 ...445 >>> numlist.add_parse_action(insert_locn)446 [W:(0-9)]...447 >>> print(numlist.parse_string("0 123 321"))448 [0, '0', '123', '321']449 450 """451 self._toklist.insert(index, ins_string)452 # fixup indices in token dictionary453 for occurrences in self._tokdict.values():454 for k, (value, position) in enumerate(occurrences):455 occurrences[k] = _ParseResultsWithOffset(456 value, position + (position > index)457 )458 459 def append(self, item):460 """461 Add single element to end of ``ParseResults`` list of elements.462 463 Example:464 465 .. doctest::466 467 >>> numlist = Word(nums)[...]468 >>> print(numlist.parse_string("0 123 321"))469 ['0', '123', '321']470 471 >>> # use a parse action to compute the sum of the parsed integers,472 >>> # and add it to the end473 >>> def append_sum(tokens):474 ... tokens.append(sum(map(int, tokens)))475 ...476 >>> numlist.add_parse_action(append_sum)477 [W:(0-9)]...478 >>> print(numlist.parse_string("0 123 321"))479 ['0', '123', '321', 444]480 """481 self._toklist.append(item)482 483 def extend(self, itemseq):484 """485 Add sequence of elements to end of :class:`ParseResults` list of elements.486 487 Example:488 489 .. testcode::490 491 patt = Word(alphas)[1, ...]492 493 # use a parse action to append the reverse of the matched strings,494 # to make a palindrome495 def make_palindrome(tokens):496 tokens.extend(reversed([t[::-1] for t in tokens]))497 return ''.join(tokens)498 499 patt.add_parse_action(make_palindrome)500 print(patt.parse_string("lskdj sdlkjf lksd"))501 502 prints:503 504 .. testoutput::505 506 ['lskdjsdlkjflksddsklfjkldsjdksl']507 """508 if isinstance(itemseq, ParseResults):509 self.__iadd__(itemseq)510 else:511 self._toklist.extend(itemseq)512 513 def clear(self):514 """515 Clear all elements and results names.516 """517 del self._toklist[:]518 self._tokdict.clear()519 520 def __getattr__(self, name):521 try:522 return self[name]523 except KeyError:524 if name.startswith("__"):525 raise AttributeError(name)526 return ""527 528 def __add__(self, other: ParseResults) -> ParseResults:529 ret = self.copy()530 ret += other531 return ret532 533 def __iadd__(self, other: ParseResults) -> ParseResults:534 if not other:535 return self536 537 if other._tokdict:538 offset = len(self._toklist)539 addoffset = lambda a: offset if a < 0 else a + offset540 otheritems = other._tokdict.items()541 otherdictitems = [542 (k, _ParseResultsWithOffset(v[0], addoffset(v[1])))543 for k, vlist in otheritems544 for v in vlist545 ]546 for k, v in otherdictitems:547 self[k] = v548 if isinstance(v[0], ParseResults):549 v[0]._parent = self550 551 self._toklist += other._toklist552 self._all_names |= other._all_names553 return self554 555 def __radd__(self, other) -> ParseResults:556 if isinstance(other, int) and other == 0:557 # useful for merging many ParseResults using sum() builtin558 return self.copy()559 else:560 # this may raise a TypeError - so be it561 return other + self562 563 def __repr__(self) -> str:564 return f"{type(self).__name__}({self._toklist!r}, {self.as_dict()})"565 566 def __str__(self) -> str:567 return (568 "["569 + ", ".join(570 [571 str(i) if isinstance(i, ParseResults) else repr(i)572 for i in self._toklist573 ]574 )575 + "]"576 )577 578 def _asStringList(self, sep=""):579 out = []580 for item in self._toklist:581 if out and sep:582 out.append(sep)583 if isinstance(item, ParseResults):584 out += item._asStringList()585 else:586 out.append(str(item))587 return out588 589 def as_list(self, *, flatten: bool = False) -> list:590 """591 Returns the parse results as a nested list of matching tokens, all converted to strings.592 If ``flatten`` is True, all the nesting levels in the returned list are collapsed.593 594 Example:595 596 .. doctest::597 598 >>> patt = Word(alphas)[1, ...]599 >>> result = patt.parse_string("sldkj lsdkj sldkj")600 >>> # even though the result prints in string-like form,601 >>> # it is actually a pyparsing ParseResults602 >>> type(result)603 <class 'pyparsing.results.ParseResults'>604 >>> print(result)605 ['sldkj', 'lsdkj', 'sldkj']606 607 .. doctest::608 609 >>> # Use as_list() to create an actual list610 >>> result_list = result.as_list()611 >>> type(result_list)612 <class 'list'>613 >>> print(result_list)614 ['sldkj', 'lsdkj', 'sldkj']615 616 .. versionchanged:: 3.2.0617 New ``flatten`` argument.618 """619 620 if flatten:621 return [*_flatten(self)]622 else:623 return [624 res.as_list() if isinstance(res, ParseResults) else res625 for res in self._toklist626 ]627 628 def as_dict(self) -> dict:629 """630 Returns the named parse results as a nested dictionary.631 632 Example:633 634 .. doctest::635 636 >>> integer = pp.Word(pp.nums)637 >>> date_str = integer("year") + '/' + integer("month") + '/' + integer("day")638 639 >>> result = date_str.parse_string('1999/12/31')640 >>> type(result)641 <class 'pyparsing.results.ParseResults'>642 >>> result643 ParseResults(['1999', '/', '12', '/', '31'], {'year': '1999', 'month': '12', 'day': '31'})644 645 >>> result_dict = result.as_dict()646 >>> type(result_dict)647 <class 'dict'>648 >>> result_dict649 {'year': '1999', 'month': '12', 'day': '31'}650 651 >>> # even though a ParseResults supports dict-like access,652 >>> # sometime you just need to have a dict653 >>> import json654 >>> print(json.dumps(result))655 Traceback (most recent call last):656 TypeError: Object of type ParseResults is not JSON serializable657 >>> print(json.dumps(result.as_dict()))658 {"year": "1999", "month": "12", "day": "31"}659 """660 661 def to_item(obj):662 if isinstance(obj, ParseResults):663 return obj.as_dict() if obj.haskeys() else [to_item(v) for v in obj]664 else:665 return obj666 667 return dict((k, to_item(v)) for k, v in self.items())668 669 def copy(self) -> ParseResults:670 """671 Returns a new shallow copy of a :class:`ParseResults` object.672 :class:`ParseResults` items contained within the source are673 shared with the copy. Use :meth:`ParseResults.deepcopy` to674 create a copy with its own separate content values.675 """676 ret = ParseResults(self._toklist)677 ret._tokdict = self._tokdict.copy()678 ret._parent = self._parent679 ret._all_names |= self._all_names680 ret._name = self._name681 return ret682 683 def deepcopy(self) -> ParseResults:684 """685 Returns a new deep copy of a :class:`ParseResults` object.686 687 .. versionadded:: 3.1.0688 """689 ret = self.copy()690 # replace values with copies if they are of known mutable types691 for i, obj in enumerate(self._toklist):692 if isinstance(obj, ParseResults):693 ret._toklist[i] = obj.deepcopy()694 elif isinstance(obj, (str, bytes)):695 pass696 elif isinstance(obj, MutableMapping):697 ret._toklist[i] = dest = type(obj)()698 for k, v in obj.items():699 dest[k] = v.deepcopy() if isinstance(v, ParseResults) else v700 elif isinstance(obj, Iterable):701 ret._toklist[i] = type(obj)(702 v.deepcopy() if isinstance(v, ParseResults) else v for v in obj # type: ignore[call-arg]703 )704 return ret705 706 def get_name(self) -> str | None:707 r"""708 Returns the results name for this token expression.709 710 Useful when several different expressions might match711 at a particular location.712 713 Example:714 715 .. testcode::716 717 integer = Word(nums)718 ssn_expr = Regex(r"\d\d\d-\d\d-\d\d\d\d")719 house_number_expr = Suppress('#') + Word(nums, alphanums)720 user_data = (Group(house_number_expr)("house_number")721 | Group(ssn_expr)("ssn")722 | Group(integer)("age"))723 user_info = user_data[1, ...]724 725 result = user_info.parse_string("22 111-22-3333 #221B")726 for item in result:727 print(item.get_name(), ':', item[0])728 729 prints:730 731 .. testoutput::732 733 age : 22734 ssn : 111-22-3333735 house_number : 221B736 737 """738 if self._name:739 return self._name740 elif self._parent:741 par: ParseResults = self._parent742 parent_tokdict_items = par._tokdict.items()743 return next(744 (745 k746 for k, vlist in parent_tokdict_items747 for v, loc in vlist748 if v is self749 ),750 None,751 )752 elif (753 len(self) == 1754 and len(self._tokdict) == 1755 and next(iter(self._tokdict.values()))[0][1] in (0, -1)756 ):757 return next(iter(self._tokdict.keys()))758 else:759 return None760 761 def dump(self, indent="", full=True, include_list=True, _depth=0) -> str:762 """763 Diagnostic method for listing out the contents of764 a :class:`ParseResults`. Accepts an optional ``indent`` argument so765 that this string can be embedded in a nested display of other data.766 767 Example:768 769 .. testcode::770 771 integer = Word(nums)772 date_str = integer("year") + '/' + integer("month") + '/' + integer("day")773 774 result = date_str.parse_string('1999/12/31')775 print(result.dump())776 777 prints:778 779 .. testoutput::780 781 ['1999', '/', '12', '/', '31']782 - day: '31'783 - month: '12'784 - year: '1999'785 """786 out = []787 NL = "\n"788 out.append(indent + str(self.as_list()) if include_list else "")789 790 if not full:791 return "".join(out)792 793 if self.haskeys():794 items = sorted((str(k), v) for k, v in self.items())795 for k, v in items:796 if out:797 out.append(NL)798 out.append(f"{indent}{(' ' * _depth)}- {k}: ")799 if not isinstance(v, ParseResults):800 out.append(repr(v))801 continue802 803 if not v:804 out.append(str(v))805 continue806 807 out.append(808 v.dump(809 indent=indent,810 full=full,811 include_list=include_list,812 _depth=_depth + 1,813 )814 )815 if not any(isinstance(vv, ParseResults) for vv in self):816 return "".join(out)817 818 v = self819 incr = " "820 nl = "\n"821 for i, vv in enumerate(v):822 if isinstance(vv, ParseResults):823 vv_dump = vv.dump(824 indent=indent,825 full=full,826 include_list=include_list,827 _depth=_depth + 1,828 )829 out.append(830 f"{nl}{indent}{incr * _depth}[{i}]:{nl}{indent}{incr * (_depth + 1)}{vv_dump}"831 )832 else:833 out.append(834 f"{nl}{indent}{incr * _depth}[{i}]:{nl}{indent}{incr * (_depth + 1)}{vv}"835 )836 837 return "".join(out)838 839 def pprint(self, *args, **kwargs):840 """841 Pretty-printer for parsed results as a list, using the842 `pprint <https://docs.python.org/3/library/pprint.html>`_ module.843 Accepts additional positional or keyword args as defined for844 `pprint.pprint <https://docs.python.org/3/library/pprint.html#pprint.pprint>`_ .845 846 Example:847 848 .. testcode::849 850 ident = Word(alphas, alphanums)851 num = Word(nums)852 func = Forward()853 term = ident | num | Group('(' + func + ')')854 func <<= ident + Group(Optional(DelimitedList(term)))855 result = func.parse_string("fna a,b,(fnb c,d,200),100")856 result.pprint(width=40)857 858 prints:859 860 .. testoutput::861 862 ['fna',863 ['a',864 'b',865 ['(', 'fnb', ['c', 'd', '200'], ')'],866 '100']]867 """868 pprint.pprint(self.as_list(), *args, **kwargs)869 870 # add support for pickle protocol871 def __getstate__(self):872 return (873 self._toklist,874 (875 self._tokdict.copy(),876 None,877 self._all_names,878 self._name,879 ),880 )881 882 def __setstate__(self, state):883 self._toklist, (self._tokdict, par, inAccumNames, self._name) = state884 self._all_names = set(inAccumNames)885 self._parent = None886 887 def __getnewargs__(self):888 return self._toklist, self._name889 890 def __dir__(self):891 return dir(type(self)) + list(self.keys())892 893 @classmethod894 def from_dict(cls, other, name=None) -> ParseResults:895 """896 Helper classmethod to construct a :class:`ParseResults` from a ``dict``, preserving the897 name-value relations as results names. If an optional ``name`` argument is898 given, a nested :class:`ParseResults` will be returned.899 """900 ret = cls([])901 for k, v in other.items():902 if isinstance(v, Mapping):903 ret += cls.from_dict(v, name=k)904 else:905 ret += cls([v], name=k, aslist=_is_iterable(v))906 if name is not None:907 ret = cls([ret], name=name)908 return ret909 910 asList = as_list911 """912 .. deprecated:: 3.0.0913 use :meth:`as_list`914 """915 asDict = as_dict916 """917 .. deprecated:: 3.0.0918 use :meth:`as_dict`919 """920 getName = get_name921 """922 .. deprecated:: 3.0.0923 use :meth:`get_name`924 """925 926 927MutableMapping.register(ParseResults)928MutableSequence.register(ParseResults)929 