Skip to content

Commit 71c52e3

Browse files
authored
bpo-36829: Add _PyErr_WriteUnraisableMsg() (GH-13488)
* sys.unraisablehook: add 'err_msg' field to UnraisableHookArgs. * Use _PyErr_WriteUnraisableMsg() in _ctypes _DictRemover_call() and gc delete_garbage().
1 parent 2f0bfd2 commit 71c52e3

File tree

9 files changed

+135
-47
lines changed

9 files changed

+135
-47
lines changed

Doc/library/sys.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,11 +1566,16 @@ always available.
15661566
* *exc_type*: Exception type.
15671567
* *exc_value*: Exception value, can be ``None``.
15681568
* *exc_traceback*: Exception traceback, can be ``None``.
1569+
* *err_msg*: Error message, can be ``None``.
15691570
* *object*: Object causing the exception, can be ``None``.
15701571

15711572
:func:`sys.unraisablehook` can be overridden to control how unraisable
15721573
exceptions are handled.
15731574

1575+
The default hook formats *err_msg* and *object* as:
1576+
``f'{err_msg}: {object!r}'``; use "Exception ignored in" error message
1577+
if *err_msg* is ``None``.
1578+
15741579
See also :func:`excepthook` which handles uncaught exceptions.
15751580

15761581
.. versionadded:: 3.8

Include/cpython/pyerrors.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ PyAPI_FUNC(PyObject *) _PyUnicodeTranslateError_Create(
171171
const char *reason /* UTF-8 encoded string */
172172
);
173173

174+
PyAPI_FUNC(void) _PyErr_WriteUnraisableMsg(
175+
const char *err_msg,
176+
PyObject *obj);
174177

175178
#ifdef __cplusplus
176179
}

Lib/test/test_sys.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -878,31 +878,38 @@ def test__enablelegacywindowsfsencoding(self):
878878

879879
@test.support.cpython_only
880880
class UnraisableHookTest(unittest.TestCase):
881-
def write_unraisable_exc(self, exc, obj):
881+
def write_unraisable_exc(self, exc, err_msg, obj):
882882
import _testcapi
883883
import types
884+
err_msg2 = f"Exception ignored {err_msg}"
884885
try:
885-
_testcapi.write_unraisable_exc(exc, obj)
886+
_testcapi.write_unraisable_exc(exc, err_msg, obj)
886887
return types.SimpleNamespace(exc_type=type(exc),
887888
exc_value=exc,
888889
exc_traceback=exc.__traceback__,
890+
err_msg=err_msg2,
889891
object=obj)
890892
finally:
891893
# Explicitly break any reference cycle
892894
exc = None
893895

894896
def test_original_unraisablehook(self):
895-
obj = "an object"
896-
897-
with test.support.captured_output("stderr") as stderr:
898-
with test.support.swap_attr(sys, 'unraisablehook',
899-
sys.__unraisablehook__):
900-
self.write_unraisable_exc(ValueError(42), obj)
901-
902-
err = stderr.getvalue()
903-
self.assertIn(f'Exception ignored in: {obj!r}\n', err)
904-
self.assertIn('Traceback (most recent call last):\n', err)
905-
self.assertIn('ValueError: 42\n', err)
897+
for err_msg in (None, "original hook"):
898+
with self.subTest(err_msg=err_msg):
899+
obj = "an object"
900+
901+
with test.support.captured_output("stderr") as stderr:
902+
with test.support.swap_attr(sys, 'unraisablehook',
903+
sys.__unraisablehook__):
904+
self.write_unraisable_exc(ValueError(42), err_msg, obj)
905+
906+
err = stderr.getvalue()
907+
if err_msg is not None:
908+
self.assertIn(f'Exception ignored {err_msg}: {obj!r}\n', err)
909+
else:
910+
self.assertIn(f'Exception ignored in: {obj!r}\n', err)
911+
self.assertIn('Traceback (most recent call last):\n', err)
912+
self.assertIn('ValueError: 42\n', err)
906913

907914
def test_original_unraisablehook_err(self):
908915
# bpo-22836: PyErr_WriteUnraisable() should give sensible reports
@@ -962,8 +969,9 @@ def hook_func(args):
962969
obj = object()
963970
try:
964971
with test.support.swap_attr(sys, 'unraisablehook', hook_func):
965-
expected = self.write_unraisable_exc(ValueError(42), obj)
966-
for attr in "exc_type exc_value exc_traceback object".split():
972+
expected = self.write_unraisable_exc(ValueError(42),
973+
"custom hook", obj)
974+
for attr in "exc_type exc_value exc_traceback err_msg object".split():
967975
self.assertEqual(getattr(hook_args, attr),
968976
getattr(expected, attr),
969977
(hook_args, expected))
@@ -978,10 +986,12 @@ def hook_func(*args):
978986

979987
with test.support.captured_output("stderr") as stderr:
980988
with test.support.swap_attr(sys, 'unraisablehook', hook_func):
981-
self.write_unraisable_exc(ValueError(42), None)
989+
self.write_unraisable_exc(ValueError(42),
990+
"custom hook fail", None)
982991

983992
err = stderr.getvalue()
984-
self.assertIn(f'Exception ignored in: {hook_func!r}\n',
993+
self.assertIn(f'Exception ignored in sys.unraisablehook: '
994+
f'{hook_func!r}\n',
985995
err)
986996
self.assertIn('Traceback (most recent call last):\n', err)
987997
self.assertIn('Exception: hook_func failed\n', err)

Modules/_ctypes/_ctypes.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,9 @@ _DictRemover_call(PyObject *myself, PyObject *args, PyObject *kw)
150150
{
151151
DictRemoverObject *self = (DictRemoverObject *)myself;
152152
if (self->key && self->dict) {
153-
if (-1 == PyDict_DelItem(self->dict, self->key))
154-
/* XXX Error context */
155-
PyErr_WriteUnraisable(Py_None);
153+
if (-1 == PyDict_DelItem(self->dict, self->key)) {
154+
_PyErr_WriteUnraisableMsg("on calling _ctypes.DictRemover", NULL);
155+
}
156156
Py_CLEAR(self->key);
157157
Py_CLEAR(self->dict);
158158
}

Modules/_testcapimodule.c

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4985,13 +4985,24 @@ negative_refcount(PyObject *self, PyObject *Py_UNUSED(args))
49854985
static PyObject*
49864986
test_write_unraisable_exc(PyObject *self, PyObject *args)
49874987
{
4988-
PyObject *exc, *obj;
4989-
if (!PyArg_ParseTuple(args, "OO", &exc, &obj)) {
4988+
PyObject *exc, *err_msg, *obj;
4989+
if (!PyArg_ParseTuple(args, "OOO", &exc, &err_msg, &obj)) {
49904990
return NULL;
49914991
}
49924992

4993+
const char *err_msg_utf8;
4994+
if (err_msg != Py_None) {
4995+
err_msg_utf8 = PyUnicode_AsUTF8(err_msg);
4996+
if (err_msg_utf8 == NULL) {
4997+
return NULL;
4998+
}
4999+
}
5000+
else {
5001+
err_msg_utf8 = NULL;
5002+
}
5003+
49935004
PyErr_SetObject((PyObject *)Py_TYPE(exc), exc);
4994-
PyErr_WriteUnraisable(obj);
5005+
_PyErr_WriteUnraisableMsg(err_msg_utf8, obj);
49955006
Py_RETURN_NONE;
49965007
}
49975008

Modules/gcmodule.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -929,9 +929,8 @@ delete_garbage(struct _gc_runtime_state *state,
929929
Py_INCREF(op);
930930
(void) clear(op);
931931
if (PyErr_Occurred()) {
932-
PySys_WriteStderr("Exception ignored in tp_clear of "
933-
"%.50s\n", Py_TYPE(op)->tp_name);
934-
PyErr_WriteUnraisable(NULL);
932+
_PyErr_WriteUnraisableMsg("in tp_clear of",
933+
(PyObject*)Py_TYPE(op));
935934
}
936935
Py_DECREF(op);
937936
}

Python/clinic/sysmodule.c.h

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/errors.c

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,7 @@ static PyStructSequence_Field UnraisableHookArgs_fields[] = {
10771077
{"exc_type", "Exception type"},
10781078
{"exc_value", "Exception value"},
10791079
{"exc_traceback", "Exception traceback"},
1080+
{"err_msg", "Error message"},
10801081
{"object", "Object causing the exception"},
10811082
{0}
10821083
};
@@ -1085,7 +1086,7 @@ static PyStructSequence_Desc UnraisableHookArgs_desc = {
10851086
.name = "UnraisableHookArgs",
10861087
.doc = UnraisableHookArgs__doc__,
10871088
.fields = UnraisableHookArgs_fields,
1088-
.n_in_sequence = 4
1089+
.n_in_sequence = 5
10891090
};
10901091

10911092

@@ -1104,7 +1105,8 @@ _PyErr_Init(void)
11041105

11051106
static PyObject *
11061107
make_unraisable_hook_args(PyThreadState *tstate, PyObject *exc_type,
1107-
PyObject *exc_value, PyObject *exc_tb, PyObject *obj)
1108+
PyObject *exc_value, PyObject *exc_tb,
1109+
PyObject *err_msg, PyObject *obj)
11081110
{
11091111
PyObject *args = PyStructSequence_New(&UnraisableHookArgsType);
11101112
if (args == NULL) {
@@ -1125,6 +1127,7 @@ make_unraisable_hook_args(PyThreadState *tstate, PyObject *exc_type,
11251127
ADD_ITEM(exc_type);
11261128
ADD_ITEM(exc_value);
11271129
ADD_ITEM(exc_tb);
1130+
ADD_ITEM(err_msg);
11281131
ADD_ITEM(obj);
11291132
#undef ADD_ITEM
11301133

@@ -1145,11 +1148,21 @@ make_unraisable_hook_args(PyThreadState *tstate, PyObject *exc_type,
11451148
static int
11461149
write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
11471150
PyObject *exc_value, PyObject *exc_tb,
1148-
PyObject *obj, PyObject *file)
1151+
PyObject *err_msg, PyObject *obj, PyObject *file)
11491152
{
11501153
if (obj != NULL && obj != Py_None) {
1151-
if (PyFile_WriteString("Exception ignored in: ", file) < 0) {
1152-
return -1;
1154+
if (err_msg != NULL && err_msg != Py_None) {
1155+
if (PyFile_WriteObject(err_msg, file, Py_PRINT_RAW) < 0) {
1156+
return -1;
1157+
}
1158+
if (PyFile_WriteString(": ", file) < 0) {
1159+
return -1;
1160+
}
1161+
}
1162+
else {
1163+
if (PyFile_WriteString("Exception ignored in: ", file) < 0) {
1164+
return -1;
1165+
}
11531166
}
11541167

11551168
if (PyFile_WriteObject(obj, file, 0) < 0) {
@@ -1162,6 +1175,14 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
11621175
return -1;
11631176
}
11641177
}
1178+
else if (err_msg != NULL && err_msg != Py_None) {
1179+
if (PyFile_WriteObject(err_msg, file, Py_PRINT_RAW) < 0) {
1180+
return -1;
1181+
}
1182+
if (PyFile_WriteString(":\n", file) < 0) {
1183+
return -1;
1184+
}
1185+
}
11651186

11661187
if (exc_tb != NULL && exc_tb != Py_None) {
11671188
if (PyTraceBack_Print(exc_tb, file) < 0) {
@@ -1178,8 +1199,9 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
11781199
const char *className = PyExceptionClass_Name(exc_type);
11791200
if (className != NULL) {
11801201
const char *dot = strrchr(className, '.');
1181-
if (dot != NULL)
1202+
if (dot != NULL) {
11821203
className = dot+1;
1204+
}
11831205
}
11841206

11851207
_Py_IDENTIFIER(__module__);
@@ -1238,7 +1260,8 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
12381260

12391261
static int
12401262
write_unraisable_exc(PyThreadState *tstate, PyObject *exc_type,
1241-
PyObject *exc_value, PyObject *exc_tb, PyObject *obj)
1263+
PyObject *exc_value, PyObject *exc_tb, PyObject *err_msg,
1264+
PyObject *obj)
12421265
{
12431266
PyObject *file = _PySys_GetObjectId(&PyId_stderr);
12441267
if (file == NULL || file == Py_None) {
@@ -1249,7 +1272,7 @@ write_unraisable_exc(PyThreadState *tstate, PyObject *exc_type,
12491272
while we use it */
12501273
Py_INCREF(file);
12511274
int res = write_unraisable_exc_file(tstate, exc_type, exc_value, exc_tb,
1252-
obj, file);
1275+
err_msg, obj, file);
12531276
Py_DECREF(file);
12541277

12551278
return res;
@@ -1272,9 +1295,10 @@ _PyErr_WriteUnraisableDefaultHook(PyObject *args)
12721295
PyObject *exc_type = PyStructSequence_GET_ITEM(args, 0);
12731296
PyObject *exc_value = PyStructSequence_GET_ITEM(args, 1);
12741297
PyObject *exc_tb = PyStructSequence_GET_ITEM(args, 2);
1275-
PyObject *obj = PyStructSequence_GET_ITEM(args, 3);
1298+
PyObject *err_msg = PyStructSequence_GET_ITEM(args, 3);
1299+
PyObject *obj = PyStructSequence_GET_ITEM(args, 4);
12761300

1277-
if (write_unraisable_exc(tstate, exc_type, exc_value, exc_tb, obj) < 0) {
1301+
if (write_unraisable_exc(tstate, exc_type, exc_value, exc_tb, err_msg, obj) < 0) {
12781302
return NULL;
12791303
}
12801304
Py_RETURN_NONE;
@@ -1287,13 +1311,18 @@ _PyErr_WriteUnraisableDefaultHook(PyObject *args)
12871311
for Python to handle it. For example, when a destructor raises an exception
12881312
or during garbage collection (gc.collect()).
12891313
1314+
If err_msg_str is non-NULL, the error message is formatted as:
1315+
"Exception ignored %s" % err_msg_str. Otherwise, use "Exception ignored in"
1316+
error message.
1317+
12901318
An exception must be set when calling this function. */
12911319
void
1292-
PyErr_WriteUnraisable(PyObject *obj)
1320+
_PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
12931321
{
12941322
PyThreadState *tstate = _PyThreadState_GET();
12951323
assert(tstate != NULL);
12961324

1325+
PyObject *err_msg = NULL;
12971326
PyObject *exc_type, *exc_value, *exc_tb;
12981327
_PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
12991328

@@ -1322,13 +1351,20 @@ PyErr_WriteUnraisable(PyObject *obj)
13221351
}
13231352
}
13241353

1354+
if (err_msg_str != NULL) {
1355+
err_msg = PyUnicode_FromFormat("Exception ignored %s", err_msg_str);
1356+
if (err_msg == NULL) {
1357+
PyErr_Clear();
1358+
}
1359+
}
1360+
13251361
_Py_IDENTIFIER(unraisablehook);
13261362
PyObject *hook = _PySys_GetObjectId(&PyId_unraisablehook);
13271363
if (hook != NULL && hook != Py_None) {
13281364
PyObject *hook_args;
13291365

13301366
hook_args = make_unraisable_hook_args(tstate, exc_type, exc_value,
1331-
exc_tb, obj);
1367+
exc_tb, err_msg, obj);
13321368
if (hook_args != NULL) {
13331369
PyObject *args[1] = {hook_args};
13341370
PyObject *res = _PyObject_FastCall(hook, args, 1);
@@ -1337,6 +1373,18 @@ PyErr_WriteUnraisable(PyObject *obj)
13371373
Py_DECREF(res);
13381374
goto done;
13391375
}
1376+
1377+
err_msg_str = "Exception ignored in sys.unraisablehook";
1378+
}
1379+
else {
1380+
err_msg_str = ("Exception ignored on building "
1381+
"sys.unraisablehook arguments");
1382+
}
1383+
1384+
Py_XDECREF(err_msg);
1385+
err_msg = PyUnicode_FromString(err_msg_str);
1386+
if (err_msg == NULL) {
1387+
PyErr_Clear();
13401388
}
13411389

13421390
/* sys.unraisablehook failed: log its error using default hook */
@@ -1350,15 +1398,25 @@ PyErr_WriteUnraisable(PyObject *obj)
13501398

13511399
default_hook:
13521400
/* Call the default unraisable hook (ignore failure) */
1353-
(void)write_unraisable_exc(tstate, exc_type, exc_value, exc_tb, obj);
1401+
(void)write_unraisable_exc(tstate, exc_type, exc_value, exc_tb,
1402+
err_msg, obj);
13541403

13551404
done:
13561405
Py_XDECREF(exc_type);
13571406
Py_XDECREF(exc_value);
13581407
Py_XDECREF(exc_tb);
1408+
Py_XDECREF(err_msg);
13591409
_PyErr_Clear(tstate); /* Just in case */
13601410
}
13611411

1412+
1413+
void
1414+
PyErr_WriteUnraisable(PyObject *obj)
1415+
{
1416+
_PyErr_WriteUnraisableMsg(NULL, obj);
1417+
}
1418+
1419+
13621420
extern PyObject *PyModule_GetWarningsModule(void);
13631421

13641422

0 commit comments

Comments
 (0)