Skip to content
Draft
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
14 changes: 14 additions & 0 deletions Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -891,11 +891,25 @@ For example::
>>> {**d for d in configuration_sets}
{'color': 'yellow', 'count': 5}

In a generator expression, a starred expression is delegated to using
:keyword:`yield from <yield>` semantics: values sent into the generator with
:meth:`~generator.send` and exceptions thrown in with :meth:`~generator.throw`
are forwarded to the sub-iterator currently being unpacked. The same applies
to asynchronous generator expressions, where :meth:`~agen.asend` is forwarded
to the (synchronous) sub-iterator's :meth:`~generator.send`, and
:meth:`~agen.athrow` and :meth:`~agen.aclose` to its :meth:`~generator.throw`
(:meth:`~agen.aclose` throwing :exc:`GeneratorExit`).

.. versionadded:: 3.15

Unpacking in comprehensions using the ``*`` and ``**`` operators
was introduced in :pep:`798`.

.. versionchanged:: 3.16

Unpacking a starred expression in a generator expression delegates to the
sub-iterator using :keyword:`yield from <yield>` semantics.


.. index::
single: async for; in comprehensions
Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ New features
Other language changes
======================

* Unpacking a sub-iterable with ``*`` in a generator expression
(:pep:`798`) now delegates to the sub-iterable using
:keyword:`yield from <yield>` semantics. Values sent with
:meth:`~generator.send` and exceptions thrown with
:meth:`~generator.throw` are forwarded to the sub-iterator, and the same
applies to asynchronous generator expressions: :meth:`~agen.asend` is
forwarded to the (synchronous) sub-iterator's :meth:`~generator.send`,
while :meth:`~agen.athrow` and :meth:`~agen.aclose` are forwarded to its
:meth:`~generator.throw` (:meth:`~agen.aclose` throwing
:exc:`GeneratorExit`, as :meth:`generator.close` does).
(Contributed in :gh:`143055`.)



New modules
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_genobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **);

PyAPI_FUNC(PyObject *)_PyCoro_GetAwaitableIter(PyObject *o);
PyAPI_FUNC(PyObject *)_PyAsyncGenValueWrapperNew(PyThreadState *state, PyObject *);
PyAPI_FUNC(PyObject *)_PyAsyncGenUnpack_New(PyThreadState *state, PyObject *);

// Exported for external JIT support
PyAPI_FUNC(PyObject *) _PyCoro_ComputeOrigin(int origin_depth, _PyInterpreterFrame *current_frame);

extern PyTypeObject _PyCoroWrapper_Type;
extern PyTypeObject _PyAsyncGenWrappedValue_Type;
extern PyTypeObject _PyAsyncGenAThrow_Type;
extern PyTypeObject _PyAsyncGenUnpack_Type;

PyAPI_FUNC(PySendResult) _PyAsyncGenASend_Send(PyObject *iter, PyObject *arg, PyObject **result);

Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ struct _py_func_state {
If you add a new static type to the standard library, you may have to
update one of these numbers.
*/
#define _Py_NUM_MANAGED_PREINITIALIZED_TYPES 120
#define _Py_NUM_MANAGED_PREINITIALIZED_TYPES 121
#define _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES \
(_Py_NUM_MANAGED_PREINITIALIZED_TYPES + 83)
#define _Py_MAX_MANAGED_STATIC_EXT_TYPES 10
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_intrinsics.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
#define INTRINSIC_SUBSCRIPT_GENERIC 10
#define INTRINSIC_TYPEALIAS 11
#define INTRINSIC_BUILD_FROZENSET 12
#define INTRINSIC_ASYNC_GEN_UNPACK 13

#define MAX_INTRINSIC_1 12
#define MAX_INTRINSIC_1 13


/* Binary Functions: */
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_magic_number.h
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ Known values:
Python 3.15b1 3666 (Add SEND_VIRTUAL and SEND_ASYNC_GEN specializations)
Python 3.16a0 3700 (Initial version)
Python 3.16a0 3701 (Add CONSTANT_EMPTY_TUPLE to LOAD_COMMON_CONSTANT)
Python 3.16a0 3702 (Delegate to subiterators when unpacking in generator expressions)


Python 3.17 will start with 3750
Expand All @@ -312,7 +313,7 @@ PC/launcher.c must also be updated.

*/

#define PYC_MAGIC_NUMBER 3701
#define PYC_MAGIC_NUMBER 3702
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
(little-endian) and then appending b'\r\n'. */
#define PYC_MAGIC_NUMBER_TOKEN \
Expand Down
177 changes: 177 additions & 0 deletions Lib/test/test_unpack_ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,183 @@ def test_errors_in_getitem():

__test__ = {'doctests' : doctests}


class TestGeneratorExpressionDelegation(unittest.TestCase):
# Unpacking a sub-iterable with ``*`` in a generator expression delegates
# to the sub-iterable using ``yield from`` semantics, so that values sent
# to (and exceptions thrown into) the generator are forwarded.

def test_flatten(self):
lists = [[1, 2], [3], [], [4, 5]]
self.assertEqual(list((*sub for sub in lists)), [1, 2, 3, 4, 5])

def test_yields_from_multiple_iterables(self):
gen = (*(0, 1) for i in range(3))
self.assertEqual(list(gen), [0, 1, 0, 1, 0, 1])

def test_send_is_forwarded(self):
received = []

def sub():
while True:
received.append((yield 'value'))

gen = (*sub() for _ in range(1))
self.assertEqual(next(gen), 'value')
self.assertEqual(gen.send(42), 'value')
self.assertEqual(gen.send(7), 'value')
self.assertEqual(received, [42, 7])

def test_throw_is_forwarded(self):
caught = []

def sub():
try:
yield 1
yield 2
except ValueError as exc:
caught.append(str(exc))
yield 'after-catch'

gen = (*sub() for _ in range(1))
self.assertEqual(next(gen), 1)
self.assertEqual(gen.throw(ValueError('boom')), 'after-catch')
self.assertEqual(caught, ['boom'])

def test_close_is_forwarded(self):
closed = []

def sub():
try:
yield 1
yield 2
except GeneratorExit:
closed.append(True)
raise

gen = (*sub() for _ in range(1))
self.assertEqual(next(gen), 1)
gen.close()
self.assertEqual(closed, [True])

def test_subiterator_return_value_is_discarded(self):
def sub(n):
yield n
return 'ignored'

self.assertEqual(list((*sub(i) for i in range(3))), [0, 1, 2])


class TestAsyncGeneratorExpressionDelegation(unittest.TestCase):
# Unpacking a (synchronous) sub-iterable with ``*`` in an asynchronous
# generator expression also delegates with ``yield from`` semantics:
# asend() forwards to the sub-iterator's send(), athrow() to throw() and
# aclose() to close().
#
# These are low-level language tests, so (like test_asyncgen) the async
# generators are driven by hand rather than through an event loop.

@staticmethod
async def _aiter(seq):
for item in seq:
yield item

@staticmethod
def _run(coro):
# Drive a coroutine that is not expected to await anything real, and
# return its result. Any StopAsyncIteration (e.g. an exhausted
# asend()) is allowed to propagate.
try:
coro.send(None)
except StopIteration as exc:
return exc.value
coro.close()
raise AssertionError("coroutine awaited unexpectedly")

def _collect(self, agen):
result = []
while True:
try:
result.append(self._run(agen.asend(None)))
except StopAsyncIteration:
break
return result

def test_flatten(self):
lists = [[1, 2], [3], [], [4, 5]]
agen = (*sub async for sub in self._aiter(lists))
self.assertEqual(self._collect(agen), [1, 2, 3, 4, 5])

def test_asend_forwards_to_send(self):
received = []

def sub():
while True:
received.append((yield 'value'))

agen = (*sub() async for _ in self._aiter([0]))
self.assertEqual(self._run(agen.asend(None)), 'value')
self.assertEqual(self._run(agen.asend(99)), 'value')
self.assertEqual(self._run(agen.asend(123)), 'value')
self.assertEqual(received, [99, 123])

def test_athrow_forwards_to_throw(self):
caught = []

def sub():
try:
yield 1
yield 2
except ValueError as exc:
caught.append(str(exc))
yield 'after-catch'

agen = (*sub() async for _ in self._aiter([0]))
self.assertEqual(self._run(agen.asend(None)), 1)
self.assertEqual(self._run(agen.athrow(ValueError('boom'))),
'after-catch')
self.assertEqual(caught, ['boom'])

def test_aclose_forwards_to_close(self):
closed = []

def sub():
try:
yield 1
yield 2
except GeneratorExit:
closed.append(True)
raise

agen = (*sub() async for _ in self._aiter([0]))
self.assertEqual(self._run(agen.asend(None)), 1)
self._run(agen.aclose())
self.assertEqual(closed, [True])

def test_unpack_helper_throw_requires_argument(self):
# The internal sync-iterable wrapper is reachable via ag_await while
# suspended at the delegation; throw() must validate its arity rather
# than crash (gh-143055).
agen = (*[1, 2, 3] async for _ in self._aiter([0]))
self._run(agen.asend(None))
wrapper = agen.ag_await
self.assertIsNotNone(wrapper)
with self.assertRaises(TypeError):
wrapper.throw()
# A valid exception is still accepted and propagates out.
with self.assertRaises(ValueError):
wrapper.throw(ValueError('boom'))

def test_unpacking_async_iterable_is_a_type_error(self):
# ``*`` unpacking is synchronous; async iterables cannot be unpacked.
async def agen_fn():
yield 1

agen = (*agen_fn() async for _ in self._aiter([0]))
with self.assertRaises(TypeError):
self._run(agen.asend(None))


def load_tests(loader, tests, pattern):
tests.addTest(doctest.DocTestSuite())
return tests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Unpacking a sub-iterable with ``*`` in a generator expression (:pep:`798`)
now delegates to the sub-iterable using :keyword:`yield from <yield>`
semantics, so that values sent with :meth:`~generator.send` and exceptions
thrown with :meth:`~generator.throw` are forwarded to the sub-iterator. This
also works in asynchronous generator expressions, where
:meth:`~agen.asend` is forwarded to the (synchronous) sub-iterator's
:meth:`~generator.send`, and :meth:`~agen.athrow` and :meth:`~agen.aclose`
are forwarded to its :meth:`~generator.throw` (:meth:`~agen.aclose` throwing
:exc:`GeneratorExit`).
Loading
Loading