Skip to content

Commit b7aadb4

Browse files
authored
pythongh-105071: add PyUnstable_Exc_PrepReraiseStar to expose except* implementation in the unstable API (python#105072)
1 parent bd98b65 commit b7aadb4

File tree

7 files changed

+196
-1
lines changed

7 files changed

+196
-1
lines changed

Doc/c-api/exceptions.rst

+10
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,16 @@ Exception Objects
772772
773773
Set :attr:`~BaseException.args` of exception *ex* to *args*.
774774
775+
.. c:function:: PyObject* PyUnstable_Exc_PrepReraiseStar(PyObject *orig, PyObject *excs)
776+
777+
Implement part of the interpreter's implementation of :keyword:`!except*`.
778+
*orig* is the original exception that was caught, and *excs* is the list of
779+
the exceptions that need to be raised. This list contains the the unhandled
780+
part of *orig*, if any, as well as the exceptions that were raised from the
781+
:keyword:`!except*` clauses (so they have a different traceback from *orig*) and
782+
those that were reraised (and have the same traceback as *orig*).
783+
Return the :exc:`ExceptionGroup` that needs to be reraised in the end, or
784+
``None`` if there is nothing to reraise.
775785
776786
.. _unicodeexceptions:
777787

Include/cpython/pyerrors.h

+4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ PyAPI_FUNC(int) _PyException_AddNote(
116116
PyObject *exc,
117117
PyObject *note);
118118

119+
PyAPI_FUNC(PyObject*) PyUnstable_Exc_PrepReraiseStar(
120+
PyObject *orig,
121+
PyObject *excs);
122+
119123
/* In signalmodule.c */
120124

121125
int PySignal_SetWakeupFd(int fd);

Lib/test/test_capi/test_exceptions.py

+93
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from test import support
66
from test.support import import_helper
77
from test.support.script_helper import assert_python_failure
8+
from test.support.testcase import ExceptionIsLikeMixin
89

910
from .test_misc import decode_stderr
1011

@@ -189,5 +190,97 @@ def __repr__(self):
189190
'Normalization failed: type=Broken args=<unknown>')
190191

191192

193+
class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase):
194+
195+
def setUp(self):
196+
super().setUp()
197+
try:
198+
raise ExceptionGroup("eg", [TypeError('bad type'), ValueError(42)])
199+
except ExceptionGroup as e:
200+
self.orig = e
201+
202+
def test_invalid_args(self):
203+
with self.assertRaisesRegex(TypeError, "orig must be an exception"):
204+
_testcapi.unstable_exc_prep_reraise_star(42, [None])
205+
206+
with self.assertRaisesRegex(TypeError, "excs must be a list"):
207+
_testcapi.unstable_exc_prep_reraise_star(self.orig, 42)
208+
209+
with self.assertRaisesRegex(TypeError, "not an exception"):
210+
_testcapi.unstable_exc_prep_reraise_star(self.orig, [TypeError(42), 42])
211+
212+
with self.assertRaisesRegex(ValueError, "orig must be a raised exception"):
213+
_testcapi.unstable_exc_prep_reraise_star(ValueError(42), [TypeError(42)])
214+
215+
with self.assertRaisesRegex(ValueError, "orig must be a raised exception"):
216+
_testcapi.unstable_exc_prep_reraise_star(ExceptionGroup("eg", [ValueError(42)]),
217+
[TypeError(42)])
218+
219+
220+
def test_nothing_to_reraise(self):
221+
self.assertEqual(
222+
_testcapi.unstable_exc_prep_reraise_star(self.orig, [None]), None)
223+
224+
try:
225+
raise ValueError(42)
226+
except ValueError as e:
227+
orig = e
228+
self.assertEqual(
229+
_testcapi.unstable_exc_prep_reraise_star(orig, [None]), None)
230+
231+
def test_reraise_orig(self):
232+
orig = self.orig
233+
res = _testcapi.unstable_exc_prep_reraise_star(orig, [orig])
234+
self.assertExceptionIsLike(res, orig)
235+
236+
def test_raise_orig_parts(self):
237+
orig = self.orig
238+
match, rest = orig.split(TypeError)
239+
240+
test_cases = [
241+
([match, rest], orig),
242+
([rest, match], orig),
243+
([match], match),
244+
([rest], rest),
245+
([], None),
246+
]
247+
248+
for input, expected in test_cases:
249+
with self.subTest(input=input):
250+
res = _testcapi.unstable_exc_prep_reraise_star(orig, input)
251+
self.assertExceptionIsLike(res, expected)
252+
253+
254+
def test_raise_with_new_exceptions(self):
255+
orig = self.orig
256+
257+
match, rest = orig.split(TypeError)
258+
new1 = OSError('bad file')
259+
new2 = RuntimeError('bad runtime')
260+
261+
test_cases = [
262+
([new1, match, rest], ExceptionGroup("", [new1, orig])),
263+
([match, new1, rest], ExceptionGroup("", [new1, orig])),
264+
([match, rest, new1], ExceptionGroup("", [new1, orig])),
265+
266+
([new1, new2, match, rest], ExceptionGroup("", [new1, new2, orig])),
267+
([new1, match, new2, rest], ExceptionGroup("", [new1, new2, orig])),
268+
([new2, rest, match, new1], ExceptionGroup("", [new2, new1, orig])),
269+
([rest, new2, match, new1], ExceptionGroup("", [new2, new1, orig])),
270+
271+
272+
([new1, new2, rest], ExceptionGroup("", [new1, new2, rest])),
273+
([new1, match, new2], ExceptionGroup("", [new1, new2, match])),
274+
([rest, new2, new1], ExceptionGroup("", [new2, new1, rest])),
275+
([new1, new2], ExceptionGroup("", [new1, new2])),
276+
([new2, new1], ExceptionGroup("", [new2, new1])),
277+
]
278+
279+
for (input, expected) in test_cases:
280+
with self.subTest(input=input):
281+
res = _testcapi.unstable_exc_prep_reraise_star(orig, input)
282+
self.assertExceptionIsLike(res, expected)
283+
284+
192285
if __name__ == "__main__":
193286
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add ``PyUnstable_Exc_PrepReraiseStar`` to the unstable C api to expose the implementation of :keyword:`except* <except_star>`.

Modules/_testcapi/clinic/exceptions.c.h

+32-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_testcapi/exceptions.c

+17
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,22 @@ _testcapi_traceback_print_impl(PyObject *module, PyObject *traceback,
288288
Py_RETURN_NONE;
289289
}
290290

291+
/*[clinic input]
292+
_testcapi.unstable_exc_prep_reraise_star
293+
orig: object
294+
excs: object
295+
/
296+
To test PyUnstable_Exc_PrepReraiseStar.
297+
[clinic start generated code]*/
298+
299+
static PyObject *
300+
_testcapi_unstable_exc_prep_reraise_star_impl(PyObject *module,
301+
PyObject *orig, PyObject *excs)
302+
/*[clinic end generated code: output=850cf008e0563c77 input=27fbcda2203eb301]*/
303+
{
304+
return PyUnstable_Exc_PrepReraiseStar(orig, excs);
305+
}
306+
291307

292308
/*
293309
* Define the PyRecurdingInfinitelyError_Type
@@ -328,6 +344,7 @@ static PyMethodDef test_methods[] = {
328344
_TESTCAPI_SET_EXCEPTION_METHODDEF
329345
_TESTCAPI_TRACEBACK_PRINT_METHODDEF
330346
_TESTCAPI_WRITE_UNRAISABLE_EXC_METHODDEF
347+
_TESTCAPI_UNSTABLE_EXC_PREP_RERAISE_STAR_METHODDEF
331348
{NULL},
332349
};
333350

Objects/exceptions.c

+39
Original file line numberDiff line numberDiff line change
@@ -1351,7 +1351,10 @@ is_same_exception_metadata(PyObject *exc1, PyObject *exc2)
13511351
PyObject *
13521352
_PyExc_PrepReraiseStar(PyObject *orig, PyObject *excs)
13531353
{
1354+
/* orig must be a raised & caught exception, so it has a traceback */
13541355
assert(PyExceptionInstance_Check(orig));
1356+
assert(_PyBaseExceptionObject_cast(orig)->traceback != NULL);
1357+
13551358
assert(PyList_Check(excs));
13561359

13571360
Py_ssize_t numexcs = PyList_GET_SIZE(excs);
@@ -1438,6 +1441,42 @@ _PyExc_PrepReraiseStar(PyObject *orig, PyObject *excs)
14381441
return result;
14391442
}
14401443

1444+
PyObject *
1445+
PyUnstable_Exc_PrepReraiseStar(PyObject *orig, PyObject *excs)
1446+
{
1447+
if (orig == NULL || !PyExceptionInstance_Check(orig)) {
1448+
PyErr_SetString(PyExc_TypeError, "orig must be an exception instance");
1449+
return NULL;
1450+
}
1451+
if (excs == NULL || !PyList_Check(excs)) {
1452+
PyErr_SetString(PyExc_TypeError,
1453+
"excs must be a list of exception instances");
1454+
return NULL;
1455+
}
1456+
Py_ssize_t numexcs = PyList_GET_SIZE(excs);
1457+
for (Py_ssize_t i = 0; i < numexcs; i++) {
1458+
PyObject *exc = PyList_GET_ITEM(excs, i);
1459+
if (exc == NULL || !(PyExceptionInstance_Check(exc) || Py_IsNone(exc))) {
1460+
PyErr_Format(PyExc_TypeError,
1461+
"item %d of excs is not an exception", i);
1462+
return NULL;
1463+
}
1464+
}
1465+
1466+
/* Make sure that orig has something as traceback, in the interpreter
1467+
* it always does becuase it's a raised exception.
1468+
*/
1469+
PyObject *tb = PyException_GetTraceback(orig);
1470+
1471+
if (tb == NULL) {
1472+
PyErr_Format(PyExc_ValueError, "orig must be a raised exception");
1473+
return NULL;
1474+
}
1475+
Py_DECREF(tb);
1476+
1477+
return _PyExc_PrepReraiseStar(orig, excs);
1478+
}
1479+
14411480
static PyMemberDef BaseExceptionGroup_members[] = {
14421481
{"message", T_OBJECT, offsetof(PyBaseExceptionGroupObject, msg), READONLY,
14431482
PyDoc_STR("exception message")},

0 commit comments

Comments
 (0)