Skip to content

Commit e92219d

Browse files
bpo-29590: fix stack trace for gen.throw() with yield from (GH-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. (cherry picked from commit 8b33961) Co-authored-by: Chris Jerdonek <[email protected]>
1 parent 106c1df commit e92219d

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

Lib/test/test_generators.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,55 @@ def g():
415415
gen.throw(ValueError)
416416

417417

418+
class GeneratorStackTraceTest(unittest.TestCase):
419+
420+
def check_stack_names(self, frame, expected):
421+
names = []
422+
while frame:
423+
name = frame.f_code.co_name
424+
# Stop checking frames when we get to our test helper.
425+
if name.startswith('check_') or name.startswith('call_'):
426+
break
427+
428+
names.append(name)
429+
frame = frame.f_back
430+
431+
self.assertEqual(names, expected)
432+
433+
def check_yield_from_example(self, call_method):
434+
def f():
435+
self.check_stack_names(sys._getframe(), ['f', 'g'])
436+
try:
437+
yield
438+
except Exception:
439+
pass
440+
self.check_stack_names(sys._getframe(), ['f', 'g'])
441+
442+
def g():
443+
self.check_stack_names(sys._getframe(), ['g'])
444+
yield from f()
445+
self.check_stack_names(sys._getframe(), ['g'])
446+
447+
gen = g()
448+
gen.send(None)
449+
try:
450+
call_method(gen)
451+
except StopIteration:
452+
pass
453+
454+
def test_send_with_yield_from(self):
455+
def call_send(gen):
456+
gen.send(None)
457+
458+
self.check_yield_from_example(call_send)
459+
460+
def test_throw_with_yield_from(self):
461+
def call_throw(gen):
462+
gen.throw(RuntimeError)
463+
464+
self.check_yield_from_example(call_throw)
465+
466+
418467
class YieldFromTests(unittest.TestCase):
419468
def test_generator_gi_yieldfrom(self):
420469
def a():
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make the stack trace correct after calling :meth:`generator.throw`
2+
on a generator that has yielded from a ``yield from``.

Objects/genobject.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,21 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
414414
}
415415
if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) {
416416
/* `yf` is a generator or a coroutine. */
417+
PyThreadState *tstate = _PyThreadState_GET();
418+
PyFrameObject *f = tstate->frame;
419+
417420
gen->gi_running = 1;
421+
/* Since we are fast-tracking things by skipping the eval loop,
422+
we need to update the current frame so the stack trace
423+
will be reported correctly to the user. */
424+
/* XXX We should probably be updating the current frame
425+
somewhere in ceval.c. */
426+
tstate->frame = gen->gi_frame;
418427
/* Close the generator that we are currently iterating with
419428
'yield from' or awaiting on with 'await'. */
420429
ret = _gen_throw((PyGenObject *)yf, close_on_genexit,
421430
typ, val, tb);
431+
tstate->frame = f;
422432
gen->gi_running = 0;
423433
} else {
424434
/* `yf` is an iterator or a coroutine-like object. */

0 commit comments

Comments
 (0)