Skip to content

Commit 7602843

Browse files
committed
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 <[email protected]>
1 parent ad9309b commit 7602843

File tree

3 files changed

+279
-0
lines changed

3 files changed

+279
-0
lines changed

micropython/aiorepl/README.md

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# aiorepl
2+
3+
This library provides "asyncio REPL", a simple REPL that can be used even
4+
while your program is running, allowing you to inspect program state, create
5+
tasks, and await asynchronous functions.
6+
7+
This is inspired by Python's `asyncio` module when run via `python -m asyncio`.
8+
9+
## Background
10+
11+
The MicroPython REPL is unavailable while your program is running. This
12+
library runs a background REPL using the asyncio scheduler.
13+
14+
Furthermore, it is not possible to `await` at the main REPL because it does
15+
not know about the asyncio scheduler.
16+
17+
## Usage
18+
19+
To use this library, you need to import the library and then start the REPL task.
20+
21+
For example, in main.py:
22+
23+
```py
24+
import uasyncio as asyncio
25+
import aiorepl
26+
27+
async def demo():
28+
await asyncio.sleep_ms(1000)
29+
print("async demo")
30+
31+
state = 20
32+
33+
async def task1():
34+
while state:
35+
#print("task 1")
36+
await asyncio.sleep_ms(500)
37+
print("done")
38+
39+
async def main():
40+
print("Starting tasks...")
41+
42+
# Start other program tasks.
43+
t1 = asyncio.create_task(task1())
44+
45+
# Start the aiorepl task.
46+
repl = asyncio.create_task(aiorepl.task())
47+
48+
await asyncio.gather(t1, repl)
49+
50+
asyncio.run(main())
51+
```
52+
53+
The optional globals passed to `task([globals])` allows you to specify what
54+
will be in scope for the REPL. By default it uses `__main__`, which is the
55+
same scope as the regular REPL (and `main.py`). In the example above, the
56+
REPL will be able to call the `demo()` function as well as get/set the
57+
`state` variable.
58+
59+
Instead of the regular `>>> ` prompt, the asyncio REPL will show `--> `.
60+
61+
```
62+
--> 1+1
63+
2
64+
--> await demo()
65+
async demo
66+
--> state
67+
20
68+
--> import myapp.core
69+
--> state = await myapp.core.query_state()
70+
--> 1/0
71+
ZeroDivisionError: divide by zero
72+
--> def foo(x): return x + 1
73+
--> await asyncio.sleep(foo(3))
74+
-->
75+
```
76+
77+
History is supported via the up/down arrow keys.
78+
79+
## Cancellation
80+
81+
During command editing (the "R" phase), pressing Ctrl-C will cancel the current command and display a new prompt, like the regular REPL.
82+
83+
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.
84+
85+
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.
86+
87+
## Limitations
88+
89+
The following features are unsupported:
90+
91+
* Tab completion is not supported (also unsupported in `python -m asyncio`).
92+
* Multi-line continuation. However you can do single-line definitions of functions, see demo above.
93+
* Exception tracebacks. Only the exception type and message is shown, see demo above.
94+
* Emacs shortcuts (e.g. Ctrl-A, Ctrl-E, to move to start/end of line).
95+
* Unicode handling for input.

micropython/aiorepl/aiorepl.py

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# MIT license; Copyright (c) 2022 Jim Mussared
2+
3+
import micropython
4+
import re
5+
import sys
6+
import time
7+
import uasyncio as asyncio
8+
9+
# Import statement (needs to be global, and does not return).
10+
_RE_IMPORT = re.compile("^import ([^ ]+)( as ([^ ]+))?")
11+
_RE_FROM_IMPORT = re.compile("^from [^ ]+ import ([^ ]+)( as ([^ ]+))?")
12+
# Global variable assignment.
13+
_RE_GLOBAL = re.compile("^([a-zA-Z0-9_]+) ?=[^=]")
14+
# General assignment expression or import statement (does not return a value).
15+
_RE_ASSIGN = re.compile("[^=]=[^=]")
16+
17+
# Command hist (One reserved slot for the current command).
18+
_HISTORY_LIMIT = const(5 + 1)
19+
20+
21+
async def execute(code, g, s):
22+
if not code.strip():
23+
return
24+
25+
try:
26+
if "await " in code:
27+
# Execute the code snippet in an async context.
28+
if m := _RE_IMPORT.match(code) or _RE_FROM_IMPORT.match(code):
29+
code = f"global {m.group(3) or m.group(1)}\n {code}"
30+
elif m := _RE_GLOBAL.match(code):
31+
code = f"global {m.group(1)}\n {code}"
32+
elif not _RE_ASSIGN.search(code):
33+
code = f"return {code}"
34+
35+
code = f"""
36+
import uasyncio as asyncio
37+
async def __code():
38+
{code}
39+
40+
__exec_task = asyncio.create_task(__code())
41+
"""
42+
43+
async def kbd_intr_task(exec_task, s):
44+
while True:
45+
if ord(await s.read(1)) == 0x03:
46+
exec_task.cancel()
47+
return
48+
49+
l = {"__exec_task": None}
50+
exec(code, g, l)
51+
exec_task = l["__exec_task"]
52+
53+
# Concurrently wait for either Ctrl-C from the stream or task
54+
# completion.
55+
intr_task = asyncio.create_task(kbd_intr_task(exec_task, s))
56+
57+
try:
58+
try:
59+
return await exec_task
60+
except asyncio.CancelledError:
61+
pass
62+
finally:
63+
intr_task.cancel()
64+
try:
65+
await intr_task
66+
except asyncio.CancelledError:
67+
pass
68+
else:
69+
# Excute code snippet directly.
70+
try:
71+
try:
72+
micropython.kbd_intr(3)
73+
try:
74+
return eval(code, g)
75+
except SyntaxError:
76+
# Maybe an assignment, try with exec.
77+
return exec(code, g)
78+
except KeyboardInterrupt:
79+
pass
80+
finally:
81+
micropython.kbd_intr(-1)
82+
83+
except Exception as err:
84+
print(f"{type(err).__name__}: {err}")
85+
86+
87+
# REPL task. Invoke this with an optional mutable globals dict.
88+
async def task(g=None, prompt="--> "):
89+
print("Starting asyncio REPL...")
90+
if g is None:
91+
g = __import__("__main__").__dict__
92+
try:
93+
micropython.kbd_intr(-1)
94+
s = asyncio.StreamReader(sys.stdin)
95+
# clear = True
96+
hist = [None] * _HISTORY_LIMIT
97+
hist_i = 0 # Index of most recent entry.
98+
hist_n = 0 # Number of history entries.
99+
c = 0 # ord of most recent character.
100+
t = 0 # timestamp of most recent character.
101+
while True:
102+
hist_b = 0 # How far back in the history are we currently.
103+
sys.stdout.write(prompt)
104+
cmd = ""
105+
while True:
106+
b = await s.read(1)
107+
c = ord(b)
108+
pc = c # save previous character
109+
pt = t # save previous time
110+
t = time.ticks_ms()
111+
if c < 0x20 or c > 0x7E:
112+
if c == 0x0A:
113+
# CR
114+
sys.stdout.write("\n")
115+
if cmd:
116+
# Push current command.
117+
hist[hist_i] = cmd
118+
# Increase history length if possible, and rotate ring forward.
119+
hist_n = min(_HISTORY_LIMIT - 1, hist_n + 1)
120+
hist_i = (hist_i + 1) % _HISTORY_LIMIT
121+
122+
result = await execute(cmd, g, s)
123+
if result is not None:
124+
sys.stdout.write(repr(result))
125+
sys.stdout.write("\n")
126+
break
127+
elif c == 0x08 or c == 0x7F:
128+
# Backspace.
129+
if cmd:
130+
cmd = cmd[:-1]
131+
sys.stdout.write("\x08 \x08")
132+
elif c == 0x02:
133+
# Ctrl-B
134+
continue
135+
elif c == 0x03:
136+
# Ctrl-C
137+
if pc == 0x03 and time.ticks_diff(t, pt) < 20:
138+
# Two very quick Ctrl-C (faster than a human
139+
# typing) likely means mpremote trying to
140+
# escape.
141+
asyncio.new_event_loop()
142+
return
143+
sys.stdout.write("\n")
144+
break
145+
elif c == 0x04:
146+
# Ctrl-D
147+
sys.stdout.write("\n")
148+
# Shutdown asyncio.
149+
asyncio.new_event_loop()
150+
return
151+
elif c == 0x1B:
152+
# Start of escape sequence.
153+
key = await s.read(2)
154+
if key in ("[A", "[B"):
155+
# Stash the current command.
156+
hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd
157+
# Clear current command.
158+
b = "\x08" * len(cmd)
159+
sys.stdout.write(b)
160+
sys.stdout.write(" " * len(cmd))
161+
sys.stdout.write(b)
162+
# Go backwards or forwards in the history.
163+
if key == "[A":
164+
hist_b = min(hist_n, hist_b + 1)
165+
else:
166+
hist_b = max(0, hist_b - 1)
167+
# Update current command.
168+
cmd = hist[(hist_i - hist_b) % _HISTORY_LIMIT]
169+
sys.stdout.write(cmd)
170+
else:
171+
# sys.stdout.write("\\x")
172+
# sys.stdout.write(hex(c))
173+
pass
174+
else:
175+
sys.stdout.write(b)
176+
cmd += b
177+
finally:
178+
micropython.kbd_intr(3)

micropython/aiorepl/manifest.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
metadata(
2+
version="0.1",
3+
description="Provides an asynchronous REPL that can run concurrently with an asyncio, also allowing await expressions.",
4+
)
5+
6+
module("aiorepl.py")

0 commit comments

Comments
 (0)