mirror of
https://github.com/python/cpython.git
synced 2025-11-01 14:11:41 +00:00
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:
parent
8943bb722f
commit
e8251dc0ae
11 changed files with 55 additions and 6 deletions
|
|
@ -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:
|
The default hook formats :attr:`!err_msg` and :attr:`!object` as:
|
||||||
``f'{err_msg}: {object!r}'``; use "Exception ignored in" error message
|
``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
|
:func:`sys.unraisablehook` can be overridden to control how unraisable
|
||||||
exceptions are handled.
|
exceptions are handled.
|
||||||
|
|
||||||
|
.. versionchanged:: next
|
||||||
|
Exceptions are now printed with colorful text.
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
:func:`excepthook` which handles uncaught exceptions.
|
:func:`excepthook` which handles uncaught exceptions.
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,10 @@ Other language changes
|
||||||
* Several error messages incorrectly using the term "argument" have been corrected.
|
* Several error messages incorrectly using the term "argument" have been corrected.
|
||||||
(Contributed by Stan Ulbrych in :gh:`133382`.)
|
(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
|
New modules
|
||||||
===========
|
===========
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from test import support
|
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.os_helper import TESTFN, TESTFN_UNDECODABLE
|
||||||
from test.support.script_helper import assert_python_failure, assert_python_ok
|
from test.support.script_helper import assert_python_failure, assert_python_ok
|
||||||
from test.support.testcase import ExceptionIsLikeMixin
|
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.err_msg)
|
||||||
self.assertIsNone(cm.unraisable.object)
|
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),
|
with (support.swap_attr(sys, 'unraisablehook', None),
|
||||||
support.captured_stderr() as stderr):
|
support.captured_stderr() as stderr):
|
||||||
writeunraisable(CustomError('oops!'), hex)
|
writeunraisable(CustomError('oops!'), hex)
|
||||||
|
|
@ -387,6 +391,10 @@ def test_err_formatunraisable(self):
|
||||||
self.assertIsNone(cm.unraisable.err_msg)
|
self.assertIsNone(cm.unraisable.err_msg)
|
||||||
self.assertIsNone(cm.unraisable.object)
|
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),
|
with (support.swap_attr(sys, 'unraisablehook', None),
|
||||||
support.captured_stderr() as stderr):
|
support.captured_stderr() as stderr):
|
||||||
formatunraisable(CustomError('oops!'), b'Error in %R', [])
|
formatunraisable(CustomError('oops!'), b'Error in %R', [])
|
||||||
|
|
|
||||||
|
|
@ -489,6 +489,7 @@ def test_unmached_quote(self):
|
||||||
self.assertRegex(err.decode('ascii', 'ignore'), 'SyntaxError')
|
self.assertRegex(err.decode('ascii', 'ignore'), 'SyntaxError')
|
||||||
self.assertEqual(b'', out)
|
self.assertEqual(b'', out)
|
||||||
|
|
||||||
|
@force_not_colorized
|
||||||
def test_stdout_flush_at_shutdown(self):
|
def test_stdout_flush_at_shutdown(self):
|
||||||
# Issue #5319: if stdout.flush() fails at shutdown, an error should
|
# Issue #5319: if stdout.flush() fails at shutdown, an error should
|
||||||
# be printed out.
|
# be printed out.
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ def test_interpreter_shutdown(self):
|
||||||
self.assertFalse(err)
|
self.assertFalse(err)
|
||||||
self.assertEqual(out.strip(), b"apple")
|
self.assertEqual(out.strip(), b"apple")
|
||||||
|
|
||||||
|
@support.force_not_colorized
|
||||||
def test_submit_after_interpreter_shutdown(self):
|
def test_submit_after_interpreter_shutdown(self):
|
||||||
# Test the atexit hook for shutdown of worker threads and processes
|
# Test the atexit hook for shutdown of worker threads and processes
|
||||||
rc, out, err = assert_python_ok('-c', """if 1:
|
rc, out, err = assert_python_ok('-c', """if 1:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
import unittest
|
import unittest
|
||||||
from test import support
|
from test import support
|
||||||
from test.support import (
|
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
|
from test.support.script_helper import assert_python_ok, spawn_python
|
||||||
try:
|
try:
|
||||||
|
|
@ -353,6 +353,7 @@ def check_signum(signals):
|
||||||
|
|
||||||
@unittest.skipIf(_testcapi is None, 'need _testcapi')
|
@unittest.skipIf(_testcapi is None, 'need _testcapi')
|
||||||
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
|
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
|
||||||
|
@force_not_colorized
|
||||||
def test_wakeup_write_error(self):
|
def test_wakeup_write_error(self):
|
||||||
# Issue #16105: write() errors in the C signal handler should not
|
# Issue #16105: write() errors in the C signal handler should not
|
||||||
# pass silently.
|
# pass silently.
|
||||||
|
|
|
||||||
|
|
@ -1340,6 +1340,7 @@ def test_disable_gil_abi(self):
|
||||||
|
|
||||||
|
|
||||||
@test.support.cpython_only
|
@test.support.cpython_only
|
||||||
|
@force_not_colorized
|
||||||
class UnraisableHookTest(unittest.TestCase):
|
class UnraisableHookTest(unittest.TestCase):
|
||||||
def test_original_unraisablehook(self):
|
def test_original_unraisablehook(self):
|
||||||
_testcapi = import_helper.import_module('_testcapi')
|
_testcapi = import_helper.import_module('_testcapi')
|
||||||
|
|
|
||||||
|
|
@ -2494,6 +2494,7 @@ def test_atexit_called_once(self):
|
||||||
|
|
||||||
self.assertFalse(err)
|
self.assertFalse(err)
|
||||||
|
|
||||||
|
@force_not_colorized
|
||||||
def test_atexit_after_shutdown(self):
|
def test_atexit_after_shutdown(self):
|
||||||
# The only way to do this is by registering an atexit within
|
# The only way to do this is by registering an atexit within
|
||||||
# an atexit, which is intended to raise an exception.
|
# 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()
|
BUILTIN_EXCEPTION_LIMIT = object()
|
||||||
|
|
||||||
|
|
||||||
def _print_exception_bltin(exc, /):
|
def _print_exception_bltin(exc, file=None, /):
|
||||||
file = sys.stderr if sys.stderr is not None else sys.__stderr__
|
if file is None:
|
||||||
|
file = sys.stderr if sys.stderr is not None else sys.__stderr__
|
||||||
colorize = _colorize.can_colorize(file=file)
|
colorize = _colorize.can_colorize(file=file)
|
||||||
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
|
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.
|
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
|
static int
|
||||||
write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
|
write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
|
||||||
PyObject *exc_value, PyObject *exc_tb,
|
PyObject *exc_value, PyObject *exc_tb,
|
||||||
PyObject *err_msg, PyObject *obj, PyObject *file)
|
PyObject *err_msg, PyObject *obj, PyObject *file)
|
||||||
{
|
{
|
||||||
|
assert(file != NULL);
|
||||||
|
assert(!Py_IsNone(file));
|
||||||
|
|
||||||
if (obj != NULL && obj != Py_None) {
|
if (obj != NULL && obj != Py_None) {
|
||||||
if (err_msg != NULL && err_msg != Py_None) {
|
if (err_msg != NULL && err_msg != Py_None) {
|
||||||
if (PyFile_WriteObject(err_msg, file, Py_PRINT_RAW) < 0) {
|
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 (exc_tb != NULL && exc_tb != Py_None) {
|
||||||
if (PyTraceBack_Print(exc_tb, file) < 0) {
|
if (PyTraceBack_Print(exc_tb, file) < 0) {
|
||||||
/* continue even if writing the traceback failed */
|
/* continue even if writing the traceback failed */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue