autoawait.rst
319 lines
| 11.3 KiB
| text/x-rst
|
RstLexer
Matthias Bussonnier
|
r24466 | .. _autoawait: | ||
Matthias Bussonnier
|
r24463 | |||
Asynchronous in REPL: Autoawait | ||||
=============================== | ||||
Matthias Bussonnier
|
r24490 | .. note:: | ||
Matthias Bussonnier
|
r24546 | This feature is experimental and behavior can change between python and | ||
Matthias Bussonnier
|
r24490 | IPython version without prior deprecation. | ||
Frank Tobia
|
r25836 | Starting with IPython 7.0, and when using Python 3.6 and above, IPython offer the | ||
Matthias Bussonnier
|
r24475 | ability to run asynchronous code from the REPL. Constructs which are | ||
Matthias Bussonnier
|
r24463 | :exc:`SyntaxError` s in the Python REPL can be used seamlessly in IPython. | ||
Matthias Bussonnier
|
r24572 | The examples given here are for terminal IPython, running async code in a | ||
Matthias Bussonnier
|
r24571 | notebook interface or any other frontend using the Jupyter protocol needs | ||
Matthias Bussonnier
|
r24572 | IPykernel version 5.0 or above. The details of how async code runs in IPykernel | ||
will differ between IPython, IPykernel and their versions. | ||||
Matthias Bussonnier
|
r24475 | |||
Matthias Bussonnier
|
r24490 | When a supported library is used, IPython will automatically allow Futures and | ||
Coroutines in the REPL to be ``await`` ed. This will happen if an :ref:`await | ||||
Frank Tobia
|
r25836 | <await>` (or any other async constructs like async-with, async-for) is used at | ||
Matthias Bussonnier
|
r24490 | top level scope, or if any structure valid only in `async def | ||
Matthias Bussonnier
|
r24463 | <https://docs.python.org/3/reference/compound_stmts.html#async-def>`_ function | ||
context are present. For example, the following being a syntax error in the | ||||
Python REPL:: | ||||
Python 3.6.0 | ||||
[GCC 4.2.1] | ||||
Type "help", "copyright", "credits" or "license" for more information. | ||||
>>> import aiohttp | ||||
Sebastian Witowski
|
r24999 | >>> session = aiohttp.ClientSession() | ||
>>> result = session.get('https://api.github.com') | ||||
Matthias Bussonnier
|
r24463 | >>> response = await result | ||
File "<stdin>", line 1 | ||||
response = await result | ||||
^ | ||||
SyntaxError: invalid syntax | ||||
Should behave as expected in the IPython REPL:: | ||||
Python 3.6.0 | ||||
Type 'copyright', 'credits' or 'license' for more information | ||||
Matthias Bussonnier
|
r24475 | IPython 7.0.0 -- An enhanced Interactive Python. Type '?' for help. | ||
Matthias Bussonnier
|
r24463 | |||
In [1]: import aiohttp | ||||
Sebastian Witowski
|
r24999 | ...: session = aiohttp.ClientSession() | ||
...: result = session.get('https://api.github.com') | ||||
Matthias Bussonnier
|
r24463 | |||
In [2]: response = await result | ||||
<pause for a few 100s ms> | ||||
In [3]: await response.json() | ||||
Out[3]: | ||||
{'authorizations_url': 'https://api.github.com/authorizations', | ||||
'code_search_url': 'https://api.github.com/search/code?q={query}...', | ||||
... | ||||
} | ||||
You can use the ``c.InteractiveShell.autoawait`` configuration option and set it | ||||
Matthias Bussonnier
|
r24571 | to :any:`False` to deactivate automatic wrapping of asynchronous code. You can | ||
Matthias Bussonnier
|
r24581 | also use the :magic:`%autoawait` magic to toggle the behavior at runtime:: | ||
Matthias Bussonnier
|
r24463 | |||
In [1]: %autoawait False | ||||
In [2]: %autoawait | ||||
Matthias Bussonnier
|
r24490 | IPython autoawait is `Off`, and set to use `asyncio` | ||
Matthias Bussonnier
|
r24463 | |||
By default IPython will assume integration with Python's provided | ||||
:mod:`asyncio`, but integration with other libraries is provided. In particular | ||||
Matthias Bussonnier
|
r24490 | we provide experimental integration with the ``curio`` and ``trio`` library. | ||
Matthias Bussonnier
|
r24463 | |||
Frank Tobia
|
r25836 | You can switch the current integration by using the | ||
Matthias Bussonnier
|
r24463 | ``c.InteractiveShell.loop_runner`` option or the ``autoawait <name | ||
integration>`` magic. | ||||
For example:: | ||||
In [1]: %autoawait trio | ||||
In [2]: import trio | ||||
In [3]: async def child(i): | ||||
...: print(" child %s goes to sleep"%i) | ||||
...: await trio.sleep(2) | ||||
...: print(" child %s wakes up"%i) | ||||
In [4]: print('parent start') | ||||
...: async with trio.open_nursery() as n: | ||||
...: for i in range(5): | ||||
...: n.spawn(child, i) | ||||
...: print('parent end') | ||||
parent start | ||||
child 2 goes to sleep | ||||
child 0 goes to sleep | ||||
child 3 goes to sleep | ||||
child 1 goes to sleep | ||||
child 4 goes to sleep | ||||
<about 2 seconds pause> | ||||
child 2 wakes up | ||||
child 1 wakes up | ||||
child 0 wakes up | ||||
child 3 wakes up | ||||
child 4 wakes up | ||||
parent end | ||||
In the above example, ``async with`` at top level scope is a syntax error in | ||||
Python. | ||||
Using this mode can have unexpected consequences if used in interaction with | ||||
Matthias Bussonnier
|
r24571 | other features of IPython and various registered extensions. In particular if | ||
you are a direct or indirect user of the AST transformers, these may not apply | ||||
to your code. | ||||
Matthias Bussonnier
|
r24463 | |||
Matthias Bussonnier
|
r24490 | When using command line IPython, the default loop (or runner) does not process | ||
Matthias Bussonnier
|
r24475 | in the background, so top level asynchronous code must finish for the REPL to | ||
Frank Tobia
|
r25836 | allow you to enter more code. As with usual Python semantics, the awaitables are | ||
Matthias Bussonnier
|
r24475 | started only when awaited for the first time. That is to say, in first example, | ||
no network request is done between ``In[1]`` and ``In[2]``. | ||||
Matthias Bussonnier
|
r24463 | |||
Matthias Bussonnier
|
r24480 | Effects on IPython.embed() | ||
Matthias Bussonnier
|
r24577 | -------------------------- | ||
Matthias Bussonnier
|
r24480 | |||
Matthias Bussonnier
|
r24481 | IPython core being asynchronous, the use of ``IPython.embed()`` will now require | ||
Matthias Bussonnier
|
r24490 | a loop to run. By default IPython will use a fake coroutine runner which should | ||
allow ``IPython.embed()`` to be nested. Though this will prevent usage of the | ||||
Matthias Bussonnier
|
r24581 | :magic:`%autoawait` feature when using IPython embed. | ||
Matthias Bussonnier
|
r24480 | |||
Frank Tobia
|
r25836 | You can set a coroutine runner explicitly for ``embed()`` if you want to run | ||
asynchronous code, though the exact behavior is undefined. | ||||
Matthias Bussonnier
|
r24480 | |||
Matthias Bussonnier
|
r24500 | Effects on Magics | ||
Matthias Bussonnier
|
r24577 | ----------------- | ||
Matthias Bussonnier
|
r24500 | |||
A couple of magics (``%%timeit``, ``%timeit``, ``%%time``, ``%%prun``) have not | ||||
yet been updated to work with asynchronous code and will raise syntax errors | ||||
when trying to use top-level ``await``. We welcome any contribution to help fix | ||||
Frank Tobia
|
r25836 | those, and extra cases we haven't caught yet. We hope for better support in Core | ||
Matthias Bussonnier
|
r24500 | Python for top-level Async code. | ||
Matthias Bussonnier
|
r24463 | Internals | ||
Matthias Bussonnier
|
r24577 | --------- | ||
Matthias Bussonnier
|
r24463 | |||
Matthias Bussonnier
|
r24475 | As running asynchronous code is not supported in interactive REPL (as of Python | ||
Frank Tobia
|
r25836 | 3.7) we have to rely to a number of complex workarounds and heuristics to allow | ||
Matthias Bussonnier
|
r24490 | this to happen. It is interesting to understand how this works in order to | ||
comprehend potential bugs, or provide a custom runner. | ||||
Matthias Bussonnier
|
r24463 | |||
Among the many approaches that are at our disposition, we find only one that | ||||
Matthias Bussonnier
|
r24475 | suited out need. Under the hood we use the code object from a async-def function | ||
and run it in global namespace after modifying it to not create a new | ||||
``locals()`` scope:: | ||||
Matthias Bussonnier
|
r24463 | |||
async def inner_async(): | ||||
locals().update(**global_namespace) | ||||
# | ||||
# here is user code | ||||
# | ||||
return last_user_statement | ||||
codeobj = modify(inner_async.__code__) | ||||
coroutine = eval(codeobj, user_ns) | ||||
display(loop_runner(coroutine)) | ||||
The first thing you'll notice is that unlike classical ``exec``, there is only | ||||
Matthias Bussonnier
|
r24475 | one namespace. Second, user code runs in a function scope, and not a module | ||
Matthias Bussonnier
|
r24463 | scope. | ||
On top of the above there are significant modification to the AST of | ||||
``function``, and ``loop_runner`` can be arbitrary complex. So there is a | ||||
significant overhead to this kind of code. | ||||
By default the generated coroutine function will be consumed by Asyncio's | ||||
Thomas Grainger
|
r27664 | ``loop_runner = asyncio.get_event_loop().run_until_complete()`` method if | ||
Matthias Bussonnier
|
r24490 | ``async`` mode is deemed necessary, otherwise the coroutine will just be | ||
Frank Tobia
|
r25836 | exhausted in a simple runner. It is possible, though, to change the default | ||
Matthias Bussonnier
|
r24490 | runner. | ||
Matthias Bussonnier
|
r24463 | |||
A loop runner is a *synchronous* function responsible from running a coroutine | ||||
object. | ||||
Frank Tobia
|
r25836 | The runner is responsible for ensuring that ``coroutine`` runs to completion, | ||
and it should return the result of executing the coroutine. Let's write a | ||||
Matthias Bussonnier
|
r24463 | runner for ``trio`` that print a message when used as an exercise, ``trio`` is | ||
Frank Tobia
|
r25836 | special as it usually prefers to run a function object and make a coroutine by | ||
Matthias Bussonnier
|
r24463 | itself, we can get around this limitation by wrapping it in an async-def without | ||
parameters and passing this value to ``trio``:: | ||||
In [1]: import trio | ||||
...: from types import CoroutineType | ||||
...: | ||||
...: def trio_runner(coro:CoroutineType): | ||||
...: print('running asynchronous code') | ||||
...: async def corowrap(coro): | ||||
...: return await coro | ||||
...: return trio.run(corowrap, coro) | ||||
We can set it up by passing it to ``%autoawait``:: | ||||
In [2]: %autoawait trio_runner | ||||
In [3]: async def async_hello(name): | ||||
...: await trio.sleep(1) | ||||
...: print(f'Hello {name} world !') | ||||
...: await trio.sleep(1) | ||||
In [4]: await async_hello('async') | ||||
running asynchronous code | ||||
Hello async world ! | ||||
Asynchronous programming in python (and in particular in the REPL) is still a | ||||
Matthias Bussonnier
|
r24475 | relatively young subject. We expect some code to not behave as you expect, so | ||
feel free to contribute improvements to this codebase and give us feedback. | ||||
Matthias Bussonnier
|
r24490 | |||
We invite you to thoroughly test this feature and report any unexpected behavior | ||||
as well as propose any improvement. | ||||
Matthias Bussonnier
|
r24565 | |||
Using Autoawait in a notebook (IPykernel) | ||||
Matthias Bussonnier
|
r24577 | ----------------------------------------- | ||
Matthias Bussonnier
|
r24565 | |||
Update ipykernel to version 5.0 or greater:: | ||||
pip install ipykernel ipython --upgrade | ||||
# or | ||||
conda install ipykernel ipython --upgrade | ||||
Matthias Bussonnier
|
r24579 | This should automatically enable :magic:`autoawait` integration. Unlike | ||
terminal IPython, all code runs on ``asyncio`` eventloop, so creating a loop by | ||||
hand will not work, including with magics like :magic:`%run` or other | ||||
frameworks that create the eventloop themselves. In cases like these you can | ||||
try to use projects like `nest_asyncio | ||||
<https://github.com/erdewit/nest_asyncio>`_ and follow `this discussion | ||||
Matthias Bussonnier
|
r24565 | <https://github.com/jupyter/notebook/issues/3397#issuecomment-419386811>`_ | ||
Difference between terminal IPython and IPykernel | ||||
Matthias Bussonnier
|
r24577 | ------------------------------------------------- | ||
Matthias Bussonnier
|
r24565 | |||
Matthias Bussonnier
|
r24571 | The exact asynchronous code running behavior varies between Terminal IPython and | ||
IPykernel. The root cause of this behavior is due to IPykernel having a | ||||
Matthias Bussonnier
|
r24579 | *persistent* `asyncio` loop running, while Terminal IPython starts and stops a | ||
Frank Tobia
|
r25836 | loop for each code block. This can lead to surprising behavior in some cases if | ||
you are used to manipulating asyncio loop yourself, see for example | ||||
Matthias Bussonnier
|
r24565 | :ghissue:`11303` for a longer discussion but here are some of the astonishing | ||
cases. | ||||
Matthias Bussonnier
|
r24571 | This behavior is an implementation detail, and should not be relied upon. It can | ||
change without warnings in future versions of IPython. | ||||
Matthias Bussonnier
|
r24565 | |||
In terminal IPython a loop is started for each code blocks only if there is top | ||||
level async code:: | ||||
$ ipython | ||||
In [1]: import asyncio | ||||
...: asyncio.get_event_loop() | ||||
Out[1]: <_UnixSelectorEventLoop running=False closed=False debug=False> | ||||
In [2]: | ||||
In [2]: import asyncio | ||||
...: await asyncio.sleep(0) | ||||
...: asyncio.get_event_loop() | ||||
Out[2]: <_UnixSelectorEventLoop running=True closed=False debug=False> | ||||
See that ``running`` is ``True`` only in the case were we ``await sleep()`` | ||||
In a Notebook, with ipykernel the asyncio eventloop is always running:: | ||||
$ jupyter notebook | ||||
In [1]: import asyncio | ||||
...: loop1 = asyncio.get_event_loop() | ||||
...: loop1 | ||||
Out[1]: <_UnixSelectorEventLoop running=True closed=False debug=False> | ||||
In [2]: loop2 = asyncio.get_event_loop() | ||||
...: loop2 | ||||
Out[2]: <_UnixSelectorEventLoop running=True closed=False debug=False> | ||||
In [3]: loop1 is loop2 | ||||
Out[3]: True | ||||
Matthias Bussonnier
|
r24571 | In Terminal IPython background tasks are only processed while the foreground | ||
task is running, if and only if the foreground task is async:: | ||||
Matthias Bussonnier
|
r24565 | |||
$ ipython | ||||
In [1]: import asyncio | ||||
...: | ||||
...: async def repeat(msg, n): | ||||
...: for i in range(n): | ||||
...: print(f"{msg} {i}") | ||||
...: await asyncio.sleep(1) | ||||
...: return f"{msg} done" | ||||
...: | ||||
...: asyncio.ensure_future(repeat("background", 10)) | ||||
Out[1]: <Task pending coro=<repeat() running at <ipython-input-1-02d0ef250fe7>:3>> | ||||
In [2]: await asyncio.sleep(3) | ||||
background 0 | ||||
background 1 | ||||
background 2 | ||||
background 3 | ||||
In [3]: import time | ||||
...: time.sleep(5) | ||||
In [4]: await asyncio.sleep(3) | ||||
background 4 | ||||
background 5 | ||||
Paul Ivanov
|
r24574 | background 6g | ||
Matthias Bussonnier
|
r24565 | |||
In a Notebook, QtConsole, or any other frontend using IPykernel, background | ||||
Matthias Bussonnier
|
r24571 | tasks should behave as expected. | ||