Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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