kopia lustrzana https://github.com/micropython/micropython-lib
micropython/aiorepl: Initial version of an asyncio REPL.
This provides an async REPL with the following features: - Run interactive REPL in the background. - Execute statements using await. - Simple history. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>pull/524/head
rodzic
ad9309b669
commit
7602843209
|
@ -0,0 +1,95 @@
|
|||
# aiorepl
|
||||
|
||||
This library provides "asyncio REPL", a simple REPL that can be used even
|
||||
while your program is running, allowing you to inspect program state, create
|
||||
tasks, and await asynchronous functions.
|
||||
|
||||
This is inspired by Python's `asyncio` module when run via `python -m asyncio`.
|
||||
|
||||
## Background
|
||||
|
||||
The MicroPython REPL is unavailable while your program is running. This
|
||||
library runs a background REPL using the asyncio scheduler.
|
||||
|
||||
Furthermore, it is not possible to `await` at the main REPL because it does
|
||||
not know about the asyncio scheduler.
|
||||
|
||||
## Usage
|
||||
|
||||
To use this library, you need to import the library and then start the REPL task.
|
||||
|
||||
For example, in main.py:
|
||||
|
||||
```py
|
||||
import uasyncio as asyncio
|
||||
import aiorepl
|
||||
|
||||
async def demo():
|
||||
await asyncio.sleep_ms(1000)
|
||||
print("async demo")
|
||||
|
||||
state = 20
|
||||
|
||||
async def task1():
|
||||
while state:
|
||||
#print("task 1")
|
||||
await asyncio.sleep_ms(500)
|
||||
print("done")
|
||||
|
||||
async def main():
|
||||
print("Starting tasks...")
|
||||
|
||||
# Start other program tasks.
|
||||
t1 = asyncio.create_task(task1())
|
||||
|
||||
# Start the aiorepl task.
|
||||
repl = asyncio.create_task(aiorepl.task())
|
||||
|
||||
await asyncio.gather(t1, repl)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
The optional globals passed to `task([globals])` allows you to specify what
|
||||
will be in scope for the REPL. By default it uses `__main__`, which is the
|
||||
same scope as the regular REPL (and `main.py`). In the example above, the
|
||||
REPL will be able to call the `demo()` function as well as get/set the
|
||||
`state` variable.
|
||||
|
||||
Instead of the regular `>>> ` prompt, the asyncio REPL will show `--> `.
|
||||
|
||||
```
|
||||
--> 1+1
|
||||
2
|
||||
--> await demo()
|
||||
async demo
|
||||
--> state
|
||||
20
|
||||
--> import myapp.core
|
||||
--> state = await myapp.core.query_state()
|
||||
--> 1/0
|
||||
ZeroDivisionError: divide by zero
|
||||
--> def foo(x): return x + 1
|
||||
--> await asyncio.sleep(foo(3))
|
||||
-->
|
||||
```
|
||||
|
||||
History is supported via the up/down arrow keys.
|
||||
|
||||
## Cancellation
|
||||
|
||||
During command editing (the "R" phase), pressing Ctrl-C will cancel the current command and display a new prompt, like the regular REPL.
|
||||
|
||||
While a command is being executed, Ctrl-C will cancel the task that is executing the command. This will have no effect on blocking code (e.g. `time.sleep()`), but this should be rare in an asyncio-based program.
|
||||
|
||||
Ctrl-D at the asyncio REPL command prompt will terminate the current event loop, which will stop the running program and return to the regular REPL.
|
||||
|
||||
## Limitations
|
||||
|
||||
The following features are unsupported:
|
||||
|
||||
* Tab completion is not supported (also unsupported in `python -m asyncio`).
|
||||
* Multi-line continuation. However you can do single-line definitions of functions, see demo above.
|
||||
* Exception tracebacks. Only the exception type and message is shown, see demo above.
|
||||
* Emacs shortcuts (e.g. Ctrl-A, Ctrl-E, to move to start/end of line).
|
||||
* Unicode handling for input.
|
|
@ -0,0 +1,178 @@
|
|||
# MIT license; Copyright (c) 2022 Jim Mussared
|
||||
|
||||
import micropython
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import uasyncio as asyncio
|
||||
|
||||
# Import statement (needs to be global, and does not return).
|
||||
_RE_IMPORT = re.compile("^import ([^ ]+)( as ([^ ]+))?")
|
||||
_RE_FROM_IMPORT = re.compile("^from [^ ]+ import ([^ ]+)( as ([^ ]+))?")
|
||||
# Global variable assignment.
|
||||
_RE_GLOBAL = re.compile("^([a-zA-Z0-9_]+) ?=[^=]")
|
||||
# General assignment expression or import statement (does not return a value).
|
||||
_RE_ASSIGN = re.compile("[^=]=[^=]")
|
||||
|
||||
# Command hist (One reserved slot for the current command).
|
||||
_HISTORY_LIMIT = const(5 + 1)
|
||||
|
||||
|
||||
async def execute(code, g, s):
|
||||
if not code.strip():
|
||||
return
|
||||
|
||||
try:
|
||||
if "await " in code:
|
||||
# Execute the code snippet in an async context.
|
||||
if m := _RE_IMPORT.match(code) or _RE_FROM_IMPORT.match(code):
|
||||
code = f"global {m.group(3) or m.group(1)}\n {code}"
|
||||
elif m := _RE_GLOBAL.match(code):
|
||||
code = f"global {m.group(1)}\n {code}"
|
||||
elif not _RE_ASSIGN.search(code):
|
||||
code = f"return {code}"
|
||||
|
||||
code = f"""
|
||||
import uasyncio as asyncio
|
||||
async def __code():
|
||||
{code}
|
||||
|
||||
__exec_task = asyncio.create_task(__code())
|
||||
"""
|
||||
|
||||
async def kbd_intr_task(exec_task, s):
|
||||
while True:
|
||||
if ord(await s.read(1)) == 0x03:
|
||||
exec_task.cancel()
|
||||
return
|
||||
|
||||
l = {"__exec_task": None}
|
||||
exec(code, g, l)
|
||||
exec_task = l["__exec_task"]
|
||||
|
||||
# Concurrently wait for either Ctrl-C from the stream or task
|
||||
# completion.
|
||||
intr_task = asyncio.create_task(kbd_intr_task(exec_task, s))
|
||||
|
||||
try:
|
||||
try:
|
||||
return await exec_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
intr_task.cancel()
|
||||
try:
|
||||
await intr_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
else:
|
||||
# Excute code snippet directly.
|
||||
try:
|
||||
try:
|
||||
micropython.kbd_intr(3)
|
||||
try:
|
||||
return eval(code, g)
|
||||
except SyntaxError:
|
||||
# Maybe an assignment, try with exec.
|
||||
return exec(code, g)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
micropython.kbd_intr(-1)
|
||||
|
||||
except Exception as err:
|
||||
print(f"{type(err).__name__}: {err}")
|
||||
|
||||
|
||||
# REPL task. Invoke this with an optional mutable globals dict.
|
||||
async def task(g=None, prompt="--> "):
|
||||
print("Starting asyncio REPL...")
|
||||
if g is None:
|
||||
g = __import__("__main__").__dict__
|
||||
try:
|
||||
micropython.kbd_intr(-1)
|
||||
s = asyncio.StreamReader(sys.stdin)
|
||||
# clear = True
|
||||
hist = [None] * _HISTORY_LIMIT
|
||||
hist_i = 0 # Index of most recent entry.
|
||||
hist_n = 0 # Number of history entries.
|
||||
c = 0 # ord of most recent character.
|
||||
t = 0 # timestamp of most recent character.
|
||||
while True:
|
||||
hist_b = 0 # How far back in the history are we currently.
|
||||
sys.stdout.write(prompt)
|
||||
cmd = ""
|
||||
while True:
|
||||
b = await s.read(1)
|
||||
c = ord(b)
|
||||
pc = c # save previous character
|
||||
pt = t # save previous time
|
||||
t = time.ticks_ms()
|
||||
if c < 0x20 or c > 0x7E:
|
||||
if c == 0x0A:
|
||||
# CR
|
||||
sys.stdout.write("\n")
|
||||
if cmd:
|
||||
# Push current command.
|
||||
hist[hist_i] = cmd
|
||||
# Increase history length if possible, and rotate ring forward.
|
||||
hist_n = min(_HISTORY_LIMIT - 1, hist_n + 1)
|
||||
hist_i = (hist_i + 1) % _HISTORY_LIMIT
|
||||
|
||||
result = await execute(cmd, g, s)
|
||||
if result is not None:
|
||||
sys.stdout.write(repr(result))
|
||||
sys.stdout.write("\n")
|
||||
break
|
||||
elif c == 0x08 or c == 0x7F:
|
||||
# Backspace.
|
||||
if cmd:
|
||||
cmd = cmd[:-1]
|
||||
sys.stdout.write("\x08 \x08")
|
||||
elif c == 0x02:
|
||||
# Ctrl-B
|
||||
continue
|
||||
elif c == 0x03:
|
||||
# Ctrl-C
|
||||
if pc == 0x03 and time.ticks_diff(t, pt) < 20:
|
||||
# Two very quick Ctrl-C (faster than a human
|
||||
# typing) likely means mpremote trying to
|
||||
# escape.
|
||||
asyncio.new_event_loop()
|
||||
return
|
||||
sys.stdout.write("\n")
|
||||
break
|
||||
elif c == 0x04:
|
||||
# Ctrl-D
|
||||
sys.stdout.write("\n")
|
||||
# Shutdown asyncio.
|
||||
asyncio.new_event_loop()
|
||||
return
|
||||
elif c == 0x1B:
|
||||
# Start of escape sequence.
|
||||
key = await s.read(2)
|
||||
if key in ("[A", "[B"):
|
||||
# Stash the current command.
|
||||
hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd
|
||||
# Clear current command.
|
||||
b = "\x08" * len(cmd)
|
||||
sys.stdout.write(b)
|
||||
sys.stdout.write(" " * len(cmd))
|
||||
sys.stdout.write(b)
|
||||
# Go backwards or forwards in the history.
|
||||
if key == "[A":
|
||||
hist_b = min(hist_n, hist_b + 1)
|
||||
else:
|
||||
hist_b = max(0, hist_b - 1)
|
||||
# Update current command.
|
||||
cmd = hist[(hist_i - hist_b) % _HISTORY_LIMIT]
|
||||
sys.stdout.write(cmd)
|
||||
else:
|
||||
# sys.stdout.write("\\x")
|
||||
# sys.stdout.write(hex(c))
|
||||
pass
|
||||
else:
|
||||
sys.stdout.write(b)
|
||||
cmd += b
|
||||
finally:
|
||||
micropython.kbd_intr(3)
|
|
@ -0,0 +1,6 @@
|
|||
metadata(
|
||||
version="0.1",
|
||||
description="Provides an asynchronous REPL that can run concurrently with an asyncio, also allowing await expressions.",
|
||||
)
|
||||
|
||||
module("aiorepl.py")
|
Ładowanie…
Reference in New Issue