Skip to content

Commit 565b4f1

Browse files
Carreau1st1
andcommitted
bpo-34616: Add PyCF_ALLOW_TOP_LEVEL_AWAIT to allow top-level await (GH-13148)
Co-Authored-By: Yury Selivanov <yury@magic.io>
1 parent aa32a7e commit 565b4f1

File tree

7 files changed

+109
-8
lines changed

7 files changed

+109
-8
lines changed

Doc/library/functions.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ are always available. They are listed here in alphabetical order.
257257
can be found as the :attr:`~__future__._Feature.compiler_flag` attribute on
258258
the :class:`~__future__._Feature` instance in the :mod:`__future__` module.
259259

260+
The optional argument *flags* also controls whether the compiled source is
261+
allowed to contain top-level ``await``, ``async for`` and ``async with``.
262+
When the bit ``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` is set, the return code
263+
object has ``CO_COROUTINE`` set in ``co_code``, and can be interactively
264+
executed via ``await eval(code_object)``.
265+
260266
The argument *optimize* specifies the optimization level of the compiler; the
261267
default value of ``-1`` selects the optimization level of the interpreter as
262268
given by :option:`-O` options. Explicit levels are ``0`` (no optimization;
@@ -290,6 +296,10 @@ are always available. They are listed here in alphabetical order.
290296
Previously, :exc:`TypeError` was raised when null bytes were encountered
291297
in *source*.
292298

299+
.. versionadded:: 3.8
300+
``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` can now be passed in flags to enable
301+
support for top-level ``await``, ``async for``, and ``async with``.
302+
293303

294304
.. class:: complex([real[, imag]])
295305

Include/compile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ PyAPI_FUNC(PyCodeObject *) PyNode_Compile(struct _node *, const char *);
2323
#define PyCF_ONLY_AST 0x0400
2424
#define PyCF_IGNORE_COOKIE 0x0800
2525
#define PyCF_TYPE_COMMENTS 0x1000
26+
#define PyCF_ALLOW_TOP_LEVEL_AWAIT 0x2000
2627

2728
#ifndef Py_LIMITED_API
2829
typedef struct {

Lib/test/test_builtin.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Python test set -- built-in functions
22

33
import ast
4+
import asyncio
45
import builtins
56
import collections
67
import decimal
@@ -18,9 +19,14 @@
1819
import unittest
1920
import warnings
2021
from contextlib import ExitStack
22+
from inspect import CO_COROUTINE
23+
from itertools import product
24+
from textwrap import dedent
25+
from types import AsyncGeneratorType, FunctionType
2126
from operator import neg
2227
from test.support import (
23-
EnvironmentVarGuard, TESTFN, check_warnings, swap_attr, unlink)
28+
EnvironmentVarGuard, TESTFN, check_warnings, swap_attr, unlink,
29+
maybe_get_event_loop_policy)
2430
from test.support.script_helper import assert_python_ok
2531
from unittest.mock import MagicMock, patch
2632
try:
@@ -358,6 +364,71 @@ def f(): """doc"""
358364
rv = ns['f']()
359365
self.assertEqual(rv, tuple(expected))
360366

367+
def test_compile_top_level_await(self):
368+
"""Test whether code some top level await can be compiled.
369+
370+
Make sure it compiles only with the PyCF_ALLOW_TOP_LEVEL_AWAIT flag set,
371+
and make sure the generated code object has the CO_COROUTINE flag set in
372+
order to execute it with `await eval(.....)` instead of exec, or via a
373+
FunctionType.
374+
"""
375+
376+
# helper function just to check we can run top=level async-for
377+
async def arange(n):
378+
for i in range(n):
379+
yield i
380+
381+
modes = ('single', 'exec')
382+
code_samples = ['''a = await asyncio.sleep(0, result=1)''',
383+
'''async for i in arange(1):
384+
a = 1''',
385+
'''async with asyncio.Lock() as l:
386+
a = 1''']
387+
policy = maybe_get_event_loop_policy()
388+
try:
389+
for mode, code_sample in product(modes,code_samples):
390+
source = dedent(code_sample)
391+
with self.assertRaises(SyntaxError, msg=f"{source=} {mode=}"):
392+
compile(source, '?' , mode)
393+
394+
co = compile(source,
395+
'?',
396+
mode,
397+
flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
398+
399+
self.assertEqual(co.co_flags & CO_COROUTINE, CO_COROUTINE,
400+
msg=f"{source=} {mode=}")
401+
402+
403+
# test we can create and advance a function type
404+
globals_ = {'asyncio': asyncio, 'a':0, 'arange': arange}
405+
async_f = FunctionType(co, globals_)
406+
asyncio.run(async_f())
407+
self.assertEqual(globals_['a'], 1)
408+
409+
# test we can await-eval,
410+
globals_ = {'asyncio': asyncio, 'a':0, 'arange': arange}
411+
asyncio.run(eval(co, globals_))
412+
self.assertEqual(globals_['a'], 1)
413+
finally:
414+
asyncio.set_event_loop_policy(policy)
415+
416+
def test_compile_async_generator(self):
417+
"""
418+
With the PyCF_ALLOW_TOP_LEVEL_AWAIT flag added in 3.8, we want to
419+
make sure AsyncGenerators are still properly not marked with CO_COROUTINE
420+
"""
421+
code = dedent("""async def ticker():
422+
for i in range(10):
423+
yield i
424+
await asyncio.sleep(0)""")
425+
426+
co = compile(code, '?', 'exec', flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
427+
glob = {}
428+
exec(co, glob)
429+
self.assertEqual(type(glob['ticker']()), AsyncGeneratorType)
430+
431+
361432
def test_delattr(self):
362433
sys.spam = 1
363434
delattr(sys, 'spam')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The ``compile()`` builtin functions now support the ``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` flag, which allow to compile sources that contains top-level ``await``, ``async with`` or ``async for``. This is useful to evaluate async-code from with an already async functions; for example in a custom REPL.

Parser/asdl_c.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,8 @@ def visitModule(self, mod):
10001000
self.emit("if (!m) return NULL;", 1)
10011001
self.emit("d = PyModule_GetDict(m);", 1)
10021002
self.emit('if (PyDict_SetItemString(d, "AST", (PyObject*)&AST_type) < 0) return NULL;', 1)
1003+
self.emit('if (PyModule_AddIntMacro(m, PyCF_ALLOW_TOP_LEVEL_AWAIT) < 0)', 1)
1004+
self.emit("return NULL;", 2)
10031005
self.emit('if (PyModule_AddIntMacro(m, PyCF_ONLY_AST) < 0)', 1)
10041006
self.emit("return NULL;", 2)
10051007
self.emit('if (PyModule_AddIntMacro(m, PyCF_TYPE_COMMENTS) < 0)', 1)

Python/Python-ast.c

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/compile.c

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,7 +2609,9 @@ static int
26092609
compiler_async_for(struct compiler *c, stmt_ty s)
26102610
{
26112611
basicblock *start, *except, *end;
2612-
if (c->u->u_scope_type != COMPILER_SCOPE_ASYNC_FUNCTION) {
2612+
if (c->c_flags->cf_flags & PyCF_ALLOW_TOP_LEVEL_AWAIT){
2613+
c->u->u_ste->ste_coroutine = 1;
2614+
} else if (c->u->u_scope_type != COMPILER_SCOPE_ASYNC_FUNCTION) {
26132615
return compiler_error(c, "'async for' outside async function");
26142616
}
26152617

@@ -4564,7 +4566,9 @@ compiler_async_with(struct compiler *c, stmt_ty s, int pos)
45644566
withitem_ty item = asdl_seq_GET(s->v.AsyncWith.items, pos);
45654567

45664568
assert(s->kind == AsyncWith_kind);
4567-
if (c->u->u_scope_type != COMPILER_SCOPE_ASYNC_FUNCTION) {
4569+
if (c->c_flags->cf_flags & PyCF_ALLOW_TOP_LEVEL_AWAIT){
4570+
c->u->u_ste->ste_coroutine = 1;
4571+
} else if (c->u->u_scope_type != COMPILER_SCOPE_ASYNC_FUNCTION){
45684572
return compiler_error(c, "'async with' outside async function");
45694573
}
45704574

@@ -4773,12 +4777,16 @@ compiler_visit_expr1(struct compiler *c, expr_ty e)
47734777
ADDOP(c, YIELD_FROM);
47744778
break;
47754779
case Await_kind:
4776-
if (c->u->u_ste->ste_type != FunctionBlock)
4777-
return compiler_error(c, "'await' outside function");
4780+
if (!(c->c_flags->cf_flags & PyCF_ALLOW_TOP_LEVEL_AWAIT)){
4781+
if (c->u->u_ste->ste_type != FunctionBlock){
4782+
return compiler_error(c, "'await' outside function");
4783+
}
47784784

4779-
if (c->u->u_scope_type != COMPILER_SCOPE_ASYNC_FUNCTION &&
4780-
c->u->u_scope_type != COMPILER_SCOPE_COMPREHENSION)
4781-
return compiler_error(c, "'await' outside async function");
4785+
if (c->u->u_scope_type != COMPILER_SCOPE_ASYNC_FUNCTION &&
4786+
c->u->u_scope_type != COMPILER_SCOPE_COMPREHENSION){
4787+
return compiler_error(c, "'await' outside async function");
4788+
}
4789+
}
47824790

47834791
VISIT(c, expr, e->v.Await.value);
47844792
ADDOP(c, GET_AWAITABLE);
@@ -5712,6 +5720,12 @@ compute_code_flags(struct compiler *c)
57125720
/* (Only) inherit compilerflags in PyCF_MASK */
57135721
flags |= (c->c_flags->cf_flags & PyCF_MASK);
57145722

5723+
if ((c->c_flags->cf_flags & PyCF_ALLOW_TOP_LEVEL_AWAIT) &&
5724+
ste->ste_coroutine &&
5725+
!ste->ste_generator) {
5726+
flags |= CO_COROUTINE;
5727+
}
5728+
57155729
return flags;
57165730
}
57175731

0 commit comments

Comments
 (0)