[Python-checkins] [3.12] gh-105071: add PyUnstable_Exc_PrepReraiseStar to expose except* implementation in the unstable API (GH-105072) (#105095)

iritkatriel webhook-mailer at python.org
Tue May 30 11:50:31 EDT 2023


https://github.com/python/cpython/commit/b45df737d43e809b1c41b9b2056ecb28c3cfb288
commit: b45df737d43e809b1c41b9b2056ecb28c3cfb288
branch: 3.12
author: Irit Katriel <1055913+iritkatriel at users.noreply.github.com>
committer: iritkatriel <1055913+iritkatriel at users.noreply.github.com>
date: 2023-05-30T16:50:23+01:00
summary:

[3.12] gh-105071: add PyUnstable_Exc_PrepReraiseStar to expose except* implementation in the unstable API (GH-105072) (#105095)

(cherry picked from commit b7aadb4583b040ddc8564896b91f4e5e571c82d6)

files:
A Misc/NEWS.d/next/C API/2023-05-30-10-15-13.gh-issue-105071.dPtp7c.rst
M Doc/c-api/exceptions.rst
M Include/cpython/pyerrors.h
M Lib/test/test_capi/test_exceptions.py
M Modules/_testcapi/clinic/exceptions.c.h
M Modules/_testcapi/exceptions.c
M Objects/exceptions.c

diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst
index 4ed96f01dbbc..22666d705290 100644
--- a/Doc/c-api/exceptions.rst
+++ b/Doc/c-api/exceptions.rst
@@ -772,6 +772,16 @@ Exception Objects
 
    Set :attr:`~BaseException.args` of exception *ex* to *args*.
 
+.. c:function:: PyObject* PyUnstable_Exc_PrepReraiseStar(PyObject *orig, PyObject *excs)
+
+   Implement part of the interpreter's implementation of :keyword:`!except*`.
+   *orig* is the original exception that was caught, and *excs* is the list of
+   the exceptions that need to be raised. This list contains the the unhandled
+   part of *orig*, if any, as well as the exceptions that were raised from the
+   :keyword:`!except*` clauses (so they have a different traceback from *orig*) and
+   those that were reraised (and have the same traceback as *orig*).
+   Return the :exc:`ExceptionGroup` that needs to be reraised in the end, or
+   ``None`` if there is nothing to reraise.
 
 .. _unicodeexceptions:
 
diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h
index 758804ade2ba..156665cbdb1b 100644
--- a/Include/cpython/pyerrors.h
+++ b/Include/cpython/pyerrors.h
@@ -116,6 +116,10 @@ PyAPI_FUNC(int) _PyException_AddNote(
      PyObject *exc,
      PyObject *note);
 
+PyAPI_FUNC(PyObject*) PyUnstable_Exc_PrepReraiseStar(
+     PyObject *orig,
+     PyObject *excs);
+
 /* In signalmodule.c */
 
 int PySignal_SetWakeupFd(int fd);
diff --git a/Lib/test/test_capi/test_exceptions.py b/Lib/test/test_capi/test_exceptions.py
index 1081f40b6981..118b575cba6d 100644
--- a/Lib/test/test_capi/test_exceptions.py
+++ b/Lib/test/test_capi/test_exceptions.py
@@ -5,6 +5,7 @@
 from test import support
 from test.support import import_helper
 from test.support.script_helper import assert_python_failure
+from test.support.testcase import ExceptionIsLikeMixin
 
 from .test_misc import decode_stderr
 
@@ -189,5 +190,97 @@ def __repr__(self):
                          'Normalization failed: type=Broken args=<unknown>')
 
 
+class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase):
+
+    def setUp(self):
+        super().setUp()
+        try:
+            raise ExceptionGroup("eg", [TypeError('bad type'), ValueError(42)])
+        except ExceptionGroup as e:
+            self.orig = e
+
+    def test_invalid_args(self):
+        with self.assertRaisesRegex(TypeError, "orig must be an exception"):
+            _testcapi.unstable_exc_prep_reraise_star(42, [None])
+
+        with self.assertRaisesRegex(TypeError, "excs must be a list"):
+            _testcapi.unstable_exc_prep_reraise_star(self.orig, 42)
+
+        with self.assertRaisesRegex(TypeError, "not an exception"):
+            _testcapi.unstable_exc_prep_reraise_star(self.orig, [TypeError(42), 42])
+
+        with self.assertRaisesRegex(ValueError, "orig must be a raised exception"):
+            _testcapi.unstable_exc_prep_reraise_star(ValueError(42), [TypeError(42)])
+
+        with self.assertRaisesRegex(ValueError, "orig must be a raised exception"):
+            _testcapi.unstable_exc_prep_reraise_star(ExceptionGroup("eg", [ValueError(42)]),
+                                                     [TypeError(42)])
+
+
+    def test_nothing_to_reraise(self):
+        self.assertEqual(
+            _testcapi.unstable_exc_prep_reraise_star(self.orig, [None]), None)
+
+        try:
+            raise ValueError(42)
+        except ValueError as e:
+            orig = e
+        self.assertEqual(
+            _testcapi.unstable_exc_prep_reraise_star(orig, [None]), None)
+
+    def test_reraise_orig(self):
+        orig = self.orig
+        res = _testcapi.unstable_exc_prep_reraise_star(orig, [orig])
+        self.assertExceptionIsLike(res, orig)
+
+    def test_raise_orig_parts(self):
+        orig = self.orig
+        match, rest = orig.split(TypeError)
+
+        test_cases = [
+            ([match, rest], orig),
+            ([rest, match], orig),
+            ([match], match),
+            ([rest], rest),
+            ([], None),
+        ]
+
+        for input, expected in test_cases:
+            with self.subTest(input=input):
+                res = _testcapi.unstable_exc_prep_reraise_star(orig, input)
+                self.assertExceptionIsLike(res, expected)
+
+
+    def test_raise_with_new_exceptions(self):
+        orig = self.orig
+
+        match, rest = orig.split(TypeError)
+        new1 = OSError('bad file')
+        new2 = RuntimeError('bad runtime')
+
+        test_cases = [
+            ([new1, match, rest], ExceptionGroup("", [new1, orig])),
+            ([match, new1, rest], ExceptionGroup("", [new1, orig])),
+            ([match, rest, new1], ExceptionGroup("", [new1, orig])),
+
+            ([new1, new2, match, rest], ExceptionGroup("", [new1, new2, orig])),
+            ([new1, match, new2, rest], ExceptionGroup("", [new1, new2, orig])),
+            ([new2, rest, match, new1], ExceptionGroup("", [new2, new1, orig])),
+            ([rest, new2, match, new1], ExceptionGroup("", [new2, new1, orig])),
+
+
+            ([new1, new2, rest], ExceptionGroup("", [new1, new2, rest])),
+            ([new1, match, new2], ExceptionGroup("", [new1, new2, match])),
+            ([rest, new2, new1], ExceptionGroup("", [new2, new1, rest])),
+            ([new1, new2], ExceptionGroup("", [new1, new2])),
+            ([new2, new1], ExceptionGroup("", [new2, new1])),
+        ]
+
+        for (input, expected) in test_cases:
+            with self.subTest(input=input):
+                res = _testcapi.unstable_exc_prep_reraise_star(orig, input)
+                self.assertExceptionIsLike(res, expected)
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/Misc/NEWS.d/next/C API/2023-05-30-10-15-13.gh-issue-105071.dPtp7c.rst b/Misc/NEWS.d/next/C API/2023-05-30-10-15-13.gh-issue-105071.dPtp7c.rst
new file mode 100644
index 000000000000..3d916fcb961f
--- /dev/null
+++ b/Misc/NEWS.d/next/C API/2023-05-30-10-15-13.gh-issue-105071.dPtp7c.rst	
@@ -0,0 +1 @@
+Add ``PyUnstable_Exc_PrepReraiseStar`` to the unstable C api to expose the implementation of :keyword:`except* <except_star>`.
diff --git a/Modules/_testcapi/clinic/exceptions.c.h b/Modules/_testcapi/clinic/exceptions.c.h
index 2cc4ef3dc0d4..01730ffa2ed0 100644
--- a/Modules/_testcapi/clinic/exceptions.c.h
+++ b/Modules/_testcapi/clinic/exceptions.c.h
@@ -395,4 +395,35 @@ _testcapi_traceback_print(PyObject *module, PyObject *const *args, Py_ssize_t na
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=ec1b2e62adea9846 input=a9049054013a1b77]*/
+
+PyDoc_STRVAR(_testcapi_unstable_exc_prep_reraise_star__doc__,
+"unstable_exc_prep_reraise_star($module, orig, excs, /)\n"
+"--\n"
+"\n"
+"To test PyUnstable_Exc_PrepReraiseStar.");
+
+#define _TESTCAPI_UNSTABLE_EXC_PREP_RERAISE_STAR_METHODDEF    \
+    {"unstable_exc_prep_reraise_star", _PyCFunction_CAST(_testcapi_unstable_exc_prep_reraise_star), METH_FASTCALL, _testcapi_unstable_exc_prep_reraise_star__doc__},
+
+static PyObject *
+_testcapi_unstable_exc_prep_reraise_star_impl(PyObject *module,
+                                              PyObject *orig, PyObject *excs);
+
+static PyObject *
+_testcapi_unstable_exc_prep_reraise_star(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+{
+    PyObject *return_value = NULL;
+    PyObject *orig;
+    PyObject *excs;
+
+    if (!_PyArg_CheckPositional("unstable_exc_prep_reraise_star", nargs, 2, 2)) {
+        goto exit;
+    }
+    orig = args[0];
+    excs = args[1];
+    return_value = _testcapi_unstable_exc_prep_reraise_star_impl(module, orig, excs);
+
+exit:
+    return return_value;
+}
+/*[clinic end generated code: output=fd6aef54f195c77b input=a9049054013a1b77]*/
diff --git a/Modules/_testcapi/exceptions.c b/Modules/_testcapi/exceptions.c
index 0a9902c135a7..a627bf1717fe 100644
--- a/Modules/_testcapi/exceptions.c
+++ b/Modules/_testcapi/exceptions.c
@@ -288,6 +288,22 @@ _testcapi_traceback_print_impl(PyObject *module, PyObject *traceback,
     Py_RETURN_NONE;
 }
 
+/*[clinic input]
+_testcapi.unstable_exc_prep_reraise_star
+    orig: object
+    excs: object
+    /
+To test PyUnstable_Exc_PrepReraiseStar.
+[clinic start generated code]*/
+
+static PyObject *
+_testcapi_unstable_exc_prep_reraise_star_impl(PyObject *module,
+                                              PyObject *orig, PyObject *excs)
+/*[clinic end generated code: output=850cf008e0563c77 input=27fbcda2203eb301]*/
+{
+    return PyUnstable_Exc_PrepReraiseStar(orig, excs);
+}
+
 
 /*
  * Define the PyRecurdingInfinitelyError_Type
@@ -328,6 +344,7 @@ static PyMethodDef test_methods[] = {
     _TESTCAPI_SET_EXCEPTION_METHODDEF
     _TESTCAPI_TRACEBACK_PRINT_METHODDEF
     _TESTCAPI_WRITE_UNRAISABLE_EXC_METHODDEF
+    _TESTCAPI_UNSTABLE_EXC_PREP_RERAISE_STAR_METHODDEF
     {NULL},
 };
 
diff --git a/Objects/exceptions.c b/Objects/exceptions.c
index a8d4e3a696ce..7bec7395cc7f 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -1351,7 +1351,10 @@ is_same_exception_metadata(PyObject *exc1, PyObject *exc2)
 PyObject *
 _PyExc_PrepReraiseStar(PyObject *orig, PyObject *excs)
 {
+    /* orig must be a raised & caught exception, so it has a traceback */
     assert(PyExceptionInstance_Check(orig));
+    assert(_PyBaseExceptionObject_cast(orig)->traceback != NULL);
+
     assert(PyList_Check(excs));
 
     Py_ssize_t numexcs = PyList_GET_SIZE(excs);
@@ -1438,6 +1441,42 @@ _PyExc_PrepReraiseStar(PyObject *orig, PyObject *excs)
     return result;
 }
 
+PyObject *
+PyUnstable_Exc_PrepReraiseStar(PyObject *orig, PyObject *excs)
+{
+    if (orig == NULL || !PyExceptionInstance_Check(orig)) {
+        PyErr_SetString(PyExc_TypeError, "orig must be an exception instance");
+        return NULL;
+    }
+    if (excs == NULL || !PyList_Check(excs)) {
+        PyErr_SetString(PyExc_TypeError,
+                        "excs must be a list of exception instances");
+        return NULL;
+    }
+    Py_ssize_t numexcs = PyList_GET_SIZE(excs);
+    for (Py_ssize_t i = 0; i < numexcs; i++) {
+        PyObject *exc = PyList_GET_ITEM(excs, i);
+        if (exc == NULL || !(PyExceptionInstance_Check(exc) || Py_IsNone(exc))) {
+            PyErr_Format(PyExc_TypeError,
+                         "item %d of excs is not an exception", i);
+            return NULL;
+        }
+    }
+
+    /* Make sure that orig has something as traceback, in the interpreter
+     * it always does becuase it's a raised exception.
+     */
+    PyObject *tb = PyException_GetTraceback(orig);
+
+    if (tb == NULL) {
+        PyErr_Format(PyExc_ValueError, "orig must be a raised exception");
+        return NULL;
+    }
+    Py_DECREF(tb);
+
+    return _PyExc_PrepReraiseStar(orig, excs);
+}
+
 static PyMemberDef BaseExceptionGroup_members[] = {
     {"message", T_OBJECT, offsetof(PyBaseExceptionGroupObject, msg), READONLY,
         PyDoc_STR("exception message")},



More information about the Python-checkins mailing list