codekingpro/portable-devtools
114k
1"""Extract, format and print information about Python stack traces."""2 3import collections.abc4import itertools5import linecache6import sys7import textwrap8import warnings9import codeop10import keyword11import tokenize12import io13import _colorize14 15from contextlib import suppress16 17__all__ = ['extract_stack', 'extract_tb', 'format_exception',18 'format_exception_only', 'format_list', 'format_stack',19 'format_tb', 'print_exc', 'format_exc', 'print_exception',20 'print_last', 'print_stack', 'print_tb', 'clear_frames',21 'FrameSummary', 'StackSummary', 'TracebackException',22 'walk_stack', 'walk_tb', 'print_list']23 24#25# Formatting and printing lists of traceback lines.26#27 28 29def print_list(extracted_list, file=None):30 """Print the list of tuples as returned by extract_tb() or31 extract_stack() as a formatted stack trace to the given file."""32 if file is None:33 file = sys.stderr34 for item in StackSummary.from_list(extracted_list).format():35 print(item, file=file, end="")36 37def format_list(extracted_list):38 """Format a list of tuples or FrameSummary objects for printing.39 40 Given a list of tuples or FrameSummary objects as returned by41 extract_tb() or extract_stack(), return a list of strings ready42 for printing.43 44 Each string in the resulting list corresponds to the item with the45 same index in the argument list. Each string ends in a newline;46 the strings may contain internal newlines as well, for those items47 whose source text line is not None.48 """49 return StackSummary.from_list(extracted_list).format()50 51#52# Printing and Extracting Tracebacks.53#54 55def print_tb(tb, limit=None, file=None):56 """Print up to 'limit' stack trace entries from the traceback 'tb'.57 58 If 'limit' is omitted or None, all entries are printed. If 'file'59 is omitted or None, the output goes to sys.stderr; otherwise60 'file' should be an open file or file-like object with a write()61 method.62 """63 print_list(extract_tb(tb, limit=limit), file=file)64 65def format_tb(tb, limit=None):66 """A shorthand for 'format_list(extract_tb(tb, limit))'."""67 return extract_tb(tb, limit=limit).format()68 69def extract_tb(tb, limit=None):70 """71 Return a StackSummary object representing a list of72 pre-processed entries from traceback.73 74 This is useful for alternate formatting of stack traces. If75 'limit' is omitted or None, all entries are extracted. A76 pre-processed stack trace entry is a FrameSummary object77 containing attributes filename, lineno, name, and line78 representing the information that is usually printed for a stack79 trace. The line is a string with leading and trailing80 whitespace stripped; if the source is not available it is None.81 """82 return StackSummary._extract_from_extended_frame_gen(83 _walk_tb_with_full_positions(tb), limit=limit)84 85#86# Exception formatting and output.87#88 89_cause_message = (90 "\nThe above exception was the direct cause "91 "of the following exception:\n\n")92 93_context_message = (94 "\nDuring handling of the above exception, "95 "another exception occurred:\n\n")96 97 98class _Sentinel:99 def __repr__(self):100 return "<implicit>"101 102_sentinel = _Sentinel()103 104def _parse_value_tb(exc, value, tb):105 if (value is _sentinel) != (tb is _sentinel):106 raise ValueError("Both or neither of value and tb must be given")107 if value is tb is _sentinel:108 if exc is not None:109 if isinstance(exc, BaseException):110 return exc, exc.__traceback__111 112 raise TypeError(f'Exception expected for value, '113 f'{type(exc).__name__} found')114 else:115 return None, None116 return value, tb117 118 119def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \120 file=None, chain=True, **kwargs):121 """Print exception up to 'limit' stack trace entries from 'tb' to 'file'.122 123 This differs from print_tb() in the following ways: (1) if124 traceback is not None, it prints a header "Traceback (most recent125 call last):"; (2) it prints the exception type and value after the126 stack trace; (3) if type is SyntaxError and value has the127 appropriate format, it prints the line where the syntax error128 occurred with a caret on the next line indicating the approximate129 position of the error.130 """131 colorize = kwargs.get("colorize", False)132 value, tb = _parse_value_tb(exc, value, tb)133 te = TracebackException(type(value), value, tb, limit=limit, compact=True)134 te.print(file=file, chain=chain, colorize=colorize)135 136 137BUILTIN_EXCEPTION_LIMIT = object()138 139 140def _print_exception_bltin(exc, /):141 file = sys.stderr if sys.stderr is not None else sys.__stderr__142 colorize = _colorize.can_colorize(file=file)143 return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)144 145 146def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \147 chain=True, **kwargs):148 """Format a stack trace and the exception information.149 150 The arguments have the same meaning as the corresponding arguments151 to print_exception(). The return value is a list of strings, each152 ending in a newline and some containing internal newlines. When153 these lines are concatenated and printed, exactly the same text is154 printed as does print_exception().155 """156 colorize = kwargs.get("colorize", False)157 value, tb = _parse_value_tb(exc, value, tb)158 te = TracebackException(type(value), value, tb, limit=limit, compact=True)159 return list(te.format(chain=chain, colorize=colorize))160 161 162def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs):163 """Format the exception part of a traceback.164 165 The return value is a list of strings, each ending in a newline.166 167 The list contains the exception's message, which is168 normally a single string; however, for :exc:`SyntaxError` exceptions, it169 contains several lines that (when printed) display detailed information170 about where the syntax error occurred. Following the message, the list171 contains the exception's ``__notes__``.172 173 When *show_group* is ``True``, and the exception is an instance of174 :exc:`BaseExceptionGroup`, the nested exceptions are included as175 well, recursively, with indentation relative to their nesting depth.176 """177 colorize = kwargs.get("colorize", False)178 if value is _sentinel:179 value = exc180 te = TracebackException(type(value), value, None, compact=True)181 return list(te.format_exception_only(show_group=show_group, colorize=colorize))182 183 184# -- not official API but folk probably use these two functions.185 186def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=False):187 valuestr = _safe_string(value, 'exception')188 end_char = "\n" if insert_final_newline else ""189 if colorize:190 theme = _colorize.get_theme(force_color=True).traceback191 else:192 theme = _colorize.get_theme(force_no_color=True).traceback193 if value is None or not valuestr:194 line = f"{theme.type}{etype}{theme.reset}{end_char}"195 else:196 line = f"{theme.type}{etype}{theme.reset}: {theme.message}{valuestr}{theme.reset}{end_char}"197 return line198 199 200def _safe_string(value, what, func=str):201 try:202 return func(value)203 except:204 return f'<{what} {func.__name__}() failed>'205 206# --207 208def print_exc(limit=None, file=None, chain=True):209 """Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'."""210 print_exception(sys.exception(), limit=limit, file=file, chain=chain)211 212def format_exc(limit=None, chain=True):213 """Like print_exc() but return a string."""214 return "".join(format_exception(sys.exception(), limit=limit, chain=chain))215 216def print_last(limit=None, file=None, chain=True):217 """This is a shorthand for 'print_exception(sys.last_exc, limit=limit, file=file, chain=chain)'."""218 if not hasattr(sys, "last_exc") and not hasattr(sys, "last_type"):219 raise ValueError("no last exception")220 221 if hasattr(sys, "last_exc"):222 print_exception(sys.last_exc, limit=limit, file=file, chain=chain)223 else:224 print_exception(sys.last_type, sys.last_value, sys.last_traceback,225 limit=limit, file=file, chain=chain)226 227 228#229# Printing and Extracting Stacks.230#231 232def print_stack(f=None, limit=None, file=None):233 """Print a stack trace from its invocation point.234 235 The optional 'f' argument can be used to specify an alternate236 stack frame at which to start. The optional 'limit' and 'file'237 arguments have the same meaning as for print_exception().238 """239 if f is None:240 f = sys._getframe().f_back241 print_list(extract_stack(f, limit=limit), file=file)242 243 244def format_stack(f=None, limit=None):245 """Shorthand for 'format_list(extract_stack(f, limit))'."""246 if f is None:247 f = sys._getframe().f_back248 return format_list(extract_stack(f, limit=limit))249 250 251def extract_stack(f=None, limit=None):252 """Extract the raw traceback from the current stack frame.253 254 The return value has the same format as for extract_tb(). The255 optional 'f' and 'limit' arguments have the same meaning as for256 print_stack(). Each item in the list is a quadruple (filename,257 line number, function name, text), and the entries are in order258 from oldest to newest stack frame.259 """260 if f is None:261 f = sys._getframe().f_back262 stack = StackSummary.extract(walk_stack(f), limit=limit)263 stack.reverse()264 return stack265 266 267def clear_frames(tb):268 "Clear all references to local variables in the frames of a traceback."269 while tb is not None:270 try:271 tb.tb_frame.clear()272 except RuntimeError:273 # Ignore the exception raised if the frame is still executing.274 pass275 tb = tb.tb_next276 277 278class FrameSummary:279 """Information about a single frame from a traceback.280 281 - :attr:`filename` The filename for the frame.282 - :attr:`lineno` The line within filename for the frame that was283 active when the frame was captured.284 - :attr:`name` The name of the function or method that was executing285 when the frame was captured.286 - :attr:`line` The text from the linecache module for the287 of code that was running when the frame was captured.288 - :attr:`locals` Either None if locals were not supplied, or a dict289 mapping the name to the repr() of the variable.290 """291 292 __slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno',293 'name', '_lines', '_lines_dedented', 'locals', '_code')294 295 def __init__(self, filename, lineno, name, *, lookup_line=True,296 locals=None, line=None,297 end_lineno=None, colno=None, end_colno=None, **kwargs):298 """Construct a FrameSummary.299 300 :param lookup_line: If True, `linecache` is consulted for the source301 code line. Otherwise, the line will be looked up when first needed.302 :param locals: If supplied the frame locals, which will be captured as303 object representations.304 :param line: If provided, use this instead of looking up the line in305 the linecache.306 """307 self.filename = filename308 self.lineno = lineno309 self.end_lineno = lineno if end_lineno is None else end_lineno310 self.colno = colno311 self.end_colno = end_colno312 self.name = name313 self._code = kwargs.get("_code")314 self._lines = line315 self._lines_dedented = None316 if lookup_line:317 self.line318 self.locals = {k: _safe_string(v, 'local', func=repr)319 for k, v in locals.items()} if locals else None320 321 def __eq__(self, other):322 if isinstance(other, FrameSummary):323 return (self.filename == other.filename and324 self.lineno == other.lineno and325 self.name == other.name and326 self.locals == other.locals)327 if isinstance(other, tuple):328 return (self.filename, self.lineno, self.name, self.line) == other329 return NotImplemented330 331 def __getitem__(self, pos):332 return (self.filename, self.lineno, self.name, self.line)[pos]333 334 def __iter__(self):335 return iter([self.filename, self.lineno, self.name, self.line])336 337 def __repr__(self):338 return "<FrameSummary file {filename}, line {lineno} in {name}>".format(339 filename=self.filename, lineno=self.lineno, name=self.name)340 341 def __len__(self):342 return 4343 344 def _set_lines(self):345 if (346 self._lines is None347 and self.lineno is not None348 and self.end_lineno is not None349 ):350 lines = []351 for lineno in range(self.lineno, self.end_lineno + 1):352 # treat errors (empty string) and empty lines (newline) as the same353 line = linecache.getline(self.filename, lineno).rstrip()354 if not line and self._code is not None and self.filename.startswith("<"):355 line = linecache._getline_from_code(self._code, lineno).rstrip()356 lines.append(line)357 self._lines = "\n".join(lines) + "\n"358 359 @property360 def _original_lines(self):361 # Returns the line as-is from the source, without modifying whitespace.362 self._set_lines()363 return self._lines364 365 @property366 def _dedented_lines(self):367 # Returns _original_lines, but dedented368 self._set_lines()369 if self._lines_dedented is None and self._lines is not None:370 self._lines_dedented = textwrap.dedent(self._lines)371 return self._lines_dedented372 373 @property374 def line(self):375 self._set_lines()376 if self._lines is None:377 return None378 # return only the first line, stripped379 return self._lines.partition("\n")[0].strip()380 381 382def walk_stack(f):383 """Walk a stack yielding the frame and line number for each frame.384 385 This will follow f.f_back from the given frame. If no frame is given, the386 current stack is used. Usually used with StackSummary.extract.387 """388 if f is None:389 f = sys._getframe().f_back390 391 def walk_stack_generator(frame):392 while frame is not None:393 yield frame, frame.f_lineno394 frame = frame.f_back395 396 return walk_stack_generator(f)397 398 399def walk_tb(tb):400 """Walk a traceback yielding the frame and line number for each frame.401 402 This will follow tb.tb_next (and thus is in the opposite order to403 walk_stack). Usually used with StackSummary.extract.404 """405 while tb is not None:406 yield tb.tb_frame, tb.tb_lineno407 tb = tb.tb_next408 409 410def _walk_tb_with_full_positions(tb):411 # Internal version of walk_tb that yields full code positions including412 # end line and column information.413 while tb is not None:414 positions = _get_code_position(tb.tb_frame.f_code, tb.tb_lasti)415 # Yield tb_lineno when co_positions does not have a line number to416 # maintain behavior with walk_tb.417 if positions[0] is None:418 yield tb.tb_frame, (tb.tb_lineno, ) + positions[1:]419 else:420 yield tb.tb_frame, positions421 tb = tb.tb_next422 423 424def _get_code_position(code, instruction_index):425 if instruction_index < 0:426 return (None, None, None, None)427 positions_gen = code.co_positions()428 return next(itertools.islice(positions_gen, instruction_index // 2, None))429 430 431_RECURSIVE_CUTOFF = 3 # Also hardcoded in traceback.c.432 433 434class StackSummary(list):435 """A list of FrameSummary objects, representing a stack of frames."""436 437 @classmethod438 def extract(klass, frame_gen, *, limit=None, lookup_lines=True,439 capture_locals=False):440 """Create a StackSummary from a traceback or stack object.441 442 :param frame_gen: A generator that yields (frame, lineno) tuples443 whose summaries are to be included in the stack.444 :param limit: None to include all frames or the number of frames to445 include.446 :param lookup_lines: If True, lookup lines for each frame immediately,447 otherwise lookup is deferred until the frame is rendered.448 :param capture_locals: If True, the local variables from each frame will449 be captured as object representations into the FrameSummary.450 """451 def extended_frame_gen():452 for f, lineno in frame_gen:453 yield f, (lineno, None, None, None)454 455 return klass._extract_from_extended_frame_gen(456 extended_frame_gen(), limit=limit, lookup_lines=lookup_lines,457 capture_locals=capture_locals)458 459 @classmethod460 def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None,461 lookup_lines=True, capture_locals=False):462 # Same as extract but operates on a frame generator that yields463 # (frame, (lineno, end_lineno, colno, end_colno)) in the stack.464 # Only lineno is required, the remaining fields can be None if the465 # information is not available.466 builtin_limit = limit is BUILTIN_EXCEPTION_LIMIT467 if limit is None or builtin_limit:468 limit = getattr(sys, 'tracebacklimit', None)469 if limit is not None and limit < 0:470 limit = 0471 if limit is not None:472 if builtin_limit:473 frame_gen = tuple(frame_gen)474 frame_gen = frame_gen[len(frame_gen) - limit:]475 elif limit >= 0:476 frame_gen = itertools.islice(frame_gen, limit)477 else:478 frame_gen = collections.deque(frame_gen, maxlen=-limit)479 480 result = klass()481 fnames = set()482 for f, (lineno, end_lineno, colno, end_colno) in frame_gen:483 co = f.f_code484 filename = co.co_filename485 name = co.co_name486 fnames.add(filename)487 linecache.lazycache(filename, f.f_globals)488 # Must defer line lookups until we have called checkcache.489 if capture_locals:490 f_locals = f.f_locals491 else:492 f_locals = None493 result.append(494 FrameSummary(filename, lineno, name,495 lookup_line=False, locals=f_locals,496 end_lineno=end_lineno, colno=colno, end_colno=end_colno,497 _code=f.f_code,498 )499 )500 for filename in fnames:501 linecache.checkcache(filename)502 503 # If immediate lookup was desired, trigger lookups now.504 if lookup_lines:505 for f in result:506 f.line507 return result508 509 @classmethod510 def from_list(klass, a_list):511 """512 Create a StackSummary object from a supplied list of513 FrameSummary objects or old-style list of tuples.514 """515 # While doing a fast-path check for isinstance(a_list, StackSummary) is516 # appealing, idlelib.run.cleanup_traceback and other similar code may517 # break this by making arbitrary frames plain tuples, so we need to518 # check on a frame by frame basis.519 result = StackSummary()520 for frame in a_list:521 if isinstance(frame, FrameSummary):522 result.append(frame)523 else:524 filename, lineno, name, line = frame525 result.append(FrameSummary(filename, lineno, name, line=line))526 return result527 528 def format_frame_summary(self, frame_summary, **kwargs):529 """Format the lines for a single FrameSummary.530 531 Returns a string representing one frame involved in the stack. This532 gets called for every frame to be printed in the stack summary.533 """534 colorize = kwargs.get("colorize", False)535 row = []536 filename = frame_summary.filename537 if frame_summary.filename.startswith("<stdin-") and frame_summary.filename.endswith('>'):538 filename = "<stdin>"539 if colorize:540 theme = _colorize.get_theme(force_color=True).traceback541 else:542 theme = _colorize.get_theme(force_no_color=True).traceback543 row.append(544 ' File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(545 theme.filename,546 filename,547 theme.reset,548 theme.line_no,549 frame_summary.lineno,550 theme.reset,551 theme.frame,552 frame_summary.name,553 theme.reset,554 )555 )556 if frame_summary._dedented_lines and frame_summary._dedented_lines.strip():557 if (558 frame_summary.colno is None or559 frame_summary.end_colno is None560 ):561 # only output first line if column information is missing562 row.append(textwrap.indent(frame_summary.line, ' ') + "\n")563 else:564 # get first and last line565 all_lines_original = frame_summary._original_lines.splitlines()566 first_line = all_lines_original[0]567 # assume all_lines_original has enough lines (since we constructed it)568 last_line = all_lines_original[frame_summary.end_lineno - frame_summary.lineno]569 570 # character index of the start/end of the instruction571 start_offset = _byte_offset_to_character_offset(first_line, frame_summary.colno)572 end_offset = _byte_offset_to_character_offset(last_line, frame_summary.end_colno)573 574 all_lines = frame_summary._dedented_lines.splitlines()[575 :frame_summary.end_lineno - frame_summary.lineno + 1576 ]577 578 # adjust start/end offset based on dedent579 dedent_characters = len(first_line) - len(all_lines[0])580 start_offset = max(0, start_offset - dedent_characters)581 end_offset = max(0, end_offset - dedent_characters)582 583 # When showing this on a terminal, some of the non-ASCII characters584 # might be rendered as double-width characters, so we need to take585 # that into account when calculating the length of the line.586 dp_start_offset = _display_width(all_lines[0], offset=start_offset)587 dp_end_offset = _display_width(all_lines[-1], offset=end_offset)588 589 # get exact code segment corresponding to the instruction590 segment = "\n".join(all_lines)591 segment = segment[start_offset:len(segment) - (len(all_lines[-1]) - end_offset)]592 593 # attempt to parse for anchors594 anchors = None595 show_carets = False596 with suppress(Exception):597 anchors = _extract_caret_anchors_from_line_segment(segment)598 show_carets = self._should_show_carets(start_offset, end_offset, all_lines, anchors)599 600 result = []601 602 # only display first line, last line, and lines around anchor start/end603 significant_lines = {0, len(all_lines) - 1}604 605 anchors_left_end_offset = 0606 anchors_right_start_offset = 0607 primary_char = "^"608 secondary_char = "^"609 if anchors:610 anchors_left_end_offset = anchors.left_end_offset611 anchors_right_start_offset = anchors.right_start_offset612 # computed anchor positions do not take start_offset into account,613 # so account for it here614 if anchors.left_end_lineno == 0:615 anchors_left_end_offset += start_offset616 if anchors.right_start_lineno == 0:617 anchors_right_start_offset += start_offset618 619 # account for display width620 anchors_left_end_offset = _display_width(621 all_lines[anchors.left_end_lineno], offset=anchors_left_end_offset622 )623 anchors_right_start_offset = _display_width(624 all_lines[anchors.right_start_lineno], offset=anchors_right_start_offset625 )626 627 primary_char = anchors.primary_char628 secondary_char = anchors.secondary_char629 significant_lines.update(630 range(anchors.left_end_lineno - 1, anchors.left_end_lineno + 2)631 )632 significant_lines.update(633 range(anchors.right_start_lineno - 1, anchors.right_start_lineno + 2)634 )635 636 # remove bad line numbers637 significant_lines.discard(-1)638 significant_lines.discard(len(all_lines))639 640 def output_line(lineno):641 """output all_lines[lineno] along with carets"""642 result.append(all_lines[lineno] + "\n")643 if not show_carets:644 return645 num_spaces = len(all_lines[lineno]) - len(all_lines[lineno].lstrip())646 carets = []647 num_carets = dp_end_offset if lineno == len(all_lines) - 1 else _display_width(all_lines[lineno])648 # compute caret character for each position649 for col in range(num_carets):650 if col < num_spaces or (lineno == 0 and col < dp_start_offset):651 # before first non-ws char of the line, or before start of instruction652 carets.append(' ')653 elif anchors and (654 lineno > anchors.left_end_lineno or655 (lineno == anchors.left_end_lineno and col >= anchors_left_end_offset)656 ) and (657 lineno < anchors.right_start_lineno or658 (lineno == anchors.right_start_lineno and col < anchors_right_start_offset)659 ):660 # within anchors661 carets.append(secondary_char)662 else:663 carets.append(primary_char)664 if colorize:665 # Replace the previous line with a red version of it only in the parts covered666 # by the carets.667 line = result[-1]668 colorized_line_parts = []669 colorized_carets_parts = []670 671 for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):672 caret_group = list(group)673 if color == "^":674 colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)675 colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)676 elif color == "~":677 colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)678 colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)679 else:680 colorized_line_parts.append("".join(char for char, _ in caret_group))681 colorized_carets_parts.append("".join(caret for _, caret in caret_group))682 683 colorized_line = "".join(colorized_line_parts)684 colorized_carets = "".join(colorized_carets_parts)685 result[-1] = colorized_line686 result.append(colorized_carets + "\n")687 else:688 result.append("".join(carets) + "\n")689 690 # display significant lines691 sig_lines_list = sorted(significant_lines)692 for i, lineno in enumerate(sig_lines_list):693 if i:694 linediff = lineno - sig_lines_list[i - 1]695 if linediff == 2:696 # 1 line in between - just output it697 output_line(lineno - 1)698 elif linediff > 2:699 # > 1 line in between - abbreviate700 result.append(f"...<{linediff - 1} lines>...\n")701 output_line(lineno)702 703 row.append(704 textwrap.indent(textwrap.dedent("".join(result)), ' ', lambda line: True)705 )706 if frame_summary.locals:707 for name, value in sorted(frame_summary.locals.items()):708 row.append(' {name} = {value}\n'.format(name=name, value=value))709 710 return ''.join(row)711 712 def _should_show_carets(self, start_offset, end_offset, all_lines, anchors):713 with suppress(SyntaxError, ImportError):714 import ast715 tree = ast.parse('\n'.join(all_lines))716 if not tree.body:717 return False718 statement = tree.body[0]719 value = None720 def _spawns_full_line(value):721 return (722 value.lineno == 1723 and value.end_lineno == len(all_lines)724 and value.col_offset == start_offset725 and value.end_col_offset == end_offset726 )727 match statement:728 case ast.Return(value=ast.Call()):729 if isinstance(statement.value.func, ast.Name):730 value = statement.value731 case ast.Assign(value=ast.Call()):732 if (733 len(statement.targets) == 1 and734 isinstance(statement.targets[0], ast.Name)735 ):736 value = statement.value737 if value is not None and _spawns_full_line(value):738 return False739 if anchors:740 return True741 if all_lines[0][:start_offset].lstrip() or all_lines[-1][end_offset:].rstrip():742 return True743 return False744 745 def format(self, **kwargs):746 """Format the stack ready for printing.747 748 Returns a list of strings ready for printing. Each string in the749 resulting list corresponds to a single frame from the stack.750 Each string ends in a newline; the strings may contain internal751 newlines as well, for those items with source text lines.752 753 For long sequences of the same frame and line, the first few754 repetitions are shown, followed by a summary line stating the exact755 number of further repetitions.756 """757 colorize = kwargs.get("colorize", False)758 result = []759 last_file = None760 last_line = None761 last_name = None762 count = 0763 for frame_summary in self:764 formatted_frame = self.format_frame_summary(frame_summary, colorize=colorize)765 if formatted_frame is None:766 continue767 if (last_file is None or last_file != frame_summary.filename or768 last_line is None or last_line != frame_summary.lineno or769 last_name is None or last_name != frame_summary.name):770 if count > _RECURSIVE_CUTOFF:771 count -= _RECURSIVE_CUTOFF772 result.append(773 f' [Previous line repeated {count} more '774 f'time{"s" if count > 1 else ""}]\n'775 )776 last_file = frame_summary.filename777 last_line = frame_summary.lineno778 last_name = frame_summary.name779 count = 0780 count += 1781 if count > _RECURSIVE_CUTOFF:782 continue783 result.append(formatted_frame)784 785 if count > _RECURSIVE_CUTOFF:786 count -= _RECURSIVE_CUTOFF787 result.append(788 f' [Previous line repeated {count} more '789 f'time{"s" if count > 1 else ""}]\n'790 )791 return result792 793 794def _byte_offset_to_character_offset(str, offset):795 as_utf8 = str.encode('utf-8')796 return len(as_utf8[:offset].decode("utf-8", errors="replace"))797 798 799_Anchors = collections.namedtuple(800 "_Anchors",801 [802 "left_end_lineno",803 "left_end_offset",804 "right_start_lineno",805 "right_start_offset",806 "primary_char",807 "secondary_char",808 ],809 defaults=["~", "^"]810)811 812def _extract_caret_anchors_from_line_segment(segment):813 """814 Given source code `segment` corresponding to a FrameSummary, determine:815 - for binary ops, the location of the binary op816 - for indexing and function calls, the location of the brackets.817 `segment` is expected to be a valid Python expression.818 """819 import ast820 821 try:822 # Without parentheses, `segment` is parsed as a statement.823 # Binary ops, subscripts, and calls are expressions, so824 # we can wrap them with parentheses to parse them as825 # (possibly multi-line) expressions.826 # e.g. if we try to highlight the addition in827 # x = (828 # a +829 # b830 # )831 # then we would ast.parse832 # a +833 # b834 # which is not a valid statement because of the newline.835 # Adding brackets makes it a valid expression.836 # (837 # a +838 # b839 # )840 # Line locations will be different than the original,841 # which is taken into account later on.842 tree = ast.parse(f"(\n{segment}\n)")843 except SyntaxError:844 return None845 846 if len(tree.body) != 1:847 return None848 849 lines = segment.splitlines()850 851 def normalize(lineno, offset):852 """Get character index given byte offset"""853 return _byte_offset_to_character_offset(lines[lineno], offset)854 855 def next_valid_char(lineno, col):856 """Gets the next valid character index in `lines`, if857 the current location is not valid. Handles empty lines.858 """859 while lineno < len(lines) and col >= len(lines[lineno]):860 col = 0861 lineno += 1862 assert lineno < len(lines) and col < len(lines[lineno])863 return lineno, col864 865 def increment(lineno, col):866 """Get the next valid character index in `lines`."""867 col += 1868 lineno, col = next_valid_char(lineno, col)869 return lineno, col870 871 def nextline(lineno, col):872 """Get the next valid character at least on the next line"""873 col = 0874 lineno += 1875 lineno, col = next_valid_char(lineno, col)876 return lineno, col877 878 def increment_until(lineno, col, stop):879 """Get the next valid non-"\\#" character that satisfies the `stop` predicate"""880 while True:881 ch = lines[lineno][col]882 if ch in "\\#":883 lineno, col = nextline(lineno, col)884 elif not stop(ch):885 lineno, col = increment(lineno, col)886 else:887 break888 return lineno, col889 890 def setup_positions(expr, force_valid=True):891 """Get the lineno/col position of the end of `expr`. If `force_valid` is True,892 forces the position to be a valid character (e.g. if the position is beyond the893 end of the line, move to the next line)894 """895 # -2 since end_lineno is 1-indexed and because we added an extra896 # bracket + newline to `segment` when calling ast.parse897 lineno = expr.end_lineno - 2898 col = normalize(lineno, expr.end_col_offset)899 return next_valid_char(lineno, col) if force_valid else (lineno, col)900 901 statement = tree.body[0]902 match statement:903 case ast.Expr(expr):904 match expr:905 case ast.BinOp():906 # ast gives these locations for BinOp subexpressions907 # ( left_expr ) + ( right_expr )908 # left^^^^^ right^^^^^909 lineno, col = setup_positions(expr.left)910 911 # First operator character is the first non-space/')' character912 lineno, col = increment_until(lineno, col, lambda x: not x.isspace() and x != ')')913 914 # binary op is 1 or 2 characters long, on the same line,915 # before the right subexpression916 right_col = col + 1917 if (918 right_col < len(lines[lineno])919 and (920 # operator char should not be in the right subexpression921 expr.right.lineno - 2 > lineno or922 right_col < normalize(expr.right.lineno - 2, expr.right.col_offset)923 )924 and not (ch := lines[lineno][right_col]).isspace()925 and ch not in "\\#"926 ):927 right_col += 1928 929 # right_col can be invalid since it is exclusive930 return _Anchors(lineno, col, lineno, right_col)931 case ast.Subscript():932 # ast gives these locations for value and slice subexpressions933 # ( value_expr ) [ slice_expr ]934 # value^^^^^ slice^^^^^935 # subscript^^^^^^^^^^^^^^^^^^^^936 937 # find left bracket938 left_lineno, left_col = setup_positions(expr.value)939 left_lineno, left_col = increment_until(left_lineno, left_col, lambda x: x == '[')940 # find right bracket (final character of expression)941 right_lineno, right_col = setup_positions(expr, force_valid=False)942 return _Anchors(left_lineno, left_col, right_lineno, right_col)943 case ast.Call():944 # ast gives these locations for function call expressions945 # ( func_expr ) (args, kwargs)946 # func^^^^^947 # call^^^^^^^^^^^^^^^^^^^^^^^^948 949 # find left bracket950 left_lineno, left_col = setup_positions(expr.func)951 left_lineno, left_col = increment_until(left_lineno, left_col, lambda x: x == '(')952 # find right bracket (final character of expression)953 right_lineno, right_col = setup_positions(expr, force_valid=False)954 return _Anchors(left_lineno, left_col, right_lineno, right_col)955 956 return None957 958_WIDE_CHAR_SPECIFIERS = "WF"959 960def _display_width(line, offset=None):961 """Calculate the extra amount of width space the given source962 code segment might take if it were to be displayed on a fixed963 width output device. Supports wide unicode characters and emojis."""964 965 if offset is None:966 offset = len(line)967 968 # Fast track for ASCII-only strings969 if line.isascii():970 return offset971 972 import unicodedata973 974 return sum(975 2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1976 for char in line[:offset]977 )978 979 980 981class _ExceptionPrintContext:982 def __init__(self):983 self.seen = set()984 self.exception_group_depth = 0985 self.need_close = False986 987 def indent(self):988 return ' ' * (2 * self.exception_group_depth)989 990 def emit(self, text_gen, margin_char=None):991 if margin_char is None:992 margin_char = '|'993 indent_str = self.indent()994 if self.exception_group_depth:995 indent_str += margin_char + ' '996 997 if isinstance(text_gen, str):998 yield textwrap.indent(text_gen, indent_str, lambda line: True)999 else:1000 for text in text_gen:1001 yield textwrap.indent(text, indent_str, lambda line: True)1002 1003 1004class TracebackException:1005 """An exception ready for rendering.1006 1007 The traceback module captures enough attributes from the original exception1008 to this intermediary form to ensure that no references are held, while1009 still being able to fully print or format it.1010 1011 max_group_width and max_group_depth control the formatting of exception1012 groups. The depth refers to the nesting level of the group, and the width1013 refers to the size of a single exception group's exceptions array. The1014 formatted output is truncated when either limit is exceeded.1015 1016 Use `from_exception` to create TracebackException instances from exception1017 objects, or the constructor to create TracebackException instances from1018 individual components.1019 1020 - :attr:`__cause__` A TracebackException of the original *__cause__*.1021 - :attr:`__context__` A TracebackException of the original *__context__*.1022 - :attr:`exceptions` For exception groups - a list of TracebackException1023 instances for the nested *exceptions*. ``None`` for other exceptions.1024 - :attr:`__suppress_context__` The *__suppress_context__* value from the1025 original exception.1026 - :attr:`stack` A `StackSummary` representing the traceback.1027 - :attr:`exc_type` (deprecated) The class of the original traceback.1028 - :attr:`exc_type_str` String display of exc_type1029 - :attr:`filename` For syntax errors - the filename where the error1030 occurred.1031 - :attr:`lineno` For syntax errors - the linenumber where the error1032 occurred.1033 - :attr:`end_lineno` For syntax errors - the end linenumber where the error1034 occurred. Can be `None` if not present.1035 - :attr:`text` For syntax errors - the text where the error1036 occurred.1037 - :attr:`offset` For syntax errors - the offset into the text where the1038 error occurred.1039 - :attr:`end_offset` For syntax errors - the end offset into the text where1040 the error occurred. Can be `None` if not present.1041 - :attr:`msg` For syntax errors - the compiler error message.1042 """1043 1044 def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,1045 lookup_lines=True, capture_locals=False, compact=False,1046 max_group_width=15, max_group_depth=10, save_exc_type=True, _seen=None):1047 # NB: we need to accept exc_traceback, exc_value, exc_traceback to1048 # permit backwards compat with the existing API, otherwise we1049 # need stub thunk objects just to glue it together.1050 # Handle loops in __cause__ or __context__.1051 is_recursive_call = _seen is not None1052 if _seen is None:1053 _seen = set()1054 _seen.add(id(exc_value))1055 1056 self.max_group_width = max_group_width1057 self.max_group_depth = max_group_depth1058 1059 self.stack = StackSummary._extract_from_extended_frame_gen(1060 _walk_tb_with_full_positions(exc_traceback),1061 limit=limit, lookup_lines=lookup_lines,1062 capture_locals=capture_locals)1063 1064 self._exc_type = exc_type if save_exc_type else None1065 1066 # Capture now to permit freeing resources: only complication is in the1067 # unofficial API _format_final_exc_line1068 self._str = _safe_string(exc_value, 'exception')1069 try:1070 self.__notes__ = getattr(exc_value, '__notes__', None)1071 except Exception as e:1072 self.__notes__ = [1073 f'Ignored error getting __notes__: {_safe_string(e, '__notes__', repr)}']1074 1075 self._is_syntax_error = False1076 self._have_exc_type = exc_type is not None1077 if exc_type is not None:1078 self.exc_type_qualname = exc_type.__qualname__1079 self.exc_type_module = exc_type.__module__1080 else:1081 self.exc_type_qualname = None1082 self.exc_type_module = None1083 1084 if exc_type and issubclass(exc_type, SyntaxError):1085 # Handle SyntaxError's specially1086 self.filename = exc_value.filename1087 lno = exc_value.lineno1088 self.lineno = str(lno) if lno is not None else None1089 end_lno = exc_value.end_lineno1090 self.end_lineno = str(end_lno) if end_lno is not None else None1091 self.text = exc_value.text1092 self.offset = exc_value.offset1093 self.end_offset = exc_value.end_offset1094 self.msg = exc_value.msg1095 self._is_syntax_error = True1096 self._exc_metadata = getattr(exc_value, "_metadata", None)1097 elif exc_type and issubclass(exc_type, ImportError) and \1098 getattr(exc_value, "name_from", None) is not None:1099 wrong_name = getattr(exc_value, "name_from", None)1100 suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)1101 if suggestion:1102 self._str += f". Did you mean: '{suggestion}'?"1103 elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \1104 getattr(exc_value, "name", None) is not None:1105 wrong_name = getattr(exc_value, "name", None)1106 suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)1107 if suggestion:1108 self._str += f". Did you mean: '{suggestion}'?"1109 if issubclass(exc_type, NameError):1110 wrong_name = getattr(exc_value, "name", None)1111 if wrong_name is not None and wrong_name in sys.stdlib_module_names:1112 if suggestion:1113 self._str += f" Or did you forget to import '{wrong_name}'?"1114 else:1115 self._str += f". Did you forget to import '{wrong_name}'?"1116 if lookup_lines:1117 self._load_lines()1118 self.__suppress_context__ = \1119 exc_value.__suppress_context__ if exc_value is not None else False1120 1121 # Convert __cause__ and __context__ to `TracebackExceptions`s, use a1122 # queue to avoid recursion (only the top-level call gets _seen == None)1123 if not is_recursive_call:1124 queue = [(self, exc_value)]1125 while queue:1126 te, e = queue.pop()1127 if (e is not None and e.__cause__ is not None1128 and id(e.__cause__) not in _seen):1129 cause = TracebackException(1130 type(e.__cause__),1131 e.__cause__,1132 e.__cause__.__traceback__,1133 limit=limit,1134 lookup_lines=lookup_lines,1135 capture_locals=capture_locals,1136 max_group_width=max_group_width,1137 max_group_depth=max_group_depth,1138 _seen=_seen)1139 else:1140 cause = None1141 1142 if compact:1143 need_context = (cause is None and1144 e is not None and1145 not e.__suppress_context__)1146 else:1147 need_context = True1148 if (e is not None and e.__context__ is not None1149 and need_context and id(e.__context__) not in _seen):1150 context = TracebackException(1151 type(e.__context__),1152 e.__context__,1153 e.__context__.__traceback__,1154 limit=limit,1155 lookup_lines=lookup_lines,1156 capture_locals=capture_locals,1157 max_group_width=max_group_width,1158 max_group_depth=max_group_depth,1159 _seen=_seen)1160 else:1161 context = None1162 1163 if e is not None and isinstance(e, BaseExceptionGroup):1164 exceptions = []1165 for exc in e.exceptions:1166 texc = TracebackException(1167 type(exc),1168 exc,1169 exc.__traceback__,1170 limit=limit,1171 lookup_lines=lookup_lines,1172 capture_locals=capture_locals,1173 max_group_width=max_group_width,1174 max_group_depth=max_group_depth,1175 _seen=_seen)1176 exceptions.append(texc)1177 else:1178 exceptions = None1179 1180 te.__cause__ = cause1181 te.__context__ = context1182 te.exceptions = exceptions1183 if cause:1184 queue.append((te.__cause__, e.__cause__))1185 if context:1186 queue.append((te.__context__, e.__context__))1187 if exceptions:1188 queue.extend(zip(te.exceptions, e.exceptions))1189 1190 @classmethod1191 def from_exception(cls, exc, *args, **kwargs):1192 """Create a TracebackException from an exception."""1193 return cls(type(exc), exc, exc.__traceback__, *args, **kwargs)1194 1195 @property1196 def exc_type(self):1197 warnings.warn('Deprecated in 3.13. Use exc_type_str instead.',1198 DeprecationWarning, stacklevel=2)1199 return self._exc_type1200 