From 4071691357394ba43a833537425e274d7ca3cd12 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 30 Oct 2023 19:01:03 +0200 Subject: [PATCH] gh-108082: C API: Add tests for PyErr_WriteUnraisable() (GH-111455) Also document the behavior when called with NULL. --- Doc/c-api/exceptions.rst | 8 +++++ Lib/test/test_capi/test_exceptions.py | 45 +++++++++++++++++++++++++++ Modules/_testcapi/exceptions.c | 17 ++++++++++ 3 files changed, 70 insertions(+) diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 2139da051e0193..f27e2bbfef05c5 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -88,9 +88,17 @@ 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. + .. versionchanged:: 3.4 + Print a traceback. Print only traceback if *obj* is ``NULL``. + + .. versionchanged:: 3.8 + Use :func:`sys.unraisablehook`. + + .. c:function:: void PyErr_DisplayException(PyObject *exc) Print the standard traceback display of ``exc`` to ``sys.stderr``, including diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py index b96cc7922a0ee7..1bff65b6559f0b 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,47 @@ 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) + class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase): diff --git a/Modules/_testcapi/exceptions.c b/Modules/_testcapi/exceptions.c index e463e6237099fe..aac672a3788ae1 100644 --- a/Modules/_testcapi/exceptions.c +++ b/Modules/_testcapi/exceptions.c @@ -303,6 +303,22 @@ _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) { + PyErr_SetRaisedException(Py_NewRef(exc)); + } + PyErr_WriteUnraisable(obj); + Py_RETURN_NONE; +} + /*[clinic input] _testcapi.unstable_exc_prep_reraise_star orig: object @@ -347,6 +363,7 @@ static PyTypeObject PyRecursingInfinitelyError_Type = { static PyMethodDef test_methods[] = { {"err_restore", err_restore, METH_VARARGS}, + {"err_writeunraisable", err_writeunraisable, METH_VARARGS}, _TESTCAPI_ERR_SET_RAISED_METHODDEF _TESTCAPI_EXCEPTION_PRINT_METHODDEF _TESTCAPI_FATAL_ERROR_METHODDEF