Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-112730: Use color to highlight error locations #112732

Merged
merged 10 commits into from
Dec 6, 2023
6 changes: 6 additions & 0 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,12 @@ conflict.

.. versionadded:: 3.13

.. envvar:: PY_COLORS

If this variable is set to ``1``, the interpreter will colorize different kinds
of output. Setting it to ``0`` deactivates this behavior.

.. versionadded:: 3.13

Debug-mode variables
~~~~~~~~~~~~~~~~~~~~
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ Important deprecations, removals or restrictions:
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:`PY_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
======================
Expand Down
95 changes: 91 additions & 4 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import inspect
import builtins
import unittest
import unittest.mock
import re
import tempfile
import random
Expand Down Expand Up @@ -41,6 +42,14 @@
class TracebackCases(unittest.TestCase):
# For now, a very minimal set of tests. I want to be sure that
# 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):
try:
Expand Down Expand Up @@ -521,7 +530,7 @@ def test_signatures(self):
self.assertEqual(
str(inspect.signature(traceback.print_exception)),
('(exc, /, value=<implicit>, tb=<implicit>, '
'limit=None, file=None, chain=True)'))
'limit=None, file=None, chain=True, **kwargs)'))

self.assertEqual(
str(inspect.signature(traceback.format_exception)),
Expand Down Expand Up @@ -3031,7 +3040,7 @@ def some_inner(k, v):

def test_custom_format_frame(self):
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}'

def some_inner():
Expand All @@ -3056,7 +3065,7 @@ def g():
tb = g()

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':
return None
return super().format_frame_summary(frame_summary)
Expand All @@ -3076,7 +3085,6 @@ def __repr__(self) -> str:
raise Exception("Unrepresentable")

class TestTracebackException(unittest.TestCase):

def do_test_smoke(self, exc, expected_type_str):
try:
raise exc
Expand Down Expand Up @@ -4245,6 +4253,85 @@ def test_levenshtein_distance_short_circuit(self):
res3 = traceback._levenshtein_distance(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_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
reset = traceback._ANSIColors.RESET
lno_foo = foo.__code__.co_firstlineno
expected = ['Traceback (most recent call last):',
f' File "{__file__}", '
f'line {lno_foo+5}, in test_colorized_traceback_is_the_default',
f' {red}foo{reset+boldr}(){reset}',
f' {red}~~~{reset+boldr}^^{reset}',
f' File "{__file__}", '
f'line {lno_foo+1}, in foo',
f' {red}1{reset+boldr}/{reset+red}0{reset}',
f' {red}~{reset+boldr}^{reset+red}~{reset}',
'ZeroDivisionError: division by zero']
self.assertEqual(actual, expected)

def test_colorized_detection_checks_for_environment_variables(self):
with unittest.mock.patch("sys.stderr") as stderr_mock:
stderr_mock.isatty.return_value = True
with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
self.assertEqual(traceback._can_colorize(), False)
with unittest.mock.patch("os.environ", {'PY_COLORS': '1'}):
self.assertEqual(traceback._can_colorize(), True)
with unittest.mock.patch("os.environ", {'PY_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', "PY_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)
stderr_mock.isatty.return_value = False
self.assertEqual(traceback._can_colorize(), False)

if __name__ == "__main__":
unittest.main()
Loading
Loading