Skip to content

Commit e91ac58

Browse files
committed
gh-108668: Preserve exception state during delegated throw
1 parent 894af95 commit e91ac58

3 files changed

Lines changed: 91 additions & 0 deletions

File tree

Lib/test/test_yield_from.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import unittest
1111
import inspect
12+
import sys
13+
import types
1214

1315
from test.support import captured_stderr, disable_gc, gc_collect
1416
from test import support
@@ -18,6 +20,83 @@ class TestPEP380Operation(unittest.TestCase):
1820
Test semantics.
1921
"""
2022

23+
def test_delegated_throw_preserves_exception_state(self):
24+
"""Delegated throw inherits the delegating generator's exception."""
25+
class Iterator:
26+
def __init__(self, gen):
27+
self.gen = gen
28+
29+
def __iter__(self):
30+
return self
31+
32+
def __next__(self):
33+
return next(self.gen)
34+
35+
def throw(self, *args):
36+
return self.gen.throw(*args)
37+
38+
for wrap in (lambda gen: gen, Iterator):
39+
with self.subTest(wrap=wrap):
40+
original = RuntimeError("original")
41+
seen = []
42+
43+
def inner():
44+
seen.append(sys.exception())
45+
try:
46+
yield
47+
except ValueError:
48+
pass
49+
seen.append(sys.exception())
50+
yield
51+
52+
def outer():
53+
try:
54+
raise original
55+
except RuntimeError:
56+
yield from wrap(inner())
57+
seen.append(sys.exception())
58+
59+
gen = outer()
60+
next(gen)
61+
try:
62+
raise LookupError("caller")
63+
except LookupError as caller:
64+
gen.throw(ValueError())
65+
self.assertIs(sys.exception(), caller)
66+
with self.assertRaises(StopIteration):
67+
next(gen)
68+
self.assertIs(sys.exception(), caller)
69+
self.assertEqual(seen, [original] * 3)
70+
71+
def test_await_throw_preserves_exception_state(self):
72+
"""Throwing through await preserves the surrounding handled exception."""
73+
original = RuntimeError("original")
74+
seen = []
75+
76+
@types.coroutine
77+
def suspend():
78+
yield
79+
80+
async def inner():
81+
try:
82+
await suspend()
83+
except ValueError:
84+
pass
85+
seen.append(sys.exception())
86+
87+
async def outer():
88+
try:
89+
raise original
90+
except RuntimeError:
91+
await inner()
92+
seen.append(sys.exception())
93+
94+
coro = outer()
95+
coro.send(None)
96+
with self.assertRaises(StopIteration):
97+
coro.throw(ValueError())
98+
self.assertEqual(seen, [original] * 2)
99+
21100
def test_delegation_of_initial_next_to_subgenerator(self):
22101
"""
23102
Test delegation of initial next() call to subgenerator
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Preserve the handled exception state when delegating a generator's
2+
:meth:`~generator.throw` through ``yield from`` or ``await``.

Objects/genobject.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,8 +682,13 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
682682
tstate->current_frame = frame;
683683
/* Close the generator that we are currently iterating with
684684
'yield from' or awaiting on with 'await'. */
685+
_PyErr_StackItem *prev_exc_info = tstate->exc_info;
686+
gen->gi_exc_state.previous_item = prev_exc_info;
687+
tstate->exc_info = &gen->gi_exc_state;
685688
ret = _gen_throw((PyGenObject *)yf, close_on_genexit,
686689
typ, val, tb);
690+
tstate->exc_info = prev_exc_info;
691+
gen->gi_exc_state.previous_item = NULL;
687692
_PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
688693
tstate->current_frame = prev;
689694
frame->previous = NULL;
@@ -704,7 +709,12 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
704709
_PyInterpreterFrame *prev = tstate->current_frame;
705710
frame->previous = prev;
706711
tstate->current_frame = frame;
712+
_PyErr_StackItem *prev_exc_info = tstate->exc_info;
713+
gen->gi_exc_state.previous_item = prev_exc_info;
714+
tstate->exc_info = &gen->gi_exc_state;
707715
ret = PyObject_CallFunctionObjArgs(meth, typ, val, tb, NULL);
716+
tstate->exc_info = prev_exc_info;
717+
gen->gi_exc_state.previous_item = NULL;
708718
_PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
709719
tstate->current_frame = prev;
710720
frame->previous = NULL;

0 commit comments

Comments
 (0)