mirror of
https://github.com/python/cpython.git
synced 2025-09-27 02:39:58 +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
|
@ -612,6 +612,27 @@ Miscellaneous options
|
||||||
.. versionadded:: 3.13
|
.. versionadded:: 3.13
|
||||||
The ``-X presite`` option.
|
The ``-X presite`` option.
|
||||||
|
|
||||||
|
Controlling Color
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The Python interpreter is configured by default to use colors to highlight
|
||||||
|
output in certain situations such as when displaying tracebacks. This
|
||||||
|
behavior can be controlled by setting different environment variables.
|
||||||
|
|
||||||
|
Setting the environment variable ``TERM`` to ``dumb`` will disable color.
|
||||||
|
|
||||||
|
If the environment variable ``FORCE_COLOR`` is set, then color will be
|
||||||
|
enabled regardless of the value of TERM. This is useful on CI systems which
|
||||||
|
aren’t terminals but can none-the-less display ANSI escape sequences.
|
||||||
|
|
||||||
|
If the environment variable ``NO_COLOR`` is set, Python will disable all color
|
||||||
|
in the output. This takes precedence over ``FORCE_COLOR``.
|
||||||
|
|
||||||
|
All these environment variables are used also by other tools to control color
|
||||||
|
output. To control the color output only in the Python interpreter, the
|
||||||
|
:envvar:`PYTHON_COLORS` environment variable can be used. This variable takes
|
||||||
|
precedence over ``NO_COLOR``, which in turn takes precedence over
|
||||||
|
``FORCE_COLOR``.
|
||||||
|
|
||||||
Options you shouldn't use
|
Options you shouldn't use
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -1110,6 +1131,12 @@ conflict.
|
||||||
|
|
||||||
.. versionadded:: 3.13
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
.. envvar:: PYTHON_COLORS
|
||||||
|
|
||||||
|
If this variable is set to ``1``, the interpreter will colorize various kinds
|
||||||
|
of output. Setting it to ``0`` deactivates this behavior.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
Debug-mode variables
|
Debug-mode variables
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -85,7 +85,13 @@ Important deprecations, removals or restrictions:
|
||||||
New Features
|
New Features
|
||||||
============
|
============
|
||||||
|
|
||||||
|
Improved Error Messages
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
* The interpreter now colorizes error messages when displaying tracebacks by default.
|
||||||
|
This feature can be controlled via the new :envvar:`PYTHON_COLORS` environment
|
||||||
|
variable as well as the canonical ``NO_COLOR`` and ``FORCE_COLOR`` environment
|
||||||
|
variables. (Contributed by Pablo Galindo Salgado in :gh:`112730`.)
|
||||||
|
|
||||||
Other Language Changes
|
Other Language Changes
|
||||||
======================
|
======================
|
||||||
|
|
|
@ -8,6 +8,7 @@ import types
|
||||||
import inspect
|
import inspect
|
||||||
import builtins
|
import builtins
|
||||||
import unittest
|
import unittest
|
||||||
|
import unittest.mock
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
import random
|
import random
|
||||||
|
@ -24,6 +25,7 @@ from test.support.import_helper import forget
|
||||||
import json
|
import json
|
||||||
import textwrap
|
import textwrap
|
||||||
import traceback
|
import traceback
|
||||||
|
import contextlib
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
@ -41,6 +43,14 @@ LEVENSHTEIN_DATA_FILE = Path(__file__).parent / 'levenshtein_examples.json'
|
||||||
class TracebackCases(unittest.TestCase):
|
class TracebackCases(unittest.TestCase):
|
||||||
# For now, a very minimal set of tests. I want to be sure that
|
# For now, a very minimal set of tests. I want to be sure that
|
||||||
# formatting of SyntaxErrors works based on changes for 2.1.
|
# formatting of SyntaxErrors works based on changes for 2.1.
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.colorize = traceback._COLORIZE
|
||||||
|
traceback._COLORIZE = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
traceback._COLORIZE = self.colorize
|
||||||
|
|
||||||
def get_exception_format(self, func, exc):
|
def get_exception_format(self, func, exc):
|
||||||
try:
|
try:
|
||||||
|
@ -521,7 +531,7 @@ class TracebackCases(unittest.TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(inspect.signature(traceback.print_exception)),
|
str(inspect.signature(traceback.print_exception)),
|
||||||
('(exc, /, value=<implicit>, tb=<implicit>, '
|
('(exc, /, value=<implicit>, tb=<implicit>, '
|
||||||
'limit=None, file=None, chain=True)'))
|
'limit=None, file=None, chain=True, **kwargs)'))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(inspect.signature(traceback.format_exception)),
|
str(inspect.signature(traceback.format_exception)),
|
||||||
|
@ -3031,7 +3041,7 @@ class TestStack(unittest.TestCase):
|
||||||
|
|
||||||
def test_custom_format_frame(self):
|
def test_custom_format_frame(self):
|
||||||
class CustomStackSummary(traceback.StackSummary):
|
class CustomStackSummary(traceback.StackSummary):
|
||||||
def format_frame_summary(self, frame_summary):
|
def format_frame_summary(self, frame_summary, colorize=False):
|
||||||
return f'{frame_summary.filename}:{frame_summary.lineno}'
|
return f'{frame_summary.filename}:{frame_summary.lineno}'
|
||||||
|
|
||||||
def some_inner():
|
def some_inner():
|
||||||
|
@ -3056,7 +3066,7 @@ class TestStack(unittest.TestCase):
|
||||||
tb = g()
|
tb = g()
|
||||||
|
|
||||||
class Skip_G(traceback.StackSummary):
|
class Skip_G(traceback.StackSummary):
|
||||||
def format_frame_summary(self, frame_summary):
|
def format_frame_summary(self, frame_summary, colorize=False):
|
||||||
if frame_summary.name == 'g':
|
if frame_summary.name == 'g':
|
||||||
return None
|
return None
|
||||||
return super().format_frame_summary(frame_summary)
|
return super().format_frame_summary(frame_summary)
|
||||||
|
@ -3076,7 +3086,6 @@ class Unrepresentable:
|
||||||
raise Exception("Unrepresentable")
|
raise Exception("Unrepresentable")
|
||||||
|
|
||||||
class TestTracebackException(unittest.TestCase):
|
class TestTracebackException(unittest.TestCase):
|
||||||
|
|
||||||
def do_test_smoke(self, exc, expected_type_str):
|
def do_test_smoke(self, exc, expected_type_str):
|
||||||
try:
|
try:
|
||||||
raise exc
|
raise exc
|
||||||
|
@ -4245,6 +4254,115 @@ class MiscTest(unittest.TestCase):
|
||||||
res3 = traceback._levenshtein_distance(a, b, threshold)
|
res3 = traceback._levenshtein_distance(a, b, threshold)
|
||||||
self.assertGreater(res3, threshold, msg=(a, b, threshold))
|
self.assertGreater(res3, threshold, msg=(a, b, threshold))
|
||||||
|
|
||||||
|
class TestColorizedTraceback(unittest.TestCase):
|
||||||
|
def test_colorized_traceback(self):
|
||||||
|
def foo(*args):
|
||||||
|
x = {'a':{'b': None}}
|
||||||
|
y = x['a']['b']['c']
|
||||||
|
|
||||||
|
def baz(*args):
|
||||||
|
return foo(1,2,3,4)
|
||||||
|
|
||||||
|
def bar():
|
||||||
|
return baz(1,
|
||||||
|
2,3
|
||||||
|
,4)
|
||||||
|
try:
|
||||||
|
bar()
|
||||||
|
except Exception as e:
|
||||||
|
exc = traceback.TracebackException.from_exception(
|
||||||
|
e, capture_locals=True
|
||||||
|
)
|
||||||
|
lines = "".join(exc.format(colorize=True))
|
||||||
|
red = traceback._ANSIColors.RED
|
||||||
|
boldr = traceback._ANSIColors.BOLD_RED
|
||||||
|
reset = traceback._ANSIColors.RESET
|
||||||
|
self.assertIn("y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, lines)
|
||||||
|
self.assertIn("return " + red + "foo" + reset + boldr + "(1,2,3,4)" + reset, lines)
|
||||||
|
self.assertIn("return " + red + "baz" + reset + boldr + "(1," + reset, lines)
|
||||||
|
self.assertIn(boldr + "2,3" + reset, lines)
|
||||||
|
self.assertIn(boldr + ",4)" + reset, lines)
|
||||||
|
self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
|
||||||
|
|
||||||
|
def test_colorized_syntax_error(self):
|
||||||
|
try:
|
||||||
|
compile("a $ b", "<string>", "exec")
|
||||||
|
except SyntaxError as e:
|
||||||
|
exc = traceback.TracebackException.from_exception(
|
||||||
|
e, capture_locals=True
|
||||||
|
)
|
||||||
|
actual = "".join(exc.format(colorize=True))
|
||||||
|
red = traceback._ANSIColors.RED
|
||||||
|
magenta = traceback._ANSIColors.MAGENTA
|
||||||
|
boldm = traceback._ANSIColors.BOLD_MAGENTA
|
||||||
|
boldr = traceback._ANSIColors.BOLD_RED
|
||||||
|
reset = traceback._ANSIColors.RESET
|
||||||
|
expected = "".join([
|
||||||
|
f' File {magenta}"<string>"{reset}, line {magenta}1{reset}\n',
|
||||||
|
f' a {boldr}${reset} b\n',
|
||||||
|
f' {boldr}^{reset}\n',
|
||||||
|
f'{boldm}SyntaxError{reset}: {magenta}invalid syntax{reset}\n']
|
||||||
|
)
|
||||||
|
self.assertIn(expected, actual)
|
||||||
|
|
||||||
|
def test_colorized_traceback_is_the_default(self):
|
||||||
|
def foo():
|
||||||
|
1/0
|
||||||
|
|
||||||
|
from _testcapi import exception_print
|
||||||
|
try:
|
||||||
|
foo()
|
||||||
|
self.fail("No exception thrown.")
|
||||||
|
except Exception as e:
|
||||||
|
with captured_output("stderr") as tbstderr:
|
||||||
|
with unittest.mock.patch('traceback._can_colorize', return_value=True):
|
||||||
|
exception_print(e)
|
||||||
|
actual = tbstderr.getvalue().splitlines()
|
||||||
|
|
||||||
|
red = traceback._ANSIColors.RED
|
||||||
|
boldr = traceback._ANSIColors.BOLD_RED
|
||||||
|
magenta = traceback._ANSIColors.MAGENTA
|
||||||
|
boldm = traceback._ANSIColors.BOLD_MAGENTA
|
||||||
|
reset = traceback._ANSIColors.RESET
|
||||||
|
lno_foo = foo.__code__.co_firstlineno
|
||||||
|
expected = ['Traceback (most recent call last):',
|
||||||
|
f' File {magenta}"{__file__}"{reset}, '
|
||||||
|
f'line {magenta}{lno_foo+5}{reset}, in {magenta}test_colorized_traceback_is_the_default{reset}',
|
||||||
|
f' {red}foo{reset+boldr}(){reset}',
|
||||||
|
f' {red}~~~{reset+boldr}^^{reset}',
|
||||||
|
f' File {magenta}"{__file__}"{reset}, '
|
||||||
|
f'line {magenta}{lno_foo+1}{reset}, in {magenta}foo{reset}',
|
||||||
|
f' {red}1{reset+boldr}/{reset+red}0{reset}',
|
||||||
|
f' {red}~{reset+boldr}^{reset+red}~{reset}',
|
||||||
|
f'{boldm}ZeroDivisionError{reset}: {magenta}division by zero{reset}']
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_colorized_detection_checks_for_environment_variables(self):
|
||||||
|
if sys.platform == "win32":
|
||||||
|
virtual_patching = unittest.mock.patch("nt._supports_virtual_terminal", return_value=True)
|
||||||
|
else:
|
||||||
|
virtual_patching = contextlib.nullcontext()
|
||||||
|
with virtual_patching:
|
||||||
|
with unittest.mock.patch("os.isatty") as isatty_mock:
|
||||||
|
isatty_mock.return_value = True
|
||||||
|
with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), False)
|
||||||
|
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '1'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), True)
|
||||||
|
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '0'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), False)
|
||||||
|
with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), False)
|
||||||
|
with unittest.mock.patch("os.environ", {'NO_COLOR': '1', "PYTHON_COLORS": '1'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), True)
|
||||||
|
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), True)
|
||||||
|
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', 'NO_COLOR': '1'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), False)
|
||||||
|
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}):
|
||||||
|
self.assertEqual(traceback._can_colorize(), False)
|
||||||
|
isatty_mock.return_value = False
|
||||||
|
self.assertEqual(traceback._can_colorize(), False)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
197
Lib/traceback.py
197
Lib/traceback.py
|
@ -1,5 +1,7 @@
|
||||||
"""Extract, format and print information about Python stack traces."""
|
"""Extract, format and print information about Python stack traces."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import io
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import itertools
|
import itertools
|
||||||
import linecache
|
import linecache
|
||||||
|
@ -19,6 +21,8 @@ __all__ = ['extract_stack', 'extract_tb', 'format_exception',
|
||||||
# Formatting and printing lists of traceback lines.
|
# Formatting and printing lists of traceback lines.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
_COLORIZE = True
|
||||||
|
|
||||||
def print_list(extracted_list, file=None):
|
def print_list(extracted_list, file=None):
|
||||||
"""Print the list of tuples as returned by extract_tb() or
|
"""Print the list of tuples as returned by extract_tb() or
|
||||||
extract_stack() as a formatted stack trace to the given file."""
|
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, \
|
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'.
|
"""Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
|
||||||
|
|
||||||
This differs from print_tb() in the following ways: (1) if
|
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
|
occurred with a caret on the next line indicating the approximate
|
||||||
position of the error.
|
position of the error.
|
||||||
"""
|
"""
|
||||||
|
colorize = kwargs.get("colorize", False)
|
||||||
value, tb = _parse_value_tb(exc, value, tb)
|
value, tb = _parse_value_tb(exc, value, tb)
|
||||||
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
|
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()
|
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, /):
|
def _print_exception_bltin(exc, /):
|
||||||
file = sys.stderr if sys.stderr is not None else sys.__stderr__
|
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, \
|
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.
|
# -- 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')
|
valuestr = _safe_string(value, 'exception')
|
||||||
end_char = "\n" if insert_final_newline else ""
|
end_char = "\n" if insert_final_newline else ""
|
||||||
if value is None or not valuestr:
|
if colorize:
|
||||||
line = f"{etype}{end_char}"
|
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:
|
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
|
return line
|
||||||
|
|
||||||
def _safe_string(value, what, func=str):
|
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.
|
_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):
|
class StackSummary(list):
|
||||||
"""A list of FrameSummary objects, representing a stack of frames."""
|
"""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))
|
result.append(FrameSummary(filename, lineno, name, line=line))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def format_frame_summary(self, frame_summary):
|
def format_frame_summary(self, frame_summary, **kwargs):
|
||||||
"""Format the lines for a single FrameSummary.
|
"""Format the lines for a single FrameSummary.
|
||||||
|
|
||||||
Returns a string representing one frame involved in the stack. This
|
Returns a string representing one frame involved in the stack. This
|
||||||
gets called for every frame to be printed in the stack summary.
|
gets called for every frame to be printed in the stack summary.
|
||||||
"""
|
"""
|
||||||
|
colorize = kwargs.get("colorize", False)
|
||||||
row = []
|
row = []
|
||||||
filename = frame_summary.filename
|
filename = frame_summary.filename
|
||||||
if frame_summary.filename.startswith("<stdin>-"):
|
if frame_summary.filename.startswith("<stdin>-"):
|
||||||
filename = "<stdin>"
|
filename = "<stdin>"
|
||||||
row.append(' File "{}", line {}, in {}\n'.format(
|
if colorize:
|
||||||
filename, frame_summary.lineno, frame_summary.name))
|
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._dedented_lines and frame_summary._dedented_lines.strip():
|
||||||
if (
|
if (
|
||||||
frame_summary.colno is None or
|
frame_summary.colno is None or
|
||||||
|
@ -619,7 +679,31 @@ class StackSummary(list):
|
||||||
carets.append(secondary_char)
|
carets.append(secondary_char)
|
||||||
else:
|
else:
|
||||||
carets.append(primary_char)
|
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
|
# display significant lines
|
||||||
sig_lines_list = sorted(significant_lines)
|
sig_lines_list = sorted(significant_lines)
|
||||||
|
@ -643,7 +727,7 @@ class StackSummary(list):
|
||||||
|
|
||||||
return ''.join(row)
|
return ''.join(row)
|
||||||
|
|
||||||
def format(self):
|
def format(self, **kwargs):
|
||||||
"""Format the stack ready for printing.
|
"""Format the stack ready for printing.
|
||||||
|
|
||||||
Returns a list of strings ready for printing. Each string in the
|
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
|
repetitions are shown, followed by a summary line stating the exact
|
||||||
number of further repetitions.
|
number of further repetitions.
|
||||||
"""
|
"""
|
||||||
|
colorize = kwargs.get("colorize", False)
|
||||||
result = []
|
result = []
|
||||||
last_file = None
|
last_file = None
|
||||||
last_line = None
|
last_line = None
|
||||||
last_name = None
|
last_name = None
|
||||||
count = 0
|
count = 0
|
||||||
for frame_summary in self:
|
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:
|
if formatted_frame is None:
|
||||||
continue
|
continue
|
||||||
if (last_file is None or last_file != frame_summary.filename or
|
if (last_file is None or last_file != frame_summary.filename or
|
||||||
|
@ -1118,7 +1203,7 @@ class TracebackException:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self._str
|
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.
|
"""Format the exception part of the traceback.
|
||||||
|
|
||||||
The return value is a generator of strings, each ending in a newline.
|
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
|
:exc:`BaseExceptionGroup`, the nested exceptions are included as
|
||||||
well, recursively, with indentation relative to their nesting depth.
|
well, recursively, with indentation relative to their nesting depth.
|
||||||
"""
|
"""
|
||||||
|
colorize = kwargs.get("colorize", False)
|
||||||
|
|
||||||
indent = 3 * _depth * ' '
|
indent = 3 * _depth * ' '
|
||||||
if not self._have_exc_type:
|
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
|
return
|
||||||
|
|
||||||
stype = self.exc_type_str
|
stype = self.exc_type_str
|
||||||
|
@ -1146,16 +1232,16 @@ class TracebackException:
|
||||||
if _depth > 0:
|
if _depth > 0:
|
||||||
# Nested exceptions needs correct handling of multiline messages.
|
# Nested exceptions needs correct handling of multiline messages.
|
||||||
formatted = _format_final_exc_line(
|
formatted = _format_final_exc_line(
|
||||||
stype, self._str, insert_final_newline=False,
|
stype, self._str, insert_final_newline=False, colorize=colorize
|
||||||
).split('\n')
|
).split('\n')
|
||||||
yield from [
|
yield from [
|
||||||
indent + l + '\n'
|
indent + l + '\n'
|
||||||
for l in formatted
|
for l in formatted
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
yield _format_final_exc_line(stype, self._str)
|
yield _format_final_exc_line(stype, self._str, colorize=colorize)
|
||||||
else:
|
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 (
|
if (
|
||||||
isinstance(self.__notes__, collections.abc.Sequence)
|
isinstance(self.__notes__, collections.abc.Sequence)
|
||||||
|
@ -1169,15 +1255,26 @@ class TracebackException:
|
||||||
|
|
||||||
if self.exceptions and show_group:
|
if self.exceptions and show_group:
|
||||||
for ex in self.exceptions:
|
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)."""
|
"""Format SyntaxError exceptions (internal helper)."""
|
||||||
# Show exactly where the problem was found.
|
# Show exactly where the problem was found.
|
||||||
|
colorize = kwargs.get("colorize", False)
|
||||||
filename_suffix = ''
|
filename_suffix = ''
|
||||||
if self.lineno is not None:
|
if self.lineno is not None:
|
||||||
yield ' File "{}", line {}\n'.format(
|
if colorize:
|
||||||
self.filename or "<string>", self.lineno)
|
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:
|
elif self.filename is not None:
|
||||||
filename_suffix = ' ({})'.format(self.filename)
|
filename_suffix = ' ({})'.format(self.filename)
|
||||||
|
|
||||||
|
@ -1189,9 +1286,9 @@ class TracebackException:
|
||||||
rtext = text.rstrip('\n')
|
rtext = text.rstrip('\n')
|
||||||
ltext = rtext.lstrip(' \n\f')
|
ltext = rtext.lstrip(' \n\f')
|
||||||
spaces = len(rtext) - len(ltext)
|
spaces = len(rtext) - len(ltext)
|
||||||
yield ' {}\n'.format(ltext)
|
if self.offset is None:
|
||||||
|
yield ' {}\n'.format(ltext)
|
||||||
if self.offset is not None:
|
else:
|
||||||
offset = self.offset
|
offset = self.offset
|
||||||
end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
|
end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
|
||||||
if self.text and offset > len(self.text):
|
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
|
# Convert 1-based column offset to 0-based index into stripped text
|
||||||
colno = offset - 1 - spaces
|
colno = offset - 1 - spaces
|
||||||
end_colno = end_offset - 1 - spaces
|
end_colno = end_offset - 1 - spaces
|
||||||
|
caretspace = ' '
|
||||||
if colno >= 0:
|
if colno >= 0:
|
||||||
# non-space whitespace (likes tabs) must be kept for alignment
|
# non-space whitespace (likes tabs) must be kept for alignment
|
||||||
caretspace = ((c if c.isspace() else ' ') for c in ltext[:colno])
|
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>"
|
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.
|
"""Format the exception.
|
||||||
|
|
||||||
If chain is not *True*, *__cause__* and *__context__* will not be formatted.
|
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
|
The message indicating which exception occurred is always the last
|
||||||
string in the output.
|
string in the output.
|
||||||
"""
|
"""
|
||||||
|
colorize = kwargs.get("colorize", False)
|
||||||
if _ctx is None:
|
if _ctx is None:
|
||||||
_ctx = _ExceptionPrintContext()
|
_ctx = _ExceptionPrintContext()
|
||||||
|
|
||||||
|
@ -1253,8 +1379,8 @@ class TracebackException:
|
||||||
if exc.exceptions is None:
|
if exc.exceptions is None:
|
||||||
if exc.stack:
|
if exc.stack:
|
||||||
yield from _ctx.emit('Traceback (most recent call last):\n')
|
yield from _ctx.emit('Traceback (most recent call last):\n')
|
||||||
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))
|
||||||
elif _ctx.exception_group_depth > self.max_group_depth:
|
elif _ctx.exception_group_depth > self.max_group_depth:
|
||||||
# exception group, but depth exceeds limit
|
# exception group, but depth exceeds limit
|
||||||
yield from _ctx.emit(
|
yield from _ctx.emit(
|
||||||
|
@ -1269,9 +1395,9 @@ class TracebackException:
|
||||||
yield from _ctx.emit(
|
yield from _ctx.emit(
|
||||||
'Exception Group Traceback (most recent call last):\n',
|
'Exception Group Traceback (most recent call last):\n',
|
||||||
margin_char = '+' if is_toplevel else None)
|
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)
|
num_excs = len(exc.exceptions)
|
||||||
if num_excs <= self.max_group_width:
|
if num_excs <= self.max_group_width:
|
||||||
n = num_excs
|
n = num_excs
|
||||||
|
@ -1312,11 +1438,12 @@ class TracebackException:
|
||||||
_ctx.exception_group_depth = 0
|
_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'."""
|
"""Print the result of self.format(chain=chain) to 'file'."""
|
||||||
|
colorize = kwargs.get("colorize", False)
|
||||||
if file is None:
|
if file is None:
|
||||||
file = sys.stderr
|
file = sys.stderr
|
||||||
for line in self.format(chain=chain):
|
for line in self.format(chain=chain, colorize=colorize):
|
||||||
print(line, file=file, end="")
|
print(line, file=file, end="")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Use color to highlight error locations in tracebacks. Patch by Pablo Galindo
|
28
Modules/clinic/posixmodule.c.h
generated
28
Modules/clinic/posixmodule.c.h
generated
|
@ -11756,6 +11756,28 @@ exit:
|
||||||
|
|
||||||
#endif /* (defined(WIFEXITED) || defined(MS_WINDOWS)) */
|
#endif /* (defined(WIFEXITED) || defined(MS_WINDOWS)) */
|
||||||
|
|
||||||
|
#if defined(MS_WINDOWS)
|
||||||
|
|
||||||
|
PyDoc_STRVAR(os__supports_virtual_terminal__doc__,
|
||||||
|
"_supports_virtual_terminal($module, /)\n"
|
||||||
|
"--\n"
|
||||||
|
"\n"
|
||||||
|
"Checks if virtual terminal is supported in windows");
|
||||||
|
|
||||||
|
#define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF \
|
||||||
|
{"_supports_virtual_terminal", (PyCFunction)os__supports_virtual_terminal, METH_NOARGS, os__supports_virtual_terminal__doc__},
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
os__supports_virtual_terminal_impl(PyObject *module);
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
os__supports_virtual_terminal(PyObject *module, PyObject *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
return os__supports_virtual_terminal_impl(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* defined(MS_WINDOWS) */
|
||||||
|
|
||||||
#ifndef OS_TTYNAME_METHODDEF
|
#ifndef OS_TTYNAME_METHODDEF
|
||||||
#define OS_TTYNAME_METHODDEF
|
#define OS_TTYNAME_METHODDEF
|
||||||
#endif /* !defined(OS_TTYNAME_METHODDEF) */
|
#endif /* !defined(OS_TTYNAME_METHODDEF) */
|
||||||
|
@ -12395,4 +12417,8 @@ exit:
|
||||||
#ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
#ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
||||||
#define OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
#define OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
||||||
#endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */
|
#endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */
|
||||||
/*[clinic end generated code: output=2900675ac5219924 input=a9049054013a1b77]*/
|
|
||||||
|
#ifndef OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
|
||||||
|
#define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
|
||||||
|
#endif /* !defined(OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF) */
|
||||||
|
/*[clinic end generated code: output=ff0ec3371de19904 input=a9049054013a1b77]*/
|
||||||
|
|
|
@ -16073,6 +16073,26 @@ os_waitstatus_to_exitcode_impl(PyObject *module, PyObject *status_obj)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(MS_WINDOWS)
|
||||||
|
/*[clinic input]
|
||||||
|
os._supports_virtual_terminal
|
||||||
|
|
||||||
|
Checks if virtual terminal is supported in windows
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
os__supports_virtual_terminal_impl(PyObject *module)
|
||||||
|
/*[clinic end generated code: output=bd0556a6d9d99fe6 input=0752c98e5d321542]*/
|
||||||
|
{
|
||||||
|
DWORD mode = 0;
|
||||||
|
HANDLE handle = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
|
if (!GetConsoleMode(handle, &mode)) {
|
||||||
|
Py_RETURN_FALSE;
|
||||||
|
}
|
||||||
|
return PyBool_FromLong(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
static PyMethodDef posix_methods[] = {
|
static PyMethodDef posix_methods[] = {
|
||||||
|
|
||||||
|
@ -16277,6 +16297,8 @@ static PyMethodDef posix_methods[] = {
|
||||||
OS__PATH_ISFILE_METHODDEF
|
OS__PATH_ISFILE_METHODDEF
|
||||||
OS__PATH_ISLINK_METHODDEF
|
OS__PATH_ISLINK_METHODDEF
|
||||||
OS__PATH_EXISTS_METHODDEF
|
OS__PATH_EXISTS_METHODDEF
|
||||||
|
|
||||||
|
OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
|
||||||
{NULL, NULL} /* Sentinel */
|
{NULL, NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -293,6 +293,8 @@ static const char usage_envvars[] =
|
||||||
"PYTHON_FROZEN_MODULES : if this variable is set, it determines whether or not \n"
|
"PYTHON_FROZEN_MODULES : if this variable is set, it determines whether or not \n"
|
||||||
" frozen modules should be used. The default is \"on\" (or \"off\" if you are \n"
|
" frozen modules should be used. The default is \"on\" (or \"off\" if you are \n"
|
||||||
" running a local build).\n"
|
" running a local build).\n"
|
||||||
|
"PYTHON_COLORS : If this variable is set to 1, the interpreter will"
|
||||||
|
" colorize various kinds of output. Setting it to 0 deactivates this behavior.\n"
|
||||||
"These variables have equivalent command-line parameters (see --help for details):\n"
|
"These variables have equivalent command-line parameters (see --help for details):\n"
|
||||||
"PYTHONDEBUG : enable parser debug mode (-d)\n"
|
"PYTHONDEBUG : enable parser debug mode (-d)\n"
|
||||||
"PYTHONDONTWRITEBYTECODE : don't write .pyc files (-B)\n"
|
"PYTHONDONTWRITEBYTECODE : don't write .pyc files (-B)\n"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue