Skip to content

Commit d5a88ec

Browse files
pablogsalambv
authored andcommitted
pythongh-112730: Use color to highlight error locations (pythongh-112732)
Signed-off-by: Pablo Galindo <pablogsal@gmail.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 955862a commit d5a88ec

File tree

8 files changed

+369
-40
lines changed

8 files changed

+369
-40
lines changed

Doc/using/cmdline.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,27 @@ Miscellaneous options
612612
.. versionadded:: 3.13
613613
The ``-X presite`` option.
614614

615+
Controlling Color
616+
~~~~~~~~~~~~~~~~~
617+
618+
The Python interpreter is configured by default to use colors to highlight
619+
output in certain situations such as when displaying tracebacks. This
620+
behavior can be controlled by setting different environment variables.
621+
622+
Setting the environment variable ``TERM`` to ``dumb`` will disable color.
623+
624+
If the environment variable ``FORCE_COLOR`` is set, then color will be
625+
enabled regardless of the value of TERM. This is useful on CI systems which
626+
aren’t terminals but can none-the-less display ANSI escape sequences.
627+
628+
If the environment variable ``NO_COLOR`` is set, Python will disable all color
629+
in the output. This takes precedence over ``FORCE_COLOR``.
630+
631+
All these environment variables are used also by other tools to control color
632+
output. To control the color output only in the Python interpreter, the
633+
:envvar:`PYTHON_COLORS` environment variable can be used. This variable takes
634+
precedence over ``NO_COLOR``, which in turn takes precedence over
635+
``FORCE_COLOR``.
615636

616637
Options you shouldn't use
617638
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1110,6 +1131,12 @@ conflict.
11101131

11111132
.. versionadded:: 3.13
11121133

1134+
.. envvar:: PYTHON_COLORS
1135+
1136+
If this variable is set to ``1``, the interpreter will colorize various kinds
1137+
of output. Setting it to ``0`` deactivates this behavior.
1138+
1139+
.. versionadded:: 3.13
11131140

11141141
Debug-mode variables
11151142
~~~~~~~~~~~~~~~~~~~~

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:`PYTHON_COLORS` environment
93+
variable as well as the canonical ``NO_COLOR`` and ``FORCE_COLOR`` environment
94+
variables. (Contributed by Pablo Galindo Salgado in :gh:`112730`.)
8995

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

Lib/test/test_traceback.py

Lines changed: 122 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
@@ -24,6 +25,7 @@
2425
import json
2526
import textwrap
2627
import traceback
28+
import contextlib
2729
from functools import partial
2830
from pathlib import Path
2931

@@ -41,6 +43,14 @@
4143
class TracebackCases(unittest.TestCase):
4244
# For now, a very minimal set of tests. I want to be sure that
4345
# formatting of SyntaxErrors works based on changes for 2.1.
46+
def setUp(self):
47+
super().setUp()
48+
self.colorize = traceback._COLORIZE
49+
traceback._COLORIZE = False
50+
51+
def tearDown(self):
52+
super().tearDown()
53+
traceback._COLORIZE = self.colorize
4454

4555
def get_exception_format(self, func, exc):
4656
try:
@@ -521,7 +531,7 @@ def test_signatures(self):
521531
self.assertEqual(
522532
str(inspect.signature(traceback.print_exception)),
523533
('(exc, /, value=<implicit>, tb=<implicit>, '
524-
'limit=None, file=None, chain=True)'))
534+
'limit=None, file=None, chain=True, **kwargs)'))
525535

526536
self.assertEqual(
527537
str(inspect.signature(traceback.format_exception)),
@@ -3031,7 +3041,7 @@ def some_inner(k, v):
30313041

30323042
def test_custom_format_frame(self):
30333043
class CustomStackSummary(traceback.StackSummary):
3034-
def format_frame_summary(self, frame_summary):
3044+
def format_frame_summary(self, frame_summary, colorize=False):
30353045
return f'{frame_summary.filename}:{frame_summary.lineno}'
30363046

30373047
def some_inner():
@@ -3056,7 +3066,7 @@ def g():
30563066
tb = g()
30573067

30583068
class Skip_G(traceback.StackSummary):
3059-
def format_frame_summary(self, frame_summary):
3069+
def format_frame_summary(self, frame_summary, colorize=False):
30603070
if frame_summary.name == 'g':
30613071
return None
30623072
return super().format_frame_summary(frame_summary)
@@ -3076,7 +3086,6 @@ def __repr__(self) -> str:
30763086
raise Exception("Unrepresentable")
30773087

30783088
class TestTracebackException(unittest.TestCase):
3079-
30803089
def do_test_smoke(self, exc, expected_type_str):
30813090
try:
30823091
raise exc
@@ -4245,6 +4254,115 @@ def test_levenshtein_distance_short_circuit(self):
42454254
res3 = traceback._levenshtein_distance(a, b, threshold)
42464255
self.assertGreater(res3, threshold, msg=(a, b, threshold))
42474256

4257+
class TestColorizedTraceback(unittest.TestCase):
4258+
def test_colorized_traceback(self):
4259+
def foo(*args):
4260+
x = {'a':{'b': None}}
4261+
y = x['a']['b']['c']
4262+
4263+
def baz(*args):
4264+
return foo(1,2,3,4)
4265+
4266+
def bar():
4267+
return baz(1,
4268+
2,3
4269+
,4)
4270+
try:
4271+
bar()
4272+
except Exception as e:
4273+
exc = traceback.TracebackException.from_exception(
4274+
e, capture_locals=True
4275+
)
4276+
lines = "".join(exc.format(colorize=True))
4277+
red = traceback._ANSIColors.RED
4278+
boldr = traceback._ANSIColors.BOLD_RED
4279+
reset = traceback._ANSIColors.RESET
4280+
self.assertIn("y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, lines)
4281+
self.assertIn("return " + red + "foo" + reset + boldr + "(1,2,3,4)" + reset, lines)
4282+
self.assertIn("return " + red + "baz" + reset + boldr + "(1," + reset, lines)
4283+
self.assertIn(boldr + "2,3" + reset, lines)
4284+
self.assertIn(boldr + ",4)" + reset, lines)
4285+
self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
4286+
4287+
def test_colorized_syntax_error(self):
4288+
try:
4289+
compile("a $ b", "<string>", "exec")
4290+
except SyntaxError as e:
4291+
exc = traceback.TracebackException.from_exception(
4292+
e, capture_locals=True
4293+
)
4294+
actual = "".join(exc.format(colorize=True))
4295+
red = traceback._ANSIColors.RED
4296+
magenta = traceback._ANSIColors.MAGENTA
4297+
boldm = traceback._ANSIColors.BOLD_MAGENTA
4298+
boldr = traceback._ANSIColors.BOLD_RED
4299+
reset = traceback._ANSIColors.RESET
4300+
expected = "".join([
4301+
f' File {magenta}"<string>"{reset}, line {magenta}1{reset}\n',
4302+
f' a {boldr}${reset} b\n',
4303+
f' {boldr}^{reset}\n',
4304+
f'{boldm}SyntaxError{reset}: {magenta}invalid syntax{reset}\n']
4305+
)
4306+
self.assertIn(expected, actual)
4307+
4308+
def test_colorized_traceback_is_the_default(self):
4309+
def foo():
4310+
1/0
4311+
4312+
from _testcapi import exception_print
4313+
try:
4314+
foo()
4315+
self.fail("No exception thrown.")
4316+
except Exception as e:
4317+
with captured_output("stderr") as tbstderr:
4318+
with unittest.mock.patch('traceback._can_colorize', return_value=True):
4319+
exception_print(e)
4320+
actual = tbstderr.getvalue().splitlines()
4321+
4322+
red = traceback._ANSIColors.RED
4323+
boldr = traceback._ANSIColors.BOLD_RED
4324+
magenta = traceback._ANSIColors.MAGENTA
4325+
boldm = traceback._ANSIColors.BOLD_MAGENTA
4326+
reset = traceback._ANSIColors.RESET
4327+
lno_foo = foo.__code__.co_firstlineno
4328+
expected = ['Traceback (most recent call last):',
4329+
f' File {magenta}"{__file__}"{reset}, '
4330+
f'line {magenta}{lno_foo+5}{reset}, in {magenta}test_colorized_traceback_is_the_default{reset}',
4331+
f' {red}foo{reset+boldr}(){reset}',
4332+
f' {red}~~~{reset+boldr}^^{reset}',
4333+
f' File {magenta}"{__file__}"{reset}, '
4334+
f'line {magenta}{lno_foo+1}{reset}, in {magenta}foo{reset}',
4335+
f' {red}1{reset+boldr}/{reset+red}0{reset}',
4336+
f' {red}~{reset+boldr}^{reset+red}~{reset}',
4337+
f'{boldm}ZeroDivisionError{reset}: {magenta}division by zero{reset}']
4338+
self.assertEqual(actual, expected)
4339+
4340+
def test_colorized_detection_checks_for_environment_variables(self):
4341+
if sys.platform == "win32":
4342+
virtual_patching = unittest.mock.patch("nt._supports_virtual_terminal", return_value=True)
4343+
else:
4344+
virtual_patching = contextlib.nullcontext()
4345+
with virtual_patching:
4346+
with unittest.mock.patch("os.isatty") as isatty_mock:
4347+
isatty_mock.return_value = True
4348+
with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
4349+
self.assertEqual(traceback._can_colorize(), False)
4350+
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '1'}):
4351+
self.assertEqual(traceback._can_colorize(), True)
4352+
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '0'}):
4353+
self.assertEqual(traceback._can_colorize(), False)
4354+
with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}):
4355+
self.assertEqual(traceback._can_colorize(), False)
4356+
with unittest.mock.patch("os.environ", {'NO_COLOR': '1', "PYTHON_COLORS": '1'}):
4357+
self.assertEqual(traceback._can_colorize(), True)
4358+
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}):
4359+
self.assertEqual(traceback._can_colorize(), True)
4360+
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', 'NO_COLOR': '1'}):
4361+
self.assertEqual(traceback._can_colorize(), False)
4362+
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}):
4363+
self.assertEqual(traceback._can_colorize(), False)
4364+
isatty_mock.return_value = False
4365+
self.assertEqual(traceback._can_colorize(), False)
42484366

42494367
if __name__ == "__main__":
42504368
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