Skip to content

Commit

Permalink
gh-105071: add PyUnstable_Exc_PrepReraiseStar to expose except* imple…
Browse files Browse the repository at this point in the history
…mentation in the unstable API (#105072)
  • Loading branch information
iritkatriel authored May 30, 2023
1 parent bd98b65 commit b7aadb4
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 1 deletion.
10 changes: 10 additions & 0 deletions Doc/c-api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions Include/cpython/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
93 changes: 93 additions & 0 deletions Lib/test/test_capi/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``PyUnstable_Exc_PrepReraiseStar`` to the unstable C api to expose the implementation of :keyword:`except* <except_star>`.
33 changes: 32 additions & 1 deletion Modules/_testcapi/clinic/exceptions.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions Modules/_testcapi/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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},
};

Expand Down
39 changes: 39 additions & 0 deletions Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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")},
Expand Down

0 comments on commit b7aadb4

Please sign in to comment.