doctb.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import inspect
  2. import linecache
  3. import sys
  4. from collections.abc import Sequence
  5. from types import TracebackType
  6. from typing import Any, Optional
  7. from collections.abc import Callable
  8. import stack_data
  9. from pygments.formatters.terminal256 import Terminal256Formatter
  10. from pygments.token import Token
  11. from IPython.utils.PyColorize import Theme, TokenStream, theme_table
  12. from IPython.utils.terminal import get_terminal_size
  13. from .tbtools import (
  14. FrameInfo,
  15. TBTools,
  16. _safe_string,
  17. _tokens_filename,
  18. eqrepr,
  19. get_line_number_of_frame,
  20. nullrepr,
  21. )
  22. INDENT_SIZE = 8
  23. def _format_traceback_lines(
  24. lines: list[stack_data.Line],
  25. theme: Theme,
  26. has_colors: bool,
  27. lvals_toks: list[TokenStream],
  28. ) -> TokenStream:
  29. """
  30. Format tracebacks lines with pointing arrow, leading numbers,
  31. this assumes the stack have been extracted using stackdata.
  32. Parameters
  33. ----------
  34. lines : list[Line]
  35. """
  36. numbers_width = INDENT_SIZE - 1
  37. tokens: TokenStream = [(Token, "\n")]
  38. for stack_line in lines:
  39. if stack_line is stack_data.LINE_GAP:
  40. toks = [(Token.LinenoEm, " (...)")]
  41. tokens.extend(toks)
  42. continue
  43. lineno = stack_line.lineno
  44. line = stack_line.render(pygmented=has_colors).rstrip("\n") + "\n"
  45. if stack_line.is_current:
  46. # This is the line with the error
  47. pad = numbers_width - len(str(lineno))
  48. toks = [
  49. (Token.Prompt, theme.make_arrow(3)),
  50. (Token, " "),
  51. (Token, line),
  52. ]
  53. else:
  54. # num = "%*s" % (numbers_width, lineno)
  55. toks = [
  56. # (Token.LinenoEm, str(num)),
  57. (Token, "..."),
  58. (Token, " "),
  59. (Token, line),
  60. ]
  61. tokens.extend(toks)
  62. if lvals_toks and stack_line.is_current:
  63. for lv in lvals_toks:
  64. tokens.append((Token, " " * INDENT_SIZE))
  65. tokens.extend(lv)
  66. tokens.append((Token, "\n"))
  67. # strip the last newline
  68. tokens = tokens[:-1]
  69. return tokens
  70. class DocTB(TBTools):
  71. """
  72. A stripped down version of Verbose TB, simplified to not have too much information when
  73. running doctests
  74. """
  75. tb_highlight = ""
  76. tb_highlight_style = "default"
  77. tb_offset: int
  78. long_header: bool
  79. include_vars: bool
  80. _mode: str
  81. def __init__(
  82. self,
  83. # TODO: no default ?
  84. theme_name: str = "linux",
  85. call_pdb: bool = False,
  86. ostream: Any = None,
  87. tb_offset: int = 0,
  88. long_header: bool = False,
  89. include_vars: bool = True,
  90. check_cache: Callable[[], None] | None = None,
  91. debugger_cls: type | None = None,
  92. ):
  93. """Specify traceback offset, headers and color scheme.
  94. Define how many frames to drop from the tracebacks. Calling it with
  95. tb_offset=1 allows use of this handler in interpreters which will have
  96. their own code at the top of the traceback (VerboseTB will first
  97. remove that frame before printing the traceback info)."""
  98. assert isinstance(theme_name, str)
  99. super().__init__(
  100. theme_name=theme_name,
  101. call_pdb=call_pdb,
  102. ostream=ostream,
  103. debugger_cls=debugger_cls,
  104. )
  105. self.tb_offset = tb_offset
  106. self.long_header = long_header
  107. self.include_vars = include_vars
  108. # By default we use linecache.checkcache, but the user can provide a
  109. # different check_cache implementation. This was formerly used by the
  110. # IPython kernel for interactive code, but is no longer necessary.
  111. if check_cache is None:
  112. check_cache = linecache.checkcache
  113. self.check_cache = check_cache
  114. self.skip_hidden = True
  115. def format_record(self, frame_info: FrameInfo) -> str:
  116. """Format a single stack frame"""
  117. assert isinstance(frame_info, FrameInfo)
  118. if isinstance(frame_info._sd, stack_data.RepeatedFrames):
  119. return theme_table[self._theme_name].format(
  120. [
  121. (Token, " "),
  122. (
  123. Token.ExcName,
  124. "[... skipping similar frames: %s]" % frame_info.description,
  125. ),
  126. (Token, "\n"),
  127. ]
  128. )
  129. indent: str = " " * INDENT_SIZE
  130. assert isinstance(frame_info.lineno, int)
  131. args, varargs, varkw, locals_ = inspect.getargvalues(frame_info.frame)
  132. if frame_info.executing is not None:
  133. func = frame_info.executing.code_qualname()
  134. else:
  135. func = "?"
  136. if func == "<module>":
  137. call = ""
  138. else:
  139. # Decide whether to include variable details or not
  140. var_repr = eqrepr if self.include_vars else nullrepr
  141. try:
  142. scope = inspect.formatargvalues(
  143. args, varargs, varkw, locals_, formatvalue=var_repr
  144. )
  145. assert isinstance(scope, str)
  146. call = theme_table[self._theme_name].format(
  147. [(Token, "in "), (Token.VName, func), (Token.ValEm, scope)]
  148. )
  149. except KeyError:
  150. # This happens in situations like errors inside generator
  151. # expressions, where local variables are listed in the
  152. # line, but can't be extracted from the frame. I'm not
  153. # 100% sure this isn't actually a bug in inspect itself,
  154. # but since there's no info for us to compute with, the
  155. # best we can do is report the failure and move on. Here
  156. # we must *not* call any traceback construction again,
  157. # because that would mess up use of %debug later on. So we
  158. # simply report the failure and move on. The only
  159. # limitation will be that this frame won't have locals
  160. # listed in the call signature. Quite subtle problem...
  161. # I can't think of a good way to validate this in a unit
  162. # test, but running a script consisting of:
  163. # dict( (k,v.strip()) for (k,v) in range(10) )
  164. # will illustrate the error, if this exception catch is
  165. # disabled.
  166. call = theme_table[self._theme_name].format(
  167. [
  168. (Token, "in "),
  169. (Token.VName, func),
  170. (Token.ValEm, "(***failed resolving arguments***)"),
  171. ]
  172. )
  173. lvals_toks: list[TokenStream] = []
  174. if self.include_vars:
  175. try:
  176. # we likely want to fix stackdata at some point, but
  177. # still need a workaround.
  178. fibp = frame_info.variables_in_executing_piece
  179. for var in fibp:
  180. lvals_toks.append(
  181. [
  182. (Token, var.name),
  183. (Token, " "),
  184. (Token.ValEm, "= "),
  185. (Token.ValEm, repr(var.value)),
  186. ]
  187. )
  188. except Exception:
  189. lvals_toks.append(
  190. [
  191. (
  192. Token,
  193. "Exception trying to inspect frame. No more locals available.",
  194. ),
  195. ]
  196. )
  197. assert frame_info._sd is not None
  198. result = theme_table[self._theme_name].format(
  199. _tokens_filename(True, frame_info.filename, lineno=frame_info.lineno)
  200. )
  201. result += ", " if call else ""
  202. result += f"{call}\n"
  203. result += theme_table[self._theme_name].format(
  204. _format_traceback_lines(
  205. frame_info.lines,
  206. theme_table[self._theme_name],
  207. self.has_colors,
  208. lvals_toks,
  209. )
  210. )
  211. return result
  212. def prepare_header(self, etype: str) -> str:
  213. width = min(75, get_terminal_size()[0])
  214. head = theme_table[self._theme_name].format(
  215. [
  216. (
  217. Token,
  218. "Traceback (most recent call last):",
  219. ),
  220. (Token, " "),
  221. ]
  222. )
  223. return head
  224. def format_exception(self, etype: Any, evalue: Any) -> Any:
  225. # Get (safely) a string form of the exception info
  226. try:
  227. etype_str, evalue_str = map(str, (etype, evalue))
  228. except:
  229. # User exception is improperly defined.
  230. etype, evalue = str, sys.exc_info()[:2]
  231. etype_str, evalue_str = map(str, (etype, evalue))
  232. # PEP-678 notes
  233. notes = getattr(evalue, "__notes__", [])
  234. if not isinstance(notes, Sequence) or isinstance(notes, (str, bytes)):
  235. notes = [_safe_string(notes, "__notes__", func=repr)]
  236. # ... and format it
  237. return [
  238. theme_table[self._theme_name].format(
  239. [(Token.ExcName, etype_str), (Token, ": "), (Token, evalue_str)]
  240. ),
  241. *(
  242. theme_table[self._theme_name].format([(Token, _safe_string(n, "note"))])
  243. for n in notes
  244. ),
  245. ]
  246. def format_exception_as_a_whole(
  247. self,
  248. etype: type,
  249. evalue: Optional[BaseException],
  250. etb: Optional[TracebackType],
  251. context: int,
  252. tb_offset: Optional[int],
  253. ) -> list[list[str]]:
  254. """Formats the header, traceback and exception message for a single exception.
  255. This may be called multiple times by Python 3 exception chaining
  256. (PEP 3134).
  257. """
  258. # some locals
  259. orig_etype = etype
  260. try:
  261. etype = etype.__name__ # type: ignore[assignment]
  262. except AttributeError:
  263. pass
  264. tb_offset = self.tb_offset if tb_offset is None else tb_offset
  265. assert isinstance(tb_offset, int)
  266. head = self.prepare_header(str(etype))
  267. assert context == 1, context
  268. records = self.get_records(etb, context, tb_offset) if etb else []
  269. frames = []
  270. skipped = 0
  271. nskipped = len(records) - 1
  272. frames.append(self.format_record(records[0]))
  273. if nskipped:
  274. frames.append(
  275. theme_table[self._theme_name].format(
  276. [
  277. (Token, "\n"),
  278. (Token, " "),
  279. (Token, "[... %s skipped frames]" % nskipped),
  280. (Token, "\n"),
  281. (Token, "\n"),
  282. ]
  283. )
  284. )
  285. formatted_exception = self.format_exception(etype, evalue)
  286. return [[head] + frames + formatted_exception]
  287. def get_records(self, etb: TracebackType, context: int, tb_offset: int) -> Any:
  288. assert context == 1, context
  289. assert etb is not None
  290. context = context - 1
  291. after = context // 2
  292. before = context - after
  293. if self.has_colors:
  294. base_style = theme_table[self._theme_name].as_pygments_style()
  295. style = stack_data.style_with_executing_node(base_style, self.tb_highlight)
  296. formatter = Terminal256Formatter(style=style)
  297. else:
  298. formatter = None
  299. options = stack_data.Options(
  300. before=before,
  301. after=after,
  302. pygments_formatter=formatter,
  303. )
  304. # Let's estimate the amount of code we will have to parse/highlight.
  305. cf: Optional[TracebackType] = etb
  306. max_len = 0
  307. tbs = []
  308. while cf is not None:
  309. try:
  310. mod = inspect.getmodule(cf.tb_frame)
  311. if mod is not None:
  312. mod_name = mod.__name__
  313. root_name, *_ = mod_name.split(".")
  314. if root_name == "IPython":
  315. cf = cf.tb_next
  316. continue
  317. max_len = get_line_number_of_frame(cf.tb_frame)
  318. except OSError:
  319. max_len = 0
  320. max_len = max(max_len, max_len)
  321. tbs.append(cf)
  322. cf = getattr(cf, "tb_next", None)
  323. res = list(stack_data.FrameInfo.stack_data(etb, options=options))[tb_offset:]
  324. res2 = [FrameInfo._from_stack_data_FrameInfo(r) for r in res]
  325. return res2
  326. def structured_traceback(
  327. self,
  328. etype: type,
  329. evalue: Optional[BaseException],
  330. etb: Optional[TracebackType] = None,
  331. tb_offset: Optional[int] = None,
  332. context: int = 1,
  333. ) -> list[str]:
  334. """Return a nice text document describing the traceback."""
  335. assert context > 0
  336. assert context == 1, context
  337. formatted_exceptions: list[list[str]] = self.format_exception_as_a_whole(
  338. etype, evalue, etb, context, tb_offset
  339. )
  340. termsize = min(75, get_terminal_size()[0])
  341. theme = theme_table[self._theme_name]
  342. structured_traceback_parts: list[str] = []
  343. chained_exceptions_tb_offset = 0
  344. lines_of_context = 3
  345. exception = self.get_parts_of_chained_exception(evalue)
  346. if exception:
  347. assert evalue is not None
  348. formatted_exceptions += self.prepare_chained_exception_message(
  349. evalue.__cause__
  350. )
  351. etype, evalue, etb = exception
  352. else:
  353. evalue = None
  354. chained_exc_ids = set()
  355. while evalue:
  356. formatted_exceptions += self.format_exception_as_a_whole(
  357. etype, evalue, etb, lines_of_context, chained_exceptions_tb_offset
  358. )
  359. exception = self.get_parts_of_chained_exception(evalue)
  360. if exception and id(exception[1]) not in chained_exc_ids:
  361. chained_exc_ids.add(
  362. id(exception[1])
  363. ) # trace exception to avoid infinite 'cause' loop
  364. formatted_exceptions += self.prepare_chained_exception_message(
  365. evalue.__cause__
  366. )
  367. etype, evalue, etb = exception
  368. else:
  369. evalue = None
  370. # we want to see exceptions in a reversed order:
  371. # the first exception should be on top
  372. for fx in reversed(formatted_exceptions):
  373. structured_traceback_parts += fx
  374. return structured_traceback_parts
  375. def debugger(self, force: bool = False) -> None:
  376. raise RuntimeError("canot rundebugger in Docs mode")
  377. def handler(self, info: tuple[Any, Any, Any] | None = None) -> None:
  378. (etype, evalue, etb) = info or sys.exc_info()
  379. self.tb = etb
  380. ostream = self.ostream
  381. ostream.flush()
  382. ostream.write(self.text(etype, evalue, etb)) # type:ignore[arg-type]
  383. ostream.write("\n")
  384. ostream.flush()
  385. # Changed so an instance can just be called as VerboseTB_inst() and print
  386. # out the right info on its own.
  387. def __call__(self, etype: Any = None, evalue: Any = None, etb: Any = None) -> None:
  388. """This hook can replace sys.excepthook (for Python 2.1 or higher)."""
  389. if etb is None:
  390. self.handler()
  391. else:
  392. self.handler((etype, evalue, etb))
  393. try:
  394. self.debugger()
  395. except KeyboardInterrupt:
  396. print("\nKeyboardInterrupt")