Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions Lib/test/test_yield_from.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import unittest
import inspect
import sys
import types

from test.support import captured_stderr, disable_gc, gc_collect
from test import support
Expand All @@ -18,6 +20,83 @@ class TestPEP380Operation(unittest.TestCase):
Test semantics.
"""

def test_delegated_throw_preserves_exception_state(self):
"""Delegated throw inherits the delegating generator's exception."""
class Iterator:
def __init__(self, gen):
self.gen = gen

def __iter__(self):
return self

def __next__(self):
return next(self.gen)

def throw(self, *args):
return self.gen.throw(*args)

for wrap in (lambda gen: gen, Iterator):
with self.subTest(wrap=wrap):
original = RuntimeError("original")
seen = []

def inner():
seen.append(sys.exception())
try:
yield
except ValueError:
pass
seen.append(sys.exception())
yield

def outer():
try:
raise original
except RuntimeError:
yield from wrap(inner())
seen.append(sys.exception())

gen = outer()
next(gen)
try:
raise LookupError("caller")
except LookupError as caller:
gen.throw(ValueError())
self.assertIs(sys.exception(), caller)
with self.assertRaises(StopIteration):
next(gen)
self.assertIs(sys.exception(), caller)
self.assertEqual(seen, [original] * 3)

def test_await_throw_preserves_exception_state(self):
"""Throwing through await preserves the surrounding handled exception."""
original = RuntimeError("original")
seen = []

@types.coroutine
def suspend():
yield

async def inner():
try:
await suspend()
except ValueError:
pass
seen.append(sys.exception())

async def outer():
try:
raise original
except RuntimeError:
await inner()
seen.append(sys.exception())

coro = outer()
coro.send(None)
with self.assertRaises(StopIteration):
coro.throw(ValueError())
self.assertEqual(seen, [original] * 2)

def test_delegation_of_initial_next_to_subgenerator(self):
"""
Test delegation of initial next() call to subgenerator
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Preserve the handled exception state when delegating a generator's
:meth:`~generator.throw` through ``yield from`` or ``await``.
10 changes: 10 additions & 0 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -682,8 +682,13 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
tstate->current_frame = frame;
/* Close the generator that we are currently iterating with
'yield from' or awaiting on with 'await'. */
_PyErr_StackItem *prev_exc_info = tstate->exc_info;
gen->gi_exc_state.previous_item = prev_exc_info;
tstate->exc_info = &gen->gi_exc_state;
ret = _gen_throw((PyGenObject *)yf, close_on_genexit,
typ, val, tb);
tstate->exc_info = prev_exc_info;
gen->gi_exc_state.previous_item = NULL;
_PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
tstate->current_frame = prev;
frame->previous = NULL;
Expand All @@ -704,7 +709,12 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
_PyInterpreterFrame *prev = tstate->current_frame;
frame->previous = prev;
tstate->current_frame = frame;
_PyErr_StackItem *prev_exc_info = tstate->exc_info;
gen->gi_exc_state.previous_item = prev_exc_info;
tstate->exc_info = &gen->gi_exc_state;
ret = PyObject_CallFunctionObjArgs(meth, typ, val, tb, NULL);
tstate->exc_info = prev_exc_info;
gen->gi_exc_state.previous_item = NULL;
_PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
tstate->current_frame = prev;
frame->previous = NULL;
Expand Down
Loading