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 found_phrase = f", found {self.found}" if self.found else ""
196 return f"{self.msg}{found_phrase} (at char {self.loc}), (line:{self.lineno}, col:{self.column})"
197
198 def __str__(self) -> str:
199 return self.formatted_message()
200
201 def __repr__(self):
202 return str(self)
203
204 def mark_input_line(
205 self, marker_string: typing.Optional[str] = None, *, markerString: str = ">!<"
206 ) -> str:
207 """
208 Extracts the exception line from the input string, and marks
209 the location of the exception with a special symbol.
210 """
211 markerString = marker_string if marker_string is not None else markerString
212 line_str = self.line
213 line_column = self.column - 1
214 if markerString:
215 line_str = f"{line_str[:line_column]}{markerString}{line_str[line_column:]}"
216 return line_str.strip()
217
218 def explain(self, depth: int = 16) -> str:
219 """
220 Method to translate the Python internal traceback into a list
221 of the pyparsing expressions that caused the exception to be raised.
222
223 Parameters:
224
225 - depth (default=16) - number of levels back in the stack trace to list expression
226 and function names; if None, the full stack trace names will be listed; if 0, only
227 the failing input line, marker, and exception string will be shown
228
229 Returns a multi-line string listing the ParserElements and/or function names in the
230 exception's stack trace.
231
232 Example::
233
234 # an expression to parse 3 integers
235 expr = pp.Word(pp.nums) * 3
236 try:
237 # a failing parse - the third integer is prefixed with "A"
238 expr.parse_string("123 456 A789")
239 except pp.ParseException as pe:
240 print(pe.explain(depth=0))
241
242 prints::
243
244 123 456 A789
245 ^
246 ParseException: Expected W:(0-9), found 'A' (at char 8), (line:1, col:9)
247
248 Note: the diagnostic output will include string representations of the expressions
249 that failed to parse. These representations will be more helpful if you use `set_name` to
250 give identifiable names to your expressions. Otherwise they will use the default string
251 forms, which may be cryptic to read.
252
253 Note: pyparsing's default truncation of exception tracebacks may also truncate the
254 stack of expressions that are displayed in the ``explain`` output. To get the full listing
255 of parser expressions, you may have to set ``ParserElement.verbose_stacktrace = True``
256 """
257 return self.explain_exception(self, depth)
258
259 # Compatibility synonyms
260 # fmt: off
261 markInputline = replaced_by_pep8("markInputline", mark_input_line)
262 # fmt: on
263
264
265class ParseException(ParseBaseException):
266 """
267 Exception thrown when a parse expression doesn't match the input string
268
269 Example::
270
271 integer = Word(nums).set_name("integer")
272 try:
273 integer.parse_string("ABC")
274 except ParseException as pe:
275 print(pe, f"column: {pe.column}")
276
277 prints::
278
279 Expected integer, found 'ABC' (at char 0), (line:1, col:1) column: 1
280
281 """
282
283
284class ParseFatalException(ParseBaseException):
285 """
286 User-throwable exception thrown when inconsistent parse content
287 is found; stops all parsing immediately
288 """
289
290
291class ParseSyntaxException(ParseFatalException):
292 """
293 Just like :class:`ParseFatalException`, but thrown internally
294 when an :class:`ErrorStop<And._ErrorStop>` ('-' operator) indicates
295 that parsing is to stop immediately because an unbacktrackable
296 syntax error has been found.
297 """
298
299
300class RecursiveGrammarException(Exception):
301 """
302 Exception thrown by :class:`ParserElement.validate` if the
303 grammar could be left-recursive; parser may need to enable
304 left recursion using :class:`ParserElement.enable_left_recursion<ParserElement.enable_left_recursion>`
305
306 Deprecated: only used by deprecated method ParserElement.validate.
307 """
308
309 def __init__(self, parseElementList) -> None:
310 self.parseElementTrace = parseElementList
311
312 def __str__(self) -> str:
313 return f"RecursiveGrammarException: {self.parseElementTrace}"