exceptions.py 8.8 KB

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