From 8b33961e4bc4020d8b2d5b949ad9d5c669300e89 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 9 Jul 2020 06:27:23 -0700 Subject: [PATCH] bpo-29590: fix stack trace for gen.throw() with yield from (#19896) * Add failing test. * bpo-29590: fix stack trace for gen.throw() with yield from (GH-NNNN) When gen.throw() is called on a generator after a "yield from", the intermediate stack trace entries are lost. This commit fixes that. --- Lib/test/test_generators.py | 49 +++++++++++++++++++ .../2020-05-03-22-26-00.bpo-29590.aRz3l7.rst | 2 + Objects/genobject.c | 10 ++++ 3 files changed, 61 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index bf482213c17..3bf15228086 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -415,6 +415,55 @@ class GeneratorThrowTest(unittest.TestCase): gen.throw(ValueError) +class GeneratorStackTraceTest(unittest.TestCase): + + def check_stack_names(self, frame, expected): + names = [] + while frame: + name = frame.f_code.co_name + # Stop checking frames when we get to our test helper. + if name.startswith('check_') or name.startswith('call_'): + break + + names.append(name) + frame = frame.f_back + + self.assertEqual(names, expected) + + def check_yield_from_example(self, call_method): + def f(): + self.check_stack_names(sys._getframe(), ['f', 'g']) + try: + yield + except Exception: + pass + self.check_stack_names(sys._getframe(), ['f', 'g']) + + def g(): + self.check_stack_names(sys._getframe(), ['g']) + yield from f() + self.check_stack_names(sys._getframe(), ['g']) + + gen = g() + gen.send(None) + try: + call_method(gen) + except StopIteration: + pass + + def test_send_with_yield_from(self): + def call_send(gen): + gen.send(None) + + self.check_yield_from_example(call_send) + + def test_throw_with_yield_from(self): + def call_throw(gen): + gen.throw(RuntimeError) + + self.check_yield_from_example(call_throw) + + class YieldFromTests(unittest.TestCase): def test_generator_gi_yieldfrom(self): def a(): diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst b/Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst new file mode 100644 index 00000000000..2570c4f2c7c --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst @@ -0,0 +1,2 @@ +Make the stack trace correct after calling :meth:`generator.throw` +on a generator that has yielded from a ``yield from``. diff --git a/Objects/genobject.c b/Objects/genobject.c index 6a68c9484a6..a379fa6088e 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -415,11 +415,21 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, } if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) { /* `yf` is a generator or a coroutine. */ + PyThreadState *tstate = _PyThreadState_GET(); + PyFrameObject *f = tstate->frame; + gen->gi_running = 1; + /* Since we are fast-tracking things by skipping the eval loop, + we need to update the current frame so the stack trace + will be reported correctly to the user. */ + /* XXX We should probably be updating the current frame + somewhere in ceval.c. */ + tstate->frame = gen->gi_frame; /* 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); + tstate->frame = f; gen->gi_running = 0; } else { /* `yf` is an iterator or a coroutine-like object. */