Reputation: 76
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
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
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
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