]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-134170: Add colorization to unraisable exceptions (#134183)
authorPeter Bierma <zintensitydev@gmail.com>
Mon, 4 Aug 2025 14:35:00 +0000 (10:35 -0400)
committerGitHub <noreply@github.com>
Mon, 4 Aug 2025 14:35:00 +0000 (14:35 +0000)
Default implementation of sys.unraisablehook() now uses traceback._print_exception_bltin() to print exceptions with colorized text.

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
Doc/library/sys.rst
Doc/whatsnew/3.15.rst
Lib/test/test_capi/test_exceptions.py
Lib/test/test_cmd_line.py
Lib/test/test_concurrent_futures/test_shutdown.py
Lib/test/test_signal.py
Lib/test/test_sys.py
Lib/test/test_threading.py
Lib/traceback.py
Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst [new file with mode: 0644]
Python/errors.c

index 52f0af31c68726a9d35e80905dc703e36acf0eb5..771e0f2709a4aabc8a777465c782c3871038cb6b 100644 (file)
@@ -2152,11 +2152,16 @@ always available. Unless explicitly noted otherwise, all variables are read-only
 
    The default hook formats :attr:`!err_msg` and :attr:`!object` as:
    ``f'{err_msg}: {object!r}'``; use "Exception ignored in" error message
-   if :attr:`!err_msg` is ``None``.
+   if :attr:`!err_msg` is ``None``. Similar to the :mod:`traceback` module,
+   this adds color to exceptions by default. This can be disabled using
+   :ref:`environment variables <using-on-controlling-color>`.
 
    :func:`sys.unraisablehook` can be overridden to control how unraisable
    exceptions are handled.
 
+   .. versionchanged:: next
+      Exceptions are now printed with colorful text.
+
    .. seealso::
 
       :func:`excepthook` which handles uncaught exceptions.
index e716d7bb0f2a5c3bd47b3df1afc785d2156f3ea5..54964da473760db6250e70d17fe47ec3a54c983b 100644 (file)
@@ -200,6 +200,10 @@ Other language changes
 * Several error messages incorrectly using the term "argument" have been corrected.
   (Contributed by Stan Ulbrych in :gh:`133382`.)
 
+* Unraisable exceptions are now highlighted with color by default. This can be
+  controlled by :ref:`environment variables <using-on-controlling-color>`.
+  (Contributed by Peter Bierma in :gh:`134170`.)
+
 
 New modules
 ===========
index ade55338e63b69fa7f6a31acc6faf99fe78cd063..4967f02b007e06ebe6728d9a42d136e7fa45d611 100644 (file)
@@ -6,7 +6,7 @@ import unittest
 import textwrap
 
 from test import support
-from test.support import import_helper
+from test.support import import_helper, force_not_colorized
 from test.support.os_helper import TESTFN, TESTFN_UNDECODABLE
 from test.support.script_helper import assert_python_failure, assert_python_ok
 from test.support.testcase import ExceptionIsLikeMixin
@@ -337,6 +337,10 @@ class Test_ErrSetAndRestore(unittest.TestCase):
             self.assertIsNone(cm.unraisable.err_msg)
             self.assertIsNone(cm.unraisable.object)
 
+    @force_not_colorized
+    def test_err_writeunraisable_lines(self):
+        writeunraisable = _testcapi.err_writeunraisable
+
         with (support.swap_attr(sys, 'unraisablehook', None),
               support.captured_stderr() as stderr):
             writeunraisable(CustomError('oops!'), hex)
@@ -387,6 +391,10 @@ class Test_ErrSetAndRestore(unittest.TestCase):
             self.assertIsNone(cm.unraisable.err_msg)
             self.assertIsNone(cm.unraisable.object)
 
+    @force_not_colorized
+    def test_err_formatunraisable_lines(self):
+        formatunraisable = _testcapi.err_formatunraisable
+
         with (support.swap_attr(sys, 'unraisablehook', None),
               support.captured_stderr() as stderr):
             formatunraisable(CustomError('oops!'), b'Error in %R', [])
index f30a1874ab96d4541686b7a9efdb568f88eec178..3ed7a360d64e3c5accf077943ffad7c4731deb4c 100644 (file)
@@ -489,6 +489,7 @@ class CmdLineTest(unittest.TestCase):
         self.assertRegex(err.decode('ascii', 'ignore'), 'SyntaxError')
         self.assertEqual(b'', out)
 
+    @force_not_colorized
     def test_stdout_flush_at_shutdown(self):
         # Issue #5319: if stdout.flush() fails at shutdown, an error should
         # be printed out.
index 99b315b47e253004aca382590aac426b29e9927f..43812248104c91981eccbe0dc31dac0eb33d5b92 100644 (file)
@@ -49,6 +49,7 @@ class ExecutorShutdownTest:
         self.assertFalse(err)
         self.assertEqual(out.strip(), b"apple")
 
+    @support.force_not_colorized
     def test_submit_after_interpreter_shutdown(self):
         # Test the atexit hook for shutdown of worker threads and processes
         rc, out, err = assert_python_ok('-c', """if 1:
index 6d62d6119255a88e74faaff04ad87947a39868f4..d6cc22558ec4fafc307e8c50b3b907fbcff60f07 100644 (file)
@@ -14,7 +14,7 @@ import time
 import unittest
 from test import support
 from test.support import (
-    is_apple, is_apple_mobile, os_helper, threading_helper
+    force_not_colorized, is_apple, is_apple_mobile, os_helper, threading_helper
 )
 from test.support.script_helper import assert_python_ok, spawn_python
 try:
@@ -353,6 +353,7 @@ class WakeupSignalTests(unittest.TestCase):
 
     @unittest.skipIf(_testcapi is None, 'need _testcapi')
     @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
+    @force_not_colorized
     def test_wakeup_write_error(self):
         # Issue #16105: write() errors in the C signal handler should not
         # pass silently.
index 486bf10a0b5647840cd8cd67439dd4c6d2c6791f..f89237931b7185cee0e447a890dffc3db2c66f1a 100644 (file)
@@ -1340,6 +1340,7 @@ class SysModuleTest(unittest.TestCase):
 
 
 @test.support.cpython_only
+@force_not_colorized
 class UnraisableHookTest(unittest.TestCase):
     def test_original_unraisablehook(self):
         _testcapi = import_helper.import_module('_testcapi')
index 002a1feeb85c949046fb85ac050b881f4de77ed1..0ba78b9a1807d2a2d221a6442a05936e1b4de242 100644 (file)
@@ -2494,6 +2494,7 @@ class AtexitTests(unittest.TestCase):
 
         self.assertFalse(err)
 
+    @force_not_colorized
     def test_atexit_after_shutdown(self):
         # The only way to do this is by registering an atexit within
         # an atexit, which is intended to raise an exception.
index f0dbb6352f776047b90bba69d334c68857ae8400..318ec13cf91121cd9b7e4a1b4661fb9307e3cc8d 100644 (file)
@@ -137,8 +137,9 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
 BUILTIN_EXCEPTION_LIMIT = object()
 
 
-def _print_exception_bltin(exc, /):
-    file = sys.stderr if sys.stderr is not None else sys.__stderr__
+def _print_exception_bltin(exc, file=None, /):
+    if file is None:
+        file = sys.stderr if sys.stderr is not None else sys.__stderr__
     colorize = _colorize.can_colorize(file=file)
     return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
 
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst
new file mode 100644 (file)
index 0000000..f33a30c
--- /dev/null
@@ -0,0 +1 @@
+Add colorization to :func:`sys.unraisablehook` by default.
index a3122f76bdd87d5afc6c0542a747181621e96d20..2688396004e98bd32a46c6f97df34848f1407713 100644 (file)
@@ -1444,12 +1444,16 @@ make_unraisable_hook_args(PyThreadState *tstate, PyObject *exc_type,
 
    It can be called to log the exception of a custom sys.unraisablehook.
 
-   Do nothing if sys.stderr attribute doesn't exist or is set to None. */
+   This assumes 'file' is neither NULL nor None.
+ */
 static int
 write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
                           PyObject *exc_value, PyObject *exc_tb,
                           PyObject *err_msg, PyObject *obj, PyObject *file)
 {
+    assert(file != NULL);
+    assert(!Py_IsNone(file));
+
     if (obj != NULL && obj != Py_None) {
         if (err_msg != NULL && err_msg != Py_None) {
             if (PyFile_WriteObject(err_msg, file, Py_PRINT_RAW) < 0) {
@@ -1484,6 +1488,27 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
         }
     }
 
+    // Try printing the exception using the stdlib module.
+    // If this fails, then we have to use the C implementation.
+    PyObject *print_exception_fn = PyImport_ImportModuleAttrString("traceback",
+                                                                   "_print_exception_bltin");
+    if (print_exception_fn != NULL && PyCallable_Check(print_exception_fn)) {
+        PyObject *args[2] = {exc_value, file};
+        PyObject *result = PyObject_Vectorcall(print_exception_fn, args, 2, NULL);
+        int ok = (result != NULL);
+        Py_DECREF(print_exception_fn);
+        Py_XDECREF(result);
+        if (ok) {
+            // Nothing else to do
+            return 0;
+        }
+    }
+    else {
+        Py_XDECREF(print_exception_fn);
+    }
+    // traceback module failed, fall back to pure C
+    _PyErr_Clear(tstate);
+
     if (exc_tb != NULL && exc_tb != Py_None) {
         if (PyTraceBack_Print(exc_tb, file) < 0) {
             /* continue even if writing the traceback failed */