Skip to content

Commit cd4bf33

Browse files
graingertclaude
andcommitted
gh-143055: address review (fix throw() arity crash, doc precision)
- async_gen_unpack_throw: validate arity with _PyArg_CheckPositional so that calling throw() with no arguments on the internal wrapper (reachable via ag_await while suspended at the delegation) raises TypeError instead of crashing the interpreter via gen_set_exception(NULL, ...). - Correct the whatsnew/NEWS/reference wording: an async generator expression's aclose() throws GeneratorExit into the sub-iterator via its throw(), rather than calling close(). - Add a regression test for the throw() arity check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c1e0905 commit cd4bf33

5 files changed

Lines changed: 31 additions & 13 deletions

File tree

Doc/reference/expressions.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -895,9 +895,10 @@ In a generator expression, a starred expression is delegated to using
895895
:keyword:`yield from <yield>` semantics: values sent into the generator with
896896
:meth:`~generator.send` and exceptions thrown in with :meth:`~generator.throw`
897897
are forwarded to the sub-iterator currently being unpacked. The same applies
898-
to asynchronous generator expressions, where :meth:`~agen.asend`,
899-
:meth:`~agen.athrow` and :meth:`~agen.aclose` are forwarded to the
900-
(synchronous) sub-iterator.
898+
to asynchronous generator expressions, where :meth:`~agen.asend` is forwarded
899+
to the (synchronous) sub-iterator's :meth:`~generator.send`, and
900+
:meth:`~agen.athrow` and :meth:`~agen.aclose` to its :meth:`~generator.throw`
901+
(:meth:`~agen.aclose` throwing :exc:`GeneratorExit`).
901902

902903
.. versionadded:: 3.15
903904

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ Other language changes
8080
:keyword:`yield from <yield>` semantics. Values sent with
8181
:meth:`~generator.send` and exceptions thrown with
8282
:meth:`~generator.throw` are forwarded to the sub-iterator, and the same
83-
applies to asynchronous generator expressions, where
84-
:meth:`~agen.asend`, :meth:`~agen.athrow` and :meth:`~agen.aclose`
85-
are forwarded to the (synchronous) sub-iterator's
86-
:meth:`~generator.send`, :meth:`~generator.throw` and
87-
:meth:`~generator.close`.
83+
applies to asynchronous generator expressions: :meth:`~agen.asend` is
84+
forwarded to the (synchronous) sub-iterator's :meth:`~generator.send`,
85+
while :meth:`~agen.athrow` and :meth:`~agen.aclose` are forwarded to its
86+
:meth:`~generator.throw` (:meth:`~agen.aclose` throwing
87+
:exc:`GeneratorExit`, as :meth:`generator.close` does).
8888
(Contributed in :gh:`143055`.)
8989

9090

Lib/test/test_unpack_ex.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,20 @@ def sub():
901901
self._run(agen.aclose())
902902
self.assertEqual(closed, [True])
903903

904+
def test_unpack_helper_throw_requires_argument(self):
905+
# The internal sync-iterable wrapper is reachable via ag_await while
906+
# suspended at the delegation; throw() must validate its arity rather
907+
# than crash (gh-143055).
908+
agen = (*[1, 2, 3] async for _ in self._aiter([0]))
909+
self._run(agen.asend(None))
910+
wrapper = agen.ag_await
911+
self.assertIsNotNone(wrapper)
912+
with self.assertRaises(TypeError):
913+
wrapper.throw()
914+
# A valid exception is still accepted and propagates out.
915+
with self.assertRaises(ValueError):
916+
wrapper.throw(ValueError('boom'))
917+
904918
def test_unpacking_async_iterable_is_a_type_error(self):
905919
# ``*`` unpacking is synchronous; async iterables cannot be unpacked.
906920
async def agen_fn():

Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-12-00-00.gh-issue-143055.Yd8fG2.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ now delegates to the sub-iterable using :keyword:`yield from <yield>`
33
semantics, so that values sent with :meth:`~generator.send` and exceptions
44
thrown with :meth:`~generator.throw` are forwarded to the sub-iterator. This
55
also works in asynchronous generator expressions, where
6-
:meth:`~agen.asend`, :meth:`~agen.athrow` and :meth:`~agen.aclose`
7-
are forwarded to the (synchronous) sub-iterator's
8-
:meth:`~generator.send`, :meth:`~generator.throw` and
9-
:meth:`~generator.close`.
6+
:meth:`~agen.asend` is forwarded to the (synchronous) sub-iterator's
7+
:meth:`~generator.send`, and :meth:`~agen.athrow` and :meth:`~agen.aclose`
8+
are forwarded to its :meth:`~generator.throw` (:meth:`~agen.aclose` throwing
9+
:exc:`GeneratorExit`).

Objects/genobject.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2351,6 +2351,9 @@ async_gen_unpack_am_send(PyObject *op, PyObject *arg, PyObject **presult)
23512351
static PyObject *
23522352
async_gen_unpack_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs)
23532353
{
2354+
if (!_PyArg_CheckPositional("throw", nargs, 1, 3)) {
2355+
return NULL;
2356+
}
23542357
PyAsyncGenUnpack *o = _PyAsyncGenUnpack_CAST(op);
23552358
PyObject *meth;
23562359
if (PyObject_GetOptionalAttr(o->agu_iterator, &_Py_ID(throw), &meth) < 0) {
@@ -2361,7 +2364,7 @@ async_gen_unpack_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs)
23612364
it here. This mirrors how `yield from` behaves when throwing into
23622365
a subiterator that does not define throw(): the exception
23632366
propagates out of the delegation. */
2364-
PyObject *typ = nargs >= 1 ? args[0] : NULL;
2367+
PyObject *typ = args[0];
23652368
PyObject *val = nargs >= 2 ? args[1] : NULL;
23662369
PyObject *tb = nargs >= 3 ? args[2] : NULL;
23672370
gen_set_exception(typ, val, tb);

0 commit comments

Comments
 (0)