async_helpers.py
172 lines
| 4.9 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 | ||||
Matthias Bussonnier
|
r25022 | import inspect | ||
Matthias Bussonnier
|
r24463 | from textwrap import dedent, indent | ||
Matthias Bussonnier
|
r24490 | class _AsyncIORunner: | ||
def __call__(self, coro): | ||||
""" | ||||
Handler for asyncio autoawait | ||||
""" | ||||
import asyncio | ||||
Nikita Kniazev
|
r27072 | return asyncio.get_event_loop_policy().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) | ||||
""" | ||||
felixzhuologist
|
r24665 | def __init__(self): | ||
Matthias Bussonnier
|
r25022 | if sys.version_info >= (3,8): | ||
raise ValueError('DEPRECATED in Python 3.8+') | ||||
felixzhuologist
|
r24665 | self.depth = 0 | ||
felixzhuologist
|
r24664 | super().__init__() | ||
Matthias Bussonnier
|
r24490 | |||
Paul Ganssle
|
r24485 | def generic_visit(self, node): | ||
func_types = (ast.FunctionDef, ast.AsyncFunctionDef) | ||||
felixzhuologist
|
r24665 | invalid_types_by_depth = { | ||
0: (ast.Return, ast.Yield, ast.YieldFrom), | ||||
1: (ast.Nonlocal,) | ||||
} | ||||
should_traverse = self.depth < max(invalid_types_by_depth.keys()) | ||||
if isinstance(node, func_types) and should_traverse: | ||||
self.depth += 1 | ||||
felixzhuologist
|
r24664 | super().generic_visit(node) | ||
Matthias Bussonnier
|
r24964 | self.depth -= 1 | ||
felixzhuologist
|
r24665 | elif isinstance(node, invalid_types_by_depth[self.depth]): | ||
Paul Ganssle
|
r24485 | raise SyntaxError() | ||
else: | ||||
super().generic_visit(node) | ||||
felixzhuologist
|
r24665 | |||
Paul Ganssle
|
r24485 | 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. | ||||
yangyang
|
r25429 | Not handled yet: If the block of code has a return statement as the top | ||
Matthias Bussonnier
|
r24463 | level, it will be seen as async. This is a know limitation. | ||
""" | ||||
Matthias Bussonnier
|
r25052 | if sys.version_info > (3, 8): | ||
Matthias Bussonnier
|
r25055 | try: | ||
code = compile(cell, "<>", "exec", flags=getattr(ast,'PyCF_ALLOW_TOP_LEVEL_AWAIT', 0x0)) | ||||
return inspect.CO_COROUTINE & code.co_flags == inspect.CO_COROUTINE | ||||
yangyang
|
r25429 | except (SyntaxError, MemoryError): | ||
Matthias Bussonnier
|
r25055 | return False | ||
Matthias Bussonnier
|
r24463 | 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
|
r25024 | code = compile(cell, "<>", "exec") | ||
yangyang
|
r25429 | except (SyntaxError, MemoryError): | ||
Matthias Bussonnier
|
r24463 | 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) | ||||
yangyang
|
r25429 | except (SyntaxError, MemoryError): | ||
Matthias Bussonnier
|
r24463 | return False | ||
return True | ||||
return False | ||||