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