Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 26f7b8a

Browse files
committed
Issue #23353: Fix the exception handling of generators in PyEval_EvalFrameEx().
At entry, save or swap the exception state even if PyEval_EvalFrameEx() is called with throwflag=0. At exit, the exception state is now always restored or swapped, not only if why is WHY_YIELD or WHY_RETURN. Patch co-written with Antoine Pitrou.
1 parent fdc9953 commit 26f7b8a

3 files changed

Lines changed: 119 additions & 3 deletions

File tree

Lib/test/test_generators.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,115 @@ def gen():
5050
self.assertEqual(gc.garbage, old_garbage)
5151

5252

53+
class ExceptionTest(unittest.TestCase):
54+
# Tests for the issue #23353: check that the currently handled exception
55+
# is correctly saved/restored in PyEval_EvalFrameEx().
56+
57+
def test_except_throw(self):
58+
def store_raise_exc_generator():
59+
try:
60+
self.assertEqual(sys.exc_info()[0], None)
61+
yield
62+
except Exception as exc:
63+
# exception raised by gen.throw(exc)
64+
self.assertEqual(sys.exc_info()[0], ValueError)
65+
self.assertIsNone(exc.__context__)
66+
yield
67+
68+
# ensure that the exception is not lost
69+
self.assertEqual(sys.exc_info()[0], ValueError)
70+
yield
71+
72+
# we should be able to raise back the ValueError
73+
raise
74+
75+
make = store_raise_exc_generator()
76+
next(make)
77+
78+
try:
79+
raise ValueError()
80+
except Exception as exc:
81+
try:
82+
make.throw(exc)
83+
except Exception:
84+
pass
85+
86+
next(make)
87+
with self.assertRaises(ValueError) as cm:
88+
next(make)
89+
self.assertIsNone(cm.exception.__context__)
90+
91+
self.assertEqual(sys.exc_info(), (None, None, None))
92+
93+
def test_except_next(self):
94+
def gen():
95+
self.assertEqual(sys.exc_info()[0], ValueError)
96+
yield "done"
97+
98+
g = gen()
99+
try:
100+
raise ValueError
101+
except Exception:
102+
self.assertEqual(next(g), "done")
103+
self.assertEqual(sys.exc_info(), (None, None, None))
104+
105+
def test_except_gen_except(self):
106+
def gen():
107+
try:
108+
self.assertEqual(sys.exc_info()[0], None)
109+
yield
110+
# we are called from "except ValueError:", TypeError must
111+
# inherit ValueError in its context
112+
raise TypeError()
113+
except TypeError as exc:
114+
self.assertEqual(sys.exc_info()[0], TypeError)
115+
self.assertEqual(type(exc.__context__), ValueError)
116+
# here we are still called from the "except ValueError:"
117+
self.assertEqual(sys.exc_info()[0], ValueError)
118+
yield
119+
self.assertIsNone(sys.exc_info()[0])
120+
yield "done"
121+
122+
g = gen()
123+
next(g)
124+
try:
125+
raise ValueError
126+
except Exception:
127+
next(g)
128+
129+
self.assertEqual(next(g), "done")
130+
self.assertEqual(sys.exc_info(), (None, None, None))
131+
132+
def test_except_throw_exception_context(self):
133+
def gen():
134+
try:
135+
try:
136+
self.assertEqual(sys.exc_info()[0], None)
137+
yield
138+
except ValueError:
139+
# we are called from "except ValueError:"
140+
self.assertEqual(sys.exc_info()[0], ValueError)
141+
raise TypeError()
142+
except Exception as exc:
143+
self.assertEqual(sys.exc_info()[0], TypeError)
144+
self.assertEqual(type(exc.__context__), ValueError)
145+
# we are still called from "except ValueError:"
146+
self.assertEqual(sys.exc_info()[0], ValueError)
147+
yield
148+
self.assertIsNone(sys.exc_info()[0])
149+
yield "done"
150+
151+
g = gen()
152+
next(g)
153+
try:
154+
raise ValueError
155+
except Exception as exc:
156+
g.throw(exc)
157+
158+
self.assertEqual(next(g), "done")
159+
self.assertEqual(sys.exc_info(), (None, None, None))
160+
161+
53162
tutorial_tests = """
54163
Let's try a simple generator:
55164

Misc/NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ Core and Builtins
5050
Library
5151
-------
5252

53+
- Issue #23353: Fix the exception handling of generators in
54+
PyEval_EvalFrameEx(). At entry, save or swap the exception state even if
55+
PyEval_EvalFrameEx() is called with throwflag=0. At exit, the exception state
56+
is now always restored or swapped, not only if why is WHY_YIELD or
57+
WHY_RETURN. Patch co-written with Antoine Pitrou.
58+
5359
- Issue #18518: timeit now rejects statements which can't be compiled outside
5460
a function or a loop (e.g. "return" or "break").
5561

Python/ceval.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,8 +1189,8 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
11891189
f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */
11901190
f->f_executing = 1;
11911191

1192-
if (co->co_flags & CO_GENERATOR && !throwflag) {
1193-
if (f->f_exc_type != NULL && f->f_exc_type != Py_None) {
1192+
if (co->co_flags & CO_GENERATOR) {
1193+
if (!throwflag && f->f_exc_type != NULL && f->f_exc_type != Py_None) {
11941194
/* We were in an except handler when we left,
11951195
restore the exception state which was put aside
11961196
(see YIELD_VALUE). */
@@ -3172,7 +3172,8 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
31723172
|| (retval == NULL && PyErr_Occurred()));
31733173

31743174
fast_yield:
3175-
if (co->co_flags & CO_GENERATOR && (why == WHY_YIELD || why == WHY_RETURN)) {
3175+
if (co->co_flags & CO_GENERATOR) {
3176+
31763177
/* The purpose of this block is to put aside the generator's exception
31773178
state and restore that of the calling frame. If the current
31783179
exception state is from the caller, we clear the exception values

0 commit comments

Comments
 (0)