async_helpers.py
157 lines
| 4.2 KiB
| text/x-python
|
PythonLexer
Matthias Bussonnier
|
r24463 | """ | ||
Async helper function that are invalid syntax on Python 3.5 and below. | ||||
Matthias Bussonnier
|
r24490 | This code is best effort, and may have edge cases not behaving as expected. In | ||
particular it contain a number of heuristics to detect whether code is | ||||
effectively async and need to run in an event loop or not. | ||||
Matthias Bussonnier
|
r24463 | |||
Matthias Bussonnier
|
r24490 | Some constructs (like top-level `return`, or `yield`) are taken care of | ||
explicitly to actually raise a SyntaxError and stay as close as possible to | ||||
Python semantics. | ||||
Matthias Bussonnier
|
r24463 | """ | ||
import ast | ||||
import sys | ||||
from textwrap import dedent, indent | ||||
Matthias Bussonnier
|
r24490 | class _AsyncIORunner: | ||
def __call__(self, coro): | ||||
""" | ||||
Handler for asyncio autoawait | ||||
""" | ||||
import asyncio | ||||
return asyncio.get_event_loop().run_until_complete(coro) | ||||
Matthias Bussonnier
|
r24474 | |||
Matthias Bussonnier
|
r24490 | def __str__(self): | ||
return 'asyncio' | ||||
_asyncio_runner = _AsyncIORunner() | ||||
Matthias Bussonnier
|
r24463 | |||
def _curio_runner(coroutine): | ||||
""" | ||||
handler for curio autoawait | ||||
""" | ||||
import curio | ||||
Matthias Bussonnier
|
r24474 | |||
Matthias Bussonnier
|
r24463 | return curio.run(coroutine) | ||
Matthias Bussonnier
|
r24481 | def _trio_runner(async_fn): | ||
Matthias Bussonnier
|
r24480 | import trio | ||
Matthias Bussonnier
|
r24490 | |||
Matthias Bussonnier
|
r24480 | async def loc(coro): | ||
""" | ||||
We need the dummy no-op async def to protect from | ||||
trio's internal. See https://github.com/python-trio/trio/issues/89 | ||||
""" | ||||
return await coro | ||||
Matthias Bussonnier
|
r24490 | |||
Matthias Bussonnier
|
r24481 | return trio.run(loc, async_fn) | ||
def _pseudo_sync_runner(coro): | ||||
""" | ||||
A runner that does not really allow async execution, and just advance the coroutine. | ||||
See discussion in https://github.com/python-trio/trio/issues/608, | ||||
Credit to Nathaniel Smith | ||||
""" | ||||
try: | ||||
coro.send(None) | ||||
except StopIteration as exc: | ||||
return exc.value | ||||
else: | ||||
# TODO: do not raise but return an execution result with the right info. | ||||
Matthias Bussonnier
|
r24490 | raise RuntimeError( | ||
"{coro_name!r} needs a real async loop".format(coro_name=coro.__name__) | ||||
) | ||||
Matthias Bussonnier
|
r24463 | |||
def _asyncify(code: str) -> str: | ||||
"""wrap code in async def definition. | ||||
And setup a bit of context to run it later. | ||||
""" | ||||
Matthias Bussonnier
|
r24474 | res = dedent( | ||
Matthias Bussonnier
|
r24490 | """ | ||
Paul Ganssle
|
r24487 | async def __wrapper__(): | ||
try: | ||||
{usercode} | ||||
finally: | ||||
locals() | ||||
""" | ||||
).format(usercode=indent(code, " " * 8)) | ||||
Matthias Bussonnier
|
r24463 | return res | ||
Paul Ganssle
|
r24485 | class _AsyncSyntaxErrorVisitor(ast.NodeVisitor): | ||
""" | ||||
Find syntax errors that would be an error in an async repl, but because | ||||
the implementation involves wrapping the repl in an async function, it | ||||
is erroneously allowed (e.g. yield or return at the top level) | ||||
""" | ||||
Matthias Bussonnier
|
r24490 | |||
Paul Ganssle
|
r24485 | def generic_visit(self, node): | ||
func_types = (ast.FunctionDef, ast.AsyncFunctionDef) | ||||
invalid_types = (ast.Return, ast.Yield, ast.YieldFrom) | ||||
if isinstance(node, func_types): | ||||
Matthias Bussonnier
|
r24490 | return # Don't recurse into functions | ||
Paul Ganssle
|
r24485 | elif isinstance(node, invalid_types): | ||
raise SyntaxError() | ||||
else: | ||||
super().generic_visit(node) | ||||
def _async_parse_cell(cell: str) -> ast.AST: | ||||
""" | ||||
This is a compatibility shim for pre-3.7 when async outside of a function | ||||
is a syntax error at the parse stage. | ||||
It will return an abstract syntax tree parsed as if async and await outside | ||||
of a function were not a syntax error. | ||||
""" | ||||
if sys.version_info < (3, 7): | ||||
# Prior to 3.7 you need to asyncify before parse | ||||
wrapped_parse_tree = ast.parse(_asyncify(cell)) | ||||
return wrapped_parse_tree.body[0].body[0] | ||||
else: | ||||
return ast.parse(cell) | ||||
Matthias Bussonnier
|
r24463 | def _should_be_async(cell: str) -> bool: | ||
"""Detect if a block of code need to be wrapped in an `async def` | ||||
Attempt to parse the block of code, it it compile we're fine. | ||||
Otherwise we wrap if and try to compile. | ||||
If it works, assume it should be async. Otherwise Return False. | ||||
Not handled yet: If the block of code has a return statement as the top | ||||
level, it will be seen as async. This is a know limitation. | ||||
""" | ||||
try: | ||||
Matthias Bussonnier
|
r24468 | # we can't limit ourself to ast.parse, as it __accepts__ to parse on | ||
# 3.7+, but just does not _compile_ | ||||
Matthias Bussonnier
|
r24474 | compile(cell, "<>", "exec") | ||
Matthias Bussonnier
|
r24463 | return False | ||
except SyntaxError: | ||||
try: | ||||
Paul Ganssle
|
r24485 | parse_tree = _async_parse_cell(cell) | ||
# Raise a SyntaxError if there are top-level return or yields | ||||
v = _AsyncSyntaxErrorVisitor() | ||||
v.visit(parse_tree) | ||||
Matthias Bussonnier
|
r24463 | except SyntaxError: | ||
return False | ||||
return True | ||||
return False | ||||