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 <vstinner@python.org>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
===========
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Add colorization to :func:`sys.unraisablehook` by default.
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user