]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-108082: C API: Add tests for PyErr_WriteUnraisable() (GH-111455)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 30 Oct 2023 17:01:03 +0000 (19:01 +0200)
committerGitHub <noreply@github.com>
Mon, 30 Oct 2023 17:01:03 +0000 (19:01 +0200)
Also document the behavior when called with NULL.

Doc/c-api/exceptions.rst
Lib/test/test_capi/test_exceptions.py
Modules/_testcapi/exceptions.c

index 2139da051e0193199f2ca4d72db29f25ff8286a9..f27e2bbfef05c59e5939a97a46e623dd40557bcc 100644 (file)
@@ -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
index b96cc7922a0ee711bd382ca2008997ea6751c4ae..1bff65b6559f0b0698f2dfaf9b4a29e884da29f9 100644 (file)
@@ -17,6 +17,10 @@ _testcapi = import_helper.import_module('_testcapi')
 
 NULL = None
 
+class CustomError(Exception):
+    pass
+
+
 class Test_Exceptions(unittest.TestCase):
 
     def test_exception(self):
@@ -270,6 +274,47 @@ class Test_ErrSetAndRestore(unittest.TestCase):
                          (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):
 
index e463e6237099fea52bf58510bbb03e0a836ea641..aac672a3788ae1423fca049c113a966c7ec6eded 100644 (file)
@@ -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