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 <jim.mussared@gmail.com>
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

Lines changed: 95 additions & 0 deletions
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

Lines changed: 178 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy