Skip to content

Commit

Permalink
pythongh-108082: Add PyErr_FormatUnraisable() function
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka committed Oct 19, 2023
1 parent e9b5399 commit de02658
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 8 deletions.
13 changes: 13 additions & 0 deletions Doc/c-api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,29 @@ 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
chained exceptions and notes.
.. versionadded:: 3.12
Raising exceptions
==================
Expand Down
1 change: 1 addition & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------
Expand Down
3 changes: 3 additions & 0 deletions Include/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
102 changes: 102 additions & 0 deletions Lib/test/test_capi/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

NULL = None

class CustomError(Exception):
pass


class Test_Exceptions(unittest.TestCase):

def test_exception(self):
Expand Down Expand Up @@ -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):

Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :c:func:`PyErr_FormatUnraisable` function.
2 changes: 2 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2478,3 +2478,5 @@
added = '3.13'
[function.PySys_AuditTuple]
added = '3.13'
[function.PyErr_FormatUnraisable]
added = '3.13'
44 changes: 44 additions & 0 deletions Modules/_testcapi/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions PC/python3dll.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 40 additions & 8 deletions Python/errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
}


Expand Down

0 comments on commit de02658

Please sign in to comment.