From f25560ef12fd6b6b3df91fcf21da32cd029515d5 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Fri, 3 Nov 2023 23:17:06 -0400 Subject: [PATCH 01/11] Add sys._set_exception() to expose the `PyErr_SetHandledExecption` function Exposing this function allows Python code to clear/set the current exception context as returned by `sys.exception()`. It is prefixed by an underscore since this functionality is not intended to be accessed by user-written code. In order to allow `sys._set_exception(None)` to clear the current exception `_PyErr_GetTopmostException` was changed to consider `Py_None` a valid exception instead of an indication to keep traversing the stack like `NULL` is. Additionally, `PyErr_SetHandledException` was updated to add `Py_None` to the exception stack instead of `NULL`. Put together, these changes allow `PyErr_SetHandledException(NULL)` to mask the entire exception chain and "clear" the current exception state as documented in `Doc/c-api/exceptions.rst`. Furthermore, since `sys._set_exception()` uses `PyErr_SetHandledException`, this allows `sys._set_exception(None)` to clear the current exception context instead of just exposing the next exception in the stack. --- Lib/test/test_sys.py | 117 ++++++++++++++++++ ...-12-19-17-27-08.gh-issue-111375.XeHOl_.rst | 2 + Python/clinic/sysmodule.c.h | 65 +++++++++- Python/errors.c | 3 +- Python/sysmodule.c | 27 ++++ 5 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 02c70403185f60d..aa5c960006940f5 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -160,6 +160,123 @@ def f(): self.assertIsInstance(e, ValueError) self.assertIs(exc, e) +class SetExceptionTests(unittest.TestCase): + + def tearDown(self): + # make sure we don't leave the global exception set + sys._set_exception(None); + + def test_set_exc_invalid_values(self): + for x in (0, "1", b"2"): + with self.assertRaises(TypeError): + sys._set_exception(x); + + def test_clear_exc(self): + try: + raise ValueError() + except ValueError: + self.assertIsInstance(sys.exception(), ValueError) + sys._set_exception(None) + self.assertIsNone(sys.exception()) + + def test_set_exc(self): + exc = ValueError() + self.assertIsNone(sys.exception()) + sys._set_exception(exc) + self.assertIs(sys.exception(), exc) + + def test_set_exc_replaced_by_new_exception_and_restored(self): + exc = ValueError() + sys._set_exception(exc) + self.assertIs(sys.exception(), exc) + try: + raise TypeError() + except TypeError: + self.assertIsInstance(sys.exception(), TypeError) + self.assertIs(sys.exception().__context__, exc) + + self.assertIs(sys.exception(), exc) + + def test_set_exc_popped_on_exit_except(self): + exc = ValueError() + try: + raise TypeError() + except TypeError: + self.assertIsInstance(sys.exception(), TypeError) + sys._set_exception(exc) + self.assertIs(sys.exception(), exc) + self.assertIsNone(sys.exception()) + + def test_cleared_exc_overridden_and_restored(self): + try: + raise ValueError() + except ValueError: + try: + raise TypeError() + except TypeError: + self.assertIsInstance(sys.exception(), TypeError) + sys._set_exception(None) + self.assertIsNone(sys.exception()) + try: + raise IndexError() + except IndexError: + self.assertIsInstance(sys.exception(), IndexError) + self.assertIsNone(sys.exception().__context__) + self.assertIsNone(sys.exception()) + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsNone(sys.exception()) + + def test_clear_exc_in_generator(self): + def inner(): + self.assertIsNone(sys.exception()) + yield + self.assertIsInstance(sys.exception(), ValueError) + sys._set_exception(None) + self.assertIsNone(sys.exception()) + yield + self.assertIsNone(sys.exception()) + + # with a single exception in exc_info stack + g = inner() + next(g) + try: + raise ValueError() + except: + self.assertIsInstance(sys.exception(), ValueError) + next(g) + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsNone(sys.exception()) + with self.assertRaises(StopIteration): + next(g) + self.assertIsNone(sys.exception()) + + # with multiple exceptions in exc_info stack by chaining generators + def outer(): + g = inner() + self.assertIsNone(sys.exception()) + yield next(g) + self.assertIsInstance(sys.exception(), TypeError) + try: + raise ValueError() + except: + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsInstance(sys.exception().__context__, TypeError) + yield next(g) + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsInstance(sys.exception(), TypeError) + + g = outer() + next(g) + try: + raise TypeError() + except: + self.assertIsInstance(sys.exception(), TypeError) + next(g) + self.assertIsInstance(sys.exception(), TypeError) + self.assertIsNone(sys.exception()) + with self.assertRaises(StopIteration): + next(g) + self.assertIsNone(sys.exception()) class ExceptHookTest(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst new file mode 100644 index 000000000000000..037211c1b20f50e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst @@ -0,0 +1,2 @@ +Add sys._set_exception() function that can set/clear the current exception +context. Patch by Carey Metcalfe. diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index 6609a88c1a9d58d..22b9ee005043d0f 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -178,6 +178,69 @@ sys_exception(PyObject *module, PyObject *Py_UNUSED(ignored)) return sys_exception_impl(module); } +PyDoc_STRVAR(sys__set_exception__doc__, +"_set_exception($module, /, exception)\n" +"--\n" +"\n" +"Set the current exception.\n" +"\n" +"Subsequent calls to sys.exception()/sys.exc_info() will return\n" +"the provided exception until another exception is raised in the\n" +"current thread or the execution stack returns to a frame where\n" +"another exception is being handled."); + +#define SYS__SET_EXCEPTION_METHODDEF \ + {"_set_exception", _PyCFunction_CAST(sys__set_exception), METH_FASTCALL|METH_KEYWORDS, sys__set_exception__doc__}, + +static PyObject * +sys__set_exception_impl(PyObject *module, PyObject *exception); + +static PyObject * +sys__set_exception(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(exception), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"exception", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "_set_exception", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject *exception; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + exception = args[0]; + return_value = sys__set_exception_impl(module, exception); + +exit: + return return_value; +} + PyDoc_STRVAR(sys_exc_info__doc__, "exc_info($module, /)\n" "--\n" @@ -2089,4 +2152,4 @@ _jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=ba849b6e4b9f1ba3 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=1aa1a448b567907b input=a9049054013a1b77]*/ diff --git a/Python/errors.c b/Python/errors.c index 48b03e5fd714b18..a1014bc6f1e0d41 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -121,7 +121,6 @@ _PyErr_GetTopmostException(PyThreadState *tstate) { exc_info = exc_info->previous_item; } - assert(!Py_IsNone(exc_info->exc_value)); return exc_info; } @@ -607,7 +606,7 @@ PyErr_GetHandledException(void) void _PyErr_SetHandledException(PyThreadState *tstate, PyObject *exc) { - Py_XSETREF(tstate->exc_info->exc_value, Py_XNewRef(exc == Py_None ? NULL : exc)); + Py_XSETREF(tstate->exc_info->exc_value, Py_XNewRef(exc ? exc : Py_None)); } void diff --git a/Python/sysmodule.c b/Python/sysmodule.c index b75d9e864a18dcb..5a394081991c8d9 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -854,6 +854,32 @@ sys_exception_impl(PyObject *module) Py_RETURN_NONE; } +/*[clinic input] +sys._set_exception + exception: object + +Set the current exception. + +Subsequent calls to sys.exception()/sys.exc_info() will return +the provided exception until another exception is raised in the +current thread or the execution stack returns to a frame where +another exception is being handled. +[clinic start generated code]*/ + +static PyObject * +sys__set_exception_impl(PyObject *module, PyObject *exception) +/*[clinic end generated code: output=39e119ee6b747085 input=46da3b45313a1cfa]*/ +{ + if (!Py_IsNone(exception) && !PyExceptionInstance_Check(exception)){ + PyErr_SetString( + PyExc_TypeError, + "must be an exception/None" + ); + return NULL; + } + PyErr_SetHandledException(exception); + Py_RETURN_NONE; +} /*[clinic input] sys.exc_info @@ -2891,6 +2917,7 @@ static PyMethodDef sys_methods[] = { SYS__CURRENT_EXCEPTIONS_METHODDEF SYS_DISPLAYHOOK_METHODDEF SYS_EXCEPTION_METHODDEF + SYS__SET_EXCEPTION_METHODDEF SYS_EXC_INFO_METHODDEF SYS_EXCEPTHOOK_METHODDEF SYS_EXIT_METHODDEF From c46ee7f2a925a290ebe0a7220e289a9f1131edf7 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Sun, 24 Dec 2023 14:33:50 -0500 Subject: [PATCH 02/11] Fix sys._set_exception() docstring wording --- Python/clinic/sysmodule.c.h | 4 ++-- Python/sysmodule.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index 22b9ee005043d0f..f5411f458e48353 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -185,7 +185,7 @@ PyDoc_STRVAR(sys__set_exception__doc__, "Set the current exception.\n" "\n" "Subsequent calls to sys.exception()/sys.exc_info() will return\n" -"the provided exception until another exception is raised in the\n" +"the provided exception until another exception is caught in the\n" "current thread or the execution stack returns to a frame where\n" "another exception is being handled."); @@ -2152,4 +2152,4 @@ _jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=1aa1a448b567907b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=756f746124b20c54 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 5a394081991c8d9..e47f04613b0f0b4 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -861,14 +861,14 @@ sys._set_exception Set the current exception. Subsequent calls to sys.exception()/sys.exc_info() will return -the provided exception until another exception is raised in the +the provided exception until another exception is caught in the current thread or the execution stack returns to a frame where another exception is being handled. [clinic start generated code]*/ static PyObject * sys__set_exception_impl(PyObject *module, PyObject *exception) -/*[clinic end generated code: output=39e119ee6b747085 input=46da3b45313a1cfa]*/ +/*[clinic end generated code: output=39e119ee6b747085 input=9c0495269be0821d]*/ { if (!Py_IsNone(exception) && !PyExceptionInstance_Check(exception)){ PyErr_SetString( From b67a8aa922057f77a48594171bfb920fc4f45b19 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Sun, 24 Dec 2023 15:15:08 -0500 Subject: [PATCH 03/11] Fix test case and add explanation in comment --- Lib/test/test_sys.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index aa5c960006940f5..72d5fb44977c73f 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -262,8 +262,16 @@ def outer(): self.assertIsInstance(sys.exception(), ValueError) self.assertIsInstance(sys.exception().__context__, TypeError) yield next(g) + # at this point the TypeError from the caller has been handled + # by the caller's except block. Even still, it should still be + # referenced as the __context__ of the current exception. self.assertIsInstance(sys.exception(), ValueError) - self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception().__context__, TypeError) + # not handling an exception, caller isn't handling one either + self.assertIsNone(sys.exception()) + with self.assertRaises(StopIteration): + next(g) + self.assertIsNone(sys.exception()) g = outer() next(g) From 9637f8b95328414af6e69b132d53ef81da17cde0 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Fri, 3 Nov 2023 01:16:21 -0400 Subject: [PATCH 04/11] Fix exception context in @contextmanagers Previously, when using a `try...yield...except` construct within a `@contextmanager` function, an exception raised by the `yield` wouldn't be properly cleared when handled the `except` block. Instead, calling `sys.exception()` would continue to return the exception, leading to confusing tracebacks and logs. This was happening due to the way that the `@contextmanager` decorator drives its decorated generator function. When an exception occurs, it uses `.throw(exc)` to throw the exception into the generator. However, even if the generator handles this exception, because the exception was thrown into it in the context of the `@contextmanager` decorator handling it (and not being finished yet), `sys.exception()` was not being reset. In order to fix this, the exception context as the `@contextmanager` decorator is `__enter__`'d is stored and set as the current exception context just before throwing the new exception into the generator. Doing this means that after the generator handles the thrown exception, `sys.exception()` reverts back to what it was when the `@contextmanager` function was started. --- Lib/contextlib.py | 12 +++ Lib/test/test_contextlib.py | 82 +++++++++++++++++++ ...-11-03-01-44-51.gh-issue-111375.qDBcCI.rst | 2 + 3 files changed, 96 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-11-03-01-44-51.gh-issue-111375.qDBcCI.rst diff --git a/Lib/contextlib.py b/Lib/contextlib.py index efc02bfa9243da6..0030d5b84e7f03e 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -168,6 +168,7 @@ class _GeneratorContextManagerBase: """Shared functionality for @contextmanager and @asynccontextmanager.""" def __init__(self, func, args, kwds): + self.exc_context = None self.gen = func(*args, **kwds) self.func, self.args, self.kwds = func, args, kwds # Issue 19330: ensure context manager instances have good docstrings @@ -196,6 +197,8 @@ class _GeneratorContextManager( """Helper for @contextmanager decorator.""" def __enter__(self): + # store the exception context on enter so it can be restored on exit + self.exc_context = sys.exception() # do not keep args and kwds alive unnecessarily # they are only needed for recreation, which is not possible anymore del self.args, self.kwds, self.func @@ -205,6 +208,9 @@ def __enter__(self): raise RuntimeError("generator didn't yield") from None def __exit__(self, typ, value, traceback): + # don't keep the stored exception alive unnecessarily + exc_context = self.exc_context + self.exc_context = None if typ is None: try: next(self.gen) @@ -221,6 +227,12 @@ def __exit__(self, typ, value, traceback): # tell if we get the same exception back value = typ() try: + # If the generator handles the exception thrown into it, the + # exception context will revert to the actual current exception + # context here. In order to make the context manager behave + # like a normal function we set the current exception context + # to what it was during the context manager's __enter__ + sys._set_exception(exc_context) self.gen.throw(value) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index e291f814edbd930..37d736aae8417cb 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -306,6 +306,88 @@ def woohoo(): with woohoo(): raise StopIteration + def test_contextmanager_handling_exception_resets_exc_info(self): + # Test that sys.exc_info() is correctly unset after handling the error + # when used within a context manager + + @contextmanager + def ctx(reraise=False): + try: + self.assertIsNone(sys.exception()) + yield + except: + self.assertIsInstance(sys.exception(), ZeroDivisionError) + if reraise: + raise + else: + self.assertIsNone(sys.exception()) + self.assertIsNone(sys.exception()) + + with ctx(): + pass + + with ctx(): + 1/0 + + with self.assertRaises(ZeroDivisionError): + with ctx(reraise=True): + 1/0 + + def test_contextmanager_while_handling(self): + # test that any exceptions currently being handled are preserved + # through the context manager + + @contextmanager + def ctx(reraise=False): + # called while handling an IndexError --> TypeError + self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception().__context__, IndexError) + exc_ctx = sys.exception() + try: + # raises a ValueError --> ZeroDivisionError + yield + except: + self.assertIsInstance(sys.exception(), ZeroDivisionError) + self.assertIsInstance(sys.exception().__context__, ValueError) + # original error context is preserved + self.assertIs(sys.exception().__context__.__context__, exc_ctx) + if reraise: + raise + + # inner error handled, context should now be the original context + self.assertIs(sys.exception(), exc_ctx) + + try: + raise IndexError() + except: + try: + raise TypeError() + except: + with ctx(): + try: + raise ValueError() + except: + self.assertIsInstance(sys.exception(), ValueError) + 1/0 + self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception(), IndexError) + + try: + raise IndexError() + except: + try: + raise TypeError() + except: + with self.assertRaises(ZeroDivisionError): + with ctx(reraise=True): + try: + raise ValueError() + except: + self.assertIsInstance(sys.exception(), ValueError) + 1/0 + self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception(), IndexError) + def _create_contextmanager_attribs(self): def attribs(**kw): def decorate(func): diff --git a/Misc/NEWS.d/next/Library/2023-11-03-01-44-51.gh-issue-111375.qDBcCI.rst b/Misc/NEWS.d/next/Library/2023-11-03-01-44-51.gh-issue-111375.qDBcCI.rst new file mode 100644 index 000000000000000..2519b7e614e9a33 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-11-03-01-44-51.gh-issue-111375.qDBcCI.rst @@ -0,0 +1,2 @@ +Fix handling of ``sys.exception()`` within ``@contextlib.contextmanager`` +functions. Patch by Carey Metcalfe. From 68c3034b83f0c1d5b77a011111b731bc2f70e7ea Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Sun, 24 Dec 2023 14:15:25 -0500 Subject: [PATCH 05/11] Reference GH issue in comment Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/contextlib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 0030d5b84e7f03e..e247a673563c636 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -228,10 +228,11 @@ def __exit__(self, typ, value, traceback): value = typ() try: # If the generator handles the exception thrown into it, the - # exception context will revert to the actual current exception + # exception context reverts to the actual current exception # context here. In order to make the context manager behave # like a normal function we set the current exception context # to what it was during the context manager's __enter__ + # (see gh-111676). sys._set_exception(exc_context) self.gen.throw(value) except StopIteration as exc: From 9433f1c3c3d46e33cb7add9f6a012acc9bfb5263 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Sun, 24 Dec 2023 14:27:55 -0500 Subject: [PATCH 06/11] Add more checks for exception context --- Lib/test/test_contextlib.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 37d736aae8417cb..91e602ff4e8cf45 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -364,12 +364,17 @@ def ctx(reraise=False): raise TypeError() except: with ctx(): + self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception().__context__, IndexError) try: raise ValueError() except: self.assertIsInstance(sys.exception(), ValueError) + self.assertIsInstance(sys.exception().__context__, TypeError) + self.assertIsInstance(sys.exception().__context__.__context__, IndexError) 1/0 self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception().__context__, IndexError) self.assertIsInstance(sys.exception(), IndexError) try: @@ -380,12 +385,17 @@ def ctx(reraise=False): except: with self.assertRaises(ZeroDivisionError): with ctx(reraise=True): + self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception().__context__, IndexError) try: raise ValueError() except: self.assertIsInstance(sys.exception(), ValueError) + self.assertIsInstance(sys.exception().__context__, TypeError) + self.assertIsInstance(sys.exception().__context__.__context__, IndexError) 1/0 self.assertIsInstance(sys.exception(), TypeError) + self.assertIsInstance(sys.exception().__context__, IndexError) self.assertIsInstance(sys.exception(), IndexError) def _create_contextmanager_attribs(self): From 5848beec9279d6eee2dea831d65aa93cc8de03f7 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Tue, 10 Jun 2025 09:17:10 -0400 Subject: [PATCH 07/11] Rename test case Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/test/test_contextlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 91e602ff4e8cf45..73c1bb7e32bafed 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -333,7 +333,7 @@ def ctx(reraise=False): with ctx(reraise=True): 1/0 - def test_contextmanager_while_handling(self): + def test_contextmanager_preserves_handled_exception(self): # test that any exceptions currently being handled are preserved # through the context manager From 3ddb38333e0110455c7792d4858c8803e601adef Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Mon, 18 May 2026 11:24:55 -0700 Subject: [PATCH 08/11] Update comment --- Lib/contextlib.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index e247a673563c636..14142e8c4e12b96 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -227,11 +227,17 @@ def __exit__(self, typ, value, traceback): # tell if we get the same exception back value = typ() try: - # If the generator handles the exception thrown into it, the - # exception context reverts to the actual current exception - # context here. In order to make the context manager behave - # like a normal function we set the current exception context - # to what it was during the context manager's __enter__ + # Throw the current exception into the generator so it can + # handle it. + # Once the generator handles the thrown exception, the + # exception context within it should revert back to what it was + # before its "yield" statement (ie. what it was in self.__enter__). + # However, since we're still currently handling the exception + # that we throw into the generator here, the exception context + # in the generator wouldn't change. + # To work around this, we forcefully set the current exception + # context to be what it was just before the generator's yield + # statement before throwing the current exception into it. # (see gh-111676). sys._set_exception(exc_context) self.gen.throw(value) From ca21f97f3155f3586ba479b3c001ae3d4d401504 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Mon, 18 May 2026 13:39:00 -0700 Subject: [PATCH 09/11] Move sys._set_exception() to _contextlib._setup_genmgr_ctx Since this is only used for a contextlib-specific hack, having it exposed in the sys module (even with an underscore) might not be a good idea. --- Lib/contextlib.py | 9 +- Lib/test/test_contextlib.py | 133 ++++++++++++++++++ Lib/test/test_sys.py | 125 ---------------- ...-12-19-17-27-08.gh-issue-111375.XeHOl_.rst | 2 - Modules/Setup.bootstrap.in | 1 + Modules/_contextlibmodule.c | 64 +++++++++ Modules/_contextlibmodules.c.h | 73 ++++++++++ Modules/clinic/_contextlibmodule.c.h | 73 ++++++++++ PCbuild/pythoncore.vcxproj | 1 + PCbuild/pythoncore.vcxproj.filters | 3 + Python/clinic/sysmodule.c.h | 65 +-------- Python/sysmodule.c | 27 ---- Tools/c-analyzer/cpython/_parser.py | 1 + 13 files changed, 358 insertions(+), 219 deletions(-) delete mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst create mode 100644 Modules/_contextlibmodule.c create mode 100644 Modules/_contextlibmodules.c.h create mode 100644 Modules/clinic/_contextlibmodule.c.h diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 14142e8c4e12b96..1660a81416aa81a 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -20,6 +20,12 @@ "chdir"] +try: + from _contextlib import _setup_genmgr_ctx +except ImportError: + _setup_genmgr_ctx = None + + class AbstractContextManager(abc.ABC): """An abstract base class for context managers.""" @@ -239,7 +245,8 @@ def __exit__(self, typ, value, traceback): # context to be what it was just before the generator's yield # statement before throwing the current exception into it. # (see gh-111676). - sys._set_exception(exc_context) + if _setup_genmgr_ctx is not None: + _setup_genmgr_ctx(exc_context) self.gen.throw(value) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 73c1bb7e32bafed..ea4f089edea22ba 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -1671,5 +1671,138 @@ def test_exception(self): self.assertEqual(os.getcwd(), old_cwd) +try: + from _contextlib import _setup_genmgr_ctx +except ImportError: + _setup_genmgr_ctx = None + +if _setup_genmgr_ctx: + class SetExceptionTests(unittest.TestCase): + + def tearDown(self): + # make sure we don't leave the global exception set + _setup_genmgr_ctx(None); + + def test_set_exc_invalid_values(self): + for x in (0, "1", b"2"): + with self.assertRaises(TypeError): + _setup_genmgr_ctx(x); + + def test_clear_exc(self): + try: + raise ValueError() + except ValueError: + self.assertIsInstance(sys.exception(), ValueError) + _setup_genmgr_ctx(None) + self.assertIsNone(sys.exception()) + + def test_set_exc(self): + exc = ValueError() + self.assertIsNone(sys.exception()) + _setup_genmgr_ctx(exc) + self.assertIs(sys.exception(), exc) + + def test_set_exc_replaced_by_new_exception_and_restored(self): + exc = ValueError() + _setup_genmgr_ctx(exc) + self.assertIs(sys.exception(), exc) + try: + raise TypeError() + except TypeError: + self.assertIsInstance(sys.exception(), TypeError) + self.assertIs(sys.exception().__context__, exc) + + self.assertIs(sys.exception(), exc) + + def test_set_exc_popped_on_exit_except(self): + exc = ValueError() + try: + raise TypeError() + except TypeError: + self.assertIsInstance(sys.exception(), TypeError) + _setup_genmgr_ctx(exc) + self.assertIs(sys.exception(), exc) + self.assertIsNone(sys.exception()) + + def test_cleared_exc_overridden_and_restored(self): + try: + raise ValueError() + except ValueError: + try: + raise TypeError() + except TypeError: + self.assertIsInstance(sys.exception(), TypeError) + _setup_genmgr_ctx(None) + self.assertIsNone(sys.exception()) + try: + raise IndexError() + except IndexError: + self.assertIsInstance(sys.exception(), IndexError) + self.assertIsNone(sys.exception().__context__) + self.assertIsNone(sys.exception()) + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsNone(sys.exception()) + + def test_clear_exc_in_generator(self): + def inner(): + self.assertIsNone(sys.exception()) + yield + self.assertIsInstance(sys.exception(), ValueError) + _setup_genmgr_ctx(None) + self.assertIsNone(sys.exception()) + yield + self.assertIsNone(sys.exception()) + + # with a single exception in exc_info stack + g = inner() + next(g) + try: + raise ValueError() + except: + self.assertIsInstance(sys.exception(), ValueError) + next(g) + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsNone(sys.exception()) + with self.assertRaises(StopIteration): + next(g) + self.assertIsNone(sys.exception()) + + # with multiple exceptions in exc_info stack by chaining generators + def outer(): + g = inner() + self.assertIsNone(sys.exception()) + yield next(g) + self.assertIsInstance(sys.exception(), TypeError) + try: + raise ValueError() + except: + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsInstance(sys.exception().__context__, TypeError) + yield next(g) + # at this point the TypeError from the caller has been handled + # by the caller's except block. Even still, it should still be + # referenced as the __context__ of the current exception. + self.assertIsInstance(sys.exception(), ValueError) + self.assertIsInstance(sys.exception().__context__, TypeError) + # not handling an exception, caller isn't handling one either + self.assertIsNone(sys.exception()) + with self.assertRaises(StopIteration): + next(g) + self.assertIsNone(sys.exception()) + + g = outer() + next(g) + try: + raise TypeError() + except: + self.assertIsInstance(sys.exception(), TypeError) + next(g) + self.assertIsInstance(sys.exception(), TypeError) + self.assertIsNone(sys.exception()) + with self.assertRaises(StopIteration): + next(g) + self.assertIsNone(sys.exception()) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 72d5fb44977c73f..02c70403185f60d 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -160,131 +160,6 @@ def f(): self.assertIsInstance(e, ValueError) self.assertIs(exc, e) -class SetExceptionTests(unittest.TestCase): - - def tearDown(self): - # make sure we don't leave the global exception set - sys._set_exception(None); - - def test_set_exc_invalid_values(self): - for x in (0, "1", b"2"): - with self.assertRaises(TypeError): - sys._set_exception(x); - - def test_clear_exc(self): - try: - raise ValueError() - except ValueError: - self.assertIsInstance(sys.exception(), ValueError) - sys._set_exception(None) - self.assertIsNone(sys.exception()) - - def test_set_exc(self): - exc = ValueError() - self.assertIsNone(sys.exception()) - sys._set_exception(exc) - self.assertIs(sys.exception(), exc) - - def test_set_exc_replaced_by_new_exception_and_restored(self): - exc = ValueError() - sys._set_exception(exc) - self.assertIs(sys.exception(), exc) - try: - raise TypeError() - except TypeError: - self.assertIsInstance(sys.exception(), TypeError) - self.assertIs(sys.exception().__context__, exc) - - self.assertIs(sys.exception(), exc) - - def test_set_exc_popped_on_exit_except(self): - exc = ValueError() - try: - raise TypeError() - except TypeError: - self.assertIsInstance(sys.exception(), TypeError) - sys._set_exception(exc) - self.assertIs(sys.exception(), exc) - self.assertIsNone(sys.exception()) - - def test_cleared_exc_overridden_and_restored(self): - try: - raise ValueError() - except ValueError: - try: - raise TypeError() - except TypeError: - self.assertIsInstance(sys.exception(), TypeError) - sys._set_exception(None) - self.assertIsNone(sys.exception()) - try: - raise IndexError() - except IndexError: - self.assertIsInstance(sys.exception(), IndexError) - self.assertIsNone(sys.exception().__context__) - self.assertIsNone(sys.exception()) - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsNone(sys.exception()) - - def test_clear_exc_in_generator(self): - def inner(): - self.assertIsNone(sys.exception()) - yield - self.assertIsInstance(sys.exception(), ValueError) - sys._set_exception(None) - self.assertIsNone(sys.exception()) - yield - self.assertIsNone(sys.exception()) - - # with a single exception in exc_info stack - g = inner() - next(g) - try: - raise ValueError() - except: - self.assertIsInstance(sys.exception(), ValueError) - next(g) - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsNone(sys.exception()) - with self.assertRaises(StopIteration): - next(g) - self.assertIsNone(sys.exception()) - - # with multiple exceptions in exc_info stack by chaining generators - def outer(): - g = inner() - self.assertIsNone(sys.exception()) - yield next(g) - self.assertIsInstance(sys.exception(), TypeError) - try: - raise ValueError() - except: - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsInstance(sys.exception().__context__, TypeError) - yield next(g) - # at this point the TypeError from the caller has been handled - # by the caller's except block. Even still, it should still be - # referenced as the __context__ of the current exception. - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsInstance(sys.exception().__context__, TypeError) - # not handling an exception, caller isn't handling one either - self.assertIsNone(sys.exception()) - with self.assertRaises(StopIteration): - next(g) - self.assertIsNone(sys.exception()) - - g = outer() - next(g) - try: - raise TypeError() - except: - self.assertIsInstance(sys.exception(), TypeError) - next(g) - self.assertIsInstance(sys.exception(), TypeError) - self.assertIsNone(sys.exception()) - with self.assertRaises(StopIteration): - next(g) - self.assertIsNone(sys.exception()) class ExceptHookTest(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst deleted file mode 100644 index 037211c1b20f50e..000000000000000 --- a/Misc/NEWS.d/next/Core_and_Builtins/2023-12-19-17-27-08.gh-issue-111375.XeHOl_.rst +++ /dev/null @@ -1,2 +0,0 @@ -Add sys._set_exception() function that can set/clear the current exception -context. Patch by Carey Metcalfe. diff --git a/Modules/Setup.bootstrap.in b/Modules/Setup.bootstrap.in index 65a1fefe72e92ec..a1c97500861c417 100644 --- a/Modules/Setup.bootstrap.in +++ b/Modules/Setup.bootstrap.in @@ -31,6 +31,7 @@ _weakref _weakref.c # commonly used core modules _abc _abc.c +_contextlib _contextlibmodule.c _functools _functoolsmodule.c _locale _localemodule.c _opcode _opcode.c diff --git a/Modules/_contextlibmodule.c b/Modules/_contextlibmodule.c new file mode 100644 index 000000000000000..2cbf5b51879f72d --- /dev/null +++ b/Modules/_contextlibmodule.c @@ -0,0 +1,64 @@ + +/* Contextlib module */ + +#include "Python.h" + +#include "clinic/_contextlibmodule.c.h" + +/*[clinic input] +module _contextlib +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=60c4760e25c64b5e]*/ + + +/*[clinic input] +_contextlib._setup_genmgr_ctx + exception: object + +Force the provided exception to be set as the current exception + +Subsequent calls to sys.exception()/sys.exc_info() will return +the provided exception until another exception is caught in the +current thread or the execution stack returns to a frame where +another exception is being handled. +[clinic start generated code]*/ + +static PyObject * +_contextlib__setup_genmgr_ctx_impl(PyObject *module, PyObject *exception) +/*[clinic end generated code: output=d8996a9f8f52204b input=d61410ba5659bf6f]*/ +{ + if (!Py_IsNone(exception) && !PyExceptionInstance_Check(exception)){ + PyErr_SetString( + PyExc_TypeError, + "must be an exception/None" + ); + return NULL; + } + PyErr_SetHandledException(exception); + Py_RETURN_NONE; +} + + +/* module level code ********************************************************/ + +PyDoc_STRVAR(_contextlib_doc, +"Helpers for contextlib."); + +static PyMethodDef contextlib_methods[] = { + _CONTEXTLIB__SETUP_GENMGR_CTX_METHODDEF + {NULL, NULL} /* sentinel */ +}; + +static struct PyModuleDef _contextlibmodule = { + PyModuleDef_HEAD_INIT, + .m_name = "_contextlib", + .m_doc = _contextlib_doc, + .m_size = -1, + .m_methods = contextlib_methods, +}; + +PyMODINIT_FUNC +PyInit__contextlib(void) +{ + return PyModule_Create(&_contextlibmodule); +} \ No newline at end of file diff --git a/Modules/_contextlibmodules.c.h b/Modules/_contextlibmodules.c.h new file mode 100644 index 000000000000000..9817725eb20d5aa --- /dev/null +++ b/Modules/_contextlibmodules.c.h @@ -0,0 +1,73 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +# include "pycore_runtime.h" // _Py_ID() +#endif +#include "pycore_modsupport.h" // _PyArg_UnpackKeywords() + +PyDoc_STRVAR(contextlib__setup_genmgr_ctx__doc__, +"_setup_genmgr_ctx($module, /, exception)\n" +"--\n" +"\n" +"Force the provided exception to be set as the current exception\n" +"\n" +"Subsequent calls to sys.exception()/sys.exc_info() will return\n" +"the provided exception until another exception is caught in the\n" +"current thread or the execution stack returns to a frame where\n" +"another exception is being handled."); + +#define CONTEXTLIB__SETUP_GENMGR_CTX_METHODDEF \ + {"_setup_genmgr_ctx", _PyCFunction_CAST(contextlib__setup_genmgr_ctx), METH_FASTCALL|METH_KEYWORDS, contextlib__setup_genmgr_ctx__doc__}, + +static PyObject * +contextlib__setup_genmgr_ctx_impl(PyObject *module, PyObject *exception); + +static PyObject * +contextlib__setup_genmgr_ctx(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(exception), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"exception", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "_setup_genmgr_ctx", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject *exception; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + exception = args[0]; + return_value = contextlib__setup_genmgr_ctx_impl(module, exception); + +exit: + return return_value; +} +/*[clinic end generated code: output=ec777026fc20d127 input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_contextlibmodule.c.h b/Modules/clinic/_contextlibmodule.c.h new file mode 100644 index 000000000000000..681edb4a974c275 --- /dev/null +++ b/Modules/clinic/_contextlibmodule.c.h @@ -0,0 +1,73 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +# include "pycore_runtime.h" // _Py_ID() +#endif +#include "pycore_modsupport.h" // _PyArg_UnpackKeywords() + +PyDoc_STRVAR(_contextlib__setup_genmgr_ctx__doc__, +"_setup_genmgr_ctx($module, /, exception)\n" +"--\n" +"\n" +"Force the provided exception to be set as the current exception\n" +"\n" +"Subsequent calls to sys.exception()/sys.exc_info() will return\n" +"the provided exception until another exception is caught in the\n" +"current thread or the execution stack returns to a frame where\n" +"another exception is being handled."); + +#define _CONTEXTLIB__SETUP_GENMGR_CTX_METHODDEF \ + {"_setup_genmgr_ctx", _PyCFunction_CAST(_contextlib__setup_genmgr_ctx), METH_FASTCALL|METH_KEYWORDS, _contextlib__setup_genmgr_ctx__doc__}, + +static PyObject * +_contextlib__setup_genmgr_ctx_impl(PyObject *module, PyObject *exception); + +static PyObject * +_contextlib__setup_genmgr_ctx(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(exception), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"exception", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "_setup_genmgr_ctx", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject *exception; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + exception = args[0]; + return_value = _contextlib__setup_genmgr_ctx_impl(module, exception); + +exit: + return return_value; +} +/*[clinic end generated code: output=3c01ac3e1fdf06bf input=a9049054013a1b77]*/ diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index e255ed5af19125d..68a148c37c93fbe 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -444,6 +444,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 649ee1859ff9961..6c24739e584b1c1 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -989,6 +989,9 @@ Modules + + Modules + Modules diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index f5411f458e48353..6609a88c1a9d58d 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -178,69 +178,6 @@ sys_exception(PyObject *module, PyObject *Py_UNUSED(ignored)) return sys_exception_impl(module); } -PyDoc_STRVAR(sys__set_exception__doc__, -"_set_exception($module, /, exception)\n" -"--\n" -"\n" -"Set the current exception.\n" -"\n" -"Subsequent calls to sys.exception()/sys.exc_info() will return\n" -"the provided exception until another exception is caught in the\n" -"current thread or the execution stack returns to a frame where\n" -"another exception is being handled."); - -#define SYS__SET_EXCEPTION_METHODDEF \ - {"_set_exception", _PyCFunction_CAST(sys__set_exception), METH_FASTCALL|METH_KEYWORDS, sys__set_exception__doc__}, - -static PyObject * -sys__set_exception_impl(PyObject *module, PyObject *exception); - -static PyObject * -sys__set_exception(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) -{ - PyObject *return_value = NULL; - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { &_Py_ID(exception), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"exception", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "_set_exception", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[1]; - PyObject *exception; - - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - exception = args[0]; - return_value = sys__set_exception_impl(module, exception); - -exit: - return return_value; -} - PyDoc_STRVAR(sys_exc_info__doc__, "exc_info($module, /)\n" "--\n" @@ -2152,4 +2089,4 @@ _jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=756f746124b20c54 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=ba849b6e4b9f1ba3 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index e47f04613b0f0b4..b75d9e864a18dcb 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -854,32 +854,6 @@ sys_exception_impl(PyObject *module) Py_RETURN_NONE; } -/*[clinic input] -sys._set_exception - exception: object - -Set the current exception. - -Subsequent calls to sys.exception()/sys.exc_info() will return -the provided exception until another exception is caught in the -current thread or the execution stack returns to a frame where -another exception is being handled. -[clinic start generated code]*/ - -static PyObject * -sys__set_exception_impl(PyObject *module, PyObject *exception) -/*[clinic end generated code: output=39e119ee6b747085 input=9c0495269be0821d]*/ -{ - if (!Py_IsNone(exception) && !PyExceptionInstance_Check(exception)){ - PyErr_SetString( - PyExc_TypeError, - "must be an exception/None" - ); - return NULL; - } - PyErr_SetHandledException(exception); - Py_RETURN_NONE; -} /*[clinic input] sys.exc_info @@ -2917,7 +2891,6 @@ static PyMethodDef sys_methods[] = { SYS__CURRENT_EXCEPTIONS_METHODDEF SYS_DISPLAYHOOK_METHODDEF SYS_EXCEPTION_METHODDEF - SYS__SET_EXCEPTION_METHODDEF SYS_EXC_INFO_METHODDEF SYS_EXCEPTHOOK_METHODDEF SYS_EXIT_METHODDEF diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index d7248c34c59be45..3ef310fc6e739cf 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -189,6 +189,7 @@ def format_tsv_lines(lines): ('Modules/_asynciomodule.c', 'Py_BUILD_CORE', '1'), ('Modules/_codecsmodule.c', 'Py_BUILD_CORE', '1'), ('Modules/_collectionsmodule.c', 'Py_BUILD_CORE', '1'), + ('Modules/_contextlibmodule.c', 'Py_BUILD_CORE', '1'), ('Modules/_ctypes/_ctypes.c', 'Py_BUILD_CORE', '1'), ('Modules/_ctypes/cfield.c', 'Py_BUILD_CORE', '1'), ('Modules/_cursesmodule.c', 'Py_BUILD_CORE', '1'), From d0afd15b80472af79f938a3ed29b128dd2fe9e8b Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Tue, 19 May 2026 15:11:29 -0700 Subject: [PATCH 10/11] Revert exposing a function that directly manipulates the exception state This is done as a separate commit to make reviewing the next commit (ie. the actual implementation) easier. If accepted, this commit should be squashed. --- Lib/contextlib.py | 8 -- Lib/test/test_contextlib.py | 133 --------------------------- Modules/Setup.bootstrap.in | 1 - Modules/_contextlibmodule.c | 64 ------------- Modules/_contextlibmodules.c.h | 73 --------------- Modules/clinic/_contextlibmodule.c.h | 73 --------------- PCbuild/pythoncore.vcxproj | 1 - PCbuild/pythoncore.vcxproj.filters | 3 - Tools/c-analyzer/cpython/_parser.py | 1 - 9 files changed, 357 deletions(-) delete mode 100644 Modules/_contextlibmodule.c delete mode 100644 Modules/_contextlibmodules.c.h delete mode 100644 Modules/clinic/_contextlibmodule.c.h diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 1660a81416aa81a..ef28cd2ae722bb5 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -20,12 +20,6 @@ "chdir"] -try: - from _contextlib import _setup_genmgr_ctx -except ImportError: - _setup_genmgr_ctx = None - - class AbstractContextManager(abc.ABC): """An abstract base class for context managers.""" @@ -245,8 +239,6 @@ def __exit__(self, typ, value, traceback): # context to be what it was just before the generator's yield # statement before throwing the current exception into it. # (see gh-111676). - if _setup_genmgr_ctx is not None: - _setup_genmgr_ctx(exc_context) self.gen.throw(value) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index ea4f089edea22ba..73c1bb7e32bafed 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -1671,138 +1671,5 @@ def test_exception(self): self.assertEqual(os.getcwd(), old_cwd) -try: - from _contextlib import _setup_genmgr_ctx -except ImportError: - _setup_genmgr_ctx = None - -if _setup_genmgr_ctx: - class SetExceptionTests(unittest.TestCase): - - def tearDown(self): - # make sure we don't leave the global exception set - _setup_genmgr_ctx(None); - - def test_set_exc_invalid_values(self): - for x in (0, "1", b"2"): - with self.assertRaises(TypeError): - _setup_genmgr_ctx(x); - - def test_clear_exc(self): - try: - raise ValueError() - except ValueError: - self.assertIsInstance(sys.exception(), ValueError) - _setup_genmgr_ctx(None) - self.assertIsNone(sys.exception()) - - def test_set_exc(self): - exc = ValueError() - self.assertIsNone(sys.exception()) - _setup_genmgr_ctx(exc) - self.assertIs(sys.exception(), exc) - - def test_set_exc_replaced_by_new_exception_and_restored(self): - exc = ValueError() - _setup_genmgr_ctx(exc) - self.assertIs(sys.exception(), exc) - try: - raise TypeError() - except TypeError: - self.assertIsInstance(sys.exception(), TypeError) - self.assertIs(sys.exception().__context__, exc) - - self.assertIs(sys.exception(), exc) - - def test_set_exc_popped_on_exit_except(self): - exc = ValueError() - try: - raise TypeError() - except TypeError: - self.assertIsInstance(sys.exception(), TypeError) - _setup_genmgr_ctx(exc) - self.assertIs(sys.exception(), exc) - self.assertIsNone(sys.exception()) - - def test_cleared_exc_overridden_and_restored(self): - try: - raise ValueError() - except ValueError: - try: - raise TypeError() - except TypeError: - self.assertIsInstance(sys.exception(), TypeError) - _setup_genmgr_ctx(None) - self.assertIsNone(sys.exception()) - try: - raise IndexError() - except IndexError: - self.assertIsInstance(sys.exception(), IndexError) - self.assertIsNone(sys.exception().__context__) - self.assertIsNone(sys.exception()) - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsNone(sys.exception()) - - def test_clear_exc_in_generator(self): - def inner(): - self.assertIsNone(sys.exception()) - yield - self.assertIsInstance(sys.exception(), ValueError) - _setup_genmgr_ctx(None) - self.assertIsNone(sys.exception()) - yield - self.assertIsNone(sys.exception()) - - # with a single exception in exc_info stack - g = inner() - next(g) - try: - raise ValueError() - except: - self.assertIsInstance(sys.exception(), ValueError) - next(g) - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsNone(sys.exception()) - with self.assertRaises(StopIteration): - next(g) - self.assertIsNone(sys.exception()) - - # with multiple exceptions in exc_info stack by chaining generators - def outer(): - g = inner() - self.assertIsNone(sys.exception()) - yield next(g) - self.assertIsInstance(sys.exception(), TypeError) - try: - raise ValueError() - except: - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsInstance(sys.exception().__context__, TypeError) - yield next(g) - # at this point the TypeError from the caller has been handled - # by the caller's except block. Even still, it should still be - # referenced as the __context__ of the current exception. - self.assertIsInstance(sys.exception(), ValueError) - self.assertIsInstance(sys.exception().__context__, TypeError) - # not handling an exception, caller isn't handling one either - self.assertIsNone(sys.exception()) - with self.assertRaises(StopIteration): - next(g) - self.assertIsNone(sys.exception()) - - g = outer() - next(g) - try: - raise TypeError() - except: - self.assertIsInstance(sys.exception(), TypeError) - next(g) - self.assertIsInstance(sys.exception(), TypeError) - self.assertIsNone(sys.exception()) - with self.assertRaises(StopIteration): - next(g) - self.assertIsNone(sys.exception()) - - if __name__ == "__main__": unittest.main() diff --git a/Modules/Setup.bootstrap.in b/Modules/Setup.bootstrap.in index a1c97500861c417..65a1fefe72e92ec 100644 --- a/Modules/Setup.bootstrap.in +++ b/Modules/Setup.bootstrap.in @@ -31,7 +31,6 @@ _weakref _weakref.c # commonly used core modules _abc _abc.c -_contextlib _contextlibmodule.c _functools _functoolsmodule.c _locale _localemodule.c _opcode _opcode.c diff --git a/Modules/_contextlibmodule.c b/Modules/_contextlibmodule.c deleted file mode 100644 index 2cbf5b51879f72d..000000000000000 --- a/Modules/_contextlibmodule.c +++ /dev/null @@ -1,64 +0,0 @@ - -/* Contextlib module */ - -#include "Python.h" - -#include "clinic/_contextlibmodule.c.h" - -/*[clinic input] -module _contextlib -[clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=60c4760e25c64b5e]*/ - - -/*[clinic input] -_contextlib._setup_genmgr_ctx - exception: object - -Force the provided exception to be set as the current exception - -Subsequent calls to sys.exception()/sys.exc_info() will return -the provided exception until another exception is caught in the -current thread or the execution stack returns to a frame where -another exception is being handled. -[clinic start generated code]*/ - -static PyObject * -_contextlib__setup_genmgr_ctx_impl(PyObject *module, PyObject *exception) -/*[clinic end generated code: output=d8996a9f8f52204b input=d61410ba5659bf6f]*/ -{ - if (!Py_IsNone(exception) && !PyExceptionInstance_Check(exception)){ - PyErr_SetString( - PyExc_TypeError, - "must be an exception/None" - ); - return NULL; - } - PyErr_SetHandledException(exception); - Py_RETURN_NONE; -} - - -/* module level code ********************************************************/ - -PyDoc_STRVAR(_contextlib_doc, -"Helpers for contextlib."); - -static PyMethodDef contextlib_methods[] = { - _CONTEXTLIB__SETUP_GENMGR_CTX_METHODDEF - {NULL, NULL} /* sentinel */ -}; - -static struct PyModuleDef _contextlibmodule = { - PyModuleDef_HEAD_INIT, - .m_name = "_contextlib", - .m_doc = _contextlib_doc, - .m_size = -1, - .m_methods = contextlib_methods, -}; - -PyMODINIT_FUNC -PyInit__contextlib(void) -{ - return PyModule_Create(&_contextlibmodule); -} \ No newline at end of file diff --git a/Modules/_contextlibmodules.c.h b/Modules/_contextlibmodules.c.h deleted file mode 100644 index 9817725eb20d5aa..000000000000000 --- a/Modules/_contextlibmodules.c.h +++ /dev/null @@ -1,73 +0,0 @@ -/*[clinic input] -preserve -[clinic start generated code]*/ - -#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) -# include "pycore_gc.h" // PyGC_Head -# include "pycore_runtime.h" // _Py_ID() -#endif -#include "pycore_modsupport.h" // _PyArg_UnpackKeywords() - -PyDoc_STRVAR(contextlib__setup_genmgr_ctx__doc__, -"_setup_genmgr_ctx($module, /, exception)\n" -"--\n" -"\n" -"Force the provided exception to be set as the current exception\n" -"\n" -"Subsequent calls to sys.exception()/sys.exc_info() will return\n" -"the provided exception until another exception is caught in the\n" -"current thread or the execution stack returns to a frame where\n" -"another exception is being handled."); - -#define CONTEXTLIB__SETUP_GENMGR_CTX_METHODDEF \ - {"_setup_genmgr_ctx", _PyCFunction_CAST(contextlib__setup_genmgr_ctx), METH_FASTCALL|METH_KEYWORDS, contextlib__setup_genmgr_ctx__doc__}, - -static PyObject * -contextlib__setup_genmgr_ctx_impl(PyObject *module, PyObject *exception); - -static PyObject * -contextlib__setup_genmgr_ctx(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) -{ - PyObject *return_value = NULL; - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { &_Py_ID(exception), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"exception", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "_setup_genmgr_ctx", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[1]; - PyObject *exception; - - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - exception = args[0]; - return_value = contextlib__setup_genmgr_ctx_impl(module, exception); - -exit: - return return_value; -} -/*[clinic end generated code: output=ec777026fc20d127 input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_contextlibmodule.c.h b/Modules/clinic/_contextlibmodule.c.h deleted file mode 100644 index 681edb4a974c275..000000000000000 --- a/Modules/clinic/_contextlibmodule.c.h +++ /dev/null @@ -1,73 +0,0 @@ -/*[clinic input] -preserve -[clinic start generated code]*/ - -#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) -# include "pycore_gc.h" // PyGC_Head -# include "pycore_runtime.h" // _Py_ID() -#endif -#include "pycore_modsupport.h" // _PyArg_UnpackKeywords() - -PyDoc_STRVAR(_contextlib__setup_genmgr_ctx__doc__, -"_setup_genmgr_ctx($module, /, exception)\n" -"--\n" -"\n" -"Force the provided exception to be set as the current exception\n" -"\n" -"Subsequent calls to sys.exception()/sys.exc_info() will return\n" -"the provided exception until another exception is caught in the\n" -"current thread or the execution stack returns to a frame where\n" -"another exception is being handled."); - -#define _CONTEXTLIB__SETUP_GENMGR_CTX_METHODDEF \ - {"_setup_genmgr_ctx", _PyCFunction_CAST(_contextlib__setup_genmgr_ctx), METH_FASTCALL|METH_KEYWORDS, _contextlib__setup_genmgr_ctx__doc__}, - -static PyObject * -_contextlib__setup_genmgr_ctx_impl(PyObject *module, PyObject *exception); - -static PyObject * -_contextlib__setup_genmgr_ctx(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) -{ - PyObject *return_value = NULL; - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { &_Py_ID(exception), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"exception", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "_setup_genmgr_ctx", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[1]; - PyObject *exception; - - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - exception = args[0]; - return_value = _contextlib__setup_genmgr_ctx_impl(module, exception); - -exit: - return return_value; -} -/*[clinic end generated code: output=3c01ac3e1fdf06bf input=a9049054013a1b77]*/ diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 68a148c37c93fbe..e255ed5af19125d 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -444,7 +444,6 @@ - diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 6c24739e584b1c1..649ee1859ff9961 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -989,9 +989,6 @@ Modules - - Modules - Modules diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index 3ef310fc6e739cf..d7248c34c59be45 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -189,7 +189,6 @@ def format_tsv_lines(lines): ('Modules/_asynciomodule.c', 'Py_BUILD_CORE', '1'), ('Modules/_codecsmodule.c', 'Py_BUILD_CORE', '1'), ('Modules/_collectionsmodule.c', 'Py_BUILD_CORE', '1'), - ('Modules/_contextlibmodule.c', 'Py_BUILD_CORE', '1'), ('Modules/_ctypes/_ctypes.c', 'Py_BUILD_CORE', '1'), ('Modules/_ctypes/cfield.c', 'Py_BUILD_CORE', '1'), ('Modules/_cursesmodule.c', 'Py_BUILD_CORE', '1'), From 725892ca76f6b09b0196c3a11e9d9cd9dcec3c86 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Mon, 19 May 2025 11:15:58 -0400 Subject: [PATCH 11/11] Add exc_context parameter to {gen/coro}.throw() This solves the exception context leaking into the generator by allowing an alternate exception context to be passed in. It also removes the need for an exposed function that directly manipulates the exception state since the details are handled in the C implementation. --- Lib/contextlib.py | 7 ++- Objects/genobject.c | 108 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 86 insertions(+), 29 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index ef28cd2ae722bb5..771432cec4cb636 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -235,11 +235,10 @@ def __exit__(self, typ, value, traceback): # However, since we're still currently handling the exception # that we throw into the generator here, the exception context # in the generator wouldn't change. - # To work around this, we forcefully set the current exception - # context to be what it was just before the generator's yield - # statement before throwing the current exception into it. + # To work around this, we pass the exception context as it was + # just before the generator's yield statement into the generator. # (see gh-111676). - self.gen.throw(value) + self.gen.throw(value, exc_context=exc_context) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration diff --git a/Objects/genobject.c b/Objects/genobject.c index 8c5d720c0b9035c..61c778e8177fdcb 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -612,8 +612,8 @@ gen_throw_current_exception(PyGenObject *gen) } PyDoc_STRVAR(throw_doc, -"throw(value)\n\ -throw(type[,value[,tb]])\n\ +"throw(value, /, *, exc_context=None)\n\ +throw(type[,value[,tb]], /, *, exc_context=None)\n\ \n\ Raise exception in generator, return next yielded value or raise\n\ StopIteration.\n\ @@ -622,7 +622,8 @@ and may be removed in a future version of Python."); static PyObject * _gen_throw(PyGenObject *gen, int close_on_genexit, - PyObject *typ, PyObject *val, PyObject *tb) + PyObject *typ, PyObject *val, PyObject *tb, + PyObject *exc_context) { int8_t frame_state = FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state); do { @@ -680,7 +681,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, /* Close the generator that we are currently iterating with 'yield from' or awaiting on with 'await'. */ ret = _gen_throw((PyGenObject *)yf, close_on_genexit, - typ, val, tb); + typ, val, tb, exc_context); tstate->current_frame = prev; frame->previous = NULL; } @@ -715,21 +716,53 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, throw_here: assert(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state) == FRAME_EXECUTING); + + /* + If an exception context is provided, set it (and restore it after) + + This ensures that after the generator handles the thrown-in exception, + _PyErr_GetTopmostException will return the provided exception context + instead of whatever exception was next in the stack. + */ + PyThreadState *tstate = NULL; + PyObject *prev_exc = NULL; + if (exc_context != NULL) { + tstate = _PyThreadState_GET(); + assert(tstate != NULL); + prev_exc = tstate->exc_info->exc_value; + tstate->exc_info->exc_value = Py_XNewRef(exc_context); + } if (gen_set_exception(typ, val, tb) < 0) { + if (tstate != NULL) { + Py_XSETREF(tstate->exc_info->exc_value, prev_exc); + } FT_ATOMIC_STORE_INT8_RELEASE(gen->gi_frame_state, frame_state); return NULL; } - return gen_throw_current_exception(gen); + + PyObject *result = gen_throw_current_exception(gen); + if (tstate != NULL) { + Py_XSETREF(tstate->exc_info->exc_value, prev_exc); + } + return result; } +/* +throw(...) method of builtins.generator instance + throw(value, /, *, exc_context=None) + throw(type[,value[,tb]], /, *, exc_context=None) + Raise exception in generator, return next yielded value or raise + StopIteration. +*/ static PyObject * -gen_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs) +gen_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyGenObject *gen = _PyGen_CAST(op); PyObject *typ; PyObject *tb = NULL; PyObject *val = NULL; + PyObject *exc_context = NULL; if (!_PyArg_CheckPositional("throw", nargs, 1, 3)) { return NULL; @@ -742,6 +775,18 @@ gen_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs) return NULL; } } + + static const char * const _keywords[] = {"", "", "", "exc_context", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "throw", + }; + PyObject *argsbuf[4]; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 3, 0, 1, argsbuf); + if (!args) { + return NULL; + } + typ = args[0]; if (nargs == 3) { val = args[1]; @@ -750,7 +795,19 @@ gen_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs) else if (nargs == 2) { val = args[1]; } - return _gen_throw(gen, 1, typ, val, tb); + + if (kwnames && PyTuple_GET_SIZE(kwnames)){ + exc_context = args[3]; + if (!Py_IsNone(exc_context) && !PyExceptionInstance_Check(exc_context)){ + PyErr_SetString(PyExc_TypeError, "exc_context must be an Exception object or None"); + return NULL; + } + } + /* default to current exception */ + if (exc_context == NULL){ + exc_context = _PyErr_GetTopmostException(_PyThreadState_GET())->exc_value; + } + return _gen_throw(gen, 1, typ, val, tb, exc_context); } @@ -1020,7 +1077,7 @@ PyDoc_STRVAR(sizeof__doc__, static PyMethodDef gen_methods[] = { {"send", gen_send, METH_O, send_doc}, - {"throw", _PyCFunction_CAST(gen_throw), METH_FASTCALL, throw_doc}, + {"throw", _PyCFunction_CAST(gen_throw), METH_FASTCALL|METH_KEYWORDS, throw_doc}, {"close", gen_close, METH_NOARGS, close_doc}, {"__sizeof__", gen_sizeof, METH_NOARGS, sizeof__doc__}, {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, @@ -1357,8 +1414,8 @@ PyDoc_STRVAR(coro_send_doc, return next iterated value or raise StopIteration."); PyDoc_STRVAR(coro_throw_doc, -"throw(value)\n\ -throw(type[,value[,traceback]])\n\ +"throw(value, /, *, exc_context=None)\n\ +throw(type[,value[,tb]], *, exc_context=None)\n\ \n\ Raise exception in coroutine, return next iterated value or raise\n\ StopIteration.\n\ @@ -1371,7 +1428,7 @@ PyDoc_STRVAR(coro_close_doc, static PyMethodDef coro_methods[] = { {"send", gen_send, METH_O, coro_send_doc}, - {"throw",_PyCFunction_CAST(gen_throw), METH_FASTCALL, coro_throw_doc}, + {"throw", _PyCFunction_CAST(gen_throw), METH_FASTCALL|METH_KEYWORDS, coro_throw_doc}, {"close", gen_close, METH_NOARGS, coro_close_doc}, {"__sizeof__", gen_sizeof, METH_NOARGS, sizeof__doc__}, {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, @@ -1461,10 +1518,10 @@ coro_wrapper_send(PyObject *self, PyObject *arg) } static PyObject * -coro_wrapper_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +coro_wrapper_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyCoroWrapper *cw = _PyCoroWrapper_CAST(self); - return gen_throw((PyObject*)cw->cw_coroutine, args, nargs); + return gen_throw((PyObject*)cw->cw_coroutine, args, nargs, kwnames); } static PyObject * @@ -1484,8 +1541,8 @@ coro_wrapper_traverse(PyObject *self, visitproc visit, void *arg) static PyMethodDef coro_wrapper_methods[] = { {"send", coro_wrapper_send, METH_O, coro_send_doc}, - {"throw", _PyCFunction_CAST(coro_wrapper_throw), METH_FASTCALL, - coro_throw_doc}, + {"throw", _PyCFunction_CAST(coro_wrapper_throw), + METH_FASTCALL|METH_KEYWORDS, coro_throw_doc}, {"close", coro_wrapper_close, METH_NOARGS, coro_close_doc}, {NULL, NULL} /* Sentinel */ }; @@ -2024,7 +2081,7 @@ async_gen_asend_iternext(PyObject *ags) static PyObject * -async_gen_asend_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +async_gen_asend_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyAsyncGenASend *o = _PyAsyncGenASend_CAST(self); @@ -2048,7 +2105,7 @@ async_gen_asend_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs) o->ags_gen->ag_running_async = 1; } - PyObject *result = gen_throw((PyObject*)o->ags_gen, args, nargs); + PyObject *result = gen_throw((PyObject*)o->ags_gen, args, nargs, kwnames); result = async_gen_unwrap_value(o->ags_gen, result); if (result == NULL) { @@ -2068,7 +2125,7 @@ async_gen_asend_close(PyObject *self, PyObject *args) Py_RETURN_NONE; } - PyObject *result = async_gen_asend_throw(self, &PyExc_GeneratorExit, 1); + PyObject *result = async_gen_asend_throw(self, &PyExc_GeneratorExit, 1, NULL); if (result == NULL) { if (PyErr_ExceptionMatches(PyExc_StopIteration) || PyErr_ExceptionMatches(PyExc_StopAsyncIteration) || @@ -2096,7 +2153,8 @@ async_gen_asend_finalize(PyObject *self) static PyMethodDef async_gen_asend_methods[] = { {"send", async_gen_asend_send, METH_O, send_doc}, - {"throw", _PyCFunction_CAST(async_gen_asend_throw), METH_FASTCALL, throw_doc}, + {"throw", _PyCFunction_CAST(async_gen_asend_throw), + METH_FASTCALL|METH_KEYWORDS, throw_doc}, {"close", async_gen_asend_close, METH_NOARGS, close_doc}, {NULL, NULL} /* Sentinel */ }; @@ -2349,7 +2407,7 @@ async_gen_athrow_send(PyObject *self, PyObject *arg) retval = _gen_throw((PyGenObject *)gen, 0, /* Do not close generator when PyExc_GeneratorExit is passed */ - PyExc_GeneratorExit, NULL, NULL); + PyExc_GeneratorExit, NULL, NULL, NULL); if (retval && _PyAsyncGenWrappedValue_CheckExact(retval)) { Py_DECREF(retval); @@ -2359,7 +2417,7 @@ async_gen_athrow_send(PyObject *self, PyObject *arg) retval = _gen_throw((PyGenObject *)gen, 0, /* Do not close generator when PyExc_GeneratorExit is passed */ - o->agt_typ, o->agt_val, o->agt_tb); + o->agt_typ, o->agt_val, o->agt_tb, NULL); retval = async_gen_unwrap_value(o->agt_gen, retval); } if (retval == NULL) { @@ -2417,7 +2475,7 @@ async_gen_athrow_send(PyObject *self, PyObject *arg) static PyObject * -async_gen_athrow_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +async_gen_athrow_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyAsyncGenAThrow *o = _PyAsyncGenAThrow_CAST(self); @@ -2448,7 +2506,7 @@ async_gen_athrow_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs) o->agt_gen->ag_running_async = 1; } - PyObject *retval = gen_throw((PyObject*)o->agt_gen, args, nargs); + PyObject *retval = gen_throw((PyObject*)o->agt_gen, args, nargs, kwnames); if (o->agt_typ) { retval = async_gen_unwrap_value(o->agt_gen, retval); if (retval == NULL) { @@ -2501,7 +2559,7 @@ async_gen_athrow_close(PyObject *self, PyObject *args) Py_RETURN_NONE; } PyObject *result = async_gen_athrow_throw((PyObject*)agt, - &PyExc_GeneratorExit, 1); + &PyExc_GeneratorExit, 1, NULL); if (result == NULL) { if (PyErr_ExceptionMatches(PyExc_StopIteration) || PyErr_ExceptionMatches(PyExc_StopAsyncIteration) || @@ -2532,7 +2590,7 @@ async_gen_athrow_finalize(PyObject *op) static PyMethodDef async_gen_athrow_methods[] = { {"send", async_gen_athrow_send, METH_O, send_doc}, {"throw", _PyCFunction_CAST(async_gen_athrow_throw), - METH_FASTCALL, throw_doc}, + METH_FASTCALL|METH_KEYWORDS, throw_doc}, {"close", async_gen_athrow_close, METH_NOARGS, close_doc}, {NULL, NULL} /* Sentinel */ };