From: Peter Bierma Date: Mon, 4 Aug 2025 14:35:00 +0000 (-0400) Subject: gh-134170: Add colorization to unraisable exceptions (#134183) X-Git-Tag: v3.15.0a1~785 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e8251dc0ae6a85f6a0e427ae64fb0fe847eb3cf8;p=thirdparty%2FPython%2Fcpython.git 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 <10796600+picnixz@users.noreply.github.com> Co-authored-by: Victor Stinner --- diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 52f0af31c687..771e0f2709a4 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 `. :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 e716d7bb0f2a..54964da47376 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 `. + (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 ade55338e63b..4967f02b007e 100644 --- a/Lib/test/test_capi/test_exceptions.py +++ b/Lib/test/test_capi/test_exceptions.py @@ -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', []) diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index f30a1874ab96..3ed7a360d64e 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -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. diff --git a/Lib/test/test_concurrent_futures/test_shutdown.py b/Lib/test/test_concurrent_futures/test_shutdown.py index 99b315b47e25..43812248104c 100644 --- a/Lib/test/test_concurrent_futures/test_shutdown.py +++ b/Lib/test/test_concurrent_futures/test_shutdown.py @@ -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: diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index 6d62d6119255..d6cc22558ec4 100644 --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -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. diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 486bf10a0b56..f89237931b71 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -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') diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 002a1feeb85c..0ba78b9a1807 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -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. diff --git a/Lib/traceback.py b/Lib/traceback.py index f0dbb6352f77..318ec13cf911 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 000000000000..f33a30c7e120 --- /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 a3122f76bdd8..2688396004e9 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 */