[Python-checkins] bpo-29590: fix stack trace for gen.throw() with yield from (#19896)

Chris Jerdonek webhook-mailer at python.org
Thu Jul 9 09:27:32 EDT 2020


https://github.com/python/cpython/commit/8b33961e4bc4020d8b2d5b949ad9d5c669300e89
commit: 8b33961e4bc4020d8b2d5b949ad9d5c669300e89
branch: master
author: Chris Jerdonek <chris.jerdonek at gmail.com>
committer: GitHub <noreply at github.com>
date: 2020-07-09T14:27:23+01:00
summary:

bpo-29590: fix stack trace for gen.throw() with yield from (#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.

files:
A Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst
M Lib/test/test_generators.py
M Objects/genobject.c

diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py
index bf482213c178a..3bf152280868e 100644
--- a/Lib/test/test_generators.py
+++ b/Lib/test/test_generators.py
@@ -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():
diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst b/Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst
new file mode 100644
index 0000000000000..2570c4f2c7c0f
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst	
@@ -0,0 +1,2 @@
+Make the stack trace correct after calling :meth:`generator.throw`
+on a generator that has yielded from a ``yield from``.
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 6a68c9484a6ae..a379fa6088e16 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -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. */



More information about the Python-checkins mailing list