Skip to content

Commit

Permalink
bpo-33346: Allow async comprehensions inside implicit async comprehen…
Browse files Browse the repository at this point in the history
…sions (GH-6766)

Co-authored-by: Pablo Galindo <pablogsal@gmail.com>
  • Loading branch information
serhiy-storchaka and pablogsal authored Jul 13, 2021
1 parent 0ee0a74 commit 054e9c8
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 17 deletions.
11 changes: 8 additions & 3 deletions Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,9 @@ A comprehension in an :keyword:`!async def` function may consist of either a
:keyword:`!for` or :keyword:`!async for` clause following the leading
expression, may contain additional :keyword:`!for` or :keyword:`!async for`
clauses, and may also use :keyword:`await` expressions.
If a comprehension contains either :keyword:`!async for` clauses
or :keyword:`!await` expressions it is called an
:dfn:`asynchronous comprehension`. An asynchronous comprehension may
If a comprehension contains either :keyword:`!async for` clauses or
:keyword:`!await` expressions or other asynchronous comprehensions it is called
an :dfn:`asynchronous comprehension`. An asynchronous comprehension may
suspend the execution of the coroutine function in which it appears.
See also :pep:`530`.

Expand All @@ -230,6 +230,11 @@ See also :pep:`530`.
.. versionchanged:: 3.8
``yield`` and ``yield from`` prohibited in the implicitly nested scope.

.. versionchanged:: 3.11
Asynchronous comprehensions are now allowed inside comprehensions in
asynchronous functions. Outer comprehensions implicitly become
asynchronous.


.. _lists:

Expand Down
17 changes: 10 additions & 7 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,19 @@ See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya
and Ammar Askar in :issue:`43950`.)



Other Language Changes
======================

A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
:meth:`contextlib.ExitStack.enter_context` and
:meth:`contextlib.AsyncExitStack.enter_async_context` for objects which do not
support the :term:`context manager` or :term:`asynchronous context manager`
protocols correspondingly.
(Contributed by Serhiy Storchaka in :issue:`44471`.)
* Asynchronous comprehensions are now allowed inside comprehensions in
asynchronous functions. Outer comprehensions implicitly become
asynchronous. (Contributed by Serhiy Storchaka in :issue:`33346`.)

* A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
:meth:`contextlib.ExitStack.enter_context` and
:meth:`contextlib.AsyncExitStack.enter_async_context` for objects which do not
support the :term:`context manager` or :term:`asynchronous context manager`
protocols correspondingly.
(Contributed by Serhiy Storchaka in :issue:`44471`.)

* A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
:keyword:`with` and :keyword:`async with` statements for objects which do not
Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_coroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def __await__(self):
yield self.value


async def asynciter(iterable):
"""Convert an iterable to an asynchronous iterator."""
for x in iterable:
yield x


def run_async(coro):
assert coro.__class__ in {types.GeneratorType, types.CoroutineType}

Expand Down Expand Up @@ -125,6 +131,11 @@ def bar():
for c in b]
""",

"""async def foo():
def bar():
[[async for i in b] for b in els]
""",

"""async def foo():
def bar():
[i for i in els
Expand Down Expand Up @@ -200,6 +211,13 @@ def bar():
[i for i in els if await i]
""",

"""def bar():
[[i async for i in a] for a in elts]
""",

"""[[i async for i in a] for a in elts]
""",

"""async def foo():
await
""",
Expand Down Expand Up @@ -2011,6 +2029,60 @@ async def f():
run_async(f()),
([], {1: 1, 2: 2, 3: 3}))

def test_nested_comp(self):
async def run_list_inside_list():
return [[i + j async for i in asynciter([1, 2])] for j in [10, 20]]
self.assertEqual(
run_async(run_list_inside_list()),
([], [[11, 12], [21, 22]]))

async def run_set_inside_list():
return [{i + j async for i in asynciter([1, 2])} for j in [10, 20]]
self.assertEqual(
run_async(run_set_inside_list()),
([], [{11, 12}, {21, 22}]))

async def run_list_inside_set():
return {sum([i async for i in asynciter(range(j))]) for j in [3, 5]}
self.assertEqual(
run_async(run_list_inside_set()),
([], {3, 10}))

async def run_dict_inside_dict():
return {j: {i: i + j async for i in asynciter([1, 2])} for j in [10, 20]}
self.assertEqual(
run_async(run_dict_inside_dict()),
([], {10: {1: 11, 2: 12}, 20: {1: 21, 2: 22}}))

async def run_list_inside_gen():
gen = ([i + j async for i in asynciter([1, 2])] for j in [10, 20])
return [x async for x in gen]
self.assertEqual(
run_async(run_list_inside_gen()),
([], [[11, 12], [21, 22]]))

async def run_gen_inside_list():
gens = [(i async for i in asynciter(range(j))) for j in [3, 5]]
return [x for g in gens async for x in g]
self.assertEqual(
run_async(run_gen_inside_list()),
([], [0, 1, 2, 0, 1, 2, 3, 4]))

async def run_gen_inside_gen():
gens = ((i async for i in asynciter(range(j))) for j in [3, 5])
return [x for g in gens async for x in g]
self.assertEqual(
run_async(run_gen_inside_gen()),
([], [0, 1, 2, 0, 1, 2, 3, 4]))

async def run_list_inside_list_inside_list():
return [[[i + j + k async for i in asynciter([1, 2])]
for j in [10, 20]]
for k in [100, 200]]
self.assertEqual(
run_async(run_list_inside_list_inside_list()),
([], [[[111, 112], [121, 122]], [[211, 212], [221, 222]]]))

def test_copy(self):
async def func(): pass
coro = func()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Asynchronous comprehensions are now allowed inside comprehensions in
asynchronous functions. Outer comprehensions implicitly become
asynchronous.
14 changes: 8 additions & 6 deletions Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -4947,11 +4947,9 @@ compiler_comprehension(struct compiler *c, expr_ty e, int type,
PyCodeObject *co = NULL;
comprehension_ty outermost;
PyObject *qualname = NULL;
int scope_type = c->u->u_scope_type;
int is_async_generator = 0;
int top_level_await = IS_TOP_LEVEL_AWAIT(c);


int is_async_function = c->u->u_ste->ste_coroutine;
int is_top_level_await = IS_TOP_LEVEL_AWAIT(c);

outermost = (comprehension_ty) asdl_seq_GET(generators, 0);
if (!compiler_enter_scope(c, name, COMPILER_SCOPE_COMPREHENSION,
Expand All @@ -4963,7 +4961,11 @@ compiler_comprehension(struct compiler *c, expr_ty e, int type,

is_async_generator = c->u->u_ste->ste_coroutine;

if (is_async_generator && !is_async_function && type != COMP_GENEXP && !top_level_await) {
if (is_async_generator && type != COMP_GENEXP &&
scope_type != COMPILER_SCOPE_ASYNC_FUNCTION &&
scope_type != COMPILER_SCOPE_COMPREHENSION &&
!is_top_level_await)
{
compiler_error(c, "asynchronous comprehension outside of "
"an asynchronous function");
goto error_in_scope;
Expand Down Expand Up @@ -5002,7 +5004,7 @@ compiler_comprehension(struct compiler *c, expr_ty e, int type,
qualname = c->u->u_qualname;
Py_INCREF(qualname);
compiler_exit_scope(c);
if (top_level_await && is_async_generator){
if (is_top_level_await && is_async_generator){
c->u->u_ste->ste_coroutine = 1;
}
if (co == NULL)
Expand Down
9 changes: 8 additions & 1 deletion Python/symtable.c
Original file line number Diff line number Diff line change
Expand Up @@ -2056,7 +2056,14 @@ symtable_handle_comprehension(struct symtable *st, expr_ty e,
return 0;
}
st->st_cur->ste_generator = is_generator;
return symtable_exit_block(st);
int is_async = st->st_cur->ste_coroutine && !is_generator;
if (!symtable_exit_block(st)) {
return 0;
}
if (is_async) {
st->st_cur->ste_coroutine = 1;
}
return 1;
}

static int
Expand Down

0 comments on commit 054e9c8

Please sign in to comment.