]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-108082: Add PyErr_FormatUnraisable() function (GH-111086)
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 31 Oct 2023 21:42:44 +0000 (23:42 +0200)
committerGitHub <noreply@github.com>
Tue, 31 Oct 2023 21:42:44 +0000 (23:42 +0200)
Doc/c-api/exceptions.rst
Doc/whatsnew/3.13.rst
Include/cpython/pyerrors.h
Lib/test/test_capi/test_exceptions.py
Misc/NEWS.d/next/C API/2023-10-19-22-39-24.gh-issue-108082.uJytvc.rst [new file with mode: 0644]
Modules/_testcapi/exceptions.c
Python/errors.c

index f27e2bbfef05c59e5939a97a46e623dd40557bcc..a3a63b38c432f2b21897b88ca893e27b01483a4e 100644 (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
 ==================
 
index 197790234a1582f5ef4fe536cfaed0323dd99a6d..9181685736575dc89dfbef86fbb73fe0c94e3d93 100644 (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`.)
 
index da96eec4b35aabd6a03d73e97dc9e256f042e7a1..479b908fb7058ac5e70e2a3f25114df027ec0381 100644 (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))
index 1bff65b6559f0b0698f2dfaf9b4a29e884da29f9..1d158e3586e98dc6e5675f6351a86c40f2311ccb 100644 (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):
 
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 (file)
index 0000000..b99a829
--- /dev/null
@@ -0,0 +1 @@
+Add :c:func:`PyErr_FormatUnraisable` function.
index aac672a3788ae1423fca049c113a966c7ec6eded..42a9915143e6fa7eae3d0e7a56353c89a5d67f4d 100644 (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
index 15af39b10dc07eff438658e98cbe10e688f7cb81..f75c3e1fbd3f6e6c126cfe85e48c9ea5cc7e12ca 100644 (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);
 }