diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 7956bdd9040..7eac9d076be 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -9,6 +9,35 @@ import inspect from test import support +_testcapi = support.import_module('_testcapi') + + +# This tests to make sure that if a SIGINT arrives just before we send into a +# yield from chain, the KeyboardInterrupt is raised in the innermost +# generator (see bpo-30039). +class SignalAndYieldFromTest(unittest.TestCase): + + def generator1(self): + return (yield from self.generator2()) + + def generator2(self): + try: + yield + except KeyboardInterrupt: + return "PASSED" + else: + return "FAILED" + + def test_raise_and_yield_from(self): + gen = self.generator1() + gen.send(None) + try: + _testcapi.raise_SIGINT_then_send_None(gen) + except BaseException as _exc: + exc = _exc + self.assertIs(type(exc), StopIteration) + self.assertEqual(exc.value, "PASSED") + class FinalizationTest(unittest.TestCase): diff --git a/Misc/NEWS b/Misc/NEWS index 5948cd92fea..c6aed7f48c8 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,10 @@ What's New in Python 3.7.0 alpha 1? Core and Builtins ----------------- +- bpo-30039: If a KeyboardInterrupt happens when the interpreter is in + the middle of resuming a chain of nested 'yield from' or 'await' + calls, it's now correctly delivered to the innermost frame. + - bpo-28974: ``object.__format__(x, '')`` is now equivalent to ``str(x)`` rather than ``format(str(self), '')``. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index f9de940afe0..7f7a13ee51d 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -4028,6 +4028,29 @@ dict_get_version(PyObject *self, PyObject *args) } +static PyObject * +raise_SIGINT_then_send_None(PyObject *self, PyObject *args) +{ + PyGenObject *gen; + + if (!PyArg_ParseTuple(args, "O!", &PyGen_Type, &gen)) + return NULL; + + /* This is used in a test to check what happens if a signal arrives just + as we're in the process of entering a yield from chain (see + bpo-30039). + + Needs to be done in C, because: + - we don't have a Python wrapper for raise() + - we need to make sure that the Python-level signal handler doesn't run + *before* we enter the generator frame, which is impossible in Python + because we check for signals before every bytecode operation. + */ + raise(SIGINT); + return _PyGen_Send(gen, Py_None); +} + + static PyMethodDef TestMethods[] = { {"raise_exception", raise_exception, METH_VARARGS}, {"raise_memoryerror", (PyCFunction)raise_memoryerror, METH_NOARGS}, @@ -4232,6 +4255,7 @@ static PyMethodDef TestMethods[] = { {"tracemalloc_untrack", tracemalloc_untrack, METH_VARARGS}, {"tracemalloc_get_traceback", tracemalloc_get_traceback, METH_VARARGS}, {"dict_get_version", dict_get_version, METH_VARARGS}, + {"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Python/ceval.c b/Python/ceval.c index 23fd0880981..302070bbda7 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1064,9 +1064,20 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) Py_MakePendingCalls() above. */ if (_Py_atomic_load_relaxed(&eval_breaker)) { - if (_Py_OPCODE(*next_instr) == SETUP_FINALLY) { - /* Make the last opcode before - a try: finally: block uninterruptible. */ + if (_Py_OPCODE(*next_instr) == SETUP_FINALLY || + _Py_OPCODE(*next_instr) == YIELD_FROM) { + /* Two cases where we skip running signal handlers and other + pending calls: + - If we're about to enter the try: of a try/finally (not + *very* useful, but might help in some cases and it's + traditional) + - If we're resuming a chain of nested 'yield from' or + 'await' calls, then each frame is parked with YIELD_FROM + as its next opcode. If the user hit control-C we want to + wait until we've reached the innermost frame before + running the signal handler and raising KeyboardInterrupt + (see bpo-30039). + */ goto fast_next_opcode; } if (_Py_atomic_load_relaxed(&pendingcalls_to_do)) {