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.
This commit is contained in:
Chris Jerdonek 2020-07-09 06:27:23 -07:00 committed by GitHub
parent 96a6a6d42b
commit 8b33961e4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 0 deletions

View File

@ -415,6 +415,55 @@ class GeneratorThrowTest(unittest.TestCase):
gen.throw(ValueError) 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): class YieldFromTests(unittest.TestCase):
def test_generator_gi_yieldfrom(self): def test_generator_gi_yieldfrom(self):
def a(): def a():

View File

@ -0,0 +1,2 @@
Make the stack trace correct after calling :meth:`generator.throw`
on a generator that has yielded from a ``yield from``.

View File

@ -415,11 +415,21 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
} }
if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) { if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) {
/* `yf` is a generator or a coroutine. */ /* `yf` is a generator or a coroutine. */
PyThreadState *tstate = _PyThreadState_GET();
PyFrameObject *f = tstate->frame;
gen->gi_running = 1; 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 /* Close the generator that we are currently iterating with
'yield from' or awaiting on with 'await'. */ 'yield from' or awaiting on with 'await'. */
ret = _gen_throw((PyGenObject *)yf, close_on_genexit, ret = _gen_throw((PyGenObject *)yf, close_on_genexit,
typ, val, tb); typ, val, tb);
tstate->frame = f;
gen->gi_running = 0; gen->gi_running = 0;
} else { } else {
/* `yf` is an iterator or a coroutine-like object. */ /* `yf` is an iterator or a coroutine-like object. */