diff options
author | Chris Jerdonek <chris.jerdonek@gmail.com> | 2020-07-09 13:27:23 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-09 13:27:23 (GMT) |
commit | 8b33961e4bc4020d8b2d5b949ad9d5c669300e89 (patch) | |
tree | abed35abb8d1696af9e068b6eeb44193ac4ed00f | |
parent | 96a6a6d42be272a27562d98549bbffc0d1854669 (diff) | |
download | cpython-8b33961e4bc4020d8b2d5b949ad9d5c669300e89.zip cpython-8b33961e4bc4020d8b2d5b949ad9d5c669300e89.tar.gz cpython-8b33961e4bc4020d8b2d5b949ad9d5c669300e89.tar.bz2 |
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.
-rw-r--r-- | Lib/test/test_generators.py | 49 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst | 2 | ||||
-rw-r--r-- | Objects/genobject.c | 10 |
3 files changed, 61 insertions, 0 deletions
diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index bf48221..3bf1522 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 0000000..2570c4f --- /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 6a68c94..a379fa6 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. */ |