Skip to content

Commit ef9d9b6

Browse files
authored
bpo-36829: Add sys.unraisablehook() (GH-13187)
Add new sys.unraisablehook() function which can be overridden to control how "unraisable exceptions" are handled. It is called when an exception has occurred but there is no way for Python to handle it. For example, when a destructor raises an exception or during garbage collection (gc.collect()). Changes: * Add an internal UnraisableHookArgs type used to pass arguments to sys.unraisablehook. * Add _PyErr_WriteUnraisableDefaultHook(). * The default hook now ignores exception on writing the traceback. * test_sys now uses unittest.main() to automatically discover tests: remove test_main(). * Add _PyErr_Init(). * Fix PyErr_WriteUnraisable(): hold a strong reference to sys.stderr while using it
1 parent 2725cb0 commit ef9d9b6

File tree

12 files changed

+491
-117
lines changed

12 files changed

+491
-117
lines changed

Doc/c-api/exceptions.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ Printing and clearing
7272
7373
.. c:function:: void PyErr_WriteUnraisable(PyObject *obj)
7474
75+
Call :func:`sys.unraisablehook` using the current exception and *obj*
76+
argument.
77+
7578
This utility function prints a warning message to ``sys.stderr`` when an
7679
exception has been set but it is impossible for the interpreter to actually
7780
raise the exception. It is used, for example, when an exception occurs in an
@@ -81,6 +84,8 @@ Printing and clearing
8184
in which the unraisable exception occurred. If possible,
8285
the repr of *obj* will be printed in the warning message.
8386
87+
An exception must be set when calling this function.
88+
8489
8590
Raising exceptions
8691
==================

Doc/library/sys.rst

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,19 @@ always available.
248248
before the program exits. The handling of such top-level exceptions can be
249249
customized by assigning another three-argument function to ``sys.excepthook``.
250250

251+
See also :func:`unraisablehook` which handles unraisable exceptions.
252+
251253

252254
.. data:: __breakpointhook__
253255
__displayhook__
254256
__excepthook__
257+
__unraisablehook__
255258

256259
These objects contain the original values of ``breakpointhook``,
257-
``displayhook``, and ``excepthook`` at the start of the program. They are
258-
saved so that ``breakpointhook``, ``displayhook`` and ``excepthook`` can be
259-
restored in case they happen to get replaced with broken or alternative
260-
objects.
260+
``displayhook``, ``excepthook``, and ``unraisablehook`` at the start of the
261+
program. They are saved so that ``breakpointhook``, ``displayhook`` and
262+
``excepthook``, ``unraisablehook`` can be restored in case they happen to
263+
get replaced with broken or alternative objects.
261264

262265
.. versionadded:: 3.7
263266
__breakpointhook__
@@ -1487,6 +1490,28 @@ always available.
14871490
is suppressed and only the exception type and value are printed.
14881491

14891492

1493+
.. function:: unraisablehook(unraisable, /)
1494+
1495+
Handle an unraisable exception.
1496+
1497+
Called when an exception has occurred but there is no way for Python to
1498+
handle it. For example, when a destructor raises an exception or during
1499+
garbage collection (:func:`gc.collect`).
1500+
1501+
The *unraisable* argument has the following attributes:
1502+
1503+
* *exc_type*: Exception type.
1504+
* *exc_value*: Exception value, can be ``None``.
1505+
* *exc_traceback*: Exception traceback, can be ``None``.
1506+
* *object*: Object causing the exception, can be ``None``.
1507+
1508+
:func:`sys.unraisablehook` can be overridden to control how unraisable
1509+
exceptions are handled.
1510+
1511+
See also :func:`excepthook` which handles uncaught exceptions.
1512+
1513+
.. versionadded:: 3.8
1514+
14901515
.. data:: version
14911516

14921517
A string containing the version number of the Python interpreter plus additional

Doc/whatsnew/3.8.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,16 @@ and manipulating normal distributions of a random variable.
481481
[7.672102882379219, 12.000027119750287, 4.647488369766392]
482482

483483

484+
sys
485+
---
486+
487+
Add new :func:`sys.unraisablehook` function which can be overridden to control
488+
how "unraisable exceptions" are handled. It is called when an exception has
489+
occurred but there is no way for Python to handle it. For example, when a
490+
destructor raises an exception or during garbage collection
491+
(:func:`gc.collect`).
492+
493+
484494
tarfile
485495
-------
486496

Include/internal/pycore_pylifecycle.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ extern int _PySys_InitMain(
4848
PyInterpreterState *interp);
4949
extern _PyInitError _PyImport_Init(PyInterpreterState *interp);
5050
extern _PyInitError _PyExc_Init(void);
51+
extern _PyInitError _PyErr_Init(void);
5152
extern _PyInitError _PyBuiltins_AddExceptions(PyObject * bltinmod);
5253
extern _PyInitError _PyImportHooks_Init(void);
5354
extern int _PyFloat_Init(void);
@@ -100,8 +101,11 @@ PyAPI_FUNC(_PyInitError) _Py_PreInitializeFromCoreConfig(
100101
const _PyCoreConfig *coreconfig,
101102
const _PyArgv *args);
102103

104+
103105
PyAPI_FUNC(int) _Py_HandleSystemExit(int *exitcode_p);
104106

107+
PyAPI_FUNC(PyObject*) _PyErr_WriteUnraisableDefaultHook(PyObject *unraisable);
108+
105109
#ifdef __cplusplus
106110
}
107111
#endif

Lib/test/test_sys.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,81 @@ def test__enablelegacywindowsfsencoding(self):
876876
self.assertEqual(out, 'mbcs replace')
877877

878878

879+
@test.support.cpython_only
880+
class UnraisableHookTest(unittest.TestCase):
881+
def write_unraisable_exc(self, exc, obj):
882+
import _testcapi
883+
import types
884+
try:
885+
# raise the exception to get a traceback in the except block
886+
try:
887+
raise exc
888+
except Exception as exc2:
889+
_testcapi.write_unraisable_exc(exc2, obj)
890+
return types.SimpleNamespace(exc_type=type(exc2),
891+
exc_value=exc2,
892+
exc_traceback=exc2.__traceback__,
893+
object=obj)
894+
finally:
895+
# Explicitly break any reference cycle
896+
exc = None
897+
exc2 = None
898+
899+
def test_original_unraisablehook(self):
900+
obj = "an object"
901+
902+
with test.support.captured_output("stderr") as stderr:
903+
with test.support.swap_attr(sys, 'unraisablehook',
904+
sys.__unraisablehook__):
905+
self.write_unraisable_exc(ValueError(42), obj)
906+
907+
err = stderr.getvalue()
908+
self.assertIn(f'Exception ignored in: {obj!r}\n', err)
909+
self.assertIn('Traceback (most recent call last):\n', err)
910+
self.assertIn('ValueError: 42\n', err)
911+
912+
def test_original_unraisablehook_wrong_type(self):
913+
exc = ValueError(42)
914+
with test.support.swap_attr(sys, 'unraisablehook',
915+
sys.__unraisablehook__):
916+
with self.assertRaises(TypeError):
917+
sys.unraisablehook(exc)
918+
919+
def test_custom_unraisablehook(self):
920+
hook_args = None
921+
922+
def hook_func(args):
923+
nonlocal hook_args
924+
hook_args = args
925+
926+
obj = object()
927+
try:
928+
with test.support.swap_attr(sys, 'unraisablehook', hook_func):
929+
expected = self.write_unraisable_exc(ValueError(42), obj)
930+
for attr in "exc_type exc_value exc_traceback object".split():
931+
self.assertEqual(getattr(hook_args, attr),
932+
getattr(expected, attr),
933+
(hook_args, expected))
934+
finally:
935+
# expected and hook_args contain an exception: break reference cycle
936+
expected = None
937+
hook_args = None
938+
939+
def test_custom_unraisablehook_fail(self):
940+
def hook_func(*args):
941+
raise Exception("hook_func failed")
942+
943+
with test.support.captured_output("stderr") as stderr:
944+
with test.support.swap_attr(sys, 'unraisablehook', hook_func):
945+
self.write_unraisable_exc(ValueError(42), None)
946+
947+
err = stderr.getvalue()
948+
self.assertIn(f'Exception ignored in: {hook_func!r}\n',
949+
err)
950+
self.assertIn('Traceback (most recent call last):\n', err)
951+
self.assertIn('Exception: hook_func failed\n', err)
952+
953+
879954
@test.support.cpython_only
880955
class SizeofTest(unittest.TestCase):
881956

@@ -1277,8 +1352,5 @@ def test_asyncgen_hooks(self):
12771352
self.assertIsNone(cur.finalizer)
12781353

12791354

1280-
def test_main():
1281-
test.support.run_unittest(SysModuleTest, SizeofTest)
1282-
12831355
if __name__ == "__main__":
1284-
test_main()
1356+
unittest.main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add new :func:`sys.unraisablehook` function which can be overridden to
2+
control how "unraisable exceptions" are handled. It is called when an
3+
exception has occurred but there is no way for Python to handle it. For
4+
example, when a destructor raises an exception or during garbage collection
5+
(:func:`gc.collect`).

Modules/_testcapimodule.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4982,6 +4982,20 @@ negative_refcount(PyObject *self, PyObject *Py_UNUSED(args))
49824982
#endif
49834983

49844984

4985+
static PyObject*
4986+
test_write_unraisable_exc(PyObject *self, PyObject *args)
4987+
{
4988+
PyObject *exc, *obj;
4989+
if (!PyArg_ParseTuple(args, "OO", &exc, &obj)) {
4990+
return NULL;
4991+
}
4992+
4993+
PyErr_SetObject((PyObject *)Py_TYPE(exc), exc);
4994+
PyErr_WriteUnraisable(obj);
4995+
Py_RETURN_NONE;
4996+
}
4997+
4998+
49854999
static PyMethodDef TestMethods[] = {
49865000
{"raise_exception", raise_exception, METH_VARARGS},
49875001
{"raise_memoryerror", raise_memoryerror, METH_NOARGS},
@@ -5221,6 +5235,7 @@ static PyMethodDef TestMethods[] = {
52215235
#ifdef Py_REF_DEBUG
52225236
{"negative_refcount", negative_refcount, METH_NOARGS},
52235237
#endif
5238+
{"write_unraisable_exc", test_write_unraisable_exc, METH_VARARGS},
52245239
{NULL, NULL} /* sentinel */
52255240
};
52265241

Python/clinic/sysmodule.c.h

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)