async_helpers.py
155 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 | ||||
Min RK
|
r27192 | import asyncio | ||
Matthias Bussonnier
|
r25022 | import inspect | ||
Min RK
|
r27387 | from functools import wraps | ||
Matthias Bussonnier
|
r24463 | |||
Min RK
|
r27387 | _asyncio_event_loop = None | ||
def get_asyncio_loop(): | ||||
"""asyncio has deprecated get_event_loop | ||||
Replicate it here, with our desired semantics: | ||||
- always returns a valid, not-closed loop | ||||
- not thread-local like asyncio's, | ||||
because we only want one loop for IPython | ||||
- if called from inside a coroutine (e.g. in ipykernel), | ||||
return the running loop | ||||
.. versionadded:: 8.0 | ||||
""" | ||||
try: | ||||
return asyncio.get_running_loop() | ||||
except RuntimeError: | ||||
# not inside a coroutine, | ||||
# track our own global | ||||
pass | ||||
# not thread-local like asyncio's, | ||||
# because we only track one event loop to run for IPython itself, | ||||
# always in the main thread. | ||||
global _asyncio_event_loop | ||||
if _asyncio_event_loop is None or _asyncio_event_loop.is_closed(): | ||||
_asyncio_event_loop = asyncio.new_event_loop() | ||||
return _asyncio_event_loop | ||||
Matthias Bussonnier
|
r24463 | |||
Min RK
|
r27192 | |||
Min RK
|
r27387 | class _AsyncIORunner: | ||
Matthias Bussonnier
|
r24490 | def __call__(self, coro): | ||
""" | ||||
Handler for asyncio autoawait | ||||
""" | ||||
Min RK
|
r27387 | return get_asyncio_loop().run_until_complete(coro) | ||
Matthias Bussonnier
|
r24474 | |||
Matthias Bussonnier
|
r24490 | def __str__(self): | ||
Matthias Bussonnier
|
r27150 | return "asyncio" | ||
Matthias Bussonnier
|
r24490 | |||
_asyncio_runner = _AsyncIORunner() | ||||
Matthias Bussonnier
|
r24463 | |||
Min RK
|
r27387 | class _AsyncIOProxy: | ||
"""Proxy-object for an asyncio | ||||
Any coroutine methods will be wrapped in event_loop.run_ | ||||
""" | ||||
def __init__(self, obj, event_loop): | ||||
self._obj = obj | ||||
self._event_loop = event_loop | ||||
def __repr__(self): | ||||
return f"<_AsyncIOProxy({self._obj!r})>" | ||||
def __getattr__(self, key): | ||||
attr = getattr(self._obj, key) | ||||
if inspect.iscoroutinefunction(attr): | ||||
# if it's a coroutine method, | ||||
# return a threadsafe wrapper onto the _current_ asyncio loop | ||||
@wraps(attr) | ||||
def _wrapped(*args, **kwargs): | ||||
concurrent_future = asyncio.run_coroutine_threadsafe( | ||||
attr(*args, **kwargs), self._event_loop | ||||
) | ||||
return asyncio.wrap_future(concurrent_future) | ||||
return _wrapped | ||||
else: | ||||
return attr | ||||
def __dir__(self): | ||||
return dir(self._obj) | ||||
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 _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. | ||
""" | ||||
try: | ||||
Matthias Bussonnier
|
r27150 | 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
|
r27150 | return False | ||