tbtools.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import functools
  2. import inspect
  3. import pydoc
  4. import sys
  5. import types
  6. import warnings
  7. from types import TracebackType
  8. from typing import Any, Optional, Tuple
  9. from collections.abc import Callable
  10. import stack_data
  11. from pygments.token import Token
  12. from IPython import get_ipython
  13. from IPython.core import debugger
  14. from IPython.utils import path as util_path
  15. from IPython.utils import py3compat
  16. from IPython.utils.PyColorize import Theme, TokenStream, theme_table
  17. _sentinel = object()
  18. INDENT_SIZE = 8
  19. @functools.lru_cache
  20. def count_lines_in_py_file(filename: str) -> int:
  21. """
  22. Given a filename, returns the number of lines in the file
  23. if it ends with the extension ".py". Otherwise, returns 0.
  24. """
  25. if not filename.endswith(".py"):
  26. return 0
  27. else:
  28. try:
  29. with open(filename, "r") as file:
  30. s = sum(1 for line in file)
  31. except UnicodeError:
  32. return 0
  33. return s
  34. def get_line_number_of_frame(frame: types.FrameType) -> int:
  35. """
  36. Given a frame object, returns the total number of lines in the file
  37. containing the frame's code object, or the number of lines in the
  38. frame's source code if the file is not available.
  39. Parameters
  40. ----------
  41. frame : FrameType
  42. The frame object whose line number is to be determined.
  43. Returns
  44. -------
  45. int
  46. The total number of lines in the file containing the frame's
  47. code object, or the number of lines in the frame's source code
  48. if the file is not available.
  49. """
  50. filename = frame.f_code.co_filename
  51. if filename is None:
  52. print("No file....")
  53. lines, first = inspect.getsourcelines(frame)
  54. return first + len(lines)
  55. return count_lines_in_py_file(filename)
  56. def _safe_string(value: Any, what: Any, func: Any = str) -> str:
  57. # Copied from cpython/Lib/traceback.py
  58. try:
  59. return func(value)
  60. except:
  61. return f"<{what} {func.__name__}() failed>"
  62. def _format_traceback_lines(
  63. lines: list[stack_data.Line],
  64. theme: Theme,
  65. has_colors: bool,
  66. lvals_toks: list[TokenStream],
  67. ) -> TokenStream:
  68. """
  69. Format tracebacks lines with pointing arrow, leading numbers,
  70. this assumes the stack have been extracted using stackdata.
  71. Parameters
  72. ----------
  73. lines : list[Line]
  74. """
  75. numbers_width = INDENT_SIZE - 1
  76. tokens: TokenStream = []
  77. for stack_line in lines:
  78. if stack_line is stack_data.LINE_GAP:
  79. toks = [(Token.LinenoEm, " (...)")]
  80. tokens.extend(toks)
  81. continue
  82. lineno = stack_line.lineno
  83. line = stack_line.render(pygmented=has_colors).rstrip("\n") + "\n"
  84. if stack_line.is_current:
  85. # This is the line with the error
  86. pad = numbers_width - len(str(lineno))
  87. toks = [
  88. (Token.LinenoEm, theme.make_arrow(pad)),
  89. (Token.LinenoEm, str(lineno)),
  90. (Token, " "),
  91. (Token, line),
  92. ]
  93. else:
  94. num = "%*s" % (numbers_width, lineno)
  95. toks = [
  96. (Token.LinenoEm, str(num)),
  97. (Token, " "),
  98. (Token, line),
  99. ]
  100. tokens.extend(toks)
  101. if lvals_toks and stack_line.is_current:
  102. for lv in lvals_toks:
  103. tokens.append((Token, " " * INDENT_SIZE))
  104. tokens.extend(lv)
  105. tokens.append((Token, "\n"))
  106. # strip the last newline
  107. tokens = tokens[:-1]
  108. return tokens
  109. # some internal-use functions
  110. def text_repr(value: Any) -> str:
  111. """Hopefully pretty robust repr equivalent."""
  112. # this is pretty horrible but should always return *something*
  113. try:
  114. return pydoc.text.repr(value) # type: ignore[call-arg]
  115. except KeyboardInterrupt:
  116. raise
  117. except:
  118. try:
  119. return repr(value)
  120. except KeyboardInterrupt:
  121. raise
  122. except:
  123. try:
  124. # all still in an except block so we catch
  125. # getattr raising
  126. name = getattr(value, "__name__", None)
  127. if name:
  128. # ick, recursion
  129. return text_repr(name)
  130. klass = getattr(value, "__class__", None)
  131. if klass:
  132. return "%s instance" % text_repr(klass)
  133. return "UNRECOVERABLE REPR FAILURE"
  134. except KeyboardInterrupt:
  135. raise
  136. except:
  137. return "UNRECOVERABLE REPR FAILURE"
  138. def eqrepr(value: Any, repr: Callable[[Any], str] = text_repr) -> str:
  139. return "=%s" % repr(value)
  140. def nullrepr(value: Any, repr: Callable[[Any], str] = text_repr) -> str:
  141. return ""
  142. def _tokens_filename(
  143. em: bool,
  144. file: str | None,
  145. *,
  146. lineno: int | None = None,
  147. ) -> TokenStream:
  148. """
  149. Format filename lines with custom formatting from caching compiler or `File *.py` by default
  150. Parameters
  151. ----------
  152. em: wether bold or not
  153. file : str
  154. """
  155. Normal = Token.NormalEm if em else Token.Normal
  156. Filename = Token.FilenameEm if em else Token.Filename
  157. ipinst = get_ipython()
  158. if (
  159. ipinst is not None
  160. and (data := ipinst.compile.format_code_name(file)) is not None
  161. ):
  162. label, name = data
  163. if lineno is None:
  164. return [
  165. (Normal, label),
  166. (Normal, " "),
  167. (Filename, name),
  168. ]
  169. else:
  170. return [
  171. (Normal, label),
  172. (Normal, " "),
  173. (Filename, name),
  174. (Filename, f", line {lineno}"),
  175. ]
  176. else:
  177. name = util_path.compress_user(
  178. py3compat.cast_unicode(file or "", util_path.fs_encoding)
  179. )
  180. if lineno is None:
  181. return [
  182. (Normal, "File "),
  183. (Filename, name),
  184. ]
  185. else:
  186. return [
  187. (Normal, "File "),
  188. (Filename, f"{name}:{lineno}"),
  189. ]
  190. def _simple_format_traceback_lines(
  191. lnum: int,
  192. index: int,
  193. lines: list[tuple[str, tuple[str, bool]]],
  194. lvals_toks: list[TokenStream],
  195. theme: Theme,
  196. ) -> TokenStream:
  197. """
  198. Format tracebacks lines with pointing arrow, leading numbers
  199. This should be equivalent to _format_traceback_lines, but does not rely on stackdata
  200. to format the lines
  201. This is due to the fact that stackdata may be slow on super long and complex files.
  202. Parameters
  203. ==========
  204. lnum: int
  205. number of the target line of code.
  206. index: int
  207. which line in the list should be highlighted.
  208. lines: list[string]
  209. lvals_toks: pairs of token type and str
  210. Values of local variables, already colored, to inject just after the error line.
  211. """
  212. for item in lvals_toks:
  213. assert isinstance(item, list)
  214. for subit in item:
  215. assert isinstance(subit[1], str)
  216. numbers_width = INDENT_SIZE - 1
  217. res_toks: TokenStream = []
  218. for i, (line, (new_line, err)) in enumerate(lines, lnum - index):
  219. if not err:
  220. line = new_line
  221. colored_line = line
  222. if i == lnum:
  223. # This is the line with the error
  224. pad = numbers_width - len(str(i))
  225. line_toks = [
  226. (Token.LinenoEm, theme.make_arrow(pad)),
  227. (Token.LinenoEm, str(lnum)),
  228. (Token, " "),
  229. (Token, colored_line),
  230. ]
  231. else:
  232. padding_num = "%*s" % (numbers_width, i)
  233. line_toks = [
  234. (Token.LinenoEm, padding_num),
  235. (Token, " "),
  236. (Token, colored_line),
  237. ]
  238. res_toks.extend(line_toks)
  239. if lvals_toks and i == lnum:
  240. for lv in lvals_toks:
  241. res_toks.extend(lv)
  242. # res_toks.extend(lvals_toks)
  243. return res_toks
  244. class FrameInfo:
  245. """
  246. Mirror of stack data's FrameInfo, but so that we can bypass highlighting on
  247. really long frames.
  248. """
  249. description: Optional[str]
  250. filename: Optional[str]
  251. lineno: int
  252. # number of context lines to use
  253. context: Optional[int]
  254. raw_lines: list[str]
  255. _sd: stack_data.core.FrameInfo
  256. frame: Any
  257. @classmethod
  258. def _from_stack_data_FrameInfo(
  259. cls, frame_info: stack_data.core.FrameInfo | stack_data.core.RepeatedFrames
  260. ) -> "FrameInfo":
  261. return cls(
  262. getattr(frame_info, "description", None),
  263. getattr(frame_info, "filename", None), # type: ignore[arg-type]
  264. getattr(frame_info, "lineno", None), # type: ignore[arg-type]
  265. getattr(frame_info, "frame", None),
  266. getattr(frame_info, "code", None),
  267. sd=frame_info,
  268. context=None,
  269. )
  270. def __init__(
  271. self,
  272. description: Optional[str],
  273. filename: str,
  274. lineno: int,
  275. frame: Any,
  276. code: Optional[types.CodeType],
  277. *,
  278. sd: Any = None,
  279. context: int | None = None,
  280. ):
  281. assert isinstance(lineno, (int, type(None))), lineno
  282. self.description = description
  283. self.filename = filename
  284. self.lineno = lineno
  285. self.frame = frame
  286. self.code = code
  287. self._sd = sd
  288. self.context = context
  289. # self.lines = []
  290. if sd is None:
  291. try:
  292. # return a list of source lines and a starting line number
  293. self.raw_lines = inspect.getsourcelines(frame)[0]
  294. except OSError:
  295. self.raw_lines = [
  296. "'Could not get source, probably due dynamically evaluated source code.'"
  297. ]
  298. @property
  299. def variables_in_executing_piece(self) -> list[Any]:
  300. if self._sd is not None:
  301. return self._sd.variables_in_executing_piece # type:ignore[misc]
  302. else:
  303. return []
  304. @property
  305. def lines(self) -> list[Any]:
  306. from executing.executing import NotOneValueFound
  307. assert self._sd is not None
  308. try:
  309. return self._sd.lines # type: ignore[misc]
  310. except NotOneValueFound:
  311. class Dummy:
  312. lineno = 0
  313. is_current = False
  314. def render(self, *, pygmented: bool) -> str:
  315. return "<Error retrieving source code with stack_data see ipython/ipython#13598>"
  316. return [Dummy()]
  317. @property
  318. def executing(self) -> Any:
  319. if self._sd is not None:
  320. return self._sd.executing
  321. else:
  322. return None
  323. class TBTools:
  324. """Basic tools used by all traceback printer classes."""
  325. # Number of frames to skip when reporting tracebacks
  326. tb_offset = 0
  327. _theme_name: str
  328. _old_theme_name: str
  329. call_pdb: bool
  330. ostream: Any
  331. debugger_cls: Any
  332. pdb: Any
  333. def __init__(
  334. self,
  335. color_scheme: Any = _sentinel,
  336. call_pdb: bool = False,
  337. ostream: Any = None,
  338. *,
  339. debugger_cls: type | None = None,
  340. theme_name: str = "nocolor",
  341. ):
  342. if color_scheme is not _sentinel:
  343. assert isinstance(color_scheme, str), color_scheme
  344. warnings.warn(
  345. "color_scheme is deprecated since IPython 9.0, use theme_name instead, all lowercase",
  346. DeprecationWarning,
  347. stacklevel=2,
  348. )
  349. theme_name = color_scheme
  350. if theme_name in ["Linux", "LightBG", "Neutral", "NoColor"]:
  351. warnings.warn(
  352. f"Theme names and color schemes are lowercase in IPython 9.0 use {theme_name.lower()} instead",
  353. DeprecationWarning,
  354. stacklevel=2,
  355. )
  356. theme_name = theme_name.lower()
  357. # Whether to call the interactive pdb debugger after printing
  358. # tracebacks or not
  359. super().__init__()
  360. self.call_pdb = call_pdb
  361. # Output stream to write to. Note that we store the original value in
  362. # a private attribute and then make the public ostream a property, so
  363. # that we can delay accessing sys.stdout until runtime. The way
  364. # things are written now, the sys.stdout object is dynamically managed
  365. # so a reference to it should NEVER be stored statically. This
  366. # property approach confines this detail to a single location, and all
  367. # subclasses can simply access self.ostream for writing.
  368. self._ostream = ostream
  369. # Create color table
  370. self.set_theme_name(theme_name)
  371. self.debugger_cls = debugger_cls or debugger.Pdb
  372. if call_pdb:
  373. self.pdb = self.debugger_cls()
  374. else:
  375. self.pdb = None
  376. def _get_ostream(self) -> Any:
  377. """Output stream that exceptions are written to.
  378. Valid values are:
  379. - None: the default, which means that IPython will dynamically resolve
  380. to sys.stdout. This ensures compatibility with most tools, including
  381. Windows (where plain stdout doesn't recognize ANSI escapes).
  382. - Any object with 'write' and 'flush' attributes.
  383. """
  384. return sys.stdout if self._ostream is None else self._ostream
  385. def _set_ostream(self, val) -> None: # type:ignore[no-untyped-def]
  386. assert val is None or (hasattr(val, "write") and hasattr(val, "flush"))
  387. self._ostream = val
  388. ostream = property(_get_ostream, _set_ostream)
  389. @staticmethod
  390. def _get_chained_exception(exception_value: Any) -> Any:
  391. cause = getattr(exception_value, "__cause__", None)
  392. if cause:
  393. return cause
  394. if getattr(exception_value, "__suppress_context__", False):
  395. return None
  396. return getattr(exception_value, "__context__", None)
  397. def get_parts_of_chained_exception(
  398. self, evalue: BaseException | None
  399. ) -> Optional[Tuple[type, BaseException, TracebackType]]:
  400. chained_evalue = self._get_chained_exception(evalue)
  401. if chained_evalue:
  402. return (
  403. chained_evalue.__class__,
  404. chained_evalue,
  405. chained_evalue.__traceback__,
  406. )
  407. return None
  408. def prepare_chained_exception_message(
  409. self, cause: BaseException | None
  410. ) -> list[list[str]]:
  411. direct_cause = (
  412. "\nThe above exception was the direct cause of the following exception:\n"
  413. )
  414. exception_during_handling = (
  415. "\nDuring handling of the above exception, another exception occurred:\n"
  416. )
  417. if cause:
  418. message = [[direct_cause]]
  419. else:
  420. message = [[exception_during_handling]]
  421. return message
  422. @property
  423. def has_colors(self) -> bool:
  424. assert self._theme_name == self._theme_name.lower()
  425. return self._theme_name != "nocolor"
  426. def set_theme_name(self, name: str) -> None:
  427. assert name in theme_table
  428. assert name.lower() == name
  429. self._theme_name = name
  430. # Also set colors of debugger
  431. if hasattr(self, "pdb") and self.pdb is not None:
  432. self.pdb.set_theme_name(name)
  433. def set_colors(self, name: str) -> None:
  434. """Shorthand access to the color table scheme selector method."""
  435. # todo emit deprecation
  436. warnings.warn(
  437. "set_colors is deprecated since IPython 9.0, use set_theme_name instead",
  438. DeprecationWarning,
  439. stacklevel=2,
  440. )
  441. self.set_theme_name(name)
  442. def color_toggle(self) -> None:
  443. """Toggle between the currently active color scheme and nocolor."""
  444. if self._theme_name == "nocolor":
  445. self._theme_name = self._old_theme_name
  446. else:
  447. self._old_theme_name = self._theme_name
  448. self._theme_name = "nocolor"
  449. def stb2text(self, stb: list[str]) -> str:
  450. """Convert a structured traceback (a list) to a string."""
  451. return "\n".join(stb)
  452. def text(
  453. self,
  454. etype: type,
  455. value: BaseException | None,
  456. tb: TracebackType | None,
  457. tb_offset: Optional[int] = None,
  458. context: int = 5,
  459. ) -> str:
  460. """Return formatted traceback.
  461. Subclasses may override this if they add extra arguments.
  462. """
  463. tb_list = self.structured_traceback(etype, value, tb, tb_offset, context)
  464. return self.stb2text(tb_list)
  465. def structured_traceback(
  466. self,
  467. etype: type,
  468. evalue: BaseException | None,
  469. etb: Optional[TracebackType] = None,
  470. tb_offset: Optional[int] = None,
  471. context: int = 5,
  472. ) -> list[str]:
  473. """Return a list of traceback frames.
  474. Must be implemented by each class.
  475. """
  476. raise NotImplementedError()