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