diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index f27e2bbfef05c59..a3a63b38c432f2b 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -99,6 +99,18 @@ Printing and clearing Use :func:`sys.unraisablehook`. +.. c:function:: void PyErr_FormatUnraisable(const char *format, ...) + + Similar to :c:func:`PyErr_WriteUnraisable`, but the *format* and subsequent + parameters help format the warning message; they have the same meaning and + values as in :c:func:`PyUnicode_FromFormat`. + ``PyErr_WriteUnraisable(obj)`` is roughtly equivalent to + ``PyErr_FormatUnraisable("Exception ignored in: %R, obj)``. + If *format* is ``NULL``, only the traceback is printed. + + .. versionadded:: 3.13 + + .. c:function:: void PyErr_DisplayException(PyObject *exc) Print the standard traceback display of ``exc`` to ``sys.stderr``, including @@ -106,6 +118,7 @@ Printing and clearing .. versionadded:: 3.12 + Raising exceptions ================== diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 197790234a1582f..9181685736575dc 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -1107,6 +1107,10 @@ New Features limited C API. (Contributed by Victor Stinner in :gh:`85283`.) +* Add :c:func:`PyErr_FormatUnraisable` function: similar to + :c:func:`PyErr_WriteUnraisable`, but allow to customize the warning mesage. + (Contributed by Serhiy Storchaka in :gh:`108082`.) + * Add :c:func:`PyUnicode_AsUTF8` function to the limited C API. (Contributed by Victor Stinner in :gh:`111089`.) diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h index da96eec4b35aabd..479b908fb7058ac 100644 --- a/Include/cpython/pyerrors.h +++ b/Include/cpython/pyerrors.h @@ -120,4 +120,6 @@ PyAPI_FUNC(void) _Py_NO_RETURN _Py_FatalErrorFunc( const char *func, const char *message); +PyAPI_FUNC(void) PyErr_FormatUnraisable(const char *, ...); + #define Py_FatalError(message) _Py_FatalErrorFunc(__func__, (message)) diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py index 1bff65b6559f0b0..1d158e3586e98dc 100644 --- a/Lib/test/test_capi/test_exceptions.py +++ b/Lib/test/test_capi/test_exceptions.py @@ -315,6 +315,63 @@ def test_err_writeunraisable(self): # CRASHES writeunraisable(NULL, hex) # CRASHES writeunraisable(NULL, NULL) + def test_err_formatunraisable(self): + # Test PyErr_FormatUnraisable() + formatunraisable = _testcapi.err_formatunraisable + firstline = self.test_err_formatunraisable.__code__.co_firstlineno + + with support.catch_unraisable_exception() as cm: + formatunraisable(CustomError('oops!'), b'Error in %R', []) + self.assertEqual(cm.unraisable.exc_type, CustomError) + self.assertEqual(str(cm.unraisable.exc_value), 'oops!') + self.assertEqual(cm.unraisable.exc_traceback.tb_lineno, + firstline + 6) + self.assertEqual(cm.unraisable.err_msg, 'Error in []') + self.assertIsNone(cm.unraisable.object) + + with support.catch_unraisable_exception() as cm: + formatunraisable(CustomError('oops!'), b'undecodable \xff') + self.assertEqual(cm.unraisable.exc_type, CustomError) + self.assertEqual(str(cm.unraisable.exc_value), 'oops!') + self.assertEqual(cm.unraisable.exc_traceback.tb_lineno, + firstline + 15) + self.assertIsNone(cm.unraisable.err_msg) + self.assertIsNone(cm.unraisable.object) + + with support.catch_unraisable_exception() as cm: + formatunraisable(CustomError('oops!'), NULL) + self.assertEqual(cm.unraisable.exc_type, CustomError) + self.assertEqual(str(cm.unraisable.exc_value), 'oops!') + self.assertEqual(cm.unraisable.exc_traceback.tb_lineno, + firstline + 24) + self.assertIsNone(cm.unraisable.err_msg) + self.assertIsNone(cm.unraisable.object) + + with (support.swap_attr(sys, 'unraisablehook', None), + support.captured_stderr() as stderr): + formatunraisable(CustomError('oops!'), b'Error in %R', []) + lines = stderr.getvalue().splitlines() + self.assertEqual(lines[0], f'Error in []:') + self.assertEqual(lines[1], 'Traceback (most recent call last):') + self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!') + + with (support.swap_attr(sys, 'unraisablehook', None), + support.captured_stderr() as stderr): + formatunraisable(CustomError('oops!'), b'undecodable \xff') + lines = stderr.getvalue().splitlines() + self.assertEqual(lines[0], 'Traceback (most recent call last):') + self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!') + + with (support.swap_attr(sys, 'unraisablehook', None), + support.captured_stderr() as stderr): + formatunraisable(CustomError('oops!'), NULL) + lines = stderr.getvalue().splitlines() + self.assertEqual(lines[0], 'Traceback (most recent call last):') + self.assertEqual(lines[-1], f'{__name__}.CustomError: oops!') + + # CRASHES formatunraisable(NULL, b'Error in %R', []) + # CRASHES formatunraisable(NULL, NULL) + class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase): diff --git a/Misc/NEWS.d/next/C API/2023-10-19-22-39-24.gh-issue-108082.uJytvc.rst b/Misc/NEWS.d/next/C API/2023-10-19-22-39-24.gh-issue-108082.uJytvc.rst new file mode 100644 index 000000000000000..b99a829e3f2a52a --- /dev/null +++ b/Misc/NEWS.d/next/C API/2023-10-19-22-39-24.gh-issue-108082.uJytvc.rst @@ -0,0 +1 @@ +Add :c:func:`PyErr_FormatUnraisable` function. diff --git a/Modules/_testcapi/exceptions.c b/Modules/_testcapi/exceptions.c index aac672a3788ae14..42a9915143e6fa7 100644 --- a/Modules/_testcapi/exceptions.c +++ b/Modules/_testcapi/exceptions.c @@ -319,6 +319,30 @@ err_writeunraisable(PyObject *Py_UNUSED(module), PyObject *args) Py_RETURN_NONE; } +static PyObject * +err_formatunraisable(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *exc; + const char *fmt; + Py_ssize_t fmtlen; + PyObject *objs[10] = {NULL}; + + if (!PyArg_ParseTuple(args, "Oz#|OOOOOOOOOO", &exc, &fmt, &fmtlen, + &objs[0], &objs[1], &objs[2], &objs[3], &objs[4], + &objs[5], &objs[6], &objs[7], &objs[8], &objs[9])) + { + return NULL; + } + NULLABLE(exc); + if (exc) { + PyErr_SetRaisedException(Py_NewRef(exc)); + } + PyErr_FormatUnraisable(fmt, + objs[0], objs[1], objs[2], objs[3], objs[4], + objs[5], objs[6], objs[7], objs[8], objs[9]); + Py_RETURN_NONE; +} + /*[clinic input] _testcapi.unstable_exc_prep_reraise_star orig: object @@ -364,6 +388,7 @@ static PyTypeObject PyRecursingInfinitelyError_Type = { static PyMethodDef test_methods[] = { {"err_restore", err_restore, METH_VARARGS}, {"err_writeunraisable", err_writeunraisable, METH_VARARGS}, + {"err_formatunraisable", err_formatunraisable, METH_VARARGS}, _TESTCAPI_ERR_SET_RAISED_METHODDEF _TESTCAPI_EXCEPTION_PRINT_METHODDEF _TESTCAPI_FATAL_ERROR_METHODDEF diff --git a/Python/errors.c b/Python/errors.c index 15af39b10dc07ef..f75c3e1fbd3f6e6 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1569,14 +1569,16 @@ _PyErr_WriteUnraisableDefaultHook(PyObject *args) for Python to handle it. For example, when a destructor raises an exception or during garbage collection (gc.collect()). - If err_msg_str is non-NULL, the error message is formatted as: - "Exception ignored %s" % err_msg_str. Otherwise, use "Exception ignored in" - error message. + If format is non-NULL, the error message is formatted using format and + variable arguments as in PyUnicode_FromFormat(). + Otherwise, use "Exception ignored in" error message. An exception must be set when calling this function. */ -void -_PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj) + +static void +format_unraisable_v(const char *format, va_list va, PyObject *obj) { + const char *err_msg_str; PyThreadState *tstate = _PyThreadState_GET(); _Py_EnsureTstateNotNULL(tstate); @@ -1610,8 +1612,8 @@ _PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj) } } - if (err_msg_str != NULL) { - err_msg = PyUnicode_FromFormat("Exception ignored %s", err_msg_str); + if (format != NULL) { + err_msg = PyUnicode_FromFormatV(format, va); if (err_msg == NULL) { PyErr_Clear(); } @@ -1676,11 +1678,41 @@ _PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj) _PyErr_Clear(tstate); /* Just in case */ } +void +PyErr_FormatUnraisable(const char *format, ...) +{ + va_list va; + + va_start(va, format); + format_unraisable_v(format, va, NULL); + va_end(va); +} + +static void +format_unraisable(PyObject *obj, const char *format, ...) +{ + va_list va; + + va_start(va, format); + format_unraisable_v(format, va, obj); + va_end(va); +} + +void +_PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj) +{ + if (err_msg_str) { + format_unraisable(obj, "Exception ignored %s", err_msg_str); + } + else { + format_unraisable(obj, NULL); + } +} void PyErr_WriteUnraisable(PyObject *obj) { - _PyErr_WriteUnraisableMsg(NULL, obj); + format_unraisable(obj, NULL); }