gh-108082: Add PyErr_FormatUnraisable() function (GH-111086)

This commit is contained in:
Serhiy Storchaka 2023-10-31 23:42:44 +02:00 committed by GitHub
parent 453e96e302
commit f6a02327b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 8 deletions

View File

@ -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
==================

View File

@ -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`.)

View File

@ -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))

View File

@ -315,6 +315,63 @@ class Test_ErrSetAndRestore(unittest.TestCase):
# 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):

View File

@ -0,0 +1 @@
Add :c:func:`PyErr_FormatUnraisable` function.

View File

@ -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

View File

@ -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 @@ done:
_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);
}