_formatting.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. # traceback_exception_init() adapted from trio
  2. #
  3. # _ExceptionPrintContext and traceback_exception_format() copied from the standard
  4. # library
  5. from __future__ import annotations
  6. import collections.abc
  7. import sys
  8. import textwrap
  9. import traceback
  10. from functools import singledispatch
  11. from types import TracebackType
  12. from typing import Any, List, Optional
  13. from ._exceptions import BaseExceptionGroup
  14. max_group_width = 15
  15. max_group_depth = 10
  16. _cause_message = (
  17. "\nThe above exception was the direct cause of the following exception:\n\n"
  18. )
  19. _context_message = (
  20. "\nDuring handling of the above exception, another exception occurred:\n\n"
  21. )
  22. def _format_final_exc_line(etype, value):
  23. valuestr = _safe_string(value, "exception")
  24. if value is None or not valuestr:
  25. line = f"{etype}\n"
  26. else:
  27. line = f"{etype}: {valuestr}\n"
  28. return line
  29. def _safe_string(value, what, func=str):
  30. try:
  31. return func(value)
  32. except BaseException:
  33. return f"<{what} {func.__name__}() failed>"
  34. class _ExceptionPrintContext:
  35. def __init__(self):
  36. self.seen = set()
  37. self.exception_group_depth = 0
  38. self.need_close = False
  39. def indent(self):
  40. return " " * (2 * self.exception_group_depth)
  41. def emit(self, text_gen, margin_char=None):
  42. if margin_char is None:
  43. margin_char = "|"
  44. indent_str = self.indent()
  45. if self.exception_group_depth:
  46. indent_str += margin_char + " "
  47. if isinstance(text_gen, str):
  48. yield textwrap.indent(text_gen, indent_str, lambda line: True)
  49. else:
  50. for text in text_gen:
  51. yield textwrap.indent(text, indent_str, lambda line: True)
  52. def exceptiongroup_excepthook(
  53. etype: type[BaseException], value: BaseException, tb: TracebackType | None
  54. ) -> None:
  55. sys.stderr.write("".join(traceback.format_exception(etype, value, tb)))
  56. class PatchedTracebackException(traceback.TracebackException):
  57. def __init__(
  58. self,
  59. exc_type: type[BaseException],
  60. exc_value: BaseException,
  61. exc_traceback: TracebackType | None,
  62. *,
  63. limit: int | None = None,
  64. lookup_lines: bool = True,
  65. capture_locals: bool = False,
  66. compact: bool = False,
  67. _seen: set[int] | None = None,
  68. ) -> None:
  69. kwargs: dict[str, Any] = {}
  70. if sys.version_info >= (3, 10):
  71. kwargs["compact"] = compact
  72. is_recursive_call = _seen is not None
  73. if _seen is None:
  74. _seen = set()
  75. _seen.add(id(exc_value))
  76. self.stack = traceback.StackSummary.extract(
  77. traceback.walk_tb(exc_traceback),
  78. limit=limit,
  79. lookup_lines=lookup_lines,
  80. capture_locals=capture_locals,
  81. )
  82. self.exc_type = exc_type
  83. # Capture now to permit freeing resources: only complication is in the
  84. # unofficial API _format_final_exc_line
  85. self._str = _safe_string(exc_value, "exception")
  86. try:
  87. self.__notes__ = getattr(exc_value, "__notes__", None)
  88. except KeyError:
  89. # Workaround for https://github.com/python/cpython/issues/98778 on Python
  90. # <= 3.9, and some 3.10 and 3.11 patch versions.
  91. HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
  92. if sys.version_info[:2] <= (3, 11) and isinstance(exc_value, HTTPError):
  93. self.__notes__ = None
  94. else:
  95. raise
  96. if exc_type and issubclass(exc_type, SyntaxError):
  97. # Handle SyntaxError's specially
  98. self.filename = exc_value.filename
  99. lno = exc_value.lineno
  100. self.lineno = str(lno) if lno is not None else None
  101. self.text = exc_value.text
  102. self.offset = exc_value.offset
  103. self.msg = exc_value.msg
  104. if sys.version_info >= (3, 10):
  105. end_lno = exc_value.end_lineno
  106. self.end_lineno = str(end_lno) if end_lno is not None else None
  107. self.end_offset = exc_value.end_offset
  108. elif (
  109. exc_type
  110. and issubclass(exc_type, (NameError, AttributeError))
  111. and getattr(exc_value, "name", None) is not None
  112. ):
  113. suggestion = _compute_suggestion_error(exc_value, exc_traceback)
  114. if suggestion:
  115. self._str += f". Did you mean: '{suggestion}'?"
  116. if lookup_lines:
  117. # Force all lines in the stack to be loaded
  118. for frame in self.stack:
  119. frame.line
  120. self.__suppress_context__ = (
  121. exc_value.__suppress_context__ if exc_value is not None else False
  122. )
  123. # Convert __cause__ and __context__ to `TracebackExceptions`s, use a
  124. # queue to avoid recursion (only the top-level call gets _seen == None)
  125. if not is_recursive_call:
  126. queue = [(self, exc_value)]
  127. while queue:
  128. te, e = queue.pop()
  129. if e and e.__cause__ is not None and id(e.__cause__) not in _seen:
  130. cause = PatchedTracebackException(
  131. type(e.__cause__),
  132. e.__cause__,
  133. e.__cause__.__traceback__,
  134. limit=limit,
  135. lookup_lines=lookup_lines,
  136. capture_locals=capture_locals,
  137. _seen=_seen,
  138. )
  139. else:
  140. cause = None
  141. if compact:
  142. need_context = (
  143. cause is None and e is not None and not e.__suppress_context__
  144. )
  145. else:
  146. need_context = True
  147. if (
  148. e
  149. and e.__context__ is not None
  150. and need_context
  151. and id(e.__context__) not in _seen
  152. ):
  153. context = PatchedTracebackException(
  154. type(e.__context__),
  155. e.__context__,
  156. e.__context__.__traceback__,
  157. limit=limit,
  158. lookup_lines=lookup_lines,
  159. capture_locals=capture_locals,
  160. _seen=_seen,
  161. )
  162. else:
  163. context = None
  164. # Capture each of the exceptions in the ExceptionGroup along with each
  165. # of their causes and contexts
  166. if e and isinstance(e, BaseExceptionGroup):
  167. exceptions = []
  168. for exc in e.exceptions:
  169. texc = PatchedTracebackException(
  170. type(exc),
  171. exc,
  172. exc.__traceback__,
  173. lookup_lines=lookup_lines,
  174. capture_locals=capture_locals,
  175. _seen=_seen,
  176. )
  177. exceptions.append(texc)
  178. else:
  179. exceptions = None
  180. te.__cause__ = cause
  181. te.__context__ = context
  182. te.exceptions = exceptions
  183. if cause:
  184. queue.append((te.__cause__, e.__cause__))
  185. if context:
  186. queue.append((te.__context__, e.__context__))
  187. if exceptions:
  188. queue.extend(zip(te.exceptions, e.exceptions))
  189. def format(self, *, chain=True, _ctx=None, **kwargs):
  190. if _ctx is None:
  191. _ctx = _ExceptionPrintContext()
  192. output = []
  193. exc = self
  194. if chain:
  195. while exc:
  196. if exc.__cause__ is not None:
  197. chained_msg = _cause_message
  198. chained_exc = exc.__cause__
  199. elif exc.__context__ is not None and not exc.__suppress_context__:
  200. chained_msg = _context_message
  201. chained_exc = exc.__context__
  202. else:
  203. chained_msg = None
  204. chained_exc = None
  205. output.append((chained_msg, exc))
  206. exc = chained_exc
  207. else:
  208. output.append((None, exc))
  209. for msg, exc in reversed(output):
  210. if msg is not None:
  211. yield from _ctx.emit(msg)
  212. if getattr(exc, "exceptions", None) is None:
  213. if exc.stack:
  214. yield from _ctx.emit("Traceback (most recent call last):\n")
  215. yield from _ctx.emit(exc.stack.format())
  216. yield from _ctx.emit(exc.format_exception_only())
  217. elif _ctx.exception_group_depth > max_group_depth:
  218. # exception group, but depth exceeds limit
  219. yield from _ctx.emit(f"... (max_group_depth is {max_group_depth})\n")
  220. else:
  221. # format exception group
  222. is_toplevel = _ctx.exception_group_depth == 0
  223. if is_toplevel:
  224. _ctx.exception_group_depth += 1
  225. if exc.stack:
  226. yield from _ctx.emit(
  227. "Exception Group Traceback (most recent call last):\n",
  228. margin_char="+" if is_toplevel else None,
  229. )
  230. yield from _ctx.emit(exc.stack.format())
  231. yield from _ctx.emit(exc.format_exception_only())
  232. num_excs = len(exc.exceptions)
  233. if num_excs <= max_group_width:
  234. n = num_excs
  235. else:
  236. n = max_group_width + 1
  237. _ctx.need_close = False
  238. for i in range(n):
  239. last_exc = i == n - 1
  240. if last_exc:
  241. # The closing frame may be added by a recursive call
  242. _ctx.need_close = True
  243. if max_group_width is not None:
  244. truncated = i >= max_group_width
  245. else:
  246. truncated = False
  247. title = f"{i + 1}" if not truncated else "..."
  248. yield (
  249. _ctx.indent()
  250. + ("+-" if i == 0 else " ")
  251. + f"+---------------- {title} ----------------\n"
  252. )
  253. _ctx.exception_group_depth += 1
  254. if not truncated:
  255. yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx)
  256. else:
  257. remaining = num_excs - max_group_width
  258. plural = "s" if remaining > 1 else ""
  259. yield from _ctx.emit(
  260. f"and {remaining} more exception{plural}\n"
  261. )
  262. if last_exc and _ctx.need_close:
  263. yield _ctx.indent() + "+------------------------------------\n"
  264. _ctx.need_close = False
  265. _ctx.exception_group_depth -= 1
  266. if is_toplevel:
  267. assert _ctx.exception_group_depth == 1
  268. _ctx.exception_group_depth = 0
  269. def format_exception_only(self, **kwargs):
  270. """Format the exception part of the traceback.
  271. The return value is a generator of strings, each ending in a newline.
  272. Normally, the generator emits a single string; however, for
  273. SyntaxError exceptions, it emits several lines that (when
  274. printed) display detailed information about where the syntax
  275. error occurred.
  276. The message indicating which exception occurred is always the last
  277. string in the output.
  278. """
  279. if self.exc_type is None:
  280. yield traceback._format_final_exc_line(None, self._str)
  281. return
  282. stype = self.exc_type.__qualname__
  283. smod = self.exc_type.__module__
  284. if smod not in ("__main__", "builtins"):
  285. if not isinstance(smod, str):
  286. smod = "<unknown>"
  287. stype = smod + "." + stype
  288. if not issubclass(self.exc_type, SyntaxError):
  289. yield _format_final_exc_line(stype, self._str)
  290. elif traceback_exception_format_syntax_error is not None:
  291. yield from traceback_exception_format_syntax_error(self, stype)
  292. else:
  293. yield from traceback_exception_original_format_exception_only(self)
  294. notes = getattr(self, "__notes__", None)
  295. if isinstance(notes, collections.abc.Sequence):
  296. for note in notes:
  297. note = _safe_string(note, "note")
  298. yield from [line + "\n" for line in note.split("\n")]
  299. elif notes is not None:
  300. yield _safe_string(notes, "__notes__", func=repr)
  301. traceback_exception_original_format = traceback.TracebackException.format
  302. traceback_exception_original_format_exception_only = (
  303. traceback.TracebackException.format_exception_only
  304. )
  305. traceback_exception_format_syntax_error = getattr(
  306. traceback.TracebackException, "_format_syntax_error", None
  307. )
  308. if sys.excepthook is sys.__excepthook__:
  309. traceback.TracebackException.__init__ = ( # type: ignore[assignment]
  310. PatchedTracebackException.__init__
  311. )
  312. traceback.TracebackException.format = ( # type: ignore[assignment]
  313. PatchedTracebackException.format
  314. )
  315. traceback.TracebackException.format_exception_only = ( # type: ignore[assignment]
  316. PatchedTracebackException.format_exception_only
  317. )
  318. sys.excepthook = exceptiongroup_excepthook
  319. # Ubuntu's system Python has a sitecustomize.py file that imports
  320. # apport_python_hook and replaces sys.excepthook.
  321. #
  322. # The custom hook captures the error for crash reporting, and then calls
  323. # sys.__excepthook__ to actually print the error.
  324. #
  325. # We don't mind it capturing the error for crash reporting, but we want to
  326. # take over printing the error. So we monkeypatch the apport_python_hook
  327. # module so that instead of calling sys.__excepthook__, it calls our custom
  328. # hook.
  329. #
  330. # More details: https://github.com/python-trio/trio/issues/1065
  331. if getattr(sys.excepthook, "__name__", None) in (
  332. "apport_excepthook",
  333. # on ubuntu 22.10 the hook was renamed to partial_apport_excepthook
  334. "partial_apport_excepthook",
  335. ):
  336. # patch traceback like above
  337. traceback.TracebackException.__init__ = ( # type: ignore[assignment]
  338. PatchedTracebackException.__init__
  339. )
  340. traceback.TracebackException.format = ( # type: ignore[assignment]
  341. PatchedTracebackException.format
  342. )
  343. traceback.TracebackException.format_exception_only = ( # type: ignore[assignment]
  344. PatchedTracebackException.format_exception_only
  345. )
  346. from types import ModuleType
  347. import apport_python_hook
  348. # monkeypatch the sys module that apport has imported
  349. fake_sys = ModuleType("exceptiongroup_fake_sys")
  350. fake_sys.__dict__.update(sys.__dict__)
  351. fake_sys.__excepthook__ = exceptiongroup_excepthook
  352. apport_python_hook.sys = fake_sys
  353. @singledispatch
  354. def format_exception_only(__exc: BaseException, **kwargs: Any) -> List[str]:
  355. return list(
  356. PatchedTracebackException(
  357. type(__exc), __exc, None, compact=True
  358. ).format_exception_only()
  359. )
  360. @format_exception_only.register
  361. def _(__exc: type, value: BaseException, **kwargs: Any) -> List[str]:
  362. return format_exception_only(value)
  363. @singledispatch
  364. def format_exception(
  365. __exc: BaseException, limit: Optional[int] = None, chain: bool = True, **kwargs: Any
  366. ) -> List[str]:
  367. return list(
  368. PatchedTracebackException(
  369. type(__exc), __exc, __exc.__traceback__, limit=limit, compact=True
  370. ).format(chain=chain)
  371. )
  372. @format_exception.register
  373. def _(
  374. __exc: type,
  375. value: BaseException,
  376. tb: TracebackType,
  377. limit: Optional[int] = None,
  378. chain: bool = True,
  379. **kwargs: Any,
  380. ) -> List[str]:
  381. return format_exception(value, limit, chain)
  382. @singledispatch
  383. def print_exception(
  384. __exc: BaseException,
  385. limit: Optional[int] = None,
  386. file: Any = None,
  387. chain: bool = True,
  388. **kwargs: Any,
  389. ) -> None:
  390. if file is None:
  391. file = sys.stderr
  392. for line in PatchedTracebackException(
  393. type(__exc), __exc, __exc.__traceback__, limit=limit
  394. ).format(chain=chain):
  395. print(line, file=file, end="")
  396. @print_exception.register
  397. def _(
  398. __exc: type,
  399. value: BaseException,
  400. tb: TracebackType,
  401. limit: Optional[int] = None,
  402. file: Any = None,
  403. chain: bool = True,
  404. ) -> None:
  405. print_exception(value, limit, file, chain)
  406. def print_exc(
  407. limit: Optional[int] = None,
  408. file: Any | None = None,
  409. chain: bool = True,
  410. ) -> None:
  411. value = sys.exc_info()[1]
  412. print_exception(value, limit, file, chain)
  413. # Python levenshtein edit distance code for NameError/AttributeError
  414. # suggestions, backported from 3.12
  415. _MAX_CANDIDATE_ITEMS = 750
  416. _MAX_STRING_SIZE = 40
  417. _MOVE_COST = 2
  418. _CASE_COST = 1
  419. _SENTINEL = object()
  420. def _substitution_cost(ch_a, ch_b):
  421. if ch_a == ch_b:
  422. return 0
  423. if ch_a.lower() == ch_b.lower():
  424. return _CASE_COST
  425. return _MOVE_COST
  426. def _compute_suggestion_error(exc_value, tb):
  427. wrong_name = getattr(exc_value, "name", None)
  428. if wrong_name is None or not isinstance(wrong_name, str):
  429. return None
  430. if isinstance(exc_value, AttributeError):
  431. obj = getattr(exc_value, "obj", _SENTINEL)
  432. if obj is _SENTINEL:
  433. return None
  434. obj = exc_value.obj
  435. try:
  436. d = dir(obj)
  437. except Exception:
  438. return None
  439. else:
  440. assert isinstance(exc_value, NameError)
  441. # find most recent frame
  442. if tb is None:
  443. return None
  444. while tb.tb_next is not None:
  445. tb = tb.tb_next
  446. frame = tb.tb_frame
  447. d = list(frame.f_locals) + list(frame.f_globals) + list(frame.f_builtins)
  448. if len(d) > _MAX_CANDIDATE_ITEMS:
  449. return None
  450. wrong_name_len = len(wrong_name)
  451. if wrong_name_len > _MAX_STRING_SIZE:
  452. return None
  453. best_distance = wrong_name_len
  454. suggestion = None
  455. for possible_name in d:
  456. if possible_name == wrong_name:
  457. # A missing attribute is "found". Don't suggest it (see GH-88821).
  458. continue
  459. # No more than 1/3 of the involved characters should need changed.
  460. max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6
  461. # Don't take matches we've already beaten.
  462. max_distance = min(max_distance, best_distance - 1)
  463. current_distance = _levenshtein_distance(
  464. wrong_name, possible_name, max_distance
  465. )
  466. if current_distance > max_distance:
  467. continue
  468. if not suggestion or current_distance < best_distance:
  469. suggestion = possible_name
  470. best_distance = current_distance
  471. return suggestion
  472. def _levenshtein_distance(a, b, max_cost):
  473. # A Python implementation of Python/suggestions.c:levenshtein_distance.
  474. # Both strings are the same
  475. if a == b:
  476. return 0
  477. # Trim away common affixes
  478. pre = 0
  479. while a[pre:] and b[pre:] and a[pre] == b[pre]:
  480. pre += 1
  481. a = a[pre:]
  482. b = b[pre:]
  483. post = 0
  484. while a[: post or None] and b[: post or None] and a[post - 1] == b[post - 1]:
  485. post -= 1
  486. a = a[: post or None]
  487. b = b[: post or None]
  488. if not a or not b:
  489. return _MOVE_COST * (len(a) + len(b))
  490. if len(a) > _MAX_STRING_SIZE or len(b) > _MAX_STRING_SIZE:
  491. return max_cost + 1
  492. # Prefer shorter buffer
  493. if len(b) < len(a):
  494. a, b = b, a
  495. # Quick fail when a match is impossible
  496. if (len(b) - len(a)) * _MOVE_COST > max_cost:
  497. return max_cost + 1
  498. # Instead of producing the whole traditional len(a)-by-len(b)
  499. # matrix, we can update just one row in place.
  500. # Initialize the buffer row
  501. row = list(range(_MOVE_COST, _MOVE_COST * (len(a) + 1), _MOVE_COST))
  502. result = 0
  503. for bindex in range(len(b)):
  504. bchar = b[bindex]
  505. distance = result = bindex * _MOVE_COST
  506. minimum = sys.maxsize
  507. for index in range(len(a)):
  508. # 1) Previous distance in this row is cost(b[:b_index], a[:index])
  509. substitute = distance + _substitution_cost(bchar, a[index])
  510. # 2) cost(b[:b_index], a[:index+1]) from previous row
  511. distance = row[index]
  512. # 3) existing result is cost(b[:b_index+1], a[index])
  513. insert_delete = min(result, distance) + _MOVE_COST
  514. result = min(insert_delete, substitute)
  515. # cost(b[:b_index+1], a[:index+1])
  516. row[index] = result
  517. if result < minimum:
  518. minimum = result
  519. if minimum > max_cost:
  520. # Everything in this row is too big, so bail early.
  521. return max_cost + 1
  522. return result