Skip to content

Commit f6a0232

Browse files
pythongh-108082: Add PyErr_FormatUnraisable() function (pythonGH-111086)
1 parent 453e96e commit f6a0232

File tree

7 files changed

+142
-8
lines changed

7 files changed

+142
-8
lines changed

Doc/c-api/exceptions.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,26 @@ Printing and clearing
9999
Use :func:`sys.unraisablehook`.
100100
101101
102+
.. c:function:: void PyErr_FormatUnraisable(const char *format, ...)
103+
104+
Similar to :c:func:`PyErr_WriteUnraisable`, but the *format* and subsequent
105+
parameters help format the warning message; they have the same meaning and
106+
values as in :c:func:`PyUnicode_FromFormat`.
107+
``PyErr_WriteUnraisable(obj)`` is roughtly equivalent to
108+
``PyErr_FormatUnraisable("Exception ignored in: %R, obj)``.
109+
If *format* is ``NULL``, only the traceback is printed.
110+
111+
.. versionadded:: 3.13
112+
113+
102114
.. c:function:: void PyErr_DisplayException(PyObject *exc)
103115
104116
Print the standard traceback display of ``exc`` to ``sys.stderr``, including
105117
chained exceptions and notes.
106118
107119
.. versionadded:: 3.12
108120
121+
109122
Raising exceptions
110123
==================
111124

Doc/whatsnew/3.13.rst

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

1110+
* Add :c:func:`PyErr_FormatUnraisable` function: similar to
1111+
:c:func:`PyErr_WriteUnraisable`, but allow to customize the warning mesage.
1112+
(Contributed by Serhiy Storchaka in :gh:`108082`.)
1113+
11101114
* Add :c:func:`PyUnicode_AsUTF8` function to the limited C API.
11111115
(Contributed by Victor Stinner in :gh:`111089`.)
11121116

Include/cpython/pyerrors.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,6 @@ PyAPI_FUNC(void) _Py_NO_RETURN _Py_FatalErrorFunc(
120120
const char *func,
121121
const char *message);
122122

123+
PyAPI_FUNC(void) PyErr_FormatUnraisable(const char *, ...);
124+
123125
#define Py_FatalError(message) _Py_FatalErrorFunc(__func__, (message))

Lib/test/test_capi/test_exceptions.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,63 @@ def test_err_writeunraisable(self):
315315
# CRASHES writeunraisable(NULL, hex)
316316
# CRASHES writeunraisable(NULL, NULL)
317317

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+
318375

319376
class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase):
320377

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add :c:func:`PyErr_FormatUnraisable` function.

Modules/_testcapi/exceptions.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,30 @@ err_writeunraisable(PyObject *Py_UNUSED(module), PyObject *args)
319319
Py_RETURN_NONE;
320320
}
321321

322+
static PyObject *
323+
err_formatunraisable(PyObject *Py_UNUSED(module), PyObject *args)
324+
{
325+
PyObject *exc;
326+
const char *fmt;
327+
Py_ssize_t fmtlen;
328+
PyObject *objs[10] = {NULL};
329+
330+
if (!PyArg_ParseTuple(args, "Oz#|OOOOOOOOOO", &exc, &fmt, &fmtlen,
331+
&objs[0], &objs[1], &objs[2], &objs[3], &objs[4],
332+
&objs[5], &objs[6], &objs[7], &objs[8], &objs[9]))
333+
{
334+
return NULL;
335+
}
336+
NULLABLE(exc);
337+
if (exc) {
338+
PyErr_SetRaisedException(Py_NewRef(exc));
339+
}
340+
PyErr_FormatUnraisable(fmt,
341+
objs[0], objs[1], objs[2], objs[3], objs[4],
342+
objs[5], objs[6], objs[7], objs[8], objs[9]);
343+
Py_RETURN_NONE;
344+
}
345+
322346
/*[clinic input]
323347
_testcapi.unstable_exc_prep_reraise_star
324348
orig: object
@@ -364,6 +388,7 @@ static PyTypeObject PyRecursingInfinitelyError_Type = {
364388
static PyMethodDef test_methods[] = {
365389
{"err_restore", err_restore, METH_VARARGS},
366390
{"err_writeunraisable", err_writeunraisable, METH_VARARGS},
391+
{"err_formatunraisable", err_formatunraisable, METH_VARARGS},
367392
_TESTCAPI_ERR_SET_RAISED_METHODDEF
368393
_TESTCAPI_EXCEPTION_PRINT_METHODDEF
369394
_TESTCAPI_FATAL_ERROR_METHODDEF

Python/errors.c

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,14 +1569,16 @@ _PyErr_WriteUnraisableDefaultHook(PyObject *args)
15691569
for Python to handle it. For example, when a destructor raises an exception
15701570
or during garbage collection (gc.collect()).
15711571
1572-
If err_msg_str is non-NULL, the error message is formatted as:
1573-
"Exception ignored %s" % err_msg_str. Otherwise, use "Exception ignored in"
1574-
error message.
1572+
If format is non-NULL, the error message is formatted using format and
1573+
variable arguments as in PyUnicode_FromFormat().
1574+
Otherwise, use "Exception ignored in" error message.
15751575
15761576
An exception must be set when calling this function. */
1577-
void
1578-
_PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
1577+
1578+
static void
1579+
format_unraisable_v(const char *format, va_list va, PyObject *obj)
15791580
{
1581+
const char *err_msg_str;
15801582
PyThreadState *tstate = _PyThreadState_GET();
15811583
_Py_EnsureTstateNotNULL(tstate);
15821584

@@ -1610,8 +1612,8 @@ _PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
16101612
}
16111613
}
16121614

1613-
if (err_msg_str != NULL) {
1614-
err_msg = PyUnicode_FromFormat("Exception ignored %s", err_msg_str);
1615+
if (format != NULL) {
1616+
err_msg = PyUnicode_FromFormatV(format, va);
16151617
if (err_msg == NULL) {
16161618
PyErr_Clear();
16171619
}
@@ -1676,11 +1678,41 @@ _PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
16761678
_PyErr_Clear(tstate); /* Just in case */
16771679
}
16781680

1681+
void
1682+
PyErr_FormatUnraisable(const char *format, ...)
1683+
{
1684+
va_list va;
1685+
1686+
va_start(va, format);
1687+
format_unraisable_v(format, va, NULL);
1688+
va_end(va);
1689+
}
1690+
1691+
static void
1692+
format_unraisable(PyObject *obj, const char *format, ...)
1693+
{
1694+
va_list va;
1695+
1696+
va_start(va, format);
1697+
format_unraisable_v(format, va, obj);
1698+
va_end(va);
1699+
}
1700+
1701+
void
1702+
_PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
1703+
{
1704+
if (err_msg_str) {
1705+
format_unraisable(obj, "Exception ignored %s", err_msg_str);
1706+
}
1707+
else {
1708+
format_unraisable(obj, NULL);
1709+
}
1710+
}
16791711

16801712
void
16811713
PyErr_WriteUnraisable(PyObject *obj)
16821714
{
1683-
_PyErr_WriteUnraisableMsg(NULL, obj);
1715+
format_unraisable(obj, NULL);
16841716
}
16851717

16861718

0 commit comments

Comments
 (0)