Skip to content

Commit 182fae6

Browse files
committed
gh-112730: Use color to highlight error locations
Signed-off-by: Pablo Galindo <pablogsal@gmail.com>
1 parent c27b09c commit 182fae6

File tree

6 files changed

+201
-27
lines changed

6 files changed

+201
-27
lines changed

Doc/using/cmdline.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,12 @@ conflict.
11101110

11111111
.. versionadded:: 3.13
11121112

1113+
.. envvar:: PY_COLOR
1114+
1115+
If this variable is set to ``1``, the interpreter will colorize error locations
1116+
when displaying tracebacks. Setting it to ``0`` deactivates this behavior.
1117+
1118+
.. versionadded:: 3.13
11131119

11141120
Debug-mode variables
11151121
~~~~~~~~~~~~~~~~~~~~

Doc/whatsnew/3.13.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ Important deprecations, removals or restrictions:
8585
New Features
8686
============
8787

88+
Improved Error Messages
89+
-----------------------
8890

91+
* The interpreter now colorizes error messages when displaying tracebacks by default.
92+
This feature can be controlled via the new :envvar:`PY_COLORS` environment variable
93+
as well as the canonical ``NO_COLOR`` and ``FORCE_COLOR`` environment variables.
94+
(Contributed by Pablo Galindo Salgado in :gh:`112730`.)
8995

9096
Other Language Changes
9197
======================

Lib/test/test_traceback.py

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import inspect
99
import builtins
1010
import unittest
11+
import unittest.mock
1112
import re
1213
import tempfile
1314
import random
@@ -41,6 +42,14 @@
4142
class TracebackCases(unittest.TestCase):
4243
# For now, a very minimal set of tests. I want to be sure that
4344
# formatting of SyntaxErrors works based on changes for 2.1.
45+
def setUp(self):
46+
super().setUp()
47+
self.colorize = traceback._COLORIZE
48+
traceback._COLORIZE = False
49+
50+
def tearDown(self):
51+
super().tearDown()
52+
traceback._COLORIZE = self.colorize
4453

4554
def get_exception_format(self, func, exc):
4655
try:
@@ -521,7 +530,7 @@ def test_signatures(self):
521530
self.assertEqual(
522531
str(inspect.signature(traceback.print_exception)),
523532
('(exc, /, value=<implicit>, tb=<implicit>, '
524-
'limit=None, file=None, chain=True)'))
533+
'limit=None, file=None, chain=True, **kwargs)'))
525534

526535
self.assertEqual(
527536
str(inspect.signature(traceback.format_exception)),
@@ -3031,7 +3040,7 @@ def some_inner(k, v):
30313040

30323041
def test_custom_format_frame(self):
30333042
class CustomStackSummary(traceback.StackSummary):
3034-
def format_frame_summary(self, frame_summary):
3043+
def format_frame_summary(self, frame_summary, colorize=False):
30353044
return f'{frame_summary.filename}:{frame_summary.lineno}'
30363045

30373046
def some_inner():
@@ -3056,7 +3065,7 @@ def g():
30563065
tb = g()
30573066

30583067
class Skip_G(traceback.StackSummary):
3059-
def format_frame_summary(self, frame_summary):
3068+
def format_frame_summary(self, frame_summary, colorize=False):
30603069
if frame_summary.name == 'g':
30613070
return None
30623071
return super().format_frame_summary(frame_summary)
@@ -3076,7 +3085,6 @@ def __repr__(self) -> str:
30763085
raise Exception("Unrepresentable")
30773086

30783087
class TestTracebackException(unittest.TestCase):
3079-
30803088
def do_test_smoke(self, exc, expected_type_str):
30813089
try:
30823090
raise exc
@@ -4245,6 +4253,85 @@ def test_levenshtein_distance_short_circuit(self):
42454253
res3 = traceback._levenshtein_distance(a, b, threshold)
42464254
self.assertGreater(res3, threshold, msg=(a, b, threshold))
42474255

4256+
class TestColorizedTraceback(unittest.TestCase):
4257+
def test_colorized_traceback(self):
4258+
def foo(*args):
4259+
x = {'a':{'b': None}}
4260+
y = x['a']['b']['c']
4261+
4262+
def baz(*args):
4263+
return foo(1,2,3,4)
4264+
4265+
def bar():
4266+
return baz(1,
4267+
2,3
4268+
,4)
4269+
try:
4270+
bar()
4271+
except Exception as e:
4272+
exc = traceback.TracebackException.from_exception(
4273+
e, capture_locals=True
4274+
)
4275+
lines = "".join(exc.format(colorize=True))
4276+
red = traceback._ANSIColors.RED
4277+
boldr = traceback._ANSIColors.BOLD_RED
4278+
reset = traceback._ANSIColors.RESET
4279+
self.assertIn("y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, lines)
4280+
self.assertIn("return " + red + "foo" + reset + boldr + "(1,2,3,4)" + reset, lines)
4281+
self.assertIn("return " + red + "baz" + reset + boldr + "(1," + reset, lines)
4282+
self.assertIn(boldr + "2,3" + reset, lines)
4283+
self.assertIn(boldr + ",4)" + reset, lines)
4284+
self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
4285+
4286+
def test_colorized_traceback_is_the_default(self):
4287+
def foo():
4288+
1/0
4289+
4290+
from _testcapi import exception_print
4291+
try:
4292+
foo()
4293+
self.fail("No exception thrown.")
4294+
except Exception as e:
4295+
with captured_output("stderr") as tbstderr:
4296+
with unittest.mock.patch('traceback._can_colorize', return_value=True):
4297+
exception_print(e)
4298+
actual = tbstderr.getvalue().splitlines()
4299+
4300+
red = traceback._ANSIColors.RED
4301+
boldr = traceback._ANSIColors.BOLD_RED
4302+
reset = traceback._ANSIColors.RESET
4303+
lno_foo = foo.__code__.co_firstlineno
4304+
expected = ['Traceback (most recent call last):',
4305+
f' File "{__file__}", '
4306+
f'line {lno_foo+5}, in test_colorized_traceback_is_the_default',
4307+
f' {red}foo{reset+boldr}(){reset}',
4308+
f' {red}~~~{reset+boldr}^^{reset}',
4309+
f' File "{__file__}", '
4310+
f'line {lno_foo+1}, in foo',
4311+
f' {red}1{reset+boldr}/{reset+red}0{reset}',
4312+
f' {red}~{reset+boldr}^{reset+red}~{reset}',
4313+
'ZeroDivisionError: division by zero']
4314+
self.assertEqual(actual, expected)
4315+
4316+
def test_colorized_detection_checks_for_environment_variables(self):
4317+
with unittest.mock.patch("sys.stderr") as stderr_mock:
4318+
stderr_mock.isatty.return_value = True
4319+
with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
4320+
self.assertEqual(traceback._can_colorize(), False)
4321+
with unittest.mock.patch("os.environ", {'PY_COLORS': '1'}):
4322+
self.assertEqual(traceback._can_colorize(), True)
4323+
with unittest.mock.patch("os.environ", {'PY_COLORS': '0'}):
4324+
self.assertEqual(traceback._can_colorize(), False)
4325+
with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}):
4326+
self.assertEqual(traceback._can_colorize(), False)
4327+
with unittest.mock.patch("os.environ", {'NO_COLOR': '1', "PY_COLORS": '1'}):
4328+
self.assertEqual(traceback._can_colorize(), True)
4329+
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}):
4330+
self.assertEqual(traceback._can_colorize(), True)
4331+
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', 'NO_COLOR': '1'}):
4332+
self.assertEqual(traceback._can_colorize(), False)
4333+
stderr_mock.isatty.return_value = False
4334+
self.assertEqual(traceback._can_colorize(), False)
42484335

42494336
if __name__ == "__main__":
42504337
unittest.main()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy