summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2020-07-09 06:27:23 -0700
committerGitHub <noreply@github.com>2020-07-09 14:27:23 +0100
commit8b33961e4bc4020d8b2d5b949ad9d5c669300e89 (patch)
treeabed35abb8d1696af9e068b6eeb44193ac4ed00f
parent96a6a6d42be272a27562d98549bbffc0d1854669 (diff)
downloadcpython-git-8b33961e4bc4020d8b2d5b949ad9d5c669300e89.tar.gz
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.
-rw-r--r--Lib/test/test_generators.py49
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2020-05-03-22-26-00.bpo-29590.aRz3l7.rst2
-rw-r--r--Objects/genobject.c10
3 files changed, 61 insertions, 0 deletions
diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py
index bf482213c1..3bf1522808 100644
--- a/Lib/test/test_generators.py
+++ b/Lib/test/test_generators.py
@@ -415,6 +415,55 @@ class GeneratorThrowTest(unittest.TestCase):
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 0000000000..2570c4f2c7
--- /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 6a68c9484a..a379fa6088 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. */