repl-session 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {repl_session-0.1.0 → repl_session-0.2.0}/PKG-INFO +64 -20
- {repl_session-0.1.0 → repl_session-0.2.0}/README.md +63 -19
- {repl_session-0.1.0 → repl_session-0.2.0}/pyproject.toml +3 -1
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session/__init__.py +100 -33
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session.egg-info/PKG-INFO +64 -20
- {repl_session-0.1.0 → repl_session-0.2.0}/LICENSE +0 -0
- {repl_session-0.1.0 → repl_session-0.2.0}/setup.cfg +0 -0
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session/py.typed +0 -0
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session.egg-info/SOURCES.txt +0 -0
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session.egg-info/dependency_links.txt +0 -0
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session.egg-info/entry_points.txt +0 -0
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session.egg-info/requires.txt +0 -0
- {repl_session-0.1.0 → repl_session-0.2.0}/src/repl_session.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repl-session
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Runs a session with a Read-Eval-Print-Loop (REPL). Takes YAML as input gives JSON as output.
|
|
5
5
|
Classifier: Development Status :: 3 - Alpha
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -60,19 +60,19 @@ I like to work with [Chez Scheme](https://cisco.github.io/ChezScheme/). Suppose
|
|
|
60
60
|
```yaml
|
|
61
61
|
#| file: test/scheme.yml
|
|
62
62
|
config:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
command: "scheme --eedisable"
|
|
64
|
+
first_prompt: "> "
|
|
65
|
+
change_prompt: '(waiter-prompt-string "{key}>")'
|
|
66
|
+
prompt: "{key}> "
|
|
67
|
+
continuation_prompt: ""
|
|
68
68
|
commands:
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
- command: (* 6 7)
|
|
70
|
+
- command: |
|
|
71
71
|
(define (fac n init)
|
|
72
72
|
(if (zero? n)
|
|
73
73
|
init
|
|
74
74
|
(fac (- n 1) (* init n)))))
|
|
75
|
-
|
|
75
|
+
- command: (fac 10 1)
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Passing this to `repl-session`, it will start the Chez Scheme interpreter, waiting for the `>` prompt to appear. It then changes the prompt to a generated `uuid4` code, for instance `27e87a8a-742c-4501-b05d-b05814f5a010> `. This will make sure that we can't accidentally match something else for an interactive prompt (imagine we're generating some XML!). Since commands are also echoed to standard out, we need to strip them from the resulting output. Running this should give:
|
|
@@ -94,15 +94,27 @@ This looks very similar to the previous example:
|
|
|
94
94
|
```yaml
|
|
95
95
|
#| file: test/lua.yml
|
|
96
96
|
config:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
command: "lua"
|
|
98
|
+
first_prompt: "> "
|
|
99
|
+
change_prompt: '_PROMPT = "{key}> "; _PROMPT2 = "{key}+ "'
|
|
100
|
+
prompt: "{key}> "
|
|
101
|
+
continuation_prompt: "{key}\\+ "
|
|
102
|
+
strip_ansi: true
|
|
103
103
|
commands:
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
- command: 6 * 7
|
|
105
|
+
- command: '"Hello" .. ", " .. "World!"'
|
|
106
|
+
- command: |
|
|
107
|
+
function fac(n, m)
|
|
108
|
+
if m == nil then
|
|
109
|
+
return fac(n, 1)
|
|
110
|
+
end
|
|
111
|
+
if n == 0 then
|
|
112
|
+
return m
|
|
113
|
+
else
|
|
114
|
+
return fac(n-1, m*n)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
- command: fac(10)
|
|
106
118
|
```
|
|
107
119
|
|
|
108
120
|
The Lua REPL is not so nice. It sends ANSI escape codes and those need to be filtered out.
|
|
@@ -116,6 +128,31 @@ repl-session < test/lua.yml | jq '.commands.[].output'
|
|
|
116
128
|
"Hello, World!"
|
|
117
129
|
```
|
|
118
130
|
|
|
131
|
+
### Python
|
|
132
|
+
|
|
133
|
+
The Python REPL got a revision in version 3.13, with lots of colour and ANSI codes.
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
#| file: test/python.yml
|
|
137
|
+
config:
|
|
138
|
+
command: python -q
|
|
139
|
+
first_prompt: ">>>"
|
|
140
|
+
change_prompt: 'import sys; sys.ps1 = "{key}>>> "; sys.ps2 = "{key}+++ "'
|
|
141
|
+
prompt: "{key}>>> "
|
|
142
|
+
continuation_prompt: "{key}\\+\\+\\+ "
|
|
143
|
+
environment:
|
|
144
|
+
NO_COLOR: "1"
|
|
145
|
+
commands:
|
|
146
|
+
- command: print("Hello, World!")
|
|
147
|
+
- command: 6 * 7
|
|
148
|
+
- command: |
|
|
149
|
+
def fac(n):
|
|
150
|
+
for i in range(1, n):
|
|
151
|
+
n *= i
|
|
152
|
+
return n
|
|
153
|
+
- command: fac(10)
|
|
154
|
+
```
|
|
155
|
+
|
|
119
156
|
## Input/Output structure
|
|
120
157
|
|
|
121
158
|
The user can configure how the REPL is called and interpreted.
|
|
@@ -137,14 +174,17 @@ class ReplConfig(msgspec.Struct):
|
|
|
137
174
|
output; useful if the REPL echoes your input before answering.
|
|
138
175
|
timeout (float): Command timeout for this session in seconds.
|
|
139
176
|
"""
|
|
177
|
+
|
|
140
178
|
command: str
|
|
141
179
|
first_prompt: str
|
|
142
180
|
change_prompt: str
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
strip_command: bool = False
|
|
181
|
+
prompt: str
|
|
182
|
+
continuation_prompt: str | None = None
|
|
146
183
|
strip_ansi: bool = False
|
|
184
|
+
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
147
185
|
timeout: float = 5.0
|
|
186
|
+
|
|
187
|
+
|
|
148
188
|
```
|
|
149
189
|
|
|
150
190
|
Then, a session is a list of commands. Each command should be a UTF-8 string, and we allow to attach some meta-data like expected MIME type for the output. We can also pass an expected output in the case of a documentation test. If `output` was already given on the input, it is moved to `expected`. This way it becomes really easy to setup regression tests on your documentation. Just rerun on the generated output file.
|
|
@@ -160,6 +200,7 @@ class ReplCommand(msgspec.Struct):
|
|
|
160
200
|
output (str | None): evaluated output.
|
|
161
201
|
expected (str | None): expected output.
|
|
162
202
|
"""
|
|
203
|
+
|
|
163
204
|
command: str
|
|
164
205
|
output_type: str = "text/plain"
|
|
165
206
|
output: str | None = None
|
|
@@ -173,8 +214,11 @@ class ReplSession(msgspec.Struct):
|
|
|
173
214
|
config (ReplConfig): Config for setting up a REPL session.
|
|
174
215
|
commands (list[ReplCommand]): List of commands in the session.
|
|
175
216
|
"""
|
|
217
|
+
|
|
176
218
|
config: ReplConfig
|
|
177
219
|
commands: list[ReplCommand]
|
|
220
|
+
|
|
221
|
+
|
|
178
222
|
```
|
|
179
223
|
|
|
180
224
|
## License and contribution
|
|
@@ -42,19 +42,19 @@ I like to work with [Chez Scheme](https://cisco.github.io/ChezScheme/). Suppose
|
|
|
42
42
|
```yaml
|
|
43
43
|
#| file: test/scheme.yml
|
|
44
44
|
config:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
command: "scheme --eedisable"
|
|
46
|
+
first_prompt: "> "
|
|
47
|
+
change_prompt: '(waiter-prompt-string "{key}>")'
|
|
48
|
+
prompt: "{key}> "
|
|
49
|
+
continuation_prompt: ""
|
|
50
50
|
commands:
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
- command: (* 6 7)
|
|
52
|
+
- command: |
|
|
53
53
|
(define (fac n init)
|
|
54
54
|
(if (zero? n)
|
|
55
55
|
init
|
|
56
56
|
(fac (- n 1) (* init n)))))
|
|
57
|
-
|
|
57
|
+
- command: (fac 10 1)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Passing this to `repl-session`, it will start the Chez Scheme interpreter, waiting for the `>` prompt to appear. It then changes the prompt to a generated `uuid4` code, for instance `27e87a8a-742c-4501-b05d-b05814f5a010> `. This will make sure that we can't accidentally match something else for an interactive prompt (imagine we're generating some XML!). Since commands are also echoed to standard out, we need to strip them from the resulting output. Running this should give:
|
|
@@ -76,15 +76,27 @@ This looks very similar to the previous example:
|
|
|
76
76
|
```yaml
|
|
77
77
|
#| file: test/lua.yml
|
|
78
78
|
config:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
command: "lua"
|
|
80
|
+
first_prompt: "> "
|
|
81
|
+
change_prompt: '_PROMPT = "{key}> "; _PROMPT2 = "{key}+ "'
|
|
82
|
+
prompt: "{key}> "
|
|
83
|
+
continuation_prompt: "{key}\\+ "
|
|
84
|
+
strip_ansi: true
|
|
85
85
|
commands:
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
- command: 6 * 7
|
|
87
|
+
- command: '"Hello" .. ", " .. "World!"'
|
|
88
|
+
- command: |
|
|
89
|
+
function fac(n, m)
|
|
90
|
+
if m == nil then
|
|
91
|
+
return fac(n, 1)
|
|
92
|
+
end
|
|
93
|
+
if n == 0 then
|
|
94
|
+
return m
|
|
95
|
+
else
|
|
96
|
+
return fac(n-1, m*n)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
- command: fac(10)
|
|
88
100
|
```
|
|
89
101
|
|
|
90
102
|
The Lua REPL is not so nice. It sends ANSI escape codes and those need to be filtered out.
|
|
@@ -98,6 +110,31 @@ repl-session < test/lua.yml | jq '.commands.[].output'
|
|
|
98
110
|
"Hello, World!"
|
|
99
111
|
```
|
|
100
112
|
|
|
113
|
+
### Python
|
|
114
|
+
|
|
115
|
+
The Python REPL got a revision in version 3.13, with lots of colour and ANSI codes.
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
#| file: test/python.yml
|
|
119
|
+
config:
|
|
120
|
+
command: python -q
|
|
121
|
+
first_prompt: ">>>"
|
|
122
|
+
change_prompt: 'import sys; sys.ps1 = "{key}>>> "; sys.ps2 = "{key}+++ "'
|
|
123
|
+
prompt: "{key}>>> "
|
|
124
|
+
continuation_prompt: "{key}\\+\\+\\+ "
|
|
125
|
+
environment:
|
|
126
|
+
NO_COLOR: "1"
|
|
127
|
+
commands:
|
|
128
|
+
- command: print("Hello, World!")
|
|
129
|
+
- command: 6 * 7
|
|
130
|
+
- command: |
|
|
131
|
+
def fac(n):
|
|
132
|
+
for i in range(1, n):
|
|
133
|
+
n *= i
|
|
134
|
+
return n
|
|
135
|
+
- command: fac(10)
|
|
136
|
+
```
|
|
137
|
+
|
|
101
138
|
## Input/Output structure
|
|
102
139
|
|
|
103
140
|
The user can configure how the REPL is called and interpreted.
|
|
@@ -119,14 +156,17 @@ class ReplConfig(msgspec.Struct):
|
|
|
119
156
|
output; useful if the REPL echoes your input before answering.
|
|
120
157
|
timeout (float): Command timeout for this session in seconds.
|
|
121
158
|
"""
|
|
159
|
+
|
|
122
160
|
command: str
|
|
123
161
|
first_prompt: str
|
|
124
162
|
change_prompt: str
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
strip_command: bool = False
|
|
163
|
+
prompt: str
|
|
164
|
+
continuation_prompt: str | None = None
|
|
128
165
|
strip_ansi: bool = False
|
|
166
|
+
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
129
167
|
timeout: float = 5.0
|
|
168
|
+
|
|
169
|
+
|
|
130
170
|
```
|
|
131
171
|
|
|
132
172
|
Then, a session is a list of commands. Each command should be a UTF-8 string, and we allow to attach some meta-data like expected MIME type for the output. We can also pass an expected output in the case of a documentation test. If `output` was already given on the input, it is moved to `expected`. This way it becomes really easy to setup regression tests on your documentation. Just rerun on the generated output file.
|
|
@@ -142,6 +182,7 @@ class ReplCommand(msgspec.Struct):
|
|
|
142
182
|
output (str | None): evaluated output.
|
|
143
183
|
expected (str | None): expected output.
|
|
144
184
|
"""
|
|
185
|
+
|
|
145
186
|
command: str
|
|
146
187
|
output_type: str = "text/plain"
|
|
147
188
|
output: str | None = None
|
|
@@ -155,8 +196,11 @@ class ReplSession(msgspec.Struct):
|
|
|
155
196
|
config (ReplConfig): Config for setting up a REPL session.
|
|
156
197
|
commands (list[ReplCommand]): List of commands in the session.
|
|
157
198
|
"""
|
|
199
|
+
|
|
158
200
|
config: ReplConfig
|
|
159
201
|
commands: list[ReplCommand]
|
|
202
|
+
|
|
203
|
+
|
|
160
204
|
```
|
|
161
205
|
|
|
162
206
|
## License and contribution
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "repl-session"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Runs a session with a Read-Eval-Print-Loop (REPL). Takes YAML as input gives JSON as output."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
classifiers = [
|
|
@@ -24,6 +24,8 @@ dev = [
|
|
|
24
24
|
"entangled-cli>=2.1.13",
|
|
25
25
|
"mkdocs-macros-plugin>=1.3.9",
|
|
26
26
|
"mkdocs-material>=9.6.20",
|
|
27
|
+
"python-lsp-server>=1.13.1",
|
|
28
|
+
"ruff>=0.13.2",
|
|
27
29
|
"ty>=0.0.1a21",
|
|
28
30
|
]
|
|
29
31
|
|
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
`repl-session` is a command-line tool to evaluate a given session
|
|
5
5
|
in any REPL, and store the results.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
# ~/~ begin <<docs/index.md#imports>>[init]
|
|
8
9
|
#| id: imports
|
|
9
10
|
# from datetime import datetime, tzinfo
|
|
10
|
-
from typing import IO
|
|
11
|
+
from typing import IO, cast
|
|
11
12
|
from collections.abc import Generator, Callable
|
|
13
|
+
|
|
12
14
|
# import re
|
|
13
15
|
from contextlib import contextmanager
|
|
14
16
|
import uuid
|
|
15
17
|
import sys
|
|
16
18
|
import re
|
|
19
|
+
import logging
|
|
17
20
|
|
|
18
21
|
import pexpect
|
|
19
22
|
import msgspec
|
|
@@ -24,6 +27,7 @@ import importlib.metadata
|
|
|
24
27
|
__version__ = importlib.metadata.version("repl-session")
|
|
25
28
|
# ~/~ end
|
|
26
29
|
|
|
30
|
+
|
|
27
31
|
# ~/~ begin <<README.md#input-data>>[init]
|
|
28
32
|
#| id: input-data
|
|
29
33
|
class ReplConfig(msgspec.Struct):
|
|
@@ -41,14 +45,17 @@ class ReplConfig(msgspec.Struct):
|
|
|
41
45
|
output; useful if the REPL echoes your input before answering.
|
|
42
46
|
timeout (float): Command timeout for this session in seconds.
|
|
43
47
|
"""
|
|
48
|
+
|
|
44
49
|
command: str
|
|
45
50
|
first_prompt: str
|
|
46
51
|
change_prompt: str
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
strip_command: bool = False
|
|
52
|
+
prompt: str
|
|
53
|
+
continuation_prompt: str | None = None
|
|
50
54
|
strip_ansi: bool = False
|
|
55
|
+
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
51
56
|
timeout: float = 5.0
|
|
57
|
+
|
|
58
|
+
|
|
52
59
|
# ~/~ end
|
|
53
60
|
# ~/~ begin <<README.md#input-data>>[1]
|
|
54
61
|
#| id: input-data
|
|
@@ -61,6 +68,7 @@ class ReplCommand(msgspec.Struct):
|
|
|
61
68
|
output (str | None): evaluated output.
|
|
62
69
|
expected (str | None): expected output.
|
|
63
70
|
"""
|
|
71
|
+
|
|
64
72
|
command: str
|
|
65
73
|
output_type: str = "text/plain"
|
|
66
74
|
output: str | None = None
|
|
@@ -74,45 +82,95 @@ class ReplSession(msgspec.Struct):
|
|
|
74
82
|
config (ReplConfig): Config for setting up a REPL session.
|
|
75
83
|
commands (list[ReplCommand]): List of commands in the session.
|
|
76
84
|
"""
|
|
85
|
+
|
|
77
86
|
config: ReplConfig
|
|
78
87
|
commands: list[ReplCommand]
|
|
88
|
+
|
|
89
|
+
|
|
79
90
|
# ~/~ end
|
|
80
91
|
|
|
92
|
+
|
|
81
93
|
# ~/~ begin <<docs/index.md#repl-contextmanager>>[init]
|
|
82
94
|
#| id: repl-contextmanager
|
|
95
|
+
def spawn(config: ReplConfig):
|
|
96
|
+
child: pexpect.spawn[str] = pexpect.spawn(
|
|
97
|
+
config.command,
|
|
98
|
+
timeout=config.timeout,
|
|
99
|
+
echo=False,
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
env=config.environment,
|
|
102
|
+
)
|
|
103
|
+
return child
|
|
104
|
+
|
|
83
105
|
|
|
84
106
|
@contextmanager
|
|
85
|
-
def repl(config: ReplConfig) -> Generator[Callable[[str], str]]:
|
|
107
|
+
def repl(config: ReplConfig) -> Generator[Callable[[str], str | None]]:
|
|
86
108
|
key = str(uuid.uuid4())
|
|
109
|
+
change_prompt_cmd = config.change_prompt.format(key=key)
|
|
110
|
+
prompt = config.prompt.format(key=key)
|
|
111
|
+
continuation_prompt = (
|
|
112
|
+
config.continuation_prompt.format(key=key)
|
|
113
|
+
if config.continuation_prompt is not None
|
|
114
|
+
else None
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
child: pexpect.spawn[str]
|
|
118
|
+
with spawn(config) as child:
|
|
119
|
+
_ = child.expect(config.first_prompt)
|
|
120
|
+
_ = child.sendline(change_prompt_cmd)
|
|
121
|
+
# if config.strip_command:
|
|
122
|
+
# child.expect(key)
|
|
123
|
+
_ = child.expect(prompt)
|
|
124
|
+
|
|
125
|
+
def send(msg: str) -> str | None:
|
|
126
|
+
nonlocal prompt, continuation_prompt, change_prompt_cmd
|
|
127
|
+
lines = msg.splitlines()
|
|
128
|
+
answer: list[str] = []
|
|
129
|
+
|
|
130
|
+
still_waiting: bool = True
|
|
131
|
+
for line in lines:
|
|
132
|
+
logging.debug("sending: %s", line)
|
|
133
|
+
_ = child.sendline(line)
|
|
134
|
+
if continuation_prompt is not None:
|
|
135
|
+
logging.debug("waiting for prompt or continuation")
|
|
136
|
+
_ = child.expect(
|
|
137
|
+
f"(?P<cont>{continuation_prompt})|(?P<norm>{prompt})"
|
|
138
|
+
)
|
|
139
|
+
if not isinstance(child.match, re.Match):
|
|
140
|
+
continue
|
|
141
|
+
if child.match.group("cont") is not None:
|
|
142
|
+
logging.debug("continuation")
|
|
143
|
+
still_waiting = True
|
|
144
|
+
else:
|
|
145
|
+
logging.debug("done: %s", child.before)
|
|
146
|
+
if child.before is not None:
|
|
147
|
+
answer.append(child.before)
|
|
148
|
+
still_waiting = False
|
|
149
|
+
else:
|
|
150
|
+
logging.debug("waiting for prompt")
|
|
151
|
+
_ = child.expect(prompt)
|
|
152
|
+
if child.before:
|
|
153
|
+
answer.append(child.before)
|
|
154
|
+
still_waiting = False
|
|
155
|
+
|
|
156
|
+
if still_waiting:
|
|
157
|
+
_ = child.sendline("")
|
|
158
|
+
_ = child.expect(prompt)
|
|
159
|
+
if child.before:
|
|
160
|
+
answer.append(child.before)
|
|
161
|
+
|
|
162
|
+
if not answer:
|
|
163
|
+
return None
|
|
87
164
|
|
|
88
|
-
with pexpect.spawn(config.command, timeout=config.timeout) as child:
|
|
89
|
-
child.expect(config.first_prompt)
|
|
90
|
-
change_prompt_cmd = config.change_prompt.format(key=key)
|
|
91
|
-
if config.append_newline:
|
|
92
|
-
change_prompt_cmd = change_prompt_cmd + "\n"
|
|
93
|
-
child.send(change_prompt_cmd)
|
|
94
|
-
if config.strip_command:
|
|
95
|
-
child.expect(key)
|
|
96
|
-
prompt = config.next_prompt.format(key=key)
|
|
97
|
-
child.expect(prompt)
|
|
98
|
-
|
|
99
|
-
def send(msg: str) -> str:
|
|
100
|
-
if config.append_newline:
|
|
101
|
-
msg = msg + "\n"
|
|
102
|
-
child.send(msg)
|
|
103
|
-
child.expect("(.*)" + prompt)
|
|
104
|
-
|
|
105
|
-
answer = child.match[1].decode()
|
|
106
165
|
if config.strip_ansi:
|
|
107
|
-
ansi_escape = re.compile(r
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return answer.removeprefix(msg)
|
|
112
|
-
else:
|
|
113
|
-
return child.match[1].decode()
|
|
166
|
+
ansi_escape = re.compile(r"(\u001b\[|\x1B\[)[0-?]*[ -\/]*[@-~]")
|
|
167
|
+
return ansi_escape.sub("", answer[-1].strip())
|
|
168
|
+
|
|
169
|
+
return answer[-1].strip()
|
|
114
170
|
|
|
115
171
|
yield send
|
|
172
|
+
|
|
173
|
+
|
|
116
174
|
# ~/~ end
|
|
117
175
|
# ~/~ begin <<docs/index.md#run-session>>[init]
|
|
118
176
|
#| id: run-session
|
|
@@ -125,10 +183,11 @@ def run_session(session: ReplSession):
|
|
|
125
183
|
cmd.expected = expected
|
|
126
184
|
|
|
127
185
|
return session
|
|
186
|
+
|
|
187
|
+
|
|
128
188
|
# ~/~ end
|
|
129
189
|
# ~/~ begin <<docs/index.md#io>>[init]
|
|
130
190
|
#| id: io
|
|
131
|
-
|
|
132
191
|
def read_session(port: IO[str] = sys.stdin) -> ReplSession:
|
|
133
192
|
data: str = port.read()
|
|
134
193
|
return msgspec.yaml.decode(data, type=ReplSession)
|
|
@@ -136,12 +195,15 @@ def read_session(port: IO[str] = sys.stdin) -> ReplSession:
|
|
|
136
195
|
|
|
137
196
|
def write_session(session: ReplSession, port: IO[str] = sys.stdout):
|
|
138
197
|
data = msgspec.json.encode(session)
|
|
139
|
-
port.write(data.decode())
|
|
198
|
+
_ = port.write(data.decode())
|
|
199
|
+
|
|
200
|
+
|
|
140
201
|
# ~/~ end
|
|
141
202
|
|
|
142
203
|
|
|
143
204
|
@argh.arg("-v", "--version", help="show version and exit")
|
|
144
|
-
|
|
205
|
+
@argh.arg("-l", "--log-enable", help="show debugging output")
|
|
206
|
+
def repl_session(version: bool = False, log_enable: bool = False):
|
|
145
207
|
"""
|
|
146
208
|
repl-session runs a REPL session, reading JSON from standard input and
|
|
147
209
|
writing to standard output. Both the input and output follow the same
|
|
@@ -151,9 +213,14 @@ def repl_session(version: bool = False):
|
|
|
151
213
|
print(f"repl-session {__version__}")
|
|
152
214
|
sys.exit(0)
|
|
153
215
|
|
|
216
|
+
if log_enable:
|
|
217
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
218
|
+
|
|
154
219
|
write_session(run_session(read_session()))
|
|
155
220
|
|
|
156
221
|
|
|
157
222
|
def main():
|
|
158
223
|
argh.dispatch_command(repl_session)
|
|
224
|
+
|
|
225
|
+
|
|
159
226
|
# ~/~ end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repl-session
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Runs a session with a Read-Eval-Print-Loop (REPL). Takes YAML as input gives JSON as output.
|
|
5
5
|
Classifier: Development Status :: 3 - Alpha
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -60,19 +60,19 @@ I like to work with [Chez Scheme](https://cisco.github.io/ChezScheme/). Suppose
|
|
|
60
60
|
```yaml
|
|
61
61
|
#| file: test/scheme.yml
|
|
62
62
|
config:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
command: "scheme --eedisable"
|
|
64
|
+
first_prompt: "> "
|
|
65
|
+
change_prompt: '(waiter-prompt-string "{key}>")'
|
|
66
|
+
prompt: "{key}> "
|
|
67
|
+
continuation_prompt: ""
|
|
68
68
|
commands:
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
- command: (* 6 7)
|
|
70
|
+
- command: |
|
|
71
71
|
(define (fac n init)
|
|
72
72
|
(if (zero? n)
|
|
73
73
|
init
|
|
74
74
|
(fac (- n 1) (* init n)))))
|
|
75
|
-
|
|
75
|
+
- command: (fac 10 1)
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Passing this to `repl-session`, it will start the Chez Scheme interpreter, waiting for the `>` prompt to appear. It then changes the prompt to a generated `uuid4` code, for instance `27e87a8a-742c-4501-b05d-b05814f5a010> `. This will make sure that we can't accidentally match something else for an interactive prompt (imagine we're generating some XML!). Since commands are also echoed to standard out, we need to strip them from the resulting output. Running this should give:
|
|
@@ -94,15 +94,27 @@ This looks very similar to the previous example:
|
|
|
94
94
|
```yaml
|
|
95
95
|
#| file: test/lua.yml
|
|
96
96
|
config:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
command: "lua"
|
|
98
|
+
first_prompt: "> "
|
|
99
|
+
change_prompt: '_PROMPT = "{key}> "; _PROMPT2 = "{key}+ "'
|
|
100
|
+
prompt: "{key}> "
|
|
101
|
+
continuation_prompt: "{key}\\+ "
|
|
102
|
+
strip_ansi: true
|
|
103
103
|
commands:
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
- command: 6 * 7
|
|
105
|
+
- command: '"Hello" .. ", " .. "World!"'
|
|
106
|
+
- command: |
|
|
107
|
+
function fac(n, m)
|
|
108
|
+
if m == nil then
|
|
109
|
+
return fac(n, 1)
|
|
110
|
+
end
|
|
111
|
+
if n == 0 then
|
|
112
|
+
return m
|
|
113
|
+
else
|
|
114
|
+
return fac(n-1, m*n)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
- command: fac(10)
|
|
106
118
|
```
|
|
107
119
|
|
|
108
120
|
The Lua REPL is not so nice. It sends ANSI escape codes and those need to be filtered out.
|
|
@@ -116,6 +128,31 @@ repl-session < test/lua.yml | jq '.commands.[].output'
|
|
|
116
128
|
"Hello, World!"
|
|
117
129
|
```
|
|
118
130
|
|
|
131
|
+
### Python
|
|
132
|
+
|
|
133
|
+
The Python REPL got a revision in version 3.13, with lots of colour and ANSI codes.
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
#| file: test/python.yml
|
|
137
|
+
config:
|
|
138
|
+
command: python -q
|
|
139
|
+
first_prompt: ">>>"
|
|
140
|
+
change_prompt: 'import sys; sys.ps1 = "{key}>>> "; sys.ps2 = "{key}+++ "'
|
|
141
|
+
prompt: "{key}>>> "
|
|
142
|
+
continuation_prompt: "{key}\\+\\+\\+ "
|
|
143
|
+
environment:
|
|
144
|
+
NO_COLOR: "1"
|
|
145
|
+
commands:
|
|
146
|
+
- command: print("Hello, World!")
|
|
147
|
+
- command: 6 * 7
|
|
148
|
+
- command: |
|
|
149
|
+
def fac(n):
|
|
150
|
+
for i in range(1, n):
|
|
151
|
+
n *= i
|
|
152
|
+
return n
|
|
153
|
+
- command: fac(10)
|
|
154
|
+
```
|
|
155
|
+
|
|
119
156
|
## Input/Output structure
|
|
120
157
|
|
|
121
158
|
The user can configure how the REPL is called and interpreted.
|
|
@@ -137,14 +174,17 @@ class ReplConfig(msgspec.Struct):
|
|
|
137
174
|
output; useful if the REPL echoes your input before answering.
|
|
138
175
|
timeout (float): Command timeout for this session in seconds.
|
|
139
176
|
"""
|
|
177
|
+
|
|
140
178
|
command: str
|
|
141
179
|
first_prompt: str
|
|
142
180
|
change_prompt: str
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
strip_command: bool = False
|
|
181
|
+
prompt: str
|
|
182
|
+
continuation_prompt: str | None = None
|
|
146
183
|
strip_ansi: bool = False
|
|
184
|
+
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
147
185
|
timeout: float = 5.0
|
|
186
|
+
|
|
187
|
+
|
|
148
188
|
```
|
|
149
189
|
|
|
150
190
|
Then, a session is a list of commands. Each command should be a UTF-8 string, and we allow to attach some meta-data like expected MIME type for the output. We can also pass an expected output in the case of a documentation test. If `output` was already given on the input, it is moved to `expected`. This way it becomes really easy to setup regression tests on your documentation. Just rerun on the generated output file.
|
|
@@ -160,6 +200,7 @@ class ReplCommand(msgspec.Struct):
|
|
|
160
200
|
output (str | None): evaluated output.
|
|
161
201
|
expected (str | None): expected output.
|
|
162
202
|
"""
|
|
203
|
+
|
|
163
204
|
command: str
|
|
164
205
|
output_type: str = "text/plain"
|
|
165
206
|
output: str | None = None
|
|
@@ -173,8 +214,11 @@ class ReplSession(msgspec.Struct):
|
|
|
173
214
|
config (ReplConfig): Config for setting up a REPL session.
|
|
174
215
|
commands (list[ReplCommand]): List of commands in the session.
|
|
175
216
|
"""
|
|
217
|
+
|
|
176
218
|
config: ReplConfig
|
|
177
219
|
commands: list[ReplCommand]
|
|
220
|
+
|
|
221
|
+
|
|
178
222
|
```
|
|
179
223
|
|
|
180
224
|
## License and contribution
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|