From 28e968bbee6574a49ae53c0642d9751408efffa2 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Tue, 30 May 2023 15:03:36 +0100 Subject: [PATCH] [3.12] gh-105071: add PyUnstable_Exc_PrepReraiseStar to expose except* implementation in the unstable API (GH-105072) (cherry picked from commit b7aadb4583b040ddc8564896b91f4e5e571c82d6) Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Doc/c-api/exceptions.rst | 10 ++ Include/cpython/pyerrors.h | 4 + Lib/test/test_capi/test_exceptions.py | 93 +++++++++++++++++++ ...-05-30-10-15-13.gh-issue-105071.dPtp7c.rst | 1 + Modules/_testcapi/clinic/exceptions.c.h | 33 ++++++- Modules/_testcapi/exceptions.c | 17 ++++ Objects/exceptions.c | 39 ++++++++ 7 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/C API/2023-05-30-10-15-13.gh-issue-105071.dPtp7c.rst diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 4ed96f01dbbc3e..22666d70529061 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 758804ade2baa7..156665cbdb1ba4 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 1081f40b6981af..118b575cba6df7 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=') +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 00000000000000..3d916fcb961f62 --- /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* `. diff --git a/Modules/_testcapi/clinic/exceptions.c.h b/Modules/_testcapi/clinic/exceptions.c.h index 2cc4ef3dc0d497..01730ffa2ed036 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 0a9902c135a7e5..a627bf1717fe0c 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 a8d4e3a696ce8e..7bec7395cc7f0b 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")},