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