mirror of
https://github.com/python/cpython.git
synced 2025-08-31 14:07:50 +00:00
gh-112730: Use color to highlight error locations (gh-112732)
Signed-off-by: Pablo Galindo <pablogsal@gmail.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
parent
3870d19d15
commit
16448cab44
8 changed files with 369 additions and 40 deletions
197
Lib/traceback.py
197
Lib/traceback.py
|
@ -1,5 +1,7 @@
|
|||
"""Extract, format and print information about Python stack traces."""
|
||||
|
||||
import os
|
||||
import io
|
||||
import collections.abc
|
||||
import itertools
|
||||
import linecache
|
||||
|
@ -19,6 +21,8 @@ __all__ = ['extract_stack', 'extract_tb', 'format_exception',
|
|||
# Formatting and printing lists of traceback lines.
|
||||
#
|
||||
|
||||
_COLORIZE = True
|
||||
|
||||
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."""
|
||||
|
@ -110,7 +114,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
|
||||
|
@ -121,17 +125,44 @@ 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 _can_colorize():
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import nt
|
||||
if not nt._supports_virtual_terminal():
|
||||
return False
|
||||
except (ImportError, AttributeError):
|
||||
return False
|
||||
|
||||
if os.environ.get("PYTHON_COLORS") == "0":
|
||||
return False
|
||||
if os.environ.get("PYTHON_COLORS") == "1":
|
||||
return True
|
||||
if "NO_COLOR" in os.environ:
|
||||
return False
|
||||
if not _COLORIZE:
|
||||
return False
|
||||
if "FORCE_COLOR" in os.environ:
|
||||
return True
|
||||
if os.environ.get("TERM") == "dumb":
|
||||
return False
|
||||
try:
|
||||
return os.isatty(sys.stderr.fileno())
|
||||
except io.UnsupportedOperation:
|
||||
return sys.stderr.isatty()
|
||||
|
||||
def _print_exception_bltin(exc, /):
|
||||
file = sys.stderr if sys.stderr is not None else sys.__stderr__
|
||||
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file)
|
||||
colorize = _can_colorize()
|
||||
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
|
||||
|
||||
|
||||
def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
|
||||
|
@ -172,13 +203,19 @@ def format_exception_only(exc, /, value=_sentinel, *, show_group=False):
|
|||
|
||||
# -- not official API but folk probably use these two functions.
|
||||
|
||||
def _format_final_exc_line(etype, value, *, insert_final_newline=True):
|
||||
def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=False):
|
||||
valuestr = _safe_string(value, 'exception')
|
||||
end_char = "\n" if insert_final_newline else ""
|
||||
if value is None or not valuestr:
|
||||
line = f"{etype}{end_char}"
|
||||
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 = f"{etype}: {valuestr}{end_char}"
|
||||
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):
|
||||
|
@ -406,6 +443,14 @@ def _get_code_position(code, instruction_index):
|
|||
|
||||
_RECURSIVE_CUTOFF = 3 # Also hardcoded in traceback.c.
|
||||
|
||||
class _ANSIColors:
|
||||
RED = '\x1b[31m'
|
||||
BOLD_RED = '\x1b[1;31m'
|
||||
MAGENTA = '\x1b[35m'
|
||||
BOLD_MAGENTA = '\x1b[1;35m'
|
||||
GREY = '\x1b[90m'
|
||||
RESET = '\x1b[0m'
|
||||
|
||||
class StackSummary(list):
|
||||
"""A list of FrameSummary objects, representing a stack of frames."""
|
||||
|
||||
|
@ -496,18 +541,33 @@ 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 = []
|
||||
filename = frame_summary.filename
|
||||
if frame_summary.filename.startswith("<stdin>-"):
|
||||
filename = "<stdin>"
|
||||
row.append(' File "{}", line {}, in {}\n'.format(
|
||||
filename, frame_summary.lineno, frame_summary.name))
|
||||
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 None or
|
||||
|
@ -619,7 +679,31 @@ class StackSummary(list):
|
|||
carets.append(secondary_char)
|
||||
else:
|
||||
carets.append(primary_char)
|
||||
result.append("".join(carets) + "\n")
|
||||
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:
|
||||
result.append("".join(carets) + "\n")
|
||||
|
||||
# display significant lines
|
||||
sig_lines_list = sorted(significant_lines)
|
||||
|
@ -643,7 +727,7 @@ class StackSummary(list):
|
|||
|
||||
return ''.join(row)
|
||||
|
||||
def format(self):
|
||||
def format(self, **kwargs):
|
||||
"""Format the stack ready for printing.
|
||||
|
||||
Returns a list of strings ready for printing. Each string in the
|
||||
|
@ -655,13 +739,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
|
||||
|
@ -1118,7 +1203,7 @@ class TracebackException:
|
|||
def __str__(self):
|
||||
return self._str
|
||||
|
||||
def format_exception_only(self, *, show_group=False, _depth=0):
|
||||
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.
|
||||
|
@ -1135,10 +1220,11 @@ class TracebackException:
|
|||
:exc:`BaseExceptionGroup`, the nested exceptions are included as
|
||||
well, recursively, with indentation relative to their nesting depth.
|
||||
"""
|
||||
colorize = kwargs.get("colorize", False)
|
||||
|
||||
indent = 3 * _depth * ' '
|
||||
if not self._have_exc_type:
|
||||
yield indent + _format_final_exc_line(None, self._str)
|
||||
yield indent + _format_final_exc_line(None, self._str, colorize=colorize)
|
||||
return
|
||||
|
||||
stype = self.exc_type_str
|
||||
|
@ -1146,16 +1232,16 @@ class TracebackException:
|
|||
if _depth > 0:
|
||||
# Nested exceptions needs correct handling of multiline messages.
|
||||
formatted = _format_final_exc_line(
|
||||
stype, self._str, insert_final_newline=False,
|
||||
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)
|
||||
yield _format_final_exc_line(stype, self._str, colorize=colorize)
|
||||
else:
|
||||
yield from [indent + l for l in 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)
|
||||
|
@ -1169,15 +1255,26 @@ class TracebackException:
|
|||
|
||||
if self.exceptions and show_group:
|
||||
for ex in self.exceptions:
|
||||
yield from ex.format_exception_only(show_group=show_group, _depth=_depth+1)
|
||||
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)
|
||||
|
||||
|
@ -1189,9 +1286,9 @@ class TracebackException:
|
|||
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)
|
||||
else:
|
||||
offset = self.offset
|
||||
end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
|
||||
if self.text and offset > len(self.text):
|
||||
|
@ -1204,14 +1301,43 @@ class TracebackException:
|
|||
# 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.
|
||||
|
@ -1223,7 +1349,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()
|
||||
|
||||
|
@ -1253,8 +1379,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(
|
||||
|
@ -1269,9 +1395,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
|
||||
|
@ -1312,11 +1438,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="")
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue