Skip to content

Commit 748bc48

Browse files
[3.12] gh-108082: C API: Add tests for PyErr_WriteUnraisable() (GH-111455) (GH-111507)
Also document the behavior when called with NULL. (cherry picked from commit bca3305) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent e5b6744 commit 748bc48

File tree

3 files changed

+70
-0
lines changed

3 files changed

+70
-0
lines changed

Doc/c-api/exceptions.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,17 @@ Printing and clearing
8888
The function is called with a single argument *obj* that identifies the context
8989
in which the unraisable exception occurred. If possible,
9090
the repr of *obj* will be printed in the warning message.
91+
If *obj* is ``NULL``, only the traceback is printed.
9192
9293
An exception must be set when calling this function.
9394
95+
.. versionchanged:: 3.4
96+
Print a traceback. Print only traceback if *obj* is ``NULL``.
97+
98+
.. versionchanged:: 3.8
99+
Use :func:`sys.unraisablehook`.
100+
101+
94102
.. c:function:: void PyErr_DisplayException(PyObject *exc)
95103
96104
Print the standard traceback display of ``exc`` to ``sys.stderr``, including

Lib/test/test_capi/test_exceptions.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
NULL = None
1919

20+
class CustomError(Exception):
21+
pass
22+
23+
2024
class Test_Exceptions(unittest.TestCase):
2125

2226
def test_exception(self):
@@ -270,6 +274,47 @@ def test_setfromerrnowithfilename(self):
270274
(ENOENT, 'No such file or directory', 'file'))
271275
# CRASHES setfromerrnowithfilename(ENOENT, NULL, b'error')
272276

277+
def test_err_writeunraisable(self):
278+
# Test PyErr_WriteUnraisable()
279+
writeunraisable = _testcapi.err_writeunraisable
280+
firstline = self.test_err_writeunraisable.__code__.co_firstlineno
281+
282+
with support.catch_unraisable_exception() as cm:
283+
writeunraisable(CustomError('oops!'), hex)
284+
self.assertEqual(cm.unraisable.exc_type, CustomError)
285+
self.assertEqual(str(cm.unraisable.exc_value), 'oops!')
286+
self.assertEqual(cm.unraisable.exc_traceback.tb_lineno,
287+
firstline + 6)
288+
self.assertIsNone(cm.unraisable.err_msg)
289+
self.assertEqual(cm.unraisable.object, hex)
290+
291+
with support.catch_unraisable_exception() as cm:
292+
writeunraisable(CustomError('oops!'), NULL)
293+
self.assertEqual(cm.unraisable.exc_type, CustomError)
294+
self.assertEqual(str(cm.unraisable.exc_value), 'oops!')
295+
self.assertEqual(cm.unraisable.exc_traceback.tb_lineno,
296+
firstline + 15)
297+
self.assertIsNone(cm.unraisable.err_msg)
298+
self.assertIsNone(cm.unraisable.object)
299+
300+
with (support.swap_attr(sys, 'unraisablehook', None),
301+
support.captured_stderr() as stderr):
302+
writeunraisable(CustomError('oops!'), hex)
303+
lines = stderr.getvalue().splitlines()
304+
self.assertEqual(lines[0], f'Exception ignored in: {hex!r}')
305+
self.assertEqual(lines[1], 'Traceback (most recent call last):')
306+
self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!')
307+
308+
with (support.swap_attr(sys, 'unraisablehook', None),
309+
support.captured_stderr() as stderr):
310+
writeunraisable(CustomError('oops!'), NULL)
311+
lines = stderr.getvalue().splitlines()
312+
self.assertEqual(lines[0], 'Traceback (most recent call last):')
313+
self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!')
314+
315+
# CRASHES writeunraisable(NULL, hex)
316+
# CRASHES writeunraisable(NULL, NULL)
317+
273318

274319
class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase):
275320

Modules/_testcapi/exceptions.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,22 @@ _testcapi_traceback_print_impl(PyObject *module, PyObject *traceback,
331331
Py_RETURN_NONE;
332332
}
333333

334+
static PyObject *
335+
err_writeunraisable(PyObject *Py_UNUSED(module), PyObject *args)
336+
{
337+
PyObject *exc, *obj;
338+
if (!PyArg_ParseTuple(args, "OO", &exc, &obj)) {
339+
return NULL;
340+
}
341+
NULLABLE(exc);
342+
NULLABLE(obj);
343+
if (exc) {
344+
PyErr_SetRaisedException(Py_NewRef(exc));
345+
}
346+
PyErr_WriteUnraisable(obj);
347+
Py_RETURN_NONE;
348+
}
349+
334350
/*[clinic input]
335351
_testcapi.unstable_exc_prep_reraise_star
336352
orig: object
@@ -375,6 +391,7 @@ static PyTypeObject PyRecursingInfinitelyError_Type = {
375391

376392
static PyMethodDef test_methods[] = {
377393
{"err_restore", err_restore, METH_VARARGS},
394+
{"err_writeunraisable", err_writeunraisable, METH_VARARGS},
378395
_TESTCAPI_ERR_SET_RAISED_METHODDEF
379396
_TESTCAPI_EXCEPTION_PRINT_METHODDEF
380397
_TESTCAPI_FATAL_ERROR_METHODDEF

0 commit comments

Comments
 (0)