Skip to content

Commit bc6e729

Browse files
committed
gh-112730: Use color to highlight error locations
Signed-off-by: Pablo Galindo <[email protected]>
1 parent c27b09c commit bc6e729

File tree

3 files changed

+187
-27
lines changed

3 files changed

+187
-27
lines changed

Diff for: Lib/test/test_traceback.py

+91-4
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)