]> granicus.if.org Git - python/commitdiff
Issue #23353: Fix the exception handling of generators in PyEval_EvalFrameEx().
authorVictor Stinner <victor.stinner@gmail.com>
Sat, 31 Jan 2015 09:29:47 +0000 (10:29 +0100)
committerVictor Stinner <victor.stinner@gmail.com>
Sat, 31 Jan 2015 09:29:47 +0000 (10:29 +0100)
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.

Lib/test/test_generators.py
Misc/NEWS
Python/ceval.c

index 91afe477994e83c44680098939601a363e82c31e..7825a77339054faf81f9f91a52ac6384995cae7e 100644 (file)
@@ -50,6 +50,115 @@ class FinalizationTest(unittest.TestCase):
         self.assertEqual(gc.garbage, old_garbage)
 
 
+class ExceptionTest(unittest.TestCase):
+    # Tests for the issue #23353: check that the currently handled exception
+    # is correctly saved/restored in PyEval_EvalFrameEx().
+
+    def test_except_throw(self):
+        def store_raise_exc_generator():
+            try:
+                self.assertEqual(sys.exc_info()[0], None)
+                yield
+            except Exception as exc:
+                # exception raised by gen.throw(exc)
+                self.assertEqual(sys.exc_info()[0], ValueError)
+                self.assertIsNone(exc.__context__)
+                yield
+
+                # ensure that the exception is not lost
+                self.assertEqual(sys.exc_info()[0], ValueError)
+                yield
+
+                # we should be able to raise back the ValueError
+                raise
+
+        make = store_raise_exc_generator()
+        next(make)
+
+        try:
+            raise ValueError()
+        except Exception as exc:
+            try:
+                make.throw(exc)
+            except Exception:
+                pass
+
+        next(make)
+        with self.assertRaises(ValueError) as cm:
+            next(make)
+        self.assertIsNone(cm.exception.__context__)
+
+        self.assertEqual(sys.exc_info(), (None, None, None))
+
+    def test_except_next(self):
+        def gen():
+            self.assertEqual(sys.exc_info()[0], ValueError)
+            yield "done"
+
+        g = gen()
+        try:
+            raise ValueError
+        except Exception:
+            self.assertEqual(next(g), "done")
+        self.assertEqual(sys.exc_info(), (None, None, None))
+
+    def test_except_gen_except(self):
+        def gen():
+            try:
+                self.assertEqual(sys.exc_info()[0], None)
+                yield
+                # we are called from "except ValueError:", TypeError must
+                # inherit ValueError in its context
+                raise TypeError()
+            except TypeError as exc:
+                self.assertEqual(sys.exc_info()[0], TypeError)
+                self.assertEqual(type(exc.__context__), ValueError)
+            # here we are still called from the "except ValueError:"
+            self.assertEqual(sys.exc_info()[0], ValueError)
+            yield
+            self.assertIsNone(sys.exc_info()[0])
+            yield "done"
+
+        g = gen()
+        next(g)
+        try:
+            raise ValueError
+        except Exception:
+            next(g)
+
+        self.assertEqual(next(g), "done")
+        self.assertEqual(sys.exc_info(), (None, None, None))
+
+    def test_except_throw_exception_context(self):
+        def gen():
+            try:
+                try:
+                    self.assertEqual(sys.exc_info()[0], None)
+                    yield
+                except ValueError:
+                    # we are called from "except ValueError:"
+                    self.assertEqual(sys.exc_info()[0], ValueError)
+                    raise TypeError()
+            except Exception as exc:
+                self.assertEqual(sys.exc_info()[0], TypeError)
+                self.assertEqual(type(exc.__context__), ValueError)
+            # we are still called from "except ValueError:"
+            self.assertEqual(sys.exc_info()[0], ValueError)
+            yield
+            self.assertIsNone(sys.exc_info()[0])
+            yield "done"
+
+        g = gen()
+        next(g)
+        try:
+            raise ValueError
+        except Exception as exc:
+            g.throw(exc)
+
+        self.assertEqual(next(g), "done")
+        self.assertEqual(sys.exc_info(), (None, None, None))
+
+
 tutorial_tests = """
 Let's try a simple generator:
 
index f21c2b8459ea5372c695cb6be7d21075191aa531..f0518645b7e8aceafd3ebaf813e9a3b9c7150c71 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -50,6 +50,12 @@ Core and Builtins
 Library
 -------
 
+- 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.
+
 - Issue #18518: timeit now rejects statements which can't be compiled outside
   a function or a loop (e.g. "return" or "break").
 
index 118f2b7f2e3f82f845aa3bacbe4ffc79ed50c47f..7656b8ef540794641df5a51c22c4c9c2af51351f 100644 (file)
@@ -1189,8 +1189,8 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
     f->f_stacktop = NULL;       /* remains NULL unless yield suspends frame */
     f->f_executing = 1;
 
-    if (co->co_flags & CO_GENERATOR && !throwflag) {
-        if (f->f_exc_type != NULL && f->f_exc_type != Py_None) {
+    if (co->co_flags & CO_GENERATOR) {
+        if (!throwflag && f->f_exc_type != NULL && f->f_exc_type != Py_None) {
             /* We were in an except handler when we left,
                restore the exception state which was put aside
                (see YIELD_VALUE). */
@@ -3172,7 +3172,8 @@ fast_block_end:
             || (retval == NULL && PyErr_Occurred()));
 
 fast_yield:
-    if (co->co_flags & CO_GENERATOR && (why == WHY_YIELD || why == WHY_RETURN)) {
+    if (co->co_flags & CO_GENERATOR) {
+
         /* The purpose of this block is to put aside the generator's exception
            state and restore that of the calling frame. If the current
            exception state is from the caller, we clear the exception values