https://github.com/python/cpython/commit/e8251dc0ae6a85f6a0e427ae64fb0fe847eb3cf8
commit: e8251dc0ae6a85f6a0e427ae64fb0fe847eb3cf8
branch: main
author: Peter Bierma <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2025-08-04T14:35:00Z
summary:

gh-134170: Add colorization to unraisable exceptions (#134183)

Default implementation of sys.unraisablehook() now uses 
traceback._print_exception_bltin() to print exceptions with colorized text.

Co-authored-by: Bénédikt Tran <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst
M Doc/library/sys.rst
M Doc/whatsnew/3.15.rst
M Lib/test/test_capi/test_exceptions.py
M Lib/test/test_cmd_line.py
M Lib/test/test_concurrent_futures/test_shutdown.py
M Lib/test/test_signal.py
M Lib/test/test_sys.py
M Lib/test/test_threading.py
M Lib/traceback.py
M Python/errors.c

diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index 52f0af31c68726..771e0f2709a4aa 100644
--- a/Doc/library/sys.rst
+++ b/Doc/library/sys.rst
@@ -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.
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index e716d7bb0f2a5c..54964da473760d 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -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
 ===========
diff --git a/Lib/test/test_capi/test_exceptions.py 
b/Lib/test/test_capi/test_exceptions.py
index ade55338e63b69..4967f02b007e06 100644
--- a/Lib/test/test_capi/test_exceptions.py
+++ b/Lib/test/test_capi/test_exceptions.py
@@ -6,7 +6,7 @@
 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 @@ def test_err_writeunraisable(self):
             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 @@ def test_err_formatunraisable(self):
             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', [])
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
index f30a1874ab96d4..3ed7a360d64e3c 100644
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -489,6 +489,7 @@ def test_unmached_quote(self):
         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.
diff --git a/Lib/test/test_concurrent_futures/test_shutdown.py 
b/Lib/test/test_concurrent_futures/test_shutdown.py
index 99b315b47e2530..43812248104c91 100644
--- a/Lib/test/test_concurrent_futures/test_shutdown.py
+++ b/Lib/test/test_concurrent_futures/test_shutdown.py
@@ -49,6 +49,7 @@ def test_interpreter_shutdown(self):
         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:
diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py
index 6d62d6119255a8..d6cc22558ec4fa 100644
--- a/Lib/test/test_signal.py
+++ b/Lib/test/test_signal.py
@@ -14,7 +14,7 @@
 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 @@ def check_signum(signals):
 
     @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.
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 486bf10a0b5647..f89237931b7185 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -1340,6 +1340,7 @@ def test_disable_gil_abi(self):
 
 
 @test.support.cpython_only
+@force_not_colorized
 class UnraisableHookTest(unittest.TestCase):
     def test_original_unraisablehook(self):
         _testcapi = import_helper.import_module('_testcapi')
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
index 002a1feeb85c94..0ba78b9a1807d2 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -2494,6 +2494,7 @@ def test_atexit_called_once(self):
 
         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.
diff --git a/Lib/traceback.py b/Lib/traceback.py
index f0dbb6352f7760..318ec13cf91121 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -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
index 00000000000000..f33a30c7e120dc
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst
@@ -0,0 +1 @@
+Add colorization to :func:`sys.unraisablehook` by default.
diff --git a/Python/errors.c b/Python/errors.c
index a3122f76bdd87d..2688396004e98b 100644
--- a/Python/errors.c
+++ b/Python/errors.c
@@ -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 */

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to