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