Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5e29305

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 f3cfc52 commit 5e29305

File tree

3 files changed

+216
-0
lines changed

3 files changed

+216
-0
lines changed

‎micropython/aiorepl/README.md‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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(100)
29+
print("async demo")
30+
31+
state = 20
32+
33+
async def main():
34+
print("Starting tasks...")
35+
36+
# Start other program tasks.
37+
t1 = asyncio.create_task(task1())
38+
t2 = asyncio.create_task(task2())
39+
40+
# Start the aiorepl task, using this file's globals.
41+
repl = asyncio.create_task(aiorepl.task(globals()))
42+
43+
await asyncio.gather(t1, t2, repl)
44+
45+
asyncio.run(main())
46+
```
47+
48+
The optional globals passed to `task([globals])` allows you to specify what
49+
will be in scope for the REPL. In the example above, the REPL will be able to
50+
call the `demo()` function as well as get/set the `state` variable.
51+
52+
Instead of the regular `>>> ` prompt, the asyncio REPL will show `--> `.
53+
54+
```
55+
--> 1+1
56+
2
57+
--> await demo()
58+
async demo
59+
--> state
60+
20
61+
--> import myapp.core
62+
--> state = await myapp.core.query_state()
63+
--> 1/0
64+
ZeroDivisionError: divide by zero
65+
-->
66+
```
67+
68+
History is supported via the up/down arrow keys.
69+
70+
## Limitations
71+
72+
The following features are unsupported:
73+
74+
* Tab completion is not supported (also unsupported in `python -m asyncio`).
75+
* Multi-line expressions.
76+
* Exception tracebacks (only the exception type and message is shown, see demo above).
77+
* Emacs shortcuts (e.g. Ctrl-A, Ctrl-E, to move to start/end of line).

‎micropython/aiorepl/aiorepl.py‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# MIT license; Copyright (c) 2022 Jim Mussared
2+
3+
import micropython
4+
import re
5+
import sys
6+
import uasyncio as asyncio
7+
8+
# Import statement (needs to be global, and does not return).
9+
_RE_IMPORT = re.compile("^import ([^ ]+)( as ([^ ]+))?")
10+
_RE_FROM_IMPORT = re.compile("^from [^ ]+ import ([^ ]+)( as ([^ ]+))?")
11+
# Global variable assignment.
12+
_RE_GLOBAL = re.compile("^([a-zA-Z0-9_]+) ?=[^=]")
13+
# General assignment expression or import statement (does not return a value).
14+
_RE_ASSIGN = re.compile("[^=]=[^=]")
15+
16+
# Command hist (One reserved slot for the current command).
17+
_HISTORY_LIMIT = const(5 + 1)
18+
19+
20+
# Execute the code snippet in an async context.
21+
async def execute(code, g):
22+
if not code.strip():
23+
return
24+
25+
if m := _RE_IMPORT.match(code) or _RE_FROM_IMPORT.match(code):
26+
code = f"global {m.group(3) or m.group(1)}\n {code}"
27+
elif m := _RE_GLOBAL.match(code):
28+
code = f"global {m.group(1)}\n {code}"
29+
elif not _RE_ASSIGN.search(code):
30+
code = f"return {code}"
31+
32+
code = f"""
33+
import uasyncio as asyncio
34+
async def __code():
35+
{code}
36+
37+
__result = asyncio.get_event_loop().create_task(__code())
38+
"""
39+
40+
try:
41+
l = {}
42+
exec(code, g, l)
43+
return await l["__result"]
44+
except Exception as err:
45+
print(f"{type(err).__name__}: {err}")
46+
47+
48+
# REPL task. Invoke this with an optional mutable globals dict (e.g. `globals()`).
49+
async def task(g):
50+
print("Starting asyncio REPL...")
51+
if g is None:
52+
g = {}
53+
try:
54+
micropython.kbd_intr(-1)
55+
s = asyncio.StreamReader(sys.stdin)
56+
clear = True
57+
hist = [None] * _HISTORY_LIMIT
58+
hist_i = 0 # Index of most recent entry.
59+
hist_n = 0 # Number of history entries.
60+
while True:
61+
if not clear:
62+
sys.stdout.write("\n")
63+
clear = True
64+
hist_b = 0 # How far back in the history are we currently.
65+
sys.stdout.write("--> ")
66+
cmd = ""
67+
while True:
68+
c = await s.read(1)
69+
n = ord(c)
70+
if n < 0x20 or n > 0x7E:
71+
if n == 0x0A:
72+
# CR
73+
sys.stdout.write("\n")
74+
clear = True
75+
try:
76+
micropython.kbd_intr(3)
77+
if cmd:
78+
# Push current command.
79+
hist[hist_i] = cmd
80+
# Increase history length if possible, and rotate ring forward.
81+
hist_n = min(_HISTORY_LIMIT - 1, hist_n + 1)
82+
hist_i = (hist_i + 1) % _HISTORY_LIMIT
83+
result = await execute(cmd, g)
84+
if result is not None:
85+
sys.stdout.write(repr(result))
86+
clear = False
87+
finally:
88+
micropython.kbd_intr(-1)
89+
break
90+
elif n == 0x08 or n == 0x7F:
91+
# Backspace.
92+
if cmd:
93+
cmd = cmd[:-1]
94+
sys.stdout.write("\x08 \x08")
95+
elif n == 0x02:
96+
# Ctrl-B
97+
continue
98+
elif n == 0x03:
99+
# Ctrl-C
100+
clear = False
101+
break
102+
elif n == 0x04:
103+
# Ctrl-D
104+
sys.stdout.write("\n")
105+
return
106+
elif n == 0x1B:
107+
# Start of escape sequence.
108+
key = await s.read(2)
109+
if key in ("[A", "[B"):
110+
# Stash the current command.
111+
hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd
112+
# Clear current command.
113+
b = "\x08" * len(cmd)
114+
sys.stdout.write(b)
115+
sys.stdout.write(" " * len(cmd))
116+
sys.stdout.write(b)
117+
# Go backwards or forwards in the history.
118+
if key == "[A":
119+
hist_b = min(hist_n, hist_b + 1)
120+
else:
121+
hist_b = max(0, hist_b - 1)
122+
# Update current command.
123+
cmd = hist[(hist_i - hist_b) % _HISTORY_LIMIT]
124+
sys.stdout.write(cmd)
125+
else:
126+
# sys.stdout.write("\\x")
127+
# sys.stdout.write(hex(n))
128+
pass
129+
else:
130+
sys.stdout.write(c)
131+
cmd += c
132+
finally:
133+
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)

AltStyle によって変換されたページ (->オリジナル) /