00c9421d3b0362526b8f90dc01e8db73841e0b61
[SubU] /
1 # results.py
2 from collections.abc import MutableMapping, Mapping, MutableSequence, Iterator
3 import pprint
4 from weakref import ref as wkref
5 from typing import Tuple, Any
6
7 str_type: Tuple[type, ...] = (str, bytes)
8 _generator_type = type((_ for _ in ()))
9
10
11 class _ParseResultsWithOffset:
12     __slots__ = ["tup"]
13
14     def __init__(self, p1, p2):
15         self.tup = (p1, p2)
16
17     def __getitem__(self, i):
18         return self.tup[i]
19
20     def __getstate__(self):
21         return self.tup
22
23     def __setstate__(self, *args):
24         self.tup = args[0]
25
26
27 class ParseResults:
28     """Structured parse results, to provide multiple means of access to
29     the parsed data:
30
31     - as a list (``len(results)``)
32     - by list index (``results[0], results[1]``, etc.)
33     - by attribute (``results.<results_name>`` - see :class:`ParserElement.set_results_name`)
34
35     Example::
36
37         integer = Word(nums)
38         date_str = (integer.set_results_name("year") + '/'
39                     + integer.set_results_name("month") + '/'
40                     + integer.set_results_name("day"))
41         # equivalent form:
42         # date_str = (integer("year") + '/'
43         #             + integer("month") + '/'
44         #             + integer("day"))
45
46         # parse_string returns a ParseResults object
47         result = date_str.parse_string("1999/12/31")
48
49         def test(s, fn=repr):
50             print("{} -> {}".format(s, fn(eval(s))))
51         test("list(result)")
52         test("result[0]")
53         test("result['month']")
54         test("result.day")
55         test("'month' in result")
56         test("'minutes' in result")
57         test("result.dump()", str)
58
59     prints::
60
61         list(result) -> ['1999', '/', '12', '/', '31']
62         result[0] -> '1999'
63         result['month'] -> '12'
64         result.day -> '31'
65         'month' in result -> True
66         'minutes' in result -> False
67         result.dump() -> ['1999', '/', '12', '/', '31']
68         - day: '31'
69         - month: '12'
70         - year: '1999'
71     """
72
73     _null_values: Tuple[Any, ...] = (None, [], "", ())
74
75     __slots__ = [
76         "_name",
77         "_parent",
78         "_all_names",
79         "_modal",
80         "_toklist",
81         "_tokdict",
82         "__weakref__",
83     ]
84
85     class List(list):
86         """
87         Simple wrapper class to distinguish parsed list results that should be preserved
88         as actual Python lists, instead of being converted to :class:`ParseResults`:
89
90             LBRACK, RBRACK = map(pp.Suppress, "[]")
91             element = pp.Forward()
92             item = ppc.integer
93             element_list = LBRACK + pp.delimited_list(element) + RBRACK
94
95             # add parse actions to convert from ParseResults to actual Python collection types
96             def as_python_list(t):
97                 return pp.ParseResults.List(t.as_list())
98             element_list.add_parse_action(as_python_list)
99
100             element <<= item | element_list
101
102             element.run_tests('''
103                 100
104                 [2,3,4]
105                 [[2, 1],3,4]
106                 [(2, 1),3,4]
107                 (2,3,4)
108                 ''', post_parse=lambda s, r: (r[0], type(r[0])))
109
110         prints:
111
112             100
113             (100, <class 'int'>)
114
115             [2,3,4]
116             ([2, 3, 4], <class 'list'>)
117
118             [[2, 1],3,4]
119             ([[2, 1], 3, 4], <class 'list'>)
120
121         (Used internally by :class:`Group` when `aslist=True`.)
122         """
123
124         def __new__(cls, contained=None):
125             if contained is None:
126                 contained = []
127
128             if not isinstance(contained, list):
129                 raise TypeError(
130                     "{} may only be constructed with a list,"
131                     " not {}".format(cls.__name__, type(contained).__name__)
132                 )
133
134             return list.__new__(cls)
135
136     def __new__(cls, toklist=None, name=None, **kwargs):
137         if isinstance(toklist, ParseResults):
138             return toklist
139         self = object.__new__(cls)
140         self._name = None
141         self._parent = None
142         self._all_names = set()
143
144         if toklist is None:
145             self._toklist = []
146         elif isinstance(toklist, (list, _generator_type)):
147             self._toklist = (
148                 [toklist[:]]
149                 if isinstance(toklist, ParseResults.List)
150                 else list(toklist)
151             )
152         else:
153             self._toklist = [toklist]
154         self._tokdict = dict()
155         return self
156
157     # Performance tuning: we construct a *lot* of these, so keep this
158     # constructor as small and fast as possible
159     def __init__(
160         self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance
161     ):
162         self._modal = modal
163         if name is not None and name != "":
164             if isinstance(name, int):
165                 name = str(name)
166             if not modal:
167                 self._all_names = {name}
168             self._name = name
169             if toklist not in self._null_values:
170                 if isinstance(toklist, (str_type, type)):
171                     toklist = [toklist]
172                 if asList:
173                     if isinstance(toklist, ParseResults):
174                         self[name] = _ParseResultsWithOffset(
175                             ParseResults(toklist._toklist), 0
176                         )
177                     else:
178                         self[name] = _ParseResultsWithOffset(
179                             ParseResults(toklist[0]), 0
180                         )
181                     self[name]._name = name
182                 else:
183                     try:
184                         self[name] = toklist[0]
185                     except (KeyError, TypeError, IndexError):
186                         if toklist is not self:
187                             self[name] = toklist
188                         else:
189                             self._name = name
190
191     def __getitem__(self, i):
192         if isinstance(i, (int, slice)):
193             return self._toklist[i]
194         else:
195             if i not in self._all_names:
196                 return self._tokdict[i][-1][0]
197             else:
198                 return ParseResults([v[0] for v in self._tokdict[i]])
199
200     def __setitem__(self, k, v, isinstance=isinstance):
201         if isinstance(v, _ParseResultsWithOffset):
202             self._tokdict[k] = self._tokdict.get(k, list()) + [v]
203             sub = v[0]
204         elif isinstance(k, (int, slice)):
205             self._toklist[k] = v
206             sub = v
207         else:
208             self._tokdict[k] = self._tokdict.get(k, list()) + [
209                 _ParseResultsWithOffset(v, 0)
210             ]
211             sub = v
212         if isinstance(sub, ParseResults):
213             sub._parent = wkref(self)
214
215     def __delitem__(self, i):
216         if isinstance(i, (int, slice)):
217             mylen = len(self._toklist)
218             del self._toklist[i]
219
220             # convert int to slice
221             if isinstance(i, int):
222                 if i < 0:
223                     i += mylen
224                 i = slice(i, i + 1)
225             # get removed indices
226             removed = list(range(*i.indices(mylen)))
227             removed.reverse()
228             # fixup indices in token dictionary
229             for name, occurrences in self._tokdict.items():
230                 for j in removed:
231                     for k, (value, position) in enumerate(occurrences):
232                         occurrences[k] = _ParseResultsWithOffset(
233                             value, position - (position > j)
234                         )
235         else:
236             del self._tokdict[i]
237
238     def __contains__(self, k) -> bool:
239         return k in self._tokdict
240
241     def __len__(self) -> int:
242         return len(self._toklist)
243
244     def __bool__(self) -> bool:
245         return not not (self._toklist or self._tokdict)
246
247     def __iter__(self) -> Iterator:
248         return iter(self._toklist)
249
250     def __reversed__(self) -> Iterator:
251         return iter(self._toklist[::-1])
252
253     def keys(self):
254         return iter(self._tokdict)
255
256     def values(self):
257         return (self[k] for k in self.keys())
258
259     def items(self):
260         return ((k, self[k]) for k in self.keys())
261
262     def haskeys(self) -> bool:
263         """
264         Since ``keys()`` returns an iterator, this method is helpful in bypassing
265         code that looks for the existence of any defined results names."""
266         return bool(self._tokdict)
267
268     def pop(self, *args, **kwargs):
269         """
270         Removes and returns item at specified index (default= ``last``).
271         Supports both ``list`` and ``dict`` semantics for ``pop()``. If
272         passed no argument or an integer argument, it will use ``list``
273         semantics and pop tokens from the list of parsed tokens. If passed
274         a non-integer argument (most likely a string), it will use ``dict``
275         semantics and pop the corresponding value from any defined results
276         names. A second default return value argument is supported, just as in
277         ``dict.pop()``.
278
279         Example::
280
281             numlist = Word(nums)[...]
282             print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
283
284             def remove_first(tokens):
285                 tokens.pop(0)
286             numlist.add_parse_action(remove_first)
287             print(numlist.parse_string("0 123 321")) # -> ['123', '321']
288
289             label = Word(alphas)
290             patt = label("LABEL") + Word(nums)[1, ...]
291             print(patt.parse_string("AAB 123 321").dump())
292
293             # Use pop() in a parse action to remove named result (note that corresponding value is not
294             # removed from list form of results)
295             def remove_LABEL(tokens):
296                 tokens.pop("LABEL")
297                 return tokens
298             patt.add_parse_action(remove_LABEL)
299             print(patt.parse_string("AAB 123 321").dump())
300
301         prints::
302
303             ['AAB', '123', '321']
304             - LABEL: 'AAB'
305
306             ['AAB', '123', '321']
307         """
308         if not args:
309             args = [-1]
310         for k, v in kwargs.items():
311             if k == "default":
312                 args = (args[0], v)
313             else:
314                 raise TypeError(
315                     "pop() got an unexpected keyword argument {!r}".format(k)
316                 )
317         if isinstance(args[0], int) or len(args) == 1 or args[0] in self:
318             index = args[0]
319             ret = self[index]
320             del self[index]
321             return ret
322         else:
323             defaultvalue = args[1]
324             return defaultvalue
325
326     def get(self, key, default_value=None):
327         """
328         Returns named result matching the given key, or if there is no
329         such name, then returns the given ``default_value`` or ``None`` if no
330         ``default_value`` is specified.
331
332         Similar to ``dict.get()``.
333
334         Example::
335
336             integer = Word(nums)
337             date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
338
339             result = date_str.parse_string("1999/12/31")
340             print(result.get("year")) # -> '1999'
341             print(result.get("hour", "not specified")) # -> 'not specified'
342             print(result.get("hour")) # -> None
343         """
344         if key in self:
345             return self[key]
346         else:
347             return default_value
348
349     def insert(self, index, ins_string):
350         """
351         Inserts new element at location index in the list of parsed tokens.
352
353         Similar to ``list.insert()``.
354
355         Example::
356
357             numlist = Word(nums)[...]
358             print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
359
360             # use a parse action to insert the parse location in the front of the parsed results
361             def insert_locn(locn, tokens):
362                 tokens.insert(0, locn)
363             numlist.add_parse_action(insert_locn)
364             print(numlist.parse_string("0 123 321")) # -> [0, '0', '123', '321']
365         """
366         self._toklist.insert(index, ins_string)
367         # fixup indices in token dictionary
368         for name, occurrences in self._tokdict.items():
369             for k, (value, position) in enumerate(occurrences):
370                 occurrences[k] = _ParseResultsWithOffset(
371                     value, position + (position > index)
372                 )
373
374     def append(self, item):
375         """
376         Add single element to end of ``ParseResults`` list of elements.
377
378         Example::
379
380             numlist = Word(nums)[...]
381             print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
382
383             # use a parse action to compute the sum of the parsed integers, and add it to the end
384             def append_sum(tokens):
385                 tokens.append(sum(map(int, tokens)))
386             numlist.add_parse_action(append_sum)
387             print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321', 444]
388         """
389         self._toklist.append(item)
390
391     def extend(self, itemseq):
392         """
393         Add sequence of elements to end of ``ParseResults`` list of elements.
394
395         Example::
396
397             patt = Word(alphas)[1, ...]
398
399             # use a parse action to append the reverse of the matched strings, to make a palindrome
400             def make_palindrome(tokens):
401                 tokens.extend(reversed([t[::-1] for t in tokens]))
402                 return ''.join(tokens)
403             patt.add_parse_action(make_palindrome)
404             print(patt.parse_string("lskdj sdlkjf lksd")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl'
405         """
406         if isinstance(itemseq, ParseResults):
407             self.__iadd__(itemseq)
408         else:
409             self._toklist.extend(itemseq)
410
411     def clear(self):
412         """
413         Clear all elements and results names.
414         """
415         del self._toklist[:]
416         self._tokdict.clear()
417
418     def __getattr__(self, name):
419         try:
420             return self[name]
421         except KeyError:
422             if name.startswith("__"):
423                 raise AttributeError(name)
424             return ""
425
426     def __add__(self, other) -> "ParseResults":
427         ret = self.copy()
428         ret += other
429         return ret
430
431     def __iadd__(self, other) -> "ParseResults":
432         if other._tokdict:
433             offset = len(self._toklist)
434             addoffset = lambda a: offset if a < 0 else a + offset
435             otheritems = other._tokdict.items()
436             otherdictitems = [
437                 (k, _ParseResultsWithOffset(v[0], addoffset(v[1])))
438                 for k, vlist in otheritems
439                 for v in vlist
440             ]
441             for k, v in otherdictitems:
442                 self[k] = v
443                 if isinstance(v[0], ParseResults):
444                     v[0]._parent = wkref(self)
445
446         self._toklist += other._toklist
447         self._all_names |= other._all_names
448         return self
449
450     def __radd__(self, other) -> "ParseResults":
451         if isinstance(other, int) and other == 0:
452             # useful for merging many ParseResults using sum() builtin
453             return self.copy()
454         else:
455             # this may raise a TypeError - so be it
456             return other + self
457
458     def __repr__(self) -> str:
459         return "{}({!r}, {})".format(type(self).__name__, self._toklist, self.as_dict())
460
461     def __str__(self) -> str:
462         return (
463             "["
464             + ", ".join(
465                 [
466                     str(i) if isinstance(i, ParseResults) else repr(i)
467                     for i in self._toklist
468                 ]
469             )
470             + "]"
471         )
472
473     def _asStringList(self, sep=""):
474         out = []
475         for item in self._toklist:
476             if out and sep:
477                 out.append(sep)
478             if isinstance(item, ParseResults):
479                 out += item._asStringList()
480             else:
481                 out.append(str(item))
482         return out
483
484     def as_list(self) -> list:
485         """
486         Returns the parse results as a nested list of matching tokens, all converted to strings.
487
488         Example::
489
490             patt = Word(alphas)[1, ...]
491             result = patt.parse_string("sldkj lsdkj sldkj")
492             # even though the result prints in string-like form, it is actually a pyparsing ParseResults
493             print(type(result), result) # -> <class 'pyparsing.ParseResults'> ['sldkj', 'lsdkj', 'sldkj']
494
495             # Use as_list() to create an actual list
496             result_list = result.as_list()
497             print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']
498         """
499         return [
500             res.as_list() if isinstance(res, ParseResults) else res
501             for res in self._toklist
502         ]
503
504     def as_dict(self) -> dict:
505         """
506         Returns the named parse results as a nested dictionary.
507
508         Example::
509
510             integer = Word(nums)
511             date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
512
513             result = date_str.parse_string('12/31/1999')
514             print(type(result), repr(result)) # -> <class 'pyparsing.ParseResults'> (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]})
515
516             result_dict = result.as_dict()
517             print(type(result_dict), repr(result_dict)) # -> <class 'dict'> {'day': '1999', 'year': '12', 'month': '31'}
518
519             # even though a ParseResults supports dict-like access, sometime you just need to have a dict
520             import json
521             print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable
522             print(json.dumps(result.as_dict())) # -> {"month": "31", "day": "1999", "year": "12"}
523         """
524
525         def to_item(obj):
526             if isinstance(obj, ParseResults):
527                 return obj.as_dict() if obj.haskeys() else [to_item(v) for v in obj]
528             else:
529                 return obj
530
531         return dict((k, to_item(v)) for k, v in self.items())
532
533     def copy(self) -> "ParseResults":
534         """
535         Returns a new copy of a :class:`ParseResults` object.
536         """
537         ret = ParseResults(self._toklist)
538         ret._tokdict = self._tokdict.copy()
539         ret._parent = self._parent
540         ret._all_names |= self._all_names
541         ret._name = self._name
542         return ret
543
544     def get_name(self):
545         r"""
546         Returns the results name for this token expression. Useful when several
547         different expressions might match at a particular location.
548
549         Example::
550
551             integer = Word(nums)
552             ssn_expr = Regex(r"\d\d\d-\d\d-\d\d\d\d")
553             house_number_expr = Suppress('#') + Word(nums, alphanums)
554             user_data = (Group(house_number_expr)("house_number")
555                         | Group(ssn_expr)("ssn")
556                         | Group(integer)("age"))
557             user_info = user_data[1, ...]
558
559             result = user_info.parse_string("22 111-22-3333 #221B")
560             for item in result:
561                 print(item.get_name(), ':', item[0])
562
563         prints::
564
565             age : 22
566             ssn : 111-22-3333
567             house_number : 221B
568         """
569         if self._name:
570             return self._name
571         elif self._parent:
572             par = self._parent()
573
574             def find_in_parent(sub):
575                 return next(
576                     (
577                         k
578                         for k, vlist in par._tokdict.items()
579                         for v, loc in vlist
580                         if sub is v
581                     ),
582                     None,
583                 )
584
585             return find_in_parent(self) if par else None
586         elif (
587             len(self) == 1
588             and len(self._tokdict) == 1
589             and next(iter(self._tokdict.values()))[0][1] in (0, -1)
590         ):
591             return next(iter(self._tokdict.keys()))
592         else:
593             return None
594
595     def dump(self, indent="", full=True, include_list=True, _depth=0) -> str:
596         """
597         Diagnostic method for listing out the contents of
598         a :class:`ParseResults`. Accepts an optional ``indent`` argument so
599         that this string can be embedded in a nested display of other data.
600
601         Example::
602
603             integer = Word(nums)
604             date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
605
606             result = date_str.parse_string('1999/12/31')
607             print(result.dump())
608
609         prints::
610
611             ['1999', '/', '12', '/', '31']
612             - day: '31'
613             - month: '12'
614             - year: '1999'
615         """
616         out = []
617         NL = "\n"
618         out.append(indent + str(self.as_list()) if include_list else "")
619
620         if full:
621             if self.haskeys():
622                 items = sorted((str(k), v) for k, v in self.items())
623                 for k, v in items:
624                     if out:
625                         out.append(NL)
626                     out.append("{}{}- {}: ".format(indent, ("  " * _depth), k))
627                     if isinstance(v, ParseResults):
628                         if v:
629                             out.append(
630                                 v.dump(
631                                     indent=indent,
632                                     full=full,
633                                     include_list=include_list,
634                                     _depth=_depth + 1,
635                                 )
636                             )
637                         else:
638                             out.append(str(v))
639                     else:
640                         out.append(repr(v))
641             if any(isinstance(vv, ParseResults) for vv in self):
642                 v = self
643                 for i, vv in enumerate(v):
644                     if isinstance(vv, ParseResults):
645                         out.append(
646                             "\n{}{}[{}]:\n{}{}{}".format(
647                                 indent,
648                                 ("  " * (_depth)),
649                                 i,
650                                 indent,
651                                 ("  " * (_depth + 1)),
652                                 vv.dump(
653                                     indent=indent,
654                                     full=full,
655                                     include_list=include_list,
656                                     _depth=_depth + 1,
657                                 ),
658                             )
659                         )
660                     else:
661                         out.append(
662                             "\n%s%s[%d]:\n%s%s%s"
663                             % (
664                                 indent,
665                                 ("  " * (_depth)),
666                                 i,
667                                 indent,
668                                 ("  " * (_depth + 1)),
669                                 str(vv),
670                             )
671                         )
672
673         return "".join(out)
674
675     def pprint(self, *args, **kwargs):
676         """
677         Pretty-printer for parsed results as a list, using the
678         `pprint <https://docs.python.org/3/library/pprint.html>`_ module.
679         Accepts additional positional or keyword args as defined for
680         `pprint.pprint <https://docs.python.org/3/library/pprint.html#pprint.pprint>`_ .
681
682         Example::
683
684             ident = Word(alphas, alphanums)
685             num = Word(nums)
686             func = Forward()
687             term = ident | num | Group('(' + func + ')')
688             func <<= ident + Group(Optional(delimited_list(term)))
689             result = func.parse_string("fna a,b,(fnb c,d,200),100")
690             result.pprint(width=40)
691
692         prints::
693
694             ['fna',
695              ['a',
696               'b',
697               ['(', 'fnb', ['c', 'd', '200'], ')'],
698               '100']]
699         """
700         pprint.pprint(self.as_list(), *args, **kwargs)
701
702     # add support for pickle protocol
703     def __getstate__(self):
704         return (
705             self._toklist,
706             (
707                 self._tokdict.copy(),
708                 self._parent is not None and self._parent() or None,
709                 self._all_names,
710                 self._name,
711             ),
712         )
713
714     def __setstate__(self, state):
715         self._toklist, (self._tokdict, par, inAccumNames, self._name) = state
716         self._all_names = set(inAccumNames)
717         if par is not None:
718             self._parent = wkref(par)
719         else:
720             self._parent = None
721
722     def __getnewargs__(self):
723         return self._toklist, self._name
724
725     def __dir__(self):
726         return dir(type(self)) + list(self.keys())
727
728     @classmethod
729     def from_dict(cls, other, name=None) -> "ParseResults":
730         """
731         Helper classmethod to construct a ``ParseResults`` from a ``dict``, preserving the
732         name-value relations as results names. If an optional ``name`` argument is
733         given, a nested ``ParseResults`` will be returned.
734         """
735
736         def is_iterable(obj):
737             try:
738                 iter(obj)
739             except Exception:
740                 return False
741             else:
742                 return not isinstance(obj, str_type)
743
744         ret = cls([])
745         for k, v in other.items():
746             if isinstance(v, Mapping):
747                 ret += cls.from_dict(v, name=k)
748             else:
749                 ret += cls([v], name=k, asList=is_iterable(v))
750         if name is not None:
751             ret = cls([ret], name=name)
752         return ret
753
754     asList = as_list
755     asDict = as_dict
756     getName = get_name
757
758
759 MutableMapping.register(ParseResults)
760 MutableSequence.register(ParseResults)