exceptions.py 11 KB

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