mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +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,7 +137,8 @@ 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, /):
 | 
				
			||||||
 | 
					    if file is None:
 | 
				
			||||||
        file = sys.stderr if sys.stderr is not None else sys.__stderr__
 | 
					        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