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>
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.
* 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
===========
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
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)
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', [])
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.
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:
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:
@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.
@test.support.cpython_only
+@force_not_colorized
class UnraisableHookTest(unittest.TestCase):
def test_original_unraisablehook(self):
_testcapi = import_helper.import_module('_testcapi')
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.
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)
--- /dev/null
+Add colorization to :func:`sys.unraisablehook` by default.
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) {
}
}
+ // 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 */