exceptions.py 9.3 KB

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