Skip to content

Commit bca3305

Browse files
gh-108082: C API: Add tests for PyErr_WriteUnraisable() (GH-111455)
Also document the behavior when called with NULL.
1 parent 8eaa206 commit bca3305

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
@@ -303,6 +303,22 @@ _testcapi_traceback_print_impl(PyObject *module, PyObject *traceback,
303303
Py_RETURN_NONE;
304304
}
305305

306+
static PyObject *
307+
err_writeunraisable(PyObject *Py_UNUSED(module), PyObject *args)
308+
{
309+
PyObject *exc, *obj;
310+
if (!PyArg_ParseTuple(args, "OO", &exc, &obj)) {
311+
return NULL;
312+
}
313+
NULLABLE(exc);
314+
NULLABLE(obj);
315+
if (exc) {
316+
PyErr_SetRaisedException(Py_NewRef(exc));
317+
}
318+
PyErr_WriteUnraisable(obj);
319+
Py_RETURN_NONE;
320+
}
321+
306322
/*[clinic input]
307323
_testcapi.unstable_exc_prep_reraise_star
308324
orig: object
@@ -347,6 +363,7 @@ static PyTypeObject PyRecursingInfinitelyError_Type = {
347363

348364
static PyMethodDef test_methods[] = {
349365
{"err_restore", err_restore, METH_VARARGS},
366+
{"err_writeunraisable", err_writeunraisable, METH_VARARGS},
350367
_TESTCAPI_ERR_SET_RAISED_METHODDEF
351368
_TESTCAPI_EXCEPTION_PRINT_METHODDEF
352369
_TESTCAPI_FATAL_ERROR_METHODDEF

0 commit comments

Comments
 (0)