Skip to content

Conversation

Carreau
Copy link
Member

@Carreau Carreau commented Mar 10, 2017

In [1]: import aiohttp

In [2]: res = aiohttp.get('https://api.github.com')

In [3]: res
Out[3]: <aiohttp.client._DetachedRequestContextManager at 0x112a28828>

In [4]: resp = await res

In [5]: resp
Out[5]:
<ClientResponse(https://api.github.com) [200 OK]>
<CIMultiDictProxy('Server...DC')>

In [6]: print(await resp.json())
{'current_user_url': 'https://api.github.com/user', 'current_user_authorizations_html_url': 'https://g...

Thanks to useful discussion with @njsmith and @minrk. cc @takluyver

As await is not valid at top level, and extracting ast, or other modification of __code__ don't work,
when SyntaxError and wraping in async def is not syntax error use that.

Send globals() into the function, run the function, extract locals(), and update user_ns

@minrk
Copy link
Member

minrk commented Mar 10, 2017

This is super cool, @Carreau!

@Carreau
Copy link
Member Author

Carreau commented Mar 10, 2017 via email

@Carreau Carreau force-pushed the await-repl branch 2 times, most recently from db26de0 to 6fd07ad Compare March 10, 2017 20:09
@Carreau
Copy link
Member Author

Carreau commented Mar 10, 2017

I'm thinking of protecting it behind a flag, (on by default), so that you can disable the heuristic.
I need to test with curio/trio and add a way to change the loop.

@Carreau Carreau force-pushed the await-repl branch 2 times, most recently from 59c304b to 2c8176d Compare March 10, 2017 22:37
@Carreau
Copy link
Member Author

Carreau commented Mar 11, 2017

Ok, now compatible with trio and curio:

$ ipython --InteractiveShell.looprunner=IPython.core.interactiveshell._trio_runner
Python 3.6.0 | packaged by conda-forge | (default, Feb 10 2017, 07:08:35)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.0.0.dev -- An enhanced Interactive Python. Type '?' for help.
In [1]: async def child1():
   ...:     print("   child 1 go to sleep")
   ...:     await trio.sleep(1)
   ...:     print("child 1 wakes up")

In [2]: async def child2():
   ...:     print("   child 2 go to sleep")
   ...:     await trio.sleep(2)
   ...:     print("child 2 wakes up")
   ...:

In [3]: import trio

In [4]: print('parent start')
   ...: async with trio.open_nursery() as n:
   ...:     n.spawn(child1)
   ...:     n.spawn(child2)
   ...: print('joined')
parent start
   child 2 go to sleep
   child 1 go to sleep
child 1 wakes up
child 2 wakes up
joined

@Carreau Carreau force-pushed the await-repl branch 10 times, most recently from 962eec7 to 35336eb Compare March 13, 2017 19:56
@Carreau Carreau added this to the 6.0 milestone Mar 13, 2017
@Carreau Carreau changed the title [Prototype] Await repl Await repl Mar 14, 2017
@gnestor
Copy link
Contributor

gnestor commented Mar 15, 2017

Nice @Carreau! I have taken a shot at implementing this in jp-babel using babel-plugin-top-level-await but no luck yet. If I get a chance, I will revisit this and share any findings...

@takluyver
Copy link
Member

Did you look at how I did the transformation to run the code in the prototype asyncio magic, now adopted by @Gr1N? I haven't thought through the implications in detail, but pulling the function body out after transformation feels neater than pushing namespaces into functions.

@njsmith
Copy link
Contributor

njsmith commented Mar 15, 2017 via email

@njsmith
Copy link
Contributor

njsmith commented Mar 15, 2017 via email

@Carreau
Copy link
Member Author

Carreau commented Mar 15, 2017

blah() # should print 2

well that seem to work:

$ ipython
Python 3.6.0 |Continuum Analytics, Inc.| (default, Dec 23 2016, 13:19:00)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.0.0.dev -- An enhanced Interactive Python. Type '?' for help.

In [1]: a = 1

In [2]: async def blah():
   ...:     print(a)
   ...:

In [3]: await blah()
1

In [4]: a = 2

In [5]: await blah()
2

@takluyver I didn't found it, but it's an approach I tried. The other problem with this approach is that your await call are now blocking (are each are in a loop) and my initial need to write this was to actually make use of the concurrency to fetch parallel URL in the REPL.

I (with @njsmith help ) went a couple of other direction (rewriting codeobjects, trying to exec only the inner body of a function) all of them a dead end.

@Carreau
Copy link
Member Author

Carreau commented Mar 15, 2017

Nice @Carreau! I have taken a shot at implementing this in jp-babel using babel-plugin-top-level-await but no luck yet. If I get a chance, I will revisit this and share any findings...

At least JS have a default event loo, so that's solved :-)

@takluyver
Copy link
Member

The other problem with this approach is that your await call are now blocking

It should only block on await expressions in the top-level code, which is necessary to ensure that that is executed in the correct order. await expressions inside function definitions should still work as normal, so you can still use something like asyncio.gather() to run coroutines concurrently.

@Carreau
Copy link
Member Author

Carreau commented Mar 15, 2017

It should only block on await expressions in the top-level code, which is necessary to ensure that that is executed in the correct order.

Fair enough, I'm unsure how the AST should rewrite that though:

In [18]: async def child(i):
    ...:     print("   child %s  go to sleep"%i)
    ...:     await trio.sleep(2)
    ...:     print("   child %s  wakes up"%i)

In [19]: print('parent start')
    ...: async with trio.open_nursery() as n:
    ...:     for i in range(5):
    ...:         n.spawn(child, i)
    ...: print('parent end')

There is no await (in the second block) and top-level async with will be invalid at exec-time as well.

I'm guessing we can push the rewriting further though, I would be happy to give it a try (unsure how to treat async-with, and then you need to care about async-for as well ? and other constructs ?. I would like to not bake-in asyncio though. and in the end we should try to push for supporting this upstream instead of the crazy hack we have now.

@takluyver
Copy link
Member

takluyver commented Mar 15, 2017

Ah, right, I didn't consider async with or async for.

@Carreau
Copy link
Member Author

Carreau commented Mar 24, 2017

@njsmith Yes, it is expected, I was just suggesting that if we want to avoid this behavior changing depending on the user's Tornado/Python version we should probably implement a custom event loop policy (or some other way to make sure that ensure_future does not get the kernel event loop).

I don't want to get into the business of implementing our own event loop (at least not right now).
I looked into automatically selection loop depending on whether you have native function from curio/asyncio/trio, but that seem like a rats nest. I also think the IPython is flexible enough to set your custom runner or event loop policy with a custom magic that we do not need to put that in the core now. Though, if somethings like this get developed separately and get traction/stabilises, we can introduce that into the core.

Handler for asyncio autoawait
"""
import asyncio
return asyncio.get_event_loop().run_until_complete(coro)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if ipykernel started running on asyncio? Would this cause hangs?

@Carreau
Copy link
Member Author

Carreau commented Apr 5, 2017

What would happen if ipykernel started running on asyncio? Would this cause hangs?

Likely, but if we make ipykernel use asyncio, we can always change this handler and make a minor release. And maybe by that time python 3.7 is out and will support actual async exec. I'm going to guess there might also be weird cases when you use uvloop, or if you register a loop in a different thread to have actual async execution. But I doubt this is worth trying to tackle now.

# Compile to bytecode
try:
code_ast = compiler.ast_parse(cell, filename=cell_name)
if _should_be_async(cell) and self.autoawait:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd reverse these since autoawait is a bool and _should_be_async doesn't need to parse if the bool is False.

A compiled code object, to be executed
result : ExecutionResult, optional
An object to store exceptions that occur during execution.
async : Bool (Experimental)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General question: with async the almost-a-keyword, is there any indication that async as a variable name should be discouraged?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch – the plan is indeed to make async into a real keyword in 3.7, so it shouldn't be used as a name.

If the object is a fully qualified object name, attempt to import it and
set it as the runner, and activate autoawait."""

param = parameter_s.strip()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mixed case: 'on' 'Off'. Presumably both should be lowercase.

loop_runner_map ={
'asyncio':_asyncio_runner,
'curio':_curio_runner,
'trio':_trio_runner,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a little odd that tornado, the only async runner that we actually use in IPython, isn't supported here. Is there a reason for that?

self.shell.loop_runner = self.shell.user_ns[param]
self.shell.autoawait = True
return None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import to top-level of module

ip = get_ipython()
iprs = lambda x: ip.run_cell(dedent(x))

if sys.version_info > (3,5):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than making the test definitions themselves conditional, why not skip the tests?

async def ___wrapper___():
{usercode}
locals()
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just happened to look at this again, and noticed that I think this should be

async def __wrapper__():
    try:
        {usercode}
    finally:
        locals()

@Carreau
Copy link
Member Author

Carreau commented May 28, 2018

Closing in favor of #11155 (at least for now)

@Carreau Carreau closed this May 28, 2018
@Carreau Carreau deleted the await-repl branch May 28, 2018 23:38
@Carreau Carreau restored the await-repl branch May 28, 2018 23:38
@Carreau Carreau deleted the await-repl branch May 28, 2018 23:38
@Carreau Carreau restored the await-repl branch May 28, 2018 23:38
@Carreau Carreau deleted the await-repl branch November 7, 2024 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants