Skip to content

Commit

Permalink
bpo-29590: fix stack trace for gen.throw() with yield from (#19896)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
cjerdonek authored Jul 9, 2020
1 parent 96a6a6d commit 8b33961
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 0 deletions.
49 changes: 49 additions & 0 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,55 @@ def g():
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():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make the stack trace correct after calling :meth:`generator.throw`
on a generator that has yielded from a ``yield from``.
10 changes: 10 additions & 0 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down

0 comments on commit 8b33961

Please sign in to comment.