From e8ea2a34f2c87dc238b7294419ec5319d85bb746 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 10:49:06 -0400 Subject: [PATCH 01/20] Add colorization to unraisable exceptions. --- Python/errors.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Python/errors.c b/Python/errors.c index 81f267b043afaf..f793ae6c6824e9 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1439,8 +1439,6 @@ make_unraisable_hook_args(PyThreadState *tstate, PyObject *exc_type, return args; } - - /* Default implementation of sys.unraisablehook. It can be called to log the exception of a custom sys.unraisablehook. @@ -1485,6 +1483,20 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type, } } + // Try printing the exception with color + PyObject *print_exception_fn = PyImport_ImportModuleAttrString("traceback", + "_print_exception_bltin"); + if (print_exception_fn != NULL && PyCallable_Check(print_exception_fn)) { + PyObject *result = PyObject_CallOneArg(print_exception_fn, exc_value); + Py_DECREF(print_exception_fn); + Py_XDECREF(result); + if (result != NULL) { + return 0; + } + } + // traceback module failed, fall back to pure C + Py_XDECREF(print_exception_fn); + if (exc_tb != NULL && exc_tb != Py_None) { if (PyTraceBack_Print(exc_tb, file) < 0) { /* continue even if writing the traceback failed */ From 36e38e6d0b17e690b0977d4ed58bc66c4c7b6f95 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 10:50:52 -0400 Subject: [PATCH 02/20] Add blurb. --- .../2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst 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..7ea6df8127de8d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-10-50-46.gh-issue-134170.J0Hvmi.rst @@ -0,0 +1,2 @@ +Add colorization to unraisable exceptions by default +(:func:`sys.unraisablehook`). From 6c9d619f418c9fbbc8e6006663e7957845d07fc3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 10:53:10 -0400 Subject: [PATCH 03/20] Add whatsnew. --- Doc/whatsnew/3.15.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 987cf944972329..3018e301580835 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -77,7 +77,9 @@ 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 From d2e621a86118f9188eadb7ee8fa38412c857ce34 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 10:59:00 -0400 Subject: [PATCH 04/20] Clear exceptions upon failure. --- Python/errors.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/errors.c b/Python/errors.c index f793ae6c6824e9..09522ce7c145ad 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1495,6 +1495,7 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type, } } // traceback module failed, fall back to pure C + _PyErr_Clear(tstate); Py_XDECREF(print_exception_fn); if (exc_tb != NULL && exc_tb != Py_None) { From 411406c6218a99583b51fc6064409e77f38789a4 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:00:03 -0400 Subject: [PATCH 05/20] Remove stray newline change. --- Python/errors.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Python/errors.c b/Python/errors.c index 09522ce7c145ad..44881a8c3855d2 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1439,6 +1439,8 @@ make_unraisable_hook_args(PyThreadState *tstate, PyObject *exc_type, return args; } + + /* Default implementation of sys.unraisablehook. It can be called to log the exception of a custom sys.unraisablehook. From 4e9a0b631ac3ce61feee563188354f7bd7c78534 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:13:12 -0400 Subject: [PATCH 06/20] Fix signal tests. --- Lib/test/test_signal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index 6d62d6119255a8..fdd1f8609f0e39 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 + is_apple, is_apple_mobile, os_helper, threading_helper, force_not_colorized ) 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. From 19db581edb57edb8ab025a6d99985fa5deff4b56 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:13:41 -0400 Subject: [PATCH 07/20] Fix cmdline tests. --- Lib/test/test_cmd_line.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 1b40e0d05fe3bc..ec93f3cf715544 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -483,6 +483,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. From 6e57c96749a7ad905f4507d957a4f1c35ee8d05b Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:14:07 -0400 Subject: [PATCH 08/20] Fix sys tests. --- Lib/test/test_sys.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index fb1c8492a64d38..a41778301ade56 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1485,6 +1485,7 @@ def hook_func(args): expected = None hook_args = None + @force_not_colorized def test_custom_unraisablehook_fail(self): _testcapi = import_helper.import_module('_testcapi') from _testcapi import err_writeunraisable From 06d43ddd606a1a0587284c8e8614eca6e3d06765 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:14:36 -0400 Subject: [PATCH 09/20] Oops I lied. --- Lib/test/test_sys.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index a41778301ade56..135f6e359605c7 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1347,6 +1347,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') From ca0efb4c7f903417ce9af1554ce6e3b9a85c3e1d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:15:18 -0400 Subject: [PATCH 10/20] Fix atexit tests. --- Lib/test/test_threading.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index abe63c10c0ac7c..30d57a244e5faf 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2397,6 +2397,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. From bcfd8115dcf20f05d4ddde742b971bd29cfd448e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:19:44 -0400 Subject: [PATCH 11/20] Skip unneeded @force_not_colorized --- Lib/test/test_sys.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 135f6e359605c7..114f4a7ef3512d 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1486,7 +1486,6 @@ def hook_func(args): expected = None hook_args = None - @force_not_colorized def test_custom_unraisablehook_fail(self): _testcapi = import_helper.import_module('_testcapi') from _testcapi import err_writeunraisable From b3290191cdb9f0b596ba4dd385cbd949c29cd5e7 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:21:32 -0400 Subject: [PATCH 12/20] Fix C API tests. --- Lib/test/test_capi/test_exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py index ade55338e63b69..4607c361b13e48 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 @@ -355,6 +355,7 @@ def test_err_writeunraisable(self): # CRASHES writeunraisable(NULL, hex) # CRASHES writeunraisable(NULL, NULL) + @force_not_colorized def test_err_formatunraisable(self): # Test PyErr_FormatUnraisable() formatunraisable = _testcapi.err_formatunraisable From 8a325d33b1fe10439c362ff5eb81e7debf948a93 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:34:23 -0400 Subject: [PATCH 13/20] Fix remaining tests. --- Lib/test/test_concurrent_futures/test_shutdown.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_concurrent_futures/test_shutdown.py b/Lib/test/test_concurrent_futures/test_shutdown.py index 7a4065afd46fc8..ef4368a6d9315c 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: From 23604279ee6bc0f4f70cefd3f391e40fd59cccb3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:34:28 -0400 Subject: [PATCH 14/20] Fix remaining tests. --- Lib/test/test_capi/test_exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py index 4607c361b13e48..0dd69d1506e655 100644 --- a/Lib/test/test_capi/test_exceptions.py +++ b/Lib/test/test_capi/test_exceptions.py @@ -314,6 +314,7 @@ def test_setfromerrnowithfilename(self): (ENOENT, 'No such file or directory', 'file')) # CRASHES setfromerrnowithfilename(ENOENT, NULL, b'error') + @force_not_colorized def test_err_writeunraisable(self): # Test PyErr_WriteUnraisable() writeunraisable = _testcapi.err_writeunraisable From 9a1d88e35c6085c659c46c7b186970d032d40ae3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 11:35:46 -0400 Subject: [PATCH 15/20] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/whatsnew/3.15.rst | 1 + Lib/test/test_signal.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 3018e301580835..128cd00382992b 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -77,6 +77,7 @@ 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`.) diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index fdd1f8609f0e39..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 + force_not_colorized, is_apple, is_apple_mobile, os_helper, threading_helper ) from test.support.script_helper import assert_python_ok, spawn_python try: From a8cf99ae7820aa7a8c3d82e40c2db4dfaf0dc58a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 12:08:51 -0400 Subject: [PATCH 16/20] Pass the file instead of looking up sys.stderr again. --- Lib/traceback.py | 4 ++-- Python/errors.c | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 17b082eced6f05..b96a0090b66eda 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -137,8 +137,8 @@ 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, /): + file = file or 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/Python/errors.c b/Python/errors.c index 44881a8c3855d2..e443c67df1ff2b 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1489,7 +1489,8 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type, PyObject *print_exception_fn = PyImport_ImportModuleAttrString("traceback", "_print_exception_bltin"); if (print_exception_fn != NULL && PyCallable_Check(print_exception_fn)) { - PyObject *result = PyObject_CallOneArg(print_exception_fn, exc_value); + PyObject *args[2] = {exc_value, file}; + PyObject *result = PyObject_Vectorcall(print_exception_fn, args, 2, NULL); Py_DECREF(print_exception_fn); Py_XDECREF(result); if (result != NULL) { From b32084cbc894b98ed7327bcccd3d0d0f988eebf5 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 12:13:18 -0400 Subject: [PATCH 17/20] Change stupid test case. --- Lib/test/test_capi/test_exceptions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py index 0dd69d1506e655..b60d83e01390fc 100644 --- a/Lib/test/test_capi/test_exceptions.py +++ b/Lib/test/test_capi/test_exceptions.py @@ -314,7 +314,6 @@ def test_setfromerrnowithfilename(self): (ENOENT, 'No such file or directory', 'file')) # CRASHES setfromerrnowithfilename(ENOENT, NULL, b'error') - @force_not_colorized def test_err_writeunraisable(self): # Test PyErr_WriteUnraisable() writeunraisable = _testcapi.err_writeunraisable @@ -356,7 +355,6 @@ def test_err_writeunraisable(self): # CRASHES writeunraisable(NULL, hex) # CRASHES writeunraisable(NULL, NULL) - @force_not_colorized def test_err_formatunraisable(self): # Test PyErr_FormatUnraisable() formatunraisable = _testcapi.err_formatunraisable From 79fb9d2cca0eb42d7428f6777e8276ce054901c1 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 12:28:19 -0400 Subject: [PATCH 18/20] Please let this fix the stupid test. It doesn't repro locally for some reason. --- Lib/test/test_capi/test_exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py index b60d83e01390fc..4967f02b007e06 100644 --- a/Lib/test/test_capi/test_exceptions.py +++ b/Lib/test/test_capi/test_exceptions.py @@ -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', []) From 4c339f3cdb2f0e1c1f8ca160dc6ccfafd9f9e29c Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 18 May 2025 15:09:18 -0400 Subject: [PATCH 19/20] Add a comment to make Benedikt happy. --- Python/errors.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Python/errors.c b/Python/errors.c index e443c67df1ff2b..b005315c41e8c5 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1485,7 +1485,8 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type, } } - // Try printing the exception with color + // 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)) { @@ -1494,6 +1495,7 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type, Py_DECREF(print_exception_fn); Py_XDECREF(result); if (result != NULL) { + // Nothing else to do return 0; } } From 27939640f230ce1d838e7be9c4b32ca1fdf8c0c5 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 2 Jun 2025 06:11:48 -0400 Subject: [PATCH 20/20] Fix the comment about the file parameter. --- Python/errors.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Python/errors.c b/Python/errors.c index b005315c41e8c5..404eb6dc86ecb8 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1445,12 +1445,14 @@ 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 non-NULL. + */ 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 && !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) {