Skip to content

Commit de02658

Browse files
pythongh-108082: Add PyErr_FormatUnraisable() function
1 parent e9b5399 commit de02658

11 files changed

Lines changed: 212 additions & 8 deletions

File tree

‎Doc/c-api/exceptions.rst‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,29 @@ 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+
96+
.. c:function:: void PyErr_FormatUnraisable(const char *format, ...)
97+
98+
Similar to :c:func:`PyErr_WriteUnraisable`, but the *format* and subsequent
99+
parameters help format the warning message; they have the same meaning and
100+
values as in :c:func:`PyUnicode_FromFormat`.
101+
If *format* is ``NULL``, only the traceback is printed.
102+
103+
.. versionadded:: 3.13
104+
105+
94106
.. c:function:: void PyErr_DisplayException(PyObject *exc)
95107
96108
Print the standard traceback display of ``exc`` to ``sys.stderr``, including
97109
chained exceptions and notes.
98110
99111
.. versionadded:: 3.12
100112
113+
101114
Raising exceptions
102115
==================
103116

‎Doc/data/stable_abi.dat‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Doc/whatsnew/3.13.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,10 @@ New Features
10681068
limited C API.
10691069
(Contributed by Victor Stinner in :gh:`85283`.)
10701070

1071+
* Add :c:func:`PyErr_FormatUnraisable` function: similar to
1072+
:c:func:`PyErr_WriteUnraisable`, but allow to customize the warning mesage.
1073+
(Contributed by Serhiy Storchaka in :gh:`108082`.)
1074+
10711075

10721076
Porting to Python 3.13
10731077
----------------------

‎Include/pyerrors.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ PyAPI_FUNC(PyObject *) PyErr_NewException(
231231
PyAPI_FUNC(PyObject *) PyErr_NewExceptionWithDoc(
232232
const char *name, const char *doc, PyObject *base, PyObject *dict);
233233
PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *);
234+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030D0000
235+
PyAPI_FUNC(void) PyErr_FormatUnraisable(const char *, ...);
236+
#endif
234237

235238

236239
/* In signalmodule.c */

‎Lib/test/test_capi/test_exceptions.py‎

Lines changed: 102 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,104 @@ 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+
318+
def test_err_formatunraisable(self):
319+
# Test PyErr_FormatUnraisable()
320+
formatunraisable = _testcapi.err_formatunraisable
321+
firstline = self.test_err_formatunraisable.__code__.co_firstlineno
322+
323+
with support.catch_unraisable_exception() as cm:
324+
formatunraisable(CustomError('oops!'), b'Error in %R', [])
325+
self.assertEqual(cm.unraisable.exc_type, CustomError)
326+
self.assertEqual(str(cm.unraisable.exc_value), 'oops!')
327+
self.assertEqual(cm.unraisable.exc_traceback.tb_lineno,
328+
firstline + 6)
329+
self.assertEqual(cm.unraisable.err_msg, 'Error in []')
330+
self.assertIsNone(cm.unraisable.object)
331+
332+
with support.catch_unraisable_exception() as cm:
333+
formatunraisable(CustomError('oops!'), b'undecodable \xff')
334+
self.assertEqual(cm.unraisable.exc_type, CustomError)
335+
self.assertEqual(str(cm.unraisable.exc_value), 'oops!')
336+
self.assertEqual(cm.unraisable.exc_traceback.tb_lineno,
337+
firstline + 15)
338+
self.assertIsNone(cm.unraisable.err_msg)
339+
self.assertIsNone(cm.unraisable.object)
340+
341+
with support.catch_unraisable_exception() as cm:
342+
formatunraisable(CustomError('oops!'), NULL)
343+
self.assertEqual(cm.unraisable.exc_type, CustomError)
344+
self.assertEqual(str(cm.unraisable.exc_value), 'oops!')
345+
self.assertEqual(cm.unraisable.exc_traceback.tb_lineno,
346+
firstline + 24)
347+
self.assertIsNone(cm.unraisable.err_msg)
348+
self.assertIsNone(cm.unraisable.object)
349+
350+
with (support.swap_attr(sys, 'unraisablehook', None),
351+
support.captured_stderr() as stderr):
352+
formatunraisable(CustomError('oops!'), b'Error in %R', [])
353+
lines = stderr.getvalue().splitlines()
354+
self.assertEqual(lines[0], f'Error in []:')
355+
self.assertEqual(lines[1], 'Traceback (most recent call last):')
356+
self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!')
357+
358+
with (support.swap_attr(sys, 'unraisablehook', None),
359+
support.captured_stderr() as stderr):
360+
formatunraisable(CustomError('oops!'), b'undecodable \xff')
361+
lines = stderr.getvalue().splitlines()
362+
self.assertEqual(lines[0], 'Traceback (most recent call last):')
363+
self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!')
364+
365+
with (support.swap_attr(sys, 'unraisablehook', None),
366+
support.captured_stderr() as stderr):
367+
formatunraisable(CustomError('oops!'), NULL)
368+
lines = stderr.getvalue().splitlines()
369+
self.assertEqual(lines[0], 'Traceback (most recent call last):')
370+
self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!')
371+
372+
# CRASHES formatunraisable(NULL, b'Error in %R', [])
373+
# CRASHES formatunraisable(NULL, NULL)
374+
273375

274376
class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase):
275377

‎Lib/test/test_stable_abi_ctypes.py‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add :c:func:`PyErr_FormatUnraisable` function.

‎Misc/stable_abi.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,3 +2478,5 @@
24782478
added = '3.13'
24792479
[function.PySys_AuditTuple]
24802480
added = '3.13'
2481+
[function.PyErr_FormatUnraisable]
2482+
added = '3.13'

‎Modules/_testcapi/exceptions.c‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,48 @@ _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+
Py_INCREF(exc);
317+
PyErr_SetRaisedException(exc);
318+
}
319+
PyErr_WriteUnraisable(obj);
320+
Py_RETURN_NONE;
321+
}
322+
323+
static PyObject *
324+
err_formatunraisable(PyObject *Py_UNUSED(module), PyObject *args)
325+
{
326+
PyObject *exc;
327+
const char *fmt;
328+
Py_ssize_t fmtlen;
329+
PyObject *objs[10] = {NULL};
330+
331+
if (!PyArg_ParseTuple(args, "Oz#|OOOOOOOOOO", &exc, &fmt, &fmtlen,
332+
&objs[0], &objs[1], &objs[2], &objs[3], &objs[4],
333+
&objs[5], &objs[6], &objs[7], &objs[8], &objs[9]))
334+
{
335+
return NULL;
336+
}
337+
NULLABLE(exc);
338+
if (exc) {
339+
Py_INCREF(exc);
340+
PyErr_SetRaisedException(exc);
341+
}
342+
PyErr_FormatUnraisable(fmt,
343+
objs[0], objs[1], objs[2], objs[3], objs[4],
344+
objs[5], objs[6], objs[7], objs[8], objs[9]);
345+
Py_RETURN_NONE;
346+
}
347+
306348
/*[clinic input]
307349
_testcapi.unstable_exc_prep_reraise_star
308350
orig: object
@@ -347,6 +389,8 @@ static PyTypeObject PyRecursingInfinitelyError_Type = {
347389

348390
static PyMethodDef test_methods[] = {
349391
{"err_restore", err_restore, METH_VARARGS},
392+
{"err_writeunraisable", err_writeunraisable, METH_VARARGS},
393+
{"err_formatunraisable", err_formatunraisable, METH_VARARGS},
350394
_TESTCAPI_ERR_SET_RAISED_METHODDEF
351395
_TESTCAPI_EXCEPTION_PRINT_METHODDEF
352396
_TESTCAPI_FATAL_ERROR_METHODDEF

‎PC/python3dll.c‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)