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