File: autoawait.rst

package info (click to toggle)
ipython 8.35.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 11,696 kB
  • sloc: python: 42,461; sh: 376; makefile: 243
file content (319 lines) | stat: -rw-r--r-- 11,525 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
.. _autoawait:

Asynchronous in REPL: Autoawait
===============================

.. note::

   This feature is experimental and behavior can change between python and
   IPython version without prior deprecation.

Starting with IPython 7.0, and when using Python 3.6 and above, IPython offer the
ability to run asynchronous code from the REPL. Constructs which are
:exc:`SyntaxError` s in the Python REPL can be used seamlessly in IPython.

The examples given here are for terminal IPython, running async code in a
notebook interface or any other frontend using the Jupyter protocol needs
IPykernel version 5.0 or above. The details of how async code runs in IPykernel
will differ between IPython, IPykernel and their versions.

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
<await>` (or any other async constructs like async-with, async-for) is used at
top level scope, or if any structure valid only in `async def
<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
    >>> session = aiohttp.ClientSession()
    >>> result = session.get('https://api.github.com')
    >>> 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
    IPython 7.0.0 -- An enhanced Interactive Python. Type '?' for help.

    In [1]: import aiohttp
       ...: session = aiohttp.ClientSession()
       ...: result = session.get('https://api.github.com')

    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
to :any:`False` to deactivate automatic wrapping of asynchronous code. You can
also use the :magic:`%autoawait` magic to toggle the behavior at runtime::

    In [1]: %autoawait False

    In [2]: %autoawait
    IPython autoawait is `Off`, and set to use `asyncio`



By default IPython will assume integration with Python's provided
:mod:`asyncio`, but integration with other libraries is provided. In particular
we provide experimental integration with the ``curio`` and ``trio`` library.

You can switch the current integration by using the
``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
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.

When using command line IPython, the default loop (or runner) does not process
in the background, so top level asynchronous code must finish for the REPL to
allow you to enter more code. As with usual Python semantics, the awaitables are
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]``.


Effects on IPython.embed()
--------------------------

IPython core being asynchronous, the use of ``IPython.embed()`` will now require
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
:magic:`%autoawait` feature when using IPython embed. 

You can set a coroutine runner explicitly for ``embed()`` if you want to run
asynchronous code, though the exact behavior is undefined.

Effects on Magics
-----------------

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
those, and extra cases we haven't caught yet. We hope for better support in Core
Python for top-level Async code.

Internals
---------

As running asynchronous code is not supported in interactive REPL (as of Python
3.7) we have to rely to a number of complex workarounds and heuristics to allow
this to happen. It is interesting to understand how this works in order to
comprehend potential bugs, or provide a custom runner.

Among the many approaches that are at our disposition, we find only one that
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::

    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
one namespace. Second, user code runs in a function scope, and not a module
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
``loop_runner = asyncio.get_event_loop().run_until_complete()`` method if
``async`` mode is deemed necessary, otherwise the coroutine will just be
exhausted in a simple runner. It is possible, though, to change the default
runner.

A loop runner is a *synchronous*  function responsible from running a coroutine
object.

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
runner for ``trio`` that print a message when used as an exercise, ``trio`` is
special as it usually prefers to run a function object and make a coroutine by
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
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.

We invite you to thoroughly test this feature and report any unexpected behavior
as well as propose any improvement.

Using Autoawait in a notebook (IPykernel)
-----------------------------------------

Update ipykernel to version 5.0 or greater::

   pip install ipykernel ipython --upgrade
   # or
   conda install ipykernel ipython --upgrade

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
<https://github.com/jupyter/notebook/issues/3397#issuecomment-419386811>`_

Difference between terminal IPython and IPykernel
-------------------------------------------------

The exact asynchronous code running behavior varies between Terminal IPython and
IPykernel. The root cause of this behavior is due to IPykernel having a
*persistent* `asyncio` loop running, while Terminal IPython starts and stops a
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
:ghissue:`11303` for a longer discussion but here are some of the astonishing
cases.

This behavior is an implementation detail, and should not be relied upon. It can
change without warnings in future versions of IPython.

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

In Terminal IPython background tasks are only processed while the foreground
task is running, if and only if the foreground task is async::

   $ 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
   background 6g

In a Notebook, QtConsole, or any other frontend using IPykernel, background
tasks should behave as expected.