classification
Title: implement "Async exec"
Type: enhancement Stage:
Components: Interpreter Core Versions: Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: mbussonn, minrk, njs, willingc, yselivanov
Priority: normal Keywords:

Created on 2018-09-09 21:57 by mbussonn, last changed 2018-09-14 09:53 by mbussonn.

Messages (4)
msg324900 - (view) Author: Matthias Bussonnier (mbussonn) * Date: 2018-09-09 21:57
Hi, 

This is an issue, prompted by twitter (https://twitter.com/Mbussonn/status/1038866329971961859) and by the imminent release of IPython 7.0 that provides an async REPL to discuss the introducion of something I'll call "Async exec", the exact form can vary, but I believe the name si relatively self explanatory. 

The short description would be to allow something akin to `exec` but for asynchronous code. Typically for one to be able to write an async-repl in the generic sens that is not say not obviously liked to asyncio.

For example IPython 7.0 (current master branch) allow the following:

```

In [1]: import asyncio, trio, curio

In [2]: await asyncio.sleep(0)

In [3]: %autoawait trio

In [4]: await trio.sleep(0)

In [5]: %autoawait curio

In [6]: await curio.sleep(0)
Out[6]: 30980.70591396
```


Sleep is here and example, but you can play with aoihttp, asks, and other library and it "just works". Alternatively when using IPython via Jupyter, you can also schedule background tasks that will execute in the currently running loop. 

To reach this, we had to work around a large number of roadblock, and there is a number of missing pieces (or things especially prevented) in core Python we had to work around. To see how we did that see https://github.com/ipython/ipython/pull/11265

The core being if we have a block of async code like `await sleep(0)`, we need an asynchronous way to exec it without blocking, hence the proposal for async-exec.

During the development and test of the above feature of IPython here are some of the challenges we got with top-level async code. 

1) top-level async is invalid syntax. 

It make sens for a module for this to be invalid syntax, but not in a repl.
Since Python 3.7 we can (at least) compile it to AST, but not to bytecode.

2) It would also be extremely convenient to have a util function to tell you whether what you just compiled need to be ran async or not, from Source Code, ast tree, code object. It's surprisingly not trivial to get it always right.

So far in IPython we have to carry-over and recompute whether to actually run the compiled byte code in classical `exec` or our pseudo async-exec. You may think that `await exec()` always cover the case, but when you are already running under asyncio, you may want to tell user "no you can't run async code", and use the normal `exec` path.


3) Have  distinction in this `async exec`/`compile` between "I am compiling a module", currently `exec` mode for both exec and compile, and a "I'm compiling a _multiline_ interactive statement".

4) Not be coupled to a specific async library.

Allow new AIO library like trio/curio/... to use this.

Note that despite both using IPython, the above cover 2 use cases. 
- Terminal IPython were the loop is started/stopped between each user input. 
- Notebook kernel/Jupyter IPython where the loop is already running and task/user code can be process in background w/o pausing the loop. 

AFAICT, this would also be of potential use for other REPL (Xonsh, Bpython).

I'm happy to give more details, but I think that's a enough of a broad overview, as we should be releasing this in IPython in a few days/week, we will likely have further feedback from a wider range of users that can inform the design.
msg324915 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2018-09-10 06:17
I think the first thing is to add async "modes" to compile: in particular "async-exec" and "async-single". These would be like the current "exec" and "single" modes respectively, except that they act like the code is inside an "async def", so "await" is allowed, and executing the resulting code object produces a coroutine object that has to be iterated to actually run the code.

I guess we might want "async-eval" too just for completeness, though I'm not currently aware of any use cases for that.

A utility to check whether an AST requires async mode should be fairly straightforward. So if you want to choose on the fly, you would do:

1. ast = compile(source, filename, "async-exec", ast.PyCF_ONLY_AST)
2. use a utility check whether 'ast' contains any top-level 'await'/'async with'/'async for'
3. if so, create bytecode with compile(ast, filename, "async-exec"). If not, create bytecode with compile(ast, filename, "exec").

Once you have a code object, I think it's too late: if you use "async-exec" mode to compile a code object that doesn't actually use 'await', then it should still return a coroutine object that needs iterating, etc., just like an 'async def' that has no 'await' in it. So if you want to do this check, the AST phase is the time to do it. Maybe ast.is_ast_async(ast_obj)?

> Have distinction in this `async exec`/`compile` between "I am compiling a module", currently `exec` mode for both exec and compile, and a "I'm compiling a _multiline_ interactive statement".


This seems like a separate problem from the async stuff... I'm curious to hear how what distinction you want to make between 'exec' and a new 'multi-single' (?) mode, but maybe that should be a new issue?
msg324925 - (view) Author: Matthias Bussonnier (mbussonn) * Date: 2018-09-10 14:03
> I think the first thing is to add async "modes" to compile: in particular "async-exec" and "async-single". These would be like the current "exec" and "single" modes respectively, except that they act like the code is inside an "async def", so "await" is allowed, and executing the resulting code object produces a coroutine object that has to be iterated to actually run the code.

> This seems like a separate problem from the async stuff... I'm curious to hear how what distinction you want to make between 'exec' and a new 'multi-single' (?) mode, but maybe that should be a new issue?

Mell, in classical `exec` there is always a tension between "this is for a module" and "this is for REPL". We saw that close to 3.7 release where strings literal were moved into the AST Module `docstring` attribute.

In IPython[1] we also have to dance to trigger the display hook in a multi-line context by executing each top-level node one by one. So while we are designing a new `async-exec` while not tackle that issue at the same time and cover both use case ? That should let one category of user do the optimization they wish and get a `Module` object, without being slow down by the other. 

1: https://github.com/ipython/ipython/blob/869480ed70944ca70ad9ed70779b9c3e4320adb7/IPython/core/interactiveshell.py#L3179-L3190
msg325339 - (view) Author: Matthias Bussonnier (mbussonn) * Date: 2018-09-14 09:53
> A utility to check whether an AST requires async mode should be fairly straightforward.

Here is one case we forgot in IPython apparently :

In [1]: x = 1
   ...: def f():
   ...:     nonlocal x
   ...:     x = 10000

This is not detected as a syntax error, but considered as asyn, we took an approach that prefer false positive (try to run invalid syntax as async) than false negative (reject valid async-code). Add this to the test suite for this feature.
History
Date User Action Args
2018-09-14 09:53:53mbussonnsetmessages: + msg325339
2018-09-10 14:03:26mbussonnsetmessages: + msg324925
2018-09-10 06:17:18njssetnosy: + njs
messages: + msg324915
2018-09-09 21:59:21mbussonnsetnosy: + yselivanov, willingc, minrk
2018-09-09 21:57:12mbussonncreate