a38447bb05bd5d503a32651d6046ff8667785c0c
[SubU] /
1 # exceptions.py
2
3 import re
4 import sys
5 import typing
6
7 from .util import col, line, lineno, _collapse_string_to_ranges
8 from .unicode import pyparsing_unicode as ppu
9
10
11 class ExceptionWordUnicode(ppu.Latin1, ppu.LatinA, ppu.LatinB, ppu.Greek, ppu.Cyrillic):
12     pass
13
14
15 _extract_alphanums = _collapse_string_to_ranges(ExceptionWordUnicode.alphanums)
16 _exception_word_extractor = re.compile("([" + _extract_alphanums + "]{1,16})|.")
17
18
19 class ParseBaseException(Exception):
20     """base exception class for all parsing runtime exceptions"""
21
22     # Performance tuning: we construct a *lot* of these, so keep this
23     # constructor as small and fast as possible
24     def __init__(
25         self,
26         pstr: str,
27         loc: int = 0,
28         msg: typing.Optional[str] = None,
29         elem=None,
30     ):
31         self.loc = loc
32         if msg is None:
33             self.msg = pstr
34             self.pstr = ""
35         else:
36             self.msg = msg
37             self.pstr = pstr
38         self.parser_element = self.parserElement = elem
39         self.args = (pstr, loc, msg)
40
41     @staticmethod
42     def explain_exception(exc, depth=16):
43         """
44         Method to take an exception and translate the Python internal traceback into a list
45         of the pyparsing expressions that caused the exception to be raised.
46
47         Parameters:
48
49         - exc - exception raised during parsing (need not be a ParseException, in support
50           of Python exceptions that might be raised in a parse action)
51         - depth (default=16) - number of levels back in the stack trace to list expression
52           and function names; if None, the full stack trace names will be listed; if 0, only
53           the failing input line, marker, and exception string will be shown
54
55         Returns a multi-line string listing the ParserElements and/or function names in the
56         exception's stack trace.
57         """
58         import inspect
59         from .core import ParserElement
60
61         if depth is None:
62             depth = sys.getrecursionlimit()
63         ret = []
64         if isinstance(exc, ParseBaseException):
65             ret.append(exc.line)
66             ret.append(" " * (exc.column - 1) + "^")
67         ret.append("{}: {}".format(type(exc).__name__, exc))
68
69         if depth > 0:
70             callers = inspect.getinnerframes(exc.__traceback__, context=depth)
71             seen = set()
72             for i, ff in enumerate(callers[-depth:]):
73                 frm = ff[0]
74
75                 f_self = frm.f_locals.get("self", None)
76                 if isinstance(f_self, ParserElement):
77                     if frm.f_code.co_name not in ("parseImpl", "_parseNoCache"):
78                         continue
79                     if id(f_self) in seen:
80                         continue
81                     seen.add(id(f_self))
82
83                     self_type = type(f_self)
84                     ret.append(
85                         "{}.{} - {}".format(
86                             self_type.__module__, self_type.__name__, f_self
87                         )
88                     )
89
90                 elif f_self is not None:
91                     self_type = type(f_self)
92                     ret.append("{}.{}".format(self_type.__module__, self_type.__name__))
93
94                 else:
95                     code = frm.f_code
96                     if code.co_name in ("wrapper", "<module>"):
97                         continue
98
99                     ret.append("{}".format(code.co_name))
100
101                 depth -= 1
102                 if not depth:
103                     break
104
105         return "\n".join(ret)
106
107     @classmethod
108     def _from_exception(cls, pe):
109         """
110         internal factory method to simplify creating one type of ParseException
111         from another - avoids having __init__ signature conflicts among subclasses
112         """
113         return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)
114
115     @property
116     def line(self) -> str:
117         """
118         Return the line of text where the exception occurred.
119         """
120         return line(self.loc, self.pstr)
121
122     @property
123     def lineno(self) -> int:
124         """
125         Return the 1-based line number of text where the exception occurred.
126         """
127         return lineno(self.loc, self.pstr)
128
129     @property
130     def col(self) -> int:
131         """
132         Return the 1-based column on the line of text where the exception occurred.
133         """
134         return col(self.loc, self.pstr)
135
136     @property
137     def column(self) -> int:
138         """
139         Return the 1-based column on the line of text where the exception occurred.
140         """
141         return col(self.loc, self.pstr)
142
143     def __str__(self) -> str:
144         if self.pstr:
145             if self.loc >= len(self.pstr):
146                 foundstr = ", found end of text"
147             else:
148                 # pull out next word at error location
149                 found_match = _exception_word_extractor.match(self.pstr, self.loc)
150                 if found_match is not None:
151                     found = found_match.group(0)
152                 else:
153                     found = self.pstr[self.loc : self.loc + 1]
154                 foundstr = (", found %r" % found).replace(r"\\", "\\")
155         else:
156             foundstr = ""
157         return "{}{}  (at char {}), (line:{}, col:{})".format(
158             self.msg, foundstr, self.loc, self.lineno, self.column
159         )
160
161     def __repr__(self):
162         return str(self)
163
164     def mark_input_line(self, marker_string: str = None, *, markerString=">!<") -> str:
165         """
166         Extracts the exception line from the input string, and marks
167         the location of the exception with a special symbol.
168         """
169         markerString = marker_string if marker_string is not None else markerString
170         line_str = self.line
171         line_column = self.column - 1
172         if markerString:
173             line_str = "".join(
174                 (line_str[:line_column], markerString, line_str[line_column:])
175             )
176         return line_str.strip()
177
178     def explain(self, depth=16) -> str:
179         """
180         Method to translate the Python internal traceback into a list
181         of the pyparsing expressions that caused the exception to be raised.
182
183         Parameters:
184
185         - depth (default=16) - number of levels back in the stack trace to list expression
186           and function names; if None, the full stack trace names will be listed; if 0, only
187           the failing input line, marker, and exception string will be shown
188
189         Returns a multi-line string listing the ParserElements and/or function names in the
190         exception's stack trace.
191
192         Example::
193
194             expr = pp.Word(pp.nums) * 3
195             try:
196                 expr.parse_string("123 456 A789")
197             except pp.ParseException as pe:
198                 print(pe.explain(depth=0))
199
200         prints::
201
202             123 456 A789
203                     ^
204             ParseException: Expected W:(0-9), found 'A'  (at char 8), (line:1, col:9)
205
206         Note: the diagnostic output will include string representations of the expressions
207         that failed to parse. These representations will be more helpful if you use `set_name` to
208         give identifiable names to your expressions. Otherwise they will use the default string
209         forms, which may be cryptic to read.
210
211         Note: pyparsing's default truncation of exception tracebacks may also truncate the
212         stack of expressions that are displayed in the ``explain`` output. To get the full listing
213         of parser expressions, you may have to set ``ParserElement.verbose_stacktrace = True``
214         """
215         return self.explain_exception(self, depth)
216
217     markInputline = mark_input_line
218
219
220 class ParseException(ParseBaseException):
221     """
222     Exception thrown when a parse expression doesn't match the input string
223
224     Example::
225
226         try:
227             Word(nums).set_name("integer").parse_string("ABC")
228         except ParseException as pe:
229             print(pe)
230             print("column: {}".format(pe.column))
231
232     prints::
233
234        Expected integer (at char 0), (line:1, col:1)
235         column: 1
236
237     """
238
239
240 class ParseFatalException(ParseBaseException):
241     """
242     User-throwable exception thrown when inconsistent parse content
243     is found; stops all parsing immediately
244     """
245
246
247 class ParseSyntaxException(ParseFatalException):
248     """
249     Just like :class:`ParseFatalException`, but thrown internally
250     when an :class:`ErrorStop<And._ErrorStop>` ('-' operator) indicates
251     that parsing is to stop immediately because an unbacktrackable
252     syntax error has been found.
253     """
254
255
256 class RecursiveGrammarException(Exception):
257     """
258     Exception thrown by :class:`ParserElement.validate` if the
259     grammar could be left-recursive; parser may need to enable
260     left recursion using :class:`ParserElement.enable_left_recursion<ParserElement.enable_left_recursion>`
261     """
262
263     def __init__(self, parseElementList):
264         self.parseElementTrace = parseElementList
265
266     def __str__(self) -> str:
267         return "RecursiveGrammarException: {}".format(self.parseElementTrace)