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
16 changes: 16 additions & 0 deletions Lib/test/test_asyncgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,22 @@ async def gen():
'async generator.*StopIteration'):
to_list(gen())

def test_async_gen_athrow_stopiteration_not_started(self):
# gh-152685: StopIteration and StopAsyncIteration thrown into an
# async generator that hasn't started yet are wrapped in a
# RuntimeError, like for a started one.
async def gen():
yield 123

for exc, msg in [
(StopIteration, 'async generator raised StopIteration'),
(StopAsyncIteration, 'async generator raised StopAsyncIteration'),
]:
with self.subTest(exc=exc):
agen = gen()
with self.assertRaisesRegex(RuntimeError, msg):
agen.athrow(exc).send(None)

def test_async_gen_exception_07(self):
def sync_gen():
try:
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/test_coroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,17 @@ async def foo():

run_async(foo())

def test_throw_stopiteration_not_started(self):
# gh-152685: StopIteration thrown into a coroutine that hasn't
# started yet is wrapped in a RuntimeError, like for a started one.
async def foo():
return 1

coro = foo()
with self.assertRaisesRegex(
RuntimeError, "coroutine raised StopIteration"):
coro.throw(StopIteration)

def test_func_3(self):
async def foo():
raise StopIteration
Expand Down
45 changes: 45 additions & 0 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,51 @@ def g():
with self.assertRaises(RuntimeError) as cm:
gen.throw(ValueError)

def test_throw_stopiteration_not_started(self):
# Throwing StopIteration into a not-yet-started generator must be
# wrapped in a RuntimeError, the same as for a started one
# (PEP 479, gh-152685).
def g():
yield

for started in (False, True):
with self.subTest(started=started):
gen = g()
if started:
next(gen)
with self.assertRaisesRegex(RuntimeError,
'generator raised StopIteration'):
gen.throw(StopIteration)
# The generator is finished afterwards.
self.assertRaises(StopIteration, next, gen)

# A StopIteration subclass is wrapped too.
class MyStop(StopIteration):
pass
with self.assertRaises(RuntimeError):
g().throw(MyStop)

# The original StopIteration is kept as cause and context.
try:
g().throw(StopIteration)
except RuntimeError as exc:
self.assertIs(type(exc.__cause__), StopIteration)
self.assertIs(type(exc.__context__), StopIteration)
self.assertTrue(exc.__suppress_context__)
else:
self.fail('RuntimeError not raised')

# A non-StopIteration exception is still propagated unchanged.
with self.assertRaises(ValueError):
g().throw(ValueError)

def test_throw_stopiteration_not_started_genexpr(self):
# Same as test_throw_stopiteration_not_started, for a generator
# expression (gh-152685).
with self.assertRaisesRegex(RuntimeError,
'generator raised StopIteration'):
(x for x in ()).throw(StopIteration)


class GeneratorStackTraceTest(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Calling ``throw(StopIteration)`` on a generator, generator expression,
coroutine, or asynchronous generator that has not started running now raises
:exc:`RuntimeError`, consistent with :pep:`479` and with throwing into an
already-started generator. Previously the :exc:`StopIteration` escaped
unchanged. Patch by Joel Walker.
27 changes: 24 additions & 3 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,30 @@ gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult, int exc)
}
}
else {
assert(!PyErr_ExceptionMatches(PyExc_StopIteration));
assert(!PyAsyncGen_CheckExact(gen) ||
!PyErr_ExceptionMatches(PyExc_StopAsyncIteration));
/* The frame propagated an exception. A StopIteration (or, in an async
* generator, a StopAsyncIteration) raised inside the frame is normally
* turned into a RuntimeError by the frame's own PEP 479 handler. That
* handler does not cover the prologue where a not-yet-started generator
* resumes, so throwing such an exception into one reaches here
* unconverted (gh-152685). Apply the conversion so PEP 479 still
* holds. */
const char *msg = NULL;
if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
msg = "generator raised StopIteration";
if (PyAsyncGen_CheckExact(gen)) {
msg = "async generator raised StopIteration";
}
else if (PyCoro_CheckExact(gen)) {
msg = "coroutine raised StopIteration";
}
}
else if (PyAsyncGen_CheckExact(gen) &&
_PyErr_ExceptionMatches(tstate, PyExc_StopAsyncIteration)) {
msg = "async generator raised StopAsyncIteration";
}
if (msg != NULL) {
_PyErr_FormatFromCauseTstate(tstate, PyExc_RuntimeError, "%s", msg);
}
}

*presult = result;
Expand Down
Loading