diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 2139da051e0193..bd3514f8157cb0 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -88,9 +88,21 @@ Printing and clearing The function is called with a single argument *obj* that identifies the context in which the unraisable exception occurred. If possible, the repr of *obj* will be printed in the warning message. + If *obj* is ``NULL``, only the traceback is printed. An exception must be set when calling this function. + +.. 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`. + 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 @@ -98,6 +110,7 @@ Printing and clearing .. versionadded:: 3.12 + Raising exceptions ================== diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 811b1bd84d2417..78be2ebba20403 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -138,6 +138,7 @@ function,PyErr_DisplayException,3.12,, function,PyErr_ExceptionMatches,3.2,, function,PyErr_Fetch,3.2,, function,PyErr_Format,3.2,, +function,PyErr_FormatUnraisable,3.13,, function,PyErr_FormatV,3.5,, function,PyErr_GetExcInfo,3.7,, function,PyErr_GetHandledException,3.11,, diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 358bd1747cd2c5..db893f12b58880 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -1068,6 +1068,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`.) + Porting to Python 3.13 ---------------------- diff --git a/Include/pyerrors.h b/Include/pyerrors.h index 5d0028c116e2d8..3f440c968183a0 100644 --- a/Include/pyerrors.h +++ b/Include/pyerrors.h @@ -231,6 +231,9 @@ PyAPI_FUNC(PyObject *) PyErr_NewException( PyAPI_FUNC(PyObject *) PyErr_NewExceptionWithDoc( const char *name, const char *doc, PyObject *base, PyObject *dict); PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *); +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030D0000 +PyAPI_FUNC(void) PyErr_FormatUnraisable(const char *, ...); +#endif /* In signalmodule.c */ diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py index b96cc7922a0ee7..1d158e3586e98d 100644 --- a/Lib/test/test_capi/test_exceptions.py +++ b/Lib/test/test_capi/test_exceptions.py @@ -17,6 +17,10 @@ NULL = None +class CustomError(Exception): + pass + + class Test_Exceptions(unittest.TestCase): def test_exception(self): @@ -270,6 +274,104 @@ def test_setfromerrnowithfilename(self): (ENOENT, 'No such file or directory', 'file')) # CRASHES setfromerrnowithfilename(ENOENT, NULL, b'error') + def test_err_writeunraisable(self): + # Test PyErr_WriteUnraisable() + writeunraisable = _testcapi.err_writeunraisable + firstline = self.test_err_writeunraisable.__code__.co_firstlineno + + with support.catch_unraisable_exception() as cm: + writeunraisable(CustomError('oops!'), hex) + 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.assertIsNone(cm.unraisable.err_msg) + self.assertEqual(cm.unraisable.object, hex) + + with support.catch_unraisable_exception() as cm: + writeunraisable(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 + 15) + self.assertIsNone(cm.unraisable.err_msg) + self.assertIsNone(cm.unraisable.object) + + with (support.swap_attr(sys, 'unraisablehook', None), + support.captured_stderr() as stderr): + writeunraisable(CustomError('oops!'), hex) + lines = stderr.getvalue().splitlines() + self.assertEqual(lines[0], f'Exception ignored in: {hex!r}') + 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): + writeunraisable(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 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/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index 6d5353c22764df..ec2e6afb87c3dd 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -171,6 +171,7 @@ def test_windows_feature_macros(self): "PyErr_ExceptionMatches", "PyErr_Fetch", "PyErr_Format", + "PyErr_FormatUnraisable", "PyErr_FormatV", "PyErr_GetExcInfo", "PyErr_GetHandledException", 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 00000000000000..b99a829e3f2a52 --- /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/Misc/stable_abi.toml b/Misc/stable_abi.toml index 75c260c8f1b2be..3133ac618acc8f 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2478,3 +2478,5 @@ added = '3.13' [function.PySys_AuditTuple] added = '3.13' +[function.PyErr_FormatUnraisable] + added = '3.13' diff --git a/Modules/_testcapi/exceptions.c b/Modules/_testcapi/exceptions.c index e463e6237099fe..4151a259755b0f 100644 --- a/Modules/_testcapi/exceptions.c +++ b/Modules/_testcapi/exceptions.c @@ -303,6 +303,48 @@ _testcapi_traceback_print_impl(PyObject *module, PyObject *traceback, Py_RETURN_NONE; } +static PyObject * +err_writeunraisable(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *exc, *obj; + if (!PyArg_ParseTuple(args, "OO", &exc, &obj)) { + return NULL; + } + NULLABLE(exc); + NULLABLE(obj); + if (exc) { + Py_INCREF(exc); + PyErr_SetRaisedException(exc); + } + PyErr_WriteUnraisable(obj); + 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) { + Py_INCREF(exc); + PyErr_SetRaisedException(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 @@ -347,6 +389,8 @@ 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/PC/python3dll.c b/PC/python3dll.c index d12889f44d65b6..77bbd7fd35938e 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -198,6 +198,7 @@ EXPORT_FUNC(PyErr_DisplayException) EXPORT_FUNC(PyErr_ExceptionMatches) EXPORT_FUNC(PyErr_Fetch) EXPORT_FUNC(PyErr_Format) +EXPORT_FUNC(PyErr_FormatUnraisable) EXPORT_FUNC(PyErr_FormatV) EXPORT_FUNC(PyErr_GetExcInfo) EXPORT_FUNC(PyErr_GetHandledException) diff --git a/Python/errors.c b/Python/errors.c index 15af39b10dc07e..35b1f76d9b76cd 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(PyObject *obj, const char *format, va_list va) { + 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(NULL, format, va); + va_end(va); +} + +static void +format_unraisable(PyObject *obj, const char *format, ...) +{ + va_list va; + + va_start(va, format); + format_unraisable_v(obj, format, va); + 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); }