]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-140049: Colorize exception notes in `traceback.py` (#140051)
authorStan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Sun, 22 Mar 2026 08:54:43 +0000 (08:54 +0000)
committerGitHub <noreply@github.com>
Sun, 22 Mar 2026 08:54:43 +0000 (08:54 +0000)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Lib/_colorize.py
Lib/test/test_traceback.py
Lib/traceback.py
Misc/NEWS.d/next/Library/2025-10-13-16-43-36.gh-issue-140049.VvmAzN.rst [new file with mode: 0644]

index 5c4903f14aa86b73e2ee1f182f8067a66c9141f0..fd0ae9d6145961be6c174ad153587dd6beaf1011 100644 (file)
@@ -327,6 +327,7 @@ class Syntax(ThemeSection):
 class Traceback(ThemeSection):
     type: str = ANSIColors.BOLD_MAGENTA
     message: str = ANSIColors.MAGENTA
+    note: str = ANSIColors.CYAN
     filename: str = ANSIColors.MAGENTA
     line_no: str = ANSIColors.MAGENTA
     frame: str = ANSIColors.MAGENTA
index 7124e49b22e361c20f48fdf48e5b271139c36020..5dc11253e0d5c890755f4b021a192e3a3c4595a9 100644 (file)
@@ -40,7 +40,7 @@ test_code.co_positions = lambda _: iter([(6, 6, 0, 0)])
 test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals'])
 test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next', 'tb_lasti'])
 
-color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E"}
+color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E", "note": "n"}
 colors = {
     color_overrides.get(k, k[0].lower()): v
     for k, v in _colorize.default_theme.traceback.items()
@@ -5306,6 +5306,23 @@ class TestColorizedTraceback(unittest.TestCase):
         self.assertIn("return baz1(1,\n            2,3\n            ,4)", lines)
         self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
 
+    def test_colorized_exception_notes(self):
+        def foo():
+            raise ValueError()
+
+        try:
+            foo()
+        except Exception as e:
+            e.add_note("First note")
+            e.add_note("Second note")
+            exc = traceback.TracebackException.from_exception(e)
+
+        lines = "".join(exc.format(colorize=True))
+        note = colors["n"]
+        reset = colors["z"]
+        self.assertIn(note + "First note" + reset, lines)
+        self.assertIn(note + "Second note" + reset, lines)
+
     def test_colorized_syntax_error(self):
         try:
             compile("a $ b", "<string>", "exec")
@@ -5314,7 +5331,7 @@ class TestColorizedTraceback(unittest.TestCase):
                 e, capture_locals=True
             )
         actual = "".join(exc.format(colorize=True))
-        def expected(t, m, fn, l, f, E, e, z):
+        def expected(t, m, fn, l, f, E, e, z, n):
             return "".join(
                 [
                     f'  File {fn}"<string>"{z}, line {l}1{z}\n',
@@ -5340,7 +5357,7 @@ class TestColorizedTraceback(unittest.TestCase):
             actual = tbstderr.getvalue().splitlines()
 
         lno_foo = foo.__code__.co_firstlineno
-        def expected(t, m, fn, l, f, E, e, z):
+        def expected(t, m, fn, l, f, E, e, z, n):
             return [
                 'Traceback (most recent call last):',
                 f'  File {fn}"{__file__}"{z}, '
@@ -5373,7 +5390,7 @@ class TestColorizedTraceback(unittest.TestCase):
 
         lno_foo = foo.__code__.co_firstlineno
         actual = "".join(exc.format(colorize=True)).splitlines()
-        def expected(t, m, fn, l, f, E, e, z):
+        def expected(t, m, fn, l, f, E, e, z, n):
             return [
                 f"  + Exception Group Traceback (most recent call last):",
                 f'  |   File {fn}"{__file__}"{z}, line {l}{lno_foo+9}{z}, in {f}test_colorized_traceback_from_exception_group{z}',
index 56a72ce7f5b29354f1b1e13f7b2ae51e133903c8..1f9f151ebf5d39140366d71e01bac1594ac42d2e 100644 (file)
@@ -993,6 +993,10 @@ def _display_width(line, offset=None):
     )
 
 
+def _format_note(note, indent, theme):
+    for l in note.split("\n"):
+        yield f"{indent}{theme.note}{l}{theme.reset}\n"
+
 
 class _ExceptionPrintContext:
     def __init__(self):
@@ -1291,6 +1295,10 @@ class TracebackException:
         well, recursively, with indentation relative to their nesting depth.
         """
         colorize = kwargs.get("colorize", False)
+        if colorize:
+            theme = _colorize.get_theme(force_color=True).traceback
+        else:
+            theme = _colorize.get_theme(force_no_color=True).traceback
 
         indent = 3 * _depth * ' '
         if not self._have_exc_type:
@@ -1319,9 +1327,10 @@ class TracebackException:
         ):
             for note in self.__notes__:
                 note = _safe_string(note, 'note')
-                yield from [indent + l + '\n' for l in note.split('\n')]
+                yield from _format_note(note, indent, theme)
         elif self.__notes__ is not None:
-            yield indent + "{}\n".format(_safe_string(self.__notes__, '__notes__', func=repr))
+            note = _safe_string(self.__notes__, '__notes__', func=repr)
+            yield from _format_note(note, indent, theme)
 
         if self.exceptions and show_group:
             for ex in self.exceptions:
diff --git a/Misc/NEWS.d/next/Library/2025-10-13-16-43-36.gh-issue-140049.VvmAzN.rst b/Misc/NEWS.d/next/Library/2025-10-13-16-43-36.gh-issue-140049.VvmAzN.rst
new file mode 100644 (file)
index 0000000..d9489fd
--- /dev/null
@@ -0,0 +1 @@
+:func:`traceback.format_exception_only` now colorizes exception notes.