summaryrefslogtreecommitdiffstats
path: root/contrib/tools/python3/Lib/traceback.py
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/tools/python3/Lib/traceback.py')
-rw-r--r--contrib/tools/python3/Lib/traceback.py753
1 files changed, 603 insertions, 150 deletions
diff --git a/contrib/tools/python3/Lib/traceback.py b/contrib/tools/python3/Lib/traceback.py
index 8247d8ff8cd..b412954bd53 100644
--- a/contrib/tools/python3/Lib/traceback.py
+++ b/contrib/tools/python3/Lib/traceback.py
@@ -5,7 +5,10 @@ import itertools
import linecache
import sys
import textwrap
+import warnings
from contextlib import suppress
+import _colorize
+from _colorize import ANSIColors
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
'format_exception_only', 'format_list', 'format_stack',
@@ -18,6 +21,7 @@ __all__ = ['extract_stack', 'extract_tb', 'format_exception',
# Formatting and printing lists of traceback lines.
#
+
def print_list(extracted_list, file=None):
"""Print the list of tuples as returned by extract_tb() or
extract_stack() as a formatted stack trace to the given file."""
@@ -109,7 +113,7 @@ def _parse_value_tb(exc, value, tb):
def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
- file=None, chain=True):
+ file=None, chain=True, **kwargs):
"""Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
This differs from print_tb() in the following ways: (1) if
@@ -120,13 +124,23 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
occurred with a caret on the next line indicating the approximate
position of the error.
"""
+ colorize = kwargs.get("colorize", False)
value, tb = _parse_value_tb(exc, value, tb)
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
- te.print(file=file, chain=chain)
+ te.print(file=file, chain=chain, colorize=colorize)
+
+
+BUILTIN_EXCEPTION_LIMIT = object()
+
+
+def _print_exception_bltin(exc, /):
+ file = sys.stderr if sys.stderr is not None else sys.__stderr__
+ colorize = _colorize.can_colorize(file=file)
+ return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
- chain=True):
+ chain=True, **kwargs):
"""Format a stack trace and the exception information.
The arguments have the same meaning as the corresponding arguments
@@ -135,12 +149,13 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
these lines are concatenated and printed, exactly the same text is
printed as does print_exception().
"""
+ colorize = kwargs.get("colorize", False)
value, tb = _parse_value_tb(exc, value, tb)
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
- return list(te.format(chain=chain))
+ return list(te.format(chain=chain, colorize=colorize))
-def format_exception_only(exc, /, value=_sentinel):
+def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs):
"""Format the exception part of a traceback.
The return value is a list of strings, each ending in a newline.
@@ -150,23 +165,36 @@ def format_exception_only(exc, /, value=_sentinel):
contains several lines that (when printed) display detailed information
about where the syntax error occurred. Following the message, the list
contains the exception's ``__notes__``.
+
+ When *show_group* is ``True``, and the exception is an instance of
+ :exc:`BaseExceptionGroup`, the nested exceptions are included as
+ well, recursively, with indentation relative to their nesting depth.
"""
+ colorize = kwargs.get("colorize", False)
if value is _sentinel:
value = exc
te = TracebackException(type(value), value, None, compact=True)
- return list(te.format_exception_only())
+ return list(te.format_exception_only(show_group=show_group, colorize=colorize))
# -- not official API but folk probably use these two functions.
-def _format_final_exc_line(etype, value):
+def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=False):
valuestr = _safe_string(value, 'exception')
- if value is None or not valuestr:
- line = "%s\n" % etype
+ end_char = "\n" if insert_final_newline else ""
+ if colorize:
+ if value is None or not valuestr:
+ line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}{end_char}"
+ else:
+ line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}: {ANSIColors.MAGENTA}{valuestr}{ANSIColors.RESET}{end_char}"
else:
- line = "%s: %s\n" % (etype, valuestr)
+ if value is None or not valuestr:
+ line = f"{etype}{end_char}"
+ else:
+ line = f"{etype}: {valuestr}{end_char}"
return line
+
def _safe_string(value, what, func=str):
try:
return func(value)
@@ -176,7 +204,7 @@ def _safe_string(value, what, func=str):
# --
def print_exc(limit=None, file=None, chain=True):
- """Shorthand for 'print_exception(sys.exception(), limit, file, chain)'."""
+ """Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'."""
print_exception(sys.exception(), limit=limit, file=file, chain=chain)
def format_exc(limit=None, chain=True):
@@ -184,15 +212,15 @@ def format_exc(limit=None, chain=True):
return "".join(format_exception(sys.exception(), limit=limit, chain=chain))
def print_last(limit=None, file=None, chain=True):
- """This is a shorthand for 'print_exception(sys.last_exc, limit, file, chain)'."""
+ """This is a shorthand for 'print_exception(sys.last_exc, limit=limit, file=file, chain=chain)'."""
if not hasattr(sys, "last_exc") and not hasattr(sys, "last_type"):
raise ValueError("no last exception")
if hasattr(sys, "last_exc"):
- print_exception(sys.last_exc, limit, file, chain)
+ print_exception(sys.last_exc, limit=limit, file=file, chain=chain)
else:
print_exception(sys.last_type, sys.last_value, sys.last_traceback,
- limit, file, chain)
+ limit=limit, file=file, chain=chain)
#
@@ -260,11 +288,11 @@ class FrameSummary:
"""
__slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno',
- 'name', '_line', 'locals')
+ 'name', '_lines', '_lines_dedented', 'locals', '_code')
def __init__(self, filename, lineno, name, *, lookup_line=True,
locals=None, line=None,
- end_lineno=None, colno=None, end_colno=None):
+ end_lineno=None, colno=None, end_colno=None, **kwargs):
"""Construct a FrameSummary.
:param lookup_line: If True, `linecache` is consulted for the source
@@ -276,15 +304,17 @@ class FrameSummary:
"""
self.filename = filename
self.lineno = lineno
+ self.end_lineno = lineno if end_lineno is None else end_lineno
+ self.colno = colno
+ self.end_colno = end_colno
self.name = name
- self._line = line
+ self._code = kwargs.get("_code")
+ self._lines = line
+ self._lines_dedented = None
if lookup_line:
self.line
self.locals = {k: _safe_string(v, 'local', func=repr)
for k, v in locals.items()} if locals else None
- self.end_lineno = end_lineno
- self.colno = colno
- self.end_colno = end_colno
def __eq__(self, other):
if isinstance(other, FrameSummary):
@@ -309,19 +339,42 @@ class FrameSummary:
def __len__(self):
return 4
+ def _set_lines(self):
+ if (
+ self._lines is None
+ and self.lineno is not None
+ and self.end_lineno is not None
+ ):
+ lines = []
+ for lineno in range(self.lineno, self.end_lineno + 1):
+ # treat errors (empty string) and empty lines (newline) as the same
+ line = linecache.getline(self.filename, lineno).rstrip()
+ if not line and self._code is not None and self.filename.startswith("<"):
+ line = linecache._getline_from_code(self._code, lineno).rstrip()
+ lines.append(line)
+ self._lines = "\n".join(lines) + "\n"
+
@property
- def _original_line(self):
+ def _original_lines(self):
# Returns the line as-is from the source, without modifying whitespace.
- self.line
- return self._line
+ self._set_lines()
+ return self._lines
+
+ @property
+ def _dedented_lines(self):
+ # Returns _original_lines, but dedented
+ self._set_lines()
+ if self._lines_dedented is None and self._lines is not None:
+ self._lines_dedented = textwrap.dedent(self._lines)
+ return self._lines_dedented
@property
def line(self):
- if self._line is None:
- if self.lineno is None:
- return None
- self._line = linecache.getline(self.filename, self.lineno)
- return self._line.strip()
+ self._set_lines()
+ if self._lines is None:
+ return None
+ # return only the first line, stripped
+ return self._lines.partition("\n")[0].strip()
def walk_stack(f):
@@ -371,6 +424,7 @@ def _get_code_position(code, instruction_index):
_RECURSIVE_CUTOFF = 3 # Also hardcoded in traceback.c.
+
class StackSummary(list):
"""A list of FrameSummary objects, representing a stack of frames."""
@@ -403,12 +457,16 @@ class StackSummary(list):
# (frame, (lineno, end_lineno, colno, end_colno)) in the stack.
# Only lineno is required, the remaining fields can be None if the
# information is not available.
- if limit is None:
+ builtin_limit = limit is BUILTIN_EXCEPTION_LIMIT
+ if limit is None or builtin_limit:
limit = getattr(sys, 'tracebacklimit', None)
if limit is not None and limit < 0:
limit = 0
if limit is not None:
- if limit >= 0:
+ if builtin_limit:
+ frame_gen = tuple(frame_gen)
+ frame_gen = frame_gen[len(frame_gen) - limit:]
+ elif limit >= 0:
frame_gen = itertools.islice(frame_gen, limit)
else:
frame_gen = collections.deque(frame_gen, maxlen=-limit)
@@ -419,7 +477,6 @@ class StackSummary(list):
co = f.f_code
filename = co.co_filename
name = co.co_name
-
fnames.add(filename)
linecache.lazycache(filename, f.f_globals)
# Must defer line lookups until we have called checkcache.
@@ -427,11 +484,16 @@ class StackSummary(list):
f_locals = f.f_locals
else:
f_locals = None
- result.append(FrameSummary(
- filename, lineno, name, lookup_line=False, locals=f_locals,
- end_lineno=end_lineno, colno=colno, end_colno=end_colno))
+ result.append(
+ FrameSummary(filename, lineno, name,
+ lookup_line=False, locals=f_locals,
+ end_lineno=end_lineno, colno=colno, end_colno=end_colno,
+ _code=f.f_code,
+ )
+ )
for filename in fnames:
linecache.checkcache(filename)
+
# If immediate lookup was desired, trigger lookups now.
if lookup_lines:
for f in result:
@@ -457,72 +519,223 @@ class StackSummary(list):
result.append(FrameSummary(filename, lineno, name, line=line))
return result
- def format_frame_summary(self, frame_summary):
+ def format_frame_summary(self, frame_summary, **kwargs):
"""Format the lines for a single FrameSummary.
Returns a string representing one frame involved in the stack. This
gets called for every frame to be printed in the stack summary.
"""
+ colorize = kwargs.get("colorize", False)
row = []
- row.append(' File "{}", line {}, in {}\n'.format(
- frame_summary.filename, frame_summary.lineno, frame_summary.name))
- if frame_summary.line:
- stripped_line = frame_summary.line.strip()
- row.append(' {}\n'.format(stripped_line))
-
- line = frame_summary._original_line
- orig_line_len = len(line)
- frame_line_len = len(frame_summary.line.lstrip())
- stripped_characters = orig_line_len - frame_line_len
+ filename = frame_summary.filename
+ if frame_summary.filename.startswith("<stdin-") and frame_summary.filename.endswith('>'):
+ filename = "<stdin>"
+ if colorize:
+ row.append(' File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(
+ ANSIColors.MAGENTA,
+ filename,
+ ANSIColors.RESET,
+ ANSIColors.MAGENTA,
+ frame_summary.lineno,
+ ANSIColors.RESET,
+ ANSIColors.MAGENTA,
+ frame_summary.name,
+ ANSIColors.RESET,
+ )
+ )
+ else:
+ row.append(' File "{}", line {}, in {}\n'.format(
+ filename, frame_summary.lineno, frame_summary.name))
+ if frame_summary._dedented_lines and frame_summary._dedented_lines.strip():
if (
- frame_summary.colno is not None
- and frame_summary.end_colno is not None
+ frame_summary.colno is None or
+ frame_summary.end_colno is None
):
- start_offset = _byte_offset_to_character_offset(
- line, frame_summary.colno)
- end_offset = _byte_offset_to_character_offset(
- line, frame_summary.end_colno)
- code_segment = line[start_offset:end_offset]
+ # only output first line if column information is missing
+ row.append(textwrap.indent(frame_summary.line, ' ') + "\n")
+ else:
+ # get first and last line
+ all_lines_original = frame_summary._original_lines.splitlines()
+ first_line = all_lines_original[0]
+ # assume all_lines_original has enough lines (since we constructed it)
+ last_line = all_lines_original[frame_summary.end_lineno - frame_summary.lineno]
+
+ # character index of the start/end of the instruction
+ start_offset = _byte_offset_to_character_offset(first_line, frame_summary.colno)
+ end_offset = _byte_offset_to_character_offset(last_line, frame_summary.end_colno)
+
+ all_lines = frame_summary._dedented_lines.splitlines()[
+ :frame_summary.end_lineno - frame_summary.lineno + 1
+ ]
+
+ # adjust start/end offset based on dedent
+ dedent_characters = len(first_line) - len(all_lines[0])
+ start_offset = max(0, start_offset - dedent_characters)
+ end_offset = max(0, end_offset - dedent_characters)
+
+ # When showing this on a terminal, some of the non-ASCII characters
+ # might be rendered as double-width characters, so we need to take
+ # that into account when calculating the length of the line.
+ dp_start_offset = _display_width(all_lines[0], offset=start_offset)
+ dp_end_offset = _display_width(all_lines[-1], offset=end_offset)
+ # get exact code segment corresponding to the instruction
+ segment = "\n".join(all_lines)
+ segment = segment[start_offset:len(segment) - (len(all_lines[-1]) - end_offset)]
+
+ # attempt to parse for anchors
anchors = None
- if frame_summary.lineno == frame_summary.end_lineno:
- with suppress(Exception):
- anchors = _extract_caret_anchors_from_line_segment(code_segment)
- else:
- # Don't count the newline since the anchors only need to
- # go up until the last character of the line.
- end_offset = len(line.rstrip())
+ show_carets = False
+ with suppress(Exception):
+ anchors = _extract_caret_anchors_from_line_segment(segment)
+ show_carets = self._should_show_carets(start_offset, end_offset, all_lines, anchors)
+
+ result = []
- # show indicators if primary char doesn't span the frame line
- if end_offset - start_offset < len(stripped_line) or (
- anchors and anchors.right_start_offset - anchors.left_end_offset > 0):
- # When showing this on a terminal, some of the non-ASCII characters
- # might be rendered as double-width characters, so we need to take
- # that into account when calculating the length of the line.
- dp_start_offset = _display_width(line, start_offset) + 1
- dp_end_offset = _display_width(line, end_offset) + 1
+ # only display first line, last line, and lines around anchor start/end
+ significant_lines = {0, len(all_lines) - 1}
- row.append(' ')
- row.append(' ' * (dp_start_offset - stripped_characters))
+ anchors_left_end_offset = 0
+ anchors_right_start_offset = 0
+ primary_char = "^"
+ secondary_char = "^"
+ if anchors:
+ anchors_left_end_offset = anchors.left_end_offset
+ anchors_right_start_offset = anchors.right_start_offset
+ # computed anchor positions do not take start_offset into account,
+ # so account for it here
+ if anchors.left_end_lineno == 0:
+ anchors_left_end_offset += start_offset
+ if anchors.right_start_lineno == 0:
+ anchors_right_start_offset += start_offset
+
+ # account for display width
+ anchors_left_end_offset = _display_width(
+ all_lines[anchors.left_end_lineno], offset=anchors_left_end_offset
+ )
+ anchors_right_start_offset = _display_width(
+ all_lines[anchors.right_start_lineno], offset=anchors_right_start_offset
+ )
- if anchors:
- dp_left_end_offset = _display_width(code_segment, anchors.left_end_offset)
- dp_right_start_offset = _display_width(code_segment, anchors.right_start_offset)
- row.append(anchors.primary_char * dp_left_end_offset)
- row.append(anchors.secondary_char * (dp_right_start_offset - dp_left_end_offset))
- row.append(anchors.primary_char * (dp_end_offset - dp_start_offset - dp_right_start_offset))
+ primary_char = anchors.primary_char
+ secondary_char = anchors.secondary_char
+ significant_lines.update(
+ range(anchors.left_end_lineno - 1, anchors.left_end_lineno + 2)
+ )
+ significant_lines.update(
+ range(anchors.right_start_lineno - 1, anchors.right_start_lineno + 2)
+ )
+
+ # remove bad line numbers
+ significant_lines.discard(-1)
+ significant_lines.discard(len(all_lines))
+
+ def output_line(lineno):
+ """output all_lines[lineno] along with carets"""
+ result.append(all_lines[lineno] + "\n")
+ if not show_carets:
+ return
+ num_spaces = len(all_lines[lineno]) - len(all_lines[lineno].lstrip())
+ carets = []
+ num_carets = dp_end_offset if lineno == len(all_lines) - 1 else _display_width(all_lines[lineno])
+ # compute caret character for each position
+ for col in range(num_carets):
+ if col < num_spaces or (lineno == 0 and col < dp_start_offset):
+ # before first non-ws char of the line, or before start of instruction
+ carets.append(' ')
+ elif anchors and (
+ lineno > anchors.left_end_lineno or
+ (lineno == anchors.left_end_lineno and col >= anchors_left_end_offset)
+ ) and (
+ lineno < anchors.right_start_lineno or
+ (lineno == anchors.right_start_lineno and col < anchors_right_start_offset)
+ ):
+ # within anchors
+ carets.append(secondary_char)
+ else:
+ carets.append(primary_char)
+ if colorize:
+ # Replace the previous line with a red version of it only in the parts covered
+ # by the carets.
+ line = result[-1]
+ colorized_line_parts = []
+ colorized_carets_parts = []
+
+ for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):
+ caret_group = list(group)
+ if color == "^":
+ colorized_line_parts.append(ANSIColors.BOLD_RED + "".join(char for char, _ in caret_group) + ANSIColors.RESET)
+ colorized_carets_parts.append(ANSIColors.BOLD_RED + "".join(caret for _, caret in caret_group) + ANSIColors.RESET)
+ elif color == "~":
+ colorized_line_parts.append(ANSIColors.RED + "".join(char for char, _ in caret_group) + ANSIColors.RESET)
+ colorized_carets_parts.append(ANSIColors.RED + "".join(caret for _, caret in caret_group) + ANSIColors.RESET)
+ else:
+ colorized_line_parts.append("".join(char for char, _ in caret_group))
+ colorized_carets_parts.append("".join(caret for _, caret in caret_group))
+
+ colorized_line = "".join(colorized_line_parts)
+ colorized_carets = "".join(colorized_carets_parts)
+ result[-1] = colorized_line
+ result.append(colorized_carets + "\n")
else:
- row.append('^' * (dp_end_offset - dp_start_offset))
+ result.append("".join(carets) + "\n")
- row.append('\n')
+ # display significant lines
+ sig_lines_list = sorted(significant_lines)
+ for i, lineno in enumerate(sig_lines_list):
+ if i:
+ linediff = lineno - sig_lines_list[i - 1]
+ if linediff == 2:
+ # 1 line in between - just output it
+ output_line(lineno - 1)
+ elif linediff > 2:
+ # > 1 line in between - abbreviate
+ result.append(f"...<{linediff - 1} lines>...\n")
+ output_line(lineno)
+ row.append(
+ textwrap.indent(textwrap.dedent("".join(result)), ' ', lambda line: True)
+ )
if frame_summary.locals:
for name, value in sorted(frame_summary.locals.items()):
row.append(' {name} = {value}\n'.format(name=name, value=value))
return ''.join(row)
- def format(self):
+ def _should_show_carets(self, start_offset, end_offset, all_lines, anchors):
+ with suppress(SyntaxError, ImportError):
+ import ast
+ tree = ast.parse('\n'.join(all_lines))
+ if not tree.body:
+ return False
+ statement = tree.body[0]
+ value = None
+ def _spawns_full_line(value):
+ return (
+ value.lineno == 1
+ and value.end_lineno == len(all_lines)
+ and value.col_offset == start_offset
+ and value.end_col_offset == end_offset
+ )
+ match statement:
+ case ast.Return(value=ast.Call()):
+ if isinstance(statement.value.func, ast.Name):
+ value = statement.value
+ case ast.Assign(value=ast.Call()):
+ if (
+ len(statement.targets) == 1 and
+ isinstance(statement.targets[0], ast.Name)
+ ):
+ value = statement.value
+ if value is not None and _spawns_full_line(value):
+ return False
+ if anchors:
+ return True
+ if all_lines[0][:start_offset].lstrip() or all_lines[-1][end_offset:].rstrip():
+ return True
+ return False
+
+ def format(self, **kwargs):
"""Format the stack ready for printing.
Returns a list of strings ready for printing. Each string in the
@@ -534,13 +747,14 @@ class StackSummary(list):
repetitions are shown, followed by a summary line stating the exact
number of further repetitions.
"""
+ colorize = kwargs.get("colorize", False)
result = []
last_file = None
last_line = None
last_name = None
count = 0
for frame_summary in self:
- formatted_frame = self.format_frame_summary(frame_summary)
+ formatted_frame = self.format_frame_summary(frame_summary, colorize=colorize)
if formatted_frame is None:
continue
if (last_file is None or last_file != frame_summary.filename or
@@ -578,7 +792,9 @@ def _byte_offset_to_character_offset(str, offset):
_Anchors = collections.namedtuple(
"_Anchors",
[
+ "left_end_lineno",
"left_end_offset",
+ "right_start_lineno",
"right_start_offset",
"primary_char",
"secondary_char",
@@ -587,59 +803,161 @@ _Anchors = collections.namedtuple(
)
def _extract_caret_anchors_from_line_segment(segment):
+ """
+ Given source code `segment` corresponding to a FrameSummary, determine:
+ - for binary ops, the location of the binary op
+ - for indexing and function calls, the location of the brackets.
+ `segment` is expected to be a valid Python expression.
+ """
import ast
try:
- tree = ast.parse(segment)
+ # Without parentheses, `segment` is parsed as a statement.
+ # Binary ops, subscripts, and calls are expressions, so
+ # we can wrap them with parentheses to parse them as
+ # (possibly multi-line) expressions.
+ # e.g. if we try to highlight the addition in
+ # x = (
+ # a +
+ # b
+ # )
+ # then we would ast.parse
+ # a +
+ # b
+ # which is not a valid statement because of the newline.
+ # Adding brackets makes it a valid expression.
+ # (
+ # a +
+ # b
+ # )
+ # Line locations will be different than the original,
+ # which is taken into account later on.
+ tree = ast.parse(f"(\n{segment}\n)")
except SyntaxError:
return None
if len(tree.body) != 1:
return None
- normalize = lambda offset: _byte_offset_to_character_offset(segment, offset)
+ lines = segment.splitlines()
+
+ def normalize(lineno, offset):
+ """Get character index given byte offset"""
+ return _byte_offset_to_character_offset(lines[lineno], offset)
+
+ def next_valid_char(lineno, col):
+ """Gets the next valid character index in `lines`, if
+ the current location is not valid. Handles empty lines.
+ """
+ while lineno < len(lines) and col >= len(lines[lineno]):
+ col = 0
+ lineno += 1
+ assert lineno < len(lines) and col < len(lines[lineno])
+ return lineno, col
+
+ def increment(lineno, col):
+ """Get the next valid character index in `lines`."""
+ col += 1
+ lineno, col = next_valid_char(lineno, col)
+ return lineno, col
+
+ def nextline(lineno, col):
+ """Get the next valid character at least on the next line"""
+ col = 0
+ lineno += 1
+ lineno, col = next_valid_char(lineno, col)
+ return lineno, col
+
+ def increment_until(lineno, col, stop):
+ """Get the next valid non-"\\#" character that satisfies the `stop` predicate"""
+ while True:
+ ch = lines[lineno][col]
+ if ch in "\\#":
+ lineno, col = nextline(lineno, col)
+ elif not stop(ch):
+ lineno, col = increment(lineno, col)
+ else:
+ break
+ return lineno, col
+
+ def setup_positions(expr, force_valid=True):
+ """Get the lineno/col position of the end of `expr`. If `force_valid` is True,
+ forces the position to be a valid character (e.g. if the position is beyond the
+ end of the line, move to the next line)
+ """
+ # -2 since end_lineno is 1-indexed and because we added an extra
+ # bracket + newline to `segment` when calling ast.parse
+ lineno = expr.end_lineno - 2
+ col = normalize(lineno, expr.end_col_offset)
+ return next_valid_char(lineno, col) if force_valid else (lineno, col)
+
statement = tree.body[0]
match statement:
case ast.Expr(expr):
match expr:
case ast.BinOp():
- operator_start = normalize(expr.left.end_col_offset)
- operator_end = normalize(expr.right.col_offset)
- operator_str = segment[operator_start:operator_end]
- operator_offset = len(operator_str) - len(operator_str.lstrip())
+ # ast gives these locations for BinOp subexpressions
+ # ( left_expr ) + ( right_expr )
+ # left^^^^^ right^^^^^
+ lineno, col = setup_positions(expr.left)
+
+ # First operator character is the first non-space/')' character
+ lineno, col = increment_until(lineno, col, lambda x: not x.isspace() and x != ')')
- left_anchor = expr.left.end_col_offset + operator_offset
- right_anchor = left_anchor + 1
+ # binary op is 1 or 2 characters long, on the same line,
+ # before the right subexpression
+ right_col = col + 1
if (
- operator_offset + 1 < len(operator_str)
- and not operator_str[operator_offset + 1].isspace()
+ right_col < len(lines[lineno])
+ and (
+ # operator char should not be in the right subexpression
+ expr.right.lineno - 2 > lineno or
+ right_col < normalize(expr.right.lineno - 2, expr.right.col_offset)
+ )
+ and not (ch := lines[lineno][right_col]).isspace()
+ and ch not in "\\#"
):
- right_anchor += 1
+ right_col += 1
- while left_anchor < len(segment) and ((ch := segment[left_anchor]).isspace() or ch in ")#"):
- left_anchor += 1
- right_anchor += 1
- return _Anchors(normalize(left_anchor), normalize(right_anchor))
+ # right_col can be invalid since it is exclusive
+ return _Anchors(lineno, col, lineno, right_col)
case ast.Subscript():
- left_anchor = normalize(expr.value.end_col_offset)
- right_anchor = normalize(expr.slice.end_col_offset + 1)
- while left_anchor < len(segment) and ((ch := segment[left_anchor]).isspace() or ch != "["):
- left_anchor += 1
- while right_anchor < len(segment) and ((ch := segment[right_anchor]).isspace() or ch != "]"):
- right_anchor += 1
- if right_anchor < len(segment):
- right_anchor += 1
- return _Anchors(left_anchor, right_anchor)
+ # ast gives these locations for value and slice subexpressions
+ # ( value_expr ) [ slice_expr ]
+ # value^^^^^ slice^^^^^
+ # subscript^^^^^^^^^^^^^^^^^^^^
+
+ # find left bracket
+ left_lineno, left_col = setup_positions(expr.value)
+ left_lineno, left_col = increment_until(left_lineno, left_col, lambda x: x == '[')
+ # find right bracket (final character of expression)
+ right_lineno, right_col = setup_positions(expr, force_valid=False)
+ return _Anchors(left_lineno, left_col, right_lineno, right_col)
+ case ast.Call():
+ # ast gives these locations for function call expressions
+ # ( func_expr ) (args, kwargs)
+ # func^^^^^
+ # call^^^^^^^^^^^^^^^^^^^^^^^^
+
+ # find left bracket
+ left_lineno, left_col = setup_positions(expr.func)
+ left_lineno, left_col = increment_until(left_lineno, left_col, lambda x: x == '(')
+ # find right bracket (final character of expression)
+ right_lineno, right_col = setup_positions(expr, force_valid=False)
+ return _Anchors(left_lineno, left_col, right_lineno, right_col)
return None
_WIDE_CHAR_SPECIFIERS = "WF"
-def _display_width(line, offset):
+def _display_width(line, offset=None):
"""Calculate the extra amount of width space the given source
code segment might take if it were to be displayed on a fixed
width output device. Supports wide unicode characters and emojis."""
+ if offset is None:
+ offset = len(line)
+
# Fast track for ASCII-only strings
if line.isascii():
return offset
@@ -699,7 +1017,8 @@ class TracebackException:
- :attr:`__suppress_context__` The *__suppress_context__* value from the
original exception.
- :attr:`stack` A `StackSummary` representing the traceback.
- - :attr:`exc_type` The class of the original traceback.
+ - :attr:`exc_type` (deprecated) The class of the original traceback.
+ - :attr:`exc_type_str` String display of exc_type
- :attr:`filename` For syntax errors - the filename where the error
occurred.
- :attr:`lineno` For syntax errors - the linenumber where the error
@@ -717,7 +1036,7 @@ class TracebackException:
def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
lookup_lines=True, capture_locals=False, compact=False,
- max_group_width=15, max_group_depth=10, _seen=None):
+ max_group_width=15, max_group_depth=10, save_exc_type=True, _seen=None):
# NB: we need to accept exc_traceback, exc_value, exc_traceback to
# permit backwards compat with the existing API, otherwise we
# need stub thunk objects just to glue it together.
@@ -734,7 +1053,9 @@ class TracebackException:
_walk_tb_with_full_positions(exc_traceback),
limit=limit, lookup_lines=lookup_lines,
capture_locals=capture_locals)
- self.exc_type = exc_type
+
+ self._exc_type = exc_type if save_exc_type else None
+
# Capture now to permit freeing resources: only complication is in the
# unofficial API _format_final_exc_line
self._str = _safe_string(exc_value, 'exception')
@@ -744,6 +1065,15 @@ class TracebackException:
self.__notes__ = [
f'Ignored error getting __notes__: {_safe_string(e, '__notes__', repr)}']
+ self._is_syntax_error = False
+ self._have_exc_type = exc_type is not None
+ if exc_type is not None:
+ self.exc_type_qualname = exc_type.__qualname__
+ self.exc_type_module = exc_type.__module__
+ else:
+ self.exc_type_qualname = None
+ self.exc_type_module = None
+
if exc_type and issubclass(exc_type, SyntaxError):
# Handle SyntaxError's specially
self.filename = exc_value.filename
@@ -755,6 +1085,7 @@ class TracebackException:
self.offset = exc_value.offset
self.end_offset = exc_value.end_offset
self.msg = exc_value.msg
+ self._is_syntax_error = True
elif exc_type and issubclass(exc_type, ImportError) and \
getattr(exc_value, "name_from", None) is not None:
wrong_name = getattr(exc_value, "name_from", None)
@@ -771,9 +1102,9 @@ class TracebackException:
wrong_name = getattr(exc_value, "name", None)
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
if suggestion:
- self._str += f" Or did you forget to import '{wrong_name}'"
+ self._str += f" Or did you forget to import '{wrong_name}'?"
else:
- self._str += f". Did you forget to import '{wrong_name}'"
+ self._str += f". Did you forget to import '{wrong_name}'?"
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
@@ -785,7 +1116,7 @@ class TracebackException:
queue = [(self, exc_value)]
while queue:
te, e = queue.pop()
- if (e and e.__cause__ is not None
+ if (e is not None and e.__cause__ is not None
and id(e.__cause__) not in _seen):
cause = TracebackException(
type(e.__cause__),
@@ -806,7 +1137,7 @@ class TracebackException:
not e.__suppress_context__)
else:
need_context = True
- if (e and e.__context__ is not None
+ if (e is not None and e.__context__ is not None
and need_context and id(e.__context__) not in _seen):
context = TracebackException(
type(e.__context__),
@@ -821,7 +1152,7 @@ class TracebackException:
else:
context = None
- if e and isinstance(e, BaseExceptionGroup):
+ if e is not None and isinstance(e, BaseExceptionGroup):
exceptions = []
for exc in e.exceptions:
texc = TracebackException(
@@ -853,6 +1184,24 @@ class TracebackException:
"""Create a TracebackException from an exception."""
return cls(type(exc), exc, exc.__traceback__, *args, **kwargs)
+ @property
+ def exc_type(self):
+ warnings.warn('Deprecated in 3.13. Use exc_type_str instead.',
+ DeprecationWarning, stacklevel=2)
+ return self._exc_type
+
+ @property
+ def exc_type_str(self):
+ if not self._have_exc_type:
+ return None
+ stype = self.exc_type_qualname
+ smod = self.exc_type_module
+ if smod not in ("__main__", "builtins"):
+ if not isinstance(smod, str):
+ smod = "<unknown>"
+ stype = smod + '.' + stype
+ return stype
+
def _load_lines(self):
"""Private API. force all lines in the stack to be loaded."""
for frame in self.stack:
@@ -866,7 +1215,7 @@ class TracebackException:
def __str__(self):
return self._str
- def format_exception_only(self):
+ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
"""Format the exception part of the traceback.
The return value is a generator of strings, each ending in a newline.
@@ -878,22 +1227,33 @@ class TracebackException:
display detailed information about where the syntax error occurred.
Following the message, generator also yields
all the exception's ``__notes__``.
+
+ When *show_group* is ``True``, and the exception is an instance of
+ :exc:`BaseExceptionGroup`, the nested exceptions are included as
+ well, recursively, with indentation relative to their nesting depth.
"""
- if self.exc_type is None:
- yield _format_final_exc_line(None, self._str)
- return
+ colorize = kwargs.get("colorize", False)
- stype = self.exc_type.__qualname__
- smod = self.exc_type.__module__
- if smod not in ("__main__", "builtins"):
- if not isinstance(smod, str):
- smod = "<unknown>"
- stype = smod + '.' + stype
+ indent = 3 * _depth * ' '
+ if not self._have_exc_type:
+ yield indent + _format_final_exc_line(None, self._str, colorize=colorize)
+ return
- if not issubclass(self.exc_type, SyntaxError):
- yield _format_final_exc_line(stype, self._str)
+ stype = self.exc_type_str
+ if not self._is_syntax_error:
+ if _depth > 0:
+ # Nested exceptions needs correct handling of multiline messages.
+ formatted = _format_final_exc_line(
+ stype, self._str, insert_final_newline=False, colorize=colorize
+ ).split('\n')
+ yield from [
+ indent + l + '\n'
+ for l in formatted
+ ]
+ else:
+ yield _format_final_exc_line(stype, self._str, colorize=colorize)
else:
- yield from self._format_syntax_error(stype)
+ yield from [indent + l for l in self._format_syntax_error(stype, colorize=colorize)]
if (
isinstance(self.__notes__, collections.abc.Sequence)
@@ -901,47 +1261,106 @@ class TracebackException:
):
for note in self.__notes__:
note = _safe_string(note, 'note')
- yield from [l + '\n' for l in note.split('\n')]
+ yield from [indent + l + '\n' for l in note.split('\n')]
elif self.__notes__ is not None:
- yield "{}\n".format(_safe_string(self.__notes__, '__notes__', func=repr))
+ yield indent + "{}\n".format(_safe_string(self.__notes__, '__notes__', func=repr))
+
+ if self.exceptions and show_group:
+ for ex in self.exceptions:
+ yield from ex.format_exception_only(show_group=show_group, _depth=_depth+1, colorize=colorize)
- def _format_syntax_error(self, stype):
+ def _format_syntax_error(self, stype, **kwargs):
"""Format SyntaxError exceptions (internal helper)."""
# Show exactly where the problem was found.
+ colorize = kwargs.get("colorize", False)
filename_suffix = ''
if self.lineno is not None:
- yield ' File "{}", line {}\n'.format(
- self.filename or "<string>", self.lineno)
+ if colorize:
+ yield ' File {}"{}"{}, line {}{}{}\n'.format(
+ ANSIColors.MAGENTA,
+ self.filename or "<string>",
+ ANSIColors.RESET,
+ ANSIColors.MAGENTA,
+ self.lineno,
+ ANSIColors.RESET,
+ )
+ else:
+ yield ' File "{}", line {}\n'.format(
+ self.filename or "<string>", self.lineno)
elif self.filename is not None:
filename_suffix = ' ({})'.format(self.filename)
text = self.text
- if text is not None:
+ if isinstance(text, str):
# text = " foo\n"
# rtext = " foo"
# ltext = "foo"
rtext = text.rstrip('\n')
ltext = rtext.lstrip(' \n\f')
spaces = len(rtext) - len(ltext)
- yield ' {}\n'.format(ltext)
-
- if self.offset is not None:
+ if self.offset is None:
+ yield ' {}\n'.format(ltext)
+ elif isinstance(self.offset, int):
offset = self.offset
- end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
- if offset == end_offset or end_offset == -1:
+ if self.lineno == self.end_lineno:
+ end_offset = (
+ self.end_offset
+ if (
+ isinstance(self.end_offset, int)
+ and self.end_offset != 0
+ )
+ else offset
+ )
+ else:
+ end_offset = len(rtext) + 1
+
+ if self.text and offset > len(self.text):
+ offset = len(rtext) + 1
+ if self.text and end_offset > len(self.text):
+ end_offset = len(rtext) + 1
+ if offset >= end_offset or end_offset < 0:
end_offset = offset + 1
# Convert 1-based column offset to 0-based index into stripped text
colno = offset - 1 - spaces
end_colno = end_offset - 1 - spaces
+ caretspace = ' '
if colno >= 0:
# non-space whitespace (likes tabs) must be kept for alignment
caretspace = ((c if c.isspace() else ' ') for c in ltext[:colno])
- yield ' {}{}'.format("".join(caretspace), ('^' * (end_colno - colno) + "\n"))
+ start_color = end_color = ""
+ if colorize:
+ # colorize from colno to end_colno
+ ltext = (
+ ltext[:colno] +
+ ANSIColors.BOLD_RED + ltext[colno:end_colno] + ANSIColors.RESET +
+ ltext[end_colno:]
+ )
+ start_color = ANSIColors.BOLD_RED
+ end_color = ANSIColors.RESET
+ yield ' {}\n'.format(ltext)
+ yield ' {}{}{}{}\n'.format(
+ "".join(caretspace),
+ start_color,
+ ('^' * (end_colno - colno)),
+ end_color,
+ )
+ else:
+ yield ' {}\n'.format(ltext)
msg = self.msg or "<no detail available>"
- yield "{}: {}{}\n".format(stype, msg, filename_suffix)
+ if colorize:
+ yield "{}{}{}: {}{}{}{}\n".format(
+ ANSIColors.BOLD_MAGENTA,
+ stype,
+ ANSIColors.RESET,
+ ANSIColors.MAGENTA,
+ msg,
+ ANSIColors.RESET,
+ filename_suffix)
+ else:
+ yield "{}: {}{}\n".format(stype, msg, filename_suffix)
- def format(self, *, chain=True, _ctx=None):
+ def format(self, *, chain=True, _ctx=None, **kwargs):
"""Format the exception.
If chain is not *True*, *__cause__* and *__context__* will not be formatted.
@@ -953,7 +1372,7 @@ class TracebackException:
The message indicating which exception occurred is always the last
string in the output.
"""
-
+ colorize = kwargs.get("colorize", False)
if _ctx is None:
_ctx = _ExceptionPrintContext()
@@ -983,8 +1402,8 @@ class TracebackException:
if exc.exceptions is None:
if exc.stack:
yield from _ctx.emit('Traceback (most recent call last):\n')
- yield from _ctx.emit(exc.stack.format())
- yield from _ctx.emit(exc.format_exception_only())
+ yield from _ctx.emit(exc.stack.format(colorize=colorize))
+ yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
elif _ctx.exception_group_depth > self.max_group_depth:
# exception group, but depth exceeds limit
yield from _ctx.emit(
@@ -999,9 +1418,9 @@ class TracebackException:
yield from _ctx.emit(
'Exception Group Traceback (most recent call last):\n',
margin_char = '+' if is_toplevel else None)
- yield from _ctx.emit(exc.stack.format())
+ yield from _ctx.emit(exc.stack.format(colorize=colorize))
- yield from _ctx.emit(exc.format_exception_only())
+ yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
num_excs = len(exc.exceptions)
if num_excs <= self.max_group_width:
n = num_excs
@@ -1024,7 +1443,7 @@ class TracebackException:
f'+---------------- {title} ----------------\n')
_ctx.exception_group_depth += 1
if not truncated:
- yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx)
+ yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx, colorize=colorize)
else:
remaining = num_excs - self.max_group_width
plural = 's' if remaining > 1 else ''
@@ -1042,11 +1461,12 @@ class TracebackException:
_ctx.exception_group_depth = 0
- def print(self, *, file=None, chain=True):
+ def print(self, *, file=None, chain=True, **kwargs):
"""Print the result of self.format(chain=chain) to 'file'."""
+ colorize = kwargs.get("colorize", False)
if file is None:
file = sys.stderr
- for line in self.format(chain=chain):
+ for line in self.format(chain=chain, colorize=colorize):
print(line, file=file, end="")
@@ -1064,19 +1484,40 @@ def _substitution_cost(ch_a, ch_b):
return _MOVE_COST
+def _get_safe___dir__(obj):
+ # Use obj.__dir__() to avoid a TypeError when calling dir(obj).
+ # See gh-131001 and gh-139933.
+ try:
+ d = obj.__dir__()
+ except TypeError: # when obj is a class
+ d = type(obj).__dir__(obj)
+ return sorted(x for x in d if isinstance(x, str))
+
+
def _compute_suggestion_error(exc_value, tb, wrong_name):
if wrong_name is None or not isinstance(wrong_name, str):
return None
if isinstance(exc_value, AttributeError):
obj = exc_value.obj
try:
- d = dir(obj)
+ d = _get_safe___dir__(obj)
+ hide_underscored = (wrong_name[:1] != '_')
+ if hide_underscored and tb is not None:
+ while tb.tb_next is not None:
+ tb = tb.tb_next
+ frame = tb.tb_frame
+ if 'self' in frame.f_locals and frame.f_locals['self'] is obj:
+ hide_underscored = False
+ if hide_underscored:
+ d = [x for x in d if x[:1] != '_']
except Exception:
return None
elif isinstance(exc_value, ImportError):
try:
mod = __import__(exc_value.name)
- d = dir(mod)
+ d = _get_safe___dir__(mod)
+ if wrong_name[:1] != '_':
+ d = [x for x in d if x[:1] != '_']
except Exception:
return None
else:
@@ -1092,14 +1533,26 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
+ list(frame.f_globals)
+ list(frame.f_builtins)
)
+ d = [x for x in d if isinstance(x, str)]
# Check first if we are in a method and the instance
# has the wrong name as attribute
if 'self' in frame.f_locals:
self = frame.f_locals['self']
- if hasattr(self, wrong_name):
+ try:
+ has_wrong_name = hasattr(self, wrong_name)
+ except Exception:
+ has_wrong_name = False
+ if has_wrong_name:
return f"self.{wrong_name}"
+ try:
+ import _suggestions
+ except ImportError:
+ pass
+ else:
+ return _suggestions._generate_suggestions(d, wrong_name)
+
# Compute closest match
if len(d) > _MAX_CANDIDATE_ITEMS: