Strings
Strings

Reputation: 76

Possible to run async function using exec?

I am trying to call an async function from exec, I would expect it to work like:

def test():
    print("test")
    exec('def test2():\n    print("test")\ntest2()')
test()

Output:

test
test

So a function defined in exec is able to call itself, however we can't do such things in asyncio as:

async def test():
    print("test")
    exec('async def test2():\n    print("test")\nawait test2()')

We cant use await outside function as well as we cant call another loop from a running loop:

async def test():
    print("test")
    exec('async def test2():\n    print("test")\nasyncio.run(test2())')

Is there any solution to this problem?

Upvotes: 3

Views: 1321

Answers (3)

John Lehmann
John Lehmann

Reputation: 1036

I needed something like this for a project, and wrote an async version of code.InteractiveConsole. I also wanted to capture output, so used an idea from twisted.conch.Manhole

This is an awful hack. It only works for async code lines that start with "await". I haven't worked out how to handle the form x = await func().

import asyncio

def handle(output):
    print(f"** {output}", end="")

async def nested():
    return 42

async def main():
    localz = {"nested": nested}
    cons = AsyncConsole(handle, localz)
    await cons.interact("a = 10")
    await cons.interact("b = 20")
    await cons.interact("def fun(a, b):")
    await cons.interact("    return a + b")
    await cons.interact("")
    await cons.interact("fun(a, b)")
    await cons.interact("await nested()")

    del localz['__builtins__']
    print(f"l: {localz}")

asyncio.run(main())

Output:

** >>> a = 10
** >>> b = 20
** >>> def fun(a, b):
** ...     return a + b
** ...
** >>> fun(a, b)
30
** >>> await nested()
42
l: {'nested': <function nested at 0x100ab0820>, 'a': 10, 'b': 20, 'fun': <function fun at 0x101059480>, '_': 42}

AsyncConsole:

import string
import code
import sys
import io


class AsyncConsole(code.InteractiveConsole):
    def __init__(self, handler, locals: dict = None, filename="<console>"):
        super().__init__(locals, filename)
        self.handler = handler
        self.filename = filename
        self.output = io.StringIO()
        self.prompt1 = ">>> "
        self.prompt2 = "... "
        self.prompt = self.prompt1
        self.is_async = False

    async def runcode(self, code):
        orighook, sys.displayhook = sys.displayhook, self.displayhook
        try:
            origout, sys.stdout = sys.stdout, self.output
            try:
                exec(code, self.locals)
                if self.is_async:
                    coro = self.locals["_"]
                    obj = await coro
                    self.locals["_"] = obj
                    if obj is not None:
                        self.write(repr(obj))
            except SystemExit:
                raise
            except Exception:
                self.showtraceback()
            finally:
                sys.stdout = origout
        finally:
            sys.displayhook = orighook

    def displayhook(self, obj):
        self.locals["_"] = obj
        if obj is not None and not self.is_async:
            self.write(repr(obj))

    def write(self, data):
        self.output.write(data)

    async def runsource(self, source, filename="<input>", symbol="single"):
        try:
            code = self.compile(source, filename, symbol)
        except (OverflowError, SyntaxError, ValueError):
            # Case 1
            self.showsyntaxerror(filename)
            return False

        if code is None:
            # Case 2
            return True

        # Case 3
        await self.runcode(code)
        return False

    async def push(self, line):
        self.buffer.append(line)
        source = "\n".join(self.buffer)
        more = await self.runsource(source, self.filename)
        if not more:
            self.resetbuffer()
        return more

    async def interact(self, line):
        self.is_async = line.startswith("await ")
        self.output = io.StringIO()
        self.output.write(f"{self.prompt}{line}\n")
        if self.is_async:
            line = line[6:]
        r = await self.push(line)
        self.prompt = self.prompt2 if r else self.prompt1
        if not r and "_" in self.locals and self.locals["_"]:
            self.output.write("\n")
        self.handler(self.output.getvalue())
        return self.prompt

Upvotes: 0

Andrej Kesely
Andrej Kesely

Reputation: 195458

You can put current running loop as exec() global parameter:

import asyncio


async def test():
    print("test")
    loop = asyncio.get_running_loop()
    exec('async def test2():\n    print("test2")\nloop.create_task(test2())', {'loop': loop})

asyncio.run(test())

Prints:

test
test2

EDIT: To run asynchronously, you can return awaitable from the exec():

import asyncio

async def some_other_task():
    await asyncio.sleep(1)
    print('some_other_task')

async def test():
    loop = asyncio.get_running_loop()
    t = [None]

    exec('async def test2():\n    await asyncio.sleep(3);print("task in exec finished")\nt[0] = loop.create_task(test2())', {'asyncio': asyncio, 'loop': loop, 't': t})
    await asyncio.gather(some_other_task(), t[0])

asyncio.run(test())

Prints:

some_other_task           # <-- after 1 sec
task in exec finished     # <-- after 3 sec

Upvotes: 2

user2357112
user2357112

Reputation: 281012

Not possible. You would need some kind of "asynchronous exec" capable of suspending its own execution, and Python's syntax would need to support await outside an asynchronous function definition. Without that, there's no way for exec to return control to the event loop when it needs to suspend.

Andrej Kesely's answer does not work. It does not actually run the coroutine. It only arranges for the coroutine to run later.

Upvotes: 2

Related Questions