repl-session 0.2.0__tar.gz → 0.3.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.2.0 → repl_session-0.3.0}/PKG-INFO +57 -16
- {repl_session-0.2.0 → repl_session-0.3.0}/README.md +53 -14
- {repl_session-0.2.0 → repl_session-0.3.0}/pyproject.toml +14 -2
- repl_session-0.3.0/src/repl_session/__init__.py +115 -0
- repl_session-0.3.0/src/repl_session/jupyter.py +105 -0
- repl_session-0.3.0/src/repl_session/repl.py +121 -0
- repl_session-0.3.0/src/repl_session/schema.py +117 -0
- {repl_session-0.2.0 → repl_session-0.3.0}/src/repl_session.egg-info/PKG-INFO +57 -16
- {repl_session-0.2.0 → repl_session-0.3.0}/src/repl_session.egg-info/SOURCES.txt +5 -1
- repl_session-0.3.0/src/repl_session.egg-info/requires.txt +6 -0
- repl_session-0.3.0/test/test_examples.py +65 -0
- repl_session-0.2.0/src/repl_session/__init__.py +0 -226
- repl_session-0.2.0/src/repl_session.egg-info/requires.txt +0 -3
- {repl_session-0.2.0 → repl_session-0.3.0}/LICENSE +0 -0
- {repl_session-0.2.0 → repl_session-0.3.0}/setup.cfg +0 -0
- {repl_session-0.2.0 → repl_session-0.3.0}/src/repl_session/py.typed +0 -0
- {repl_session-0.2.0 → repl_session-0.3.0}/src/repl_session.egg-info/dependency_links.txt +0 -0
- {repl_session-0.2.0 → repl_session-0.3.0}/src/repl_session.egg-info/entry_points.txt +0 -0
- {repl_session-0.2.0 → repl_session-0.3.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.3.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
|
|
@@ -11,9 +11,11 @@ Classifier: Topic :: Utilities
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: click>=8.1
|
|
15
15
|
Requires-Dist: msgspec>=0.19.0
|
|
16
16
|
Requires-Dist: pexpect>=4.9.0
|
|
17
|
+
Provides-Extra: jupyter
|
|
18
|
+
Requires-Dist: jupyter-client>=8.6; extra == "jupyter"
|
|
17
19
|
Dynamic: license-file
|
|
18
20
|
|
|
19
21
|
# REPL Session
|
|
@@ -33,7 +35,7 @@ This is only really useful if you're hacking together a literate programming env
|
|
|
33
35
|
|
|
34
36
|
The REPL of your choice is started and interacted with through the `pexpect` library. All I/O is dealt with through `msgspec`.
|
|
35
37
|
|
|
36
|
-
The preferred way to solve this is by using `jupyter-client` in combination with an existing Jupyter kernel. However, not all languages have a Jupyter kernel available, and developing one takes a bit more than configuring `pexpect` for an existing REPL.
|
|
38
|
+
The preferred way to solve this is by using `jupyter-client` in combination with an existing Jupyter kernel. However, not all languages have a Jupyter kernel available, and developing one takes a bit more than configuring `pexpect` for an existing REPL. If a kernel is available for your language, you can use it through the optional Jupyter backend: `pip install repl-session[jupyter]`, then see [the Jupyter backend docs](https://entangled.github.io/repl-session/jupyter).
|
|
37
39
|
|
|
38
40
|
## Install
|
|
39
41
|
|
|
@@ -64,7 +66,7 @@ config:
|
|
|
64
66
|
first_prompt: "> "
|
|
65
67
|
change_prompt: '(waiter-prompt-string "{key}>")'
|
|
66
68
|
prompt: "{key}> "
|
|
67
|
-
continuation_prompt: ""
|
|
69
|
+
# continuation_prompt: ""
|
|
68
70
|
commands:
|
|
69
71
|
- command: (* 6 7)
|
|
70
72
|
- command: |
|
|
@@ -75,7 +77,7 @@ commands:
|
|
|
75
77
|
- command: (fac 10 1)
|
|
76
78
|
```
|
|
77
79
|
|
|
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!).
|
|
80
|
+
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!). Running this should give:
|
|
79
81
|
|
|
80
82
|
```bash
|
|
81
83
|
repl-session < test/scheme.yml | jq '.commands.[].output'
|
|
@@ -83,7 +85,7 @@ repl-session < test/scheme.yml | jq '.commands.[].output'
|
|
|
83
85
|
|
|
84
86
|
```
|
|
85
87
|
"42"
|
|
86
|
-
|
|
88
|
+
null
|
|
87
89
|
"3628800"
|
|
88
90
|
```
|
|
89
91
|
|
|
@@ -126,6 +128,8 @@ repl-session < test/lua.yml | jq '.commands.[].output'
|
|
|
126
128
|
```
|
|
127
129
|
"42"
|
|
128
130
|
"Hello, World!"
|
|
131
|
+
null
|
|
132
|
+
"3628800"
|
|
129
133
|
```
|
|
130
134
|
|
|
131
135
|
### Python
|
|
@@ -142,6 +146,7 @@ config:
|
|
|
142
146
|
continuation_prompt: "{key}\\+\\+\\+ "
|
|
143
147
|
environment:
|
|
144
148
|
NO_COLOR: "1"
|
|
149
|
+
PYTHON_BASIC_REPL: "1"
|
|
145
150
|
commands:
|
|
146
151
|
- command: print("Hello, World!")
|
|
147
152
|
- command: 6 * 7
|
|
@@ -153,14 +158,26 @@ commands:
|
|
|
153
158
|
- command: fac(10)
|
|
154
159
|
```
|
|
155
160
|
|
|
161
|
+
```bash
|
|
162
|
+
repl-session < test/python.yml | jq '.commands.[].output'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
"Hello, World!"
|
|
167
|
+
"42"
|
|
168
|
+
null
|
|
169
|
+
"3628800"
|
|
170
|
+
```
|
|
171
|
+
|
|
156
172
|
## Input/Output structure
|
|
157
173
|
|
|
158
|
-
The user can configure how the REPL is called and interpreted.
|
|
174
|
+
The user can configure how the REPL is called and interpreted. The `backend` field picks between this `pexpect` based REPL backend (`backend: repl`, the default) and the Jupyter backend (`backend: jupyter`).
|
|
159
175
|
|
|
160
176
|
```python
|
|
161
177
|
#| id: input-data
|
|
162
|
-
class ReplConfig(msgspec.Struct):
|
|
163
|
-
"""Configuration
|
|
178
|
+
class ReplConfig(msgspec.Struct, tag_field="backend", tag="repl"):
|
|
179
|
+
"""Configuration for the `pexpect` backend; selected with `backend: repl`,
|
|
180
|
+
which is also the default when `backend` is absent.
|
|
164
181
|
|
|
165
182
|
Attributes:
|
|
166
183
|
command (str): Command to start the REPL
|
|
@@ -183,42 +200,66 @@ class ReplConfig(msgspec.Struct):
|
|
|
183
200
|
strip_ansi: bool = False
|
|
184
201
|
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
185
202
|
timeout: float = 5.0
|
|
186
|
-
|
|
187
|
-
|
|
188
203
|
```
|
|
189
204
|
|
|
190
205
|
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.
|
|
191
206
|
|
|
207
|
+
The `output` field only holds the representation matching `output_type`. The `outputs` field records everything a command produced, in order. Each item has a `kind` (`result`, `display`, `stdout`, `stderr` or `error`) and a MIME bundle `data` with one or more representations of the same output. A Jupyter kernel may return `text/plain`, `text/html` and `image/png` together, for example. The REPL backend only sees terminal text, so it always gives a single `result`.
|
|
208
|
+
|
|
192
209
|
```python
|
|
193
210
|
#| id: input-data
|
|
211
|
+
OutputKind = Literal["result", "display", "stdout", "stderr", "error"]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ReplOutput(msgspec.Struct, omit_defaults=True):
|
|
215
|
+
"""A single piece of output produced by a command.
|
|
216
|
+
|
|
217
|
+
Attributes:
|
|
218
|
+
kind (str): One of `result` (the value of the command), `display`
|
|
219
|
+
(rich output produced along the way, e.g. a plot), `stdout`,
|
|
220
|
+
`stderr` or `error` (a traceback). The `repl` backend cannot tell
|
|
221
|
+
these apart and reports everything as a single `result`.
|
|
222
|
+
data (dict[str, str]): MIME bundle: the same output in one or more
|
|
223
|
+
representations, keyed by MIME type. Binary data is base64 encoded;
|
|
224
|
+
JSON data is serialized to a string.
|
|
225
|
+
metadata (dict[str, Any]): Per MIME type metadata, e.g. image sizes.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
kind: OutputKind
|
|
229
|
+
data: dict[str, str]
|
|
230
|
+
metadata: dict[str, Any] = msgspec.field(default_factory=dict)
|
|
231
|
+
|
|
232
|
+
|
|
194
233
|
class ReplCommand(msgspec.Struct):
|
|
195
234
|
"""A command to be sent to the REPL.
|
|
196
235
|
|
|
197
236
|
Attributes:
|
|
198
237
|
command (str): the command.
|
|
199
238
|
output_type (str): MIME type of expected output.
|
|
200
|
-
output (str | None): evaluated output
|
|
239
|
+
output (str | None): evaluated output for `output_type`.
|
|
201
240
|
expected (str | None): expected output.
|
|
241
|
+
outputs (list[ReplOutput] | None): everything the command produced, in
|
|
242
|
+
order, with all available MIME types.
|
|
202
243
|
"""
|
|
203
244
|
|
|
204
245
|
command: str
|
|
205
246
|
output_type: str = "text/plain"
|
|
206
247
|
output: str | None = None
|
|
207
248
|
expected: str | None = None
|
|
249
|
+
outputs: list[ReplOutput] | None = None
|
|
208
250
|
|
|
209
251
|
|
|
210
252
|
class ReplSession(msgspec.Struct):
|
|
211
253
|
"""A REPL session.
|
|
212
254
|
|
|
213
255
|
Attributes:
|
|
214
|
-
config (ReplConfig): Config for setting up a REPL
|
|
256
|
+
config (ReplConfig | JupyterConfig): Config for setting up a REPL
|
|
257
|
+
session, the `backend` field selects which.
|
|
215
258
|
commands (list[ReplCommand]): List of commands in the session.
|
|
216
259
|
"""
|
|
217
260
|
|
|
218
|
-
config: ReplConfig
|
|
261
|
+
config: ReplConfig | JupyterConfig
|
|
219
262
|
commands: list[ReplCommand]
|
|
220
|
-
|
|
221
|
-
|
|
222
263
|
```
|
|
223
264
|
|
|
224
265
|
## License and contribution
|
|
@@ -15,7 +15,7 @@ This is only really useful if you're hacking together a literate programming env
|
|
|
15
15
|
|
|
16
16
|
The REPL of your choice is started and interacted with through the `pexpect` library. All I/O is dealt with through `msgspec`.
|
|
17
17
|
|
|
18
|
-
The preferred way to solve this is by using `jupyter-client` in combination with an existing Jupyter kernel. However, not all languages have a Jupyter kernel available, and developing one takes a bit more than configuring `pexpect` for an existing REPL.
|
|
18
|
+
The preferred way to solve this is by using `jupyter-client` in combination with an existing Jupyter kernel. However, not all languages have a Jupyter kernel available, and developing one takes a bit more than configuring `pexpect` for an existing REPL. If a kernel is available for your language, you can use it through the optional Jupyter backend: `pip install repl-session[jupyter]`, then see [the Jupyter backend docs](https://entangled.github.io/repl-session/jupyter).
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
@@ -46,7 +46,7 @@ config:
|
|
|
46
46
|
first_prompt: "> "
|
|
47
47
|
change_prompt: '(waiter-prompt-string "{key}>")'
|
|
48
48
|
prompt: "{key}> "
|
|
49
|
-
continuation_prompt: ""
|
|
49
|
+
# continuation_prompt: ""
|
|
50
50
|
commands:
|
|
51
51
|
- command: (* 6 7)
|
|
52
52
|
- command: |
|
|
@@ -57,7 +57,7 @@ commands:
|
|
|
57
57
|
- command: (fac 10 1)
|
|
58
58
|
```
|
|
59
59
|
|
|
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!).
|
|
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!). Running this should give:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
repl-session < test/scheme.yml | jq '.commands.[].output'
|
|
@@ -65,7 +65,7 @@ repl-session < test/scheme.yml | jq '.commands.[].output'
|
|
|
65
65
|
|
|
66
66
|
```
|
|
67
67
|
"42"
|
|
68
|
-
|
|
68
|
+
null
|
|
69
69
|
"3628800"
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -108,6 +108,8 @@ repl-session < test/lua.yml | jq '.commands.[].output'
|
|
|
108
108
|
```
|
|
109
109
|
"42"
|
|
110
110
|
"Hello, World!"
|
|
111
|
+
null
|
|
112
|
+
"3628800"
|
|
111
113
|
```
|
|
112
114
|
|
|
113
115
|
### Python
|
|
@@ -124,6 +126,7 @@ config:
|
|
|
124
126
|
continuation_prompt: "{key}\\+\\+\\+ "
|
|
125
127
|
environment:
|
|
126
128
|
NO_COLOR: "1"
|
|
129
|
+
PYTHON_BASIC_REPL: "1"
|
|
127
130
|
commands:
|
|
128
131
|
- command: print("Hello, World!")
|
|
129
132
|
- command: 6 * 7
|
|
@@ -135,14 +138,26 @@ commands:
|
|
|
135
138
|
- command: fac(10)
|
|
136
139
|
```
|
|
137
140
|
|
|
141
|
+
```bash
|
|
142
|
+
repl-session < test/python.yml | jq '.commands.[].output'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
"Hello, World!"
|
|
147
|
+
"42"
|
|
148
|
+
null
|
|
149
|
+
"3628800"
|
|
150
|
+
```
|
|
151
|
+
|
|
138
152
|
## Input/Output structure
|
|
139
153
|
|
|
140
|
-
The user can configure how the REPL is called and interpreted.
|
|
154
|
+
The user can configure how the REPL is called and interpreted. The `backend` field picks between this `pexpect` based REPL backend (`backend: repl`, the default) and the Jupyter backend (`backend: jupyter`).
|
|
141
155
|
|
|
142
156
|
```python
|
|
143
157
|
#| id: input-data
|
|
144
|
-
class ReplConfig(msgspec.Struct):
|
|
145
|
-
"""Configuration
|
|
158
|
+
class ReplConfig(msgspec.Struct, tag_field="backend", tag="repl"):
|
|
159
|
+
"""Configuration for the `pexpect` backend; selected with `backend: repl`,
|
|
160
|
+
which is also the default when `backend` is absent.
|
|
146
161
|
|
|
147
162
|
Attributes:
|
|
148
163
|
command (str): Command to start the REPL
|
|
@@ -165,42 +180,66 @@ class ReplConfig(msgspec.Struct):
|
|
|
165
180
|
strip_ansi: bool = False
|
|
166
181
|
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
167
182
|
timeout: float = 5.0
|
|
168
|
-
|
|
169
|
-
|
|
170
183
|
```
|
|
171
184
|
|
|
172
185
|
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.
|
|
173
186
|
|
|
187
|
+
The `output` field only holds the representation matching `output_type`. The `outputs` field records everything a command produced, in order. Each item has a `kind` (`result`, `display`, `stdout`, `stderr` or `error`) and a MIME bundle `data` with one or more representations of the same output. A Jupyter kernel may return `text/plain`, `text/html` and `image/png` together, for example. The REPL backend only sees terminal text, so it always gives a single `result`.
|
|
188
|
+
|
|
174
189
|
```python
|
|
175
190
|
#| id: input-data
|
|
191
|
+
OutputKind = Literal["result", "display", "stdout", "stderr", "error"]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ReplOutput(msgspec.Struct, omit_defaults=True):
|
|
195
|
+
"""A single piece of output produced by a command.
|
|
196
|
+
|
|
197
|
+
Attributes:
|
|
198
|
+
kind (str): One of `result` (the value of the command), `display`
|
|
199
|
+
(rich output produced along the way, e.g. a plot), `stdout`,
|
|
200
|
+
`stderr` or `error` (a traceback). The `repl` backend cannot tell
|
|
201
|
+
these apart and reports everything as a single `result`.
|
|
202
|
+
data (dict[str, str]): MIME bundle: the same output in one or more
|
|
203
|
+
representations, keyed by MIME type. Binary data is base64 encoded;
|
|
204
|
+
JSON data is serialized to a string.
|
|
205
|
+
metadata (dict[str, Any]): Per MIME type metadata, e.g. image sizes.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
kind: OutputKind
|
|
209
|
+
data: dict[str, str]
|
|
210
|
+
metadata: dict[str, Any] = msgspec.field(default_factory=dict)
|
|
211
|
+
|
|
212
|
+
|
|
176
213
|
class ReplCommand(msgspec.Struct):
|
|
177
214
|
"""A command to be sent to the REPL.
|
|
178
215
|
|
|
179
216
|
Attributes:
|
|
180
217
|
command (str): the command.
|
|
181
218
|
output_type (str): MIME type of expected output.
|
|
182
|
-
output (str | None): evaluated output
|
|
219
|
+
output (str | None): evaluated output for `output_type`.
|
|
183
220
|
expected (str | None): expected output.
|
|
221
|
+
outputs (list[ReplOutput] | None): everything the command produced, in
|
|
222
|
+
order, with all available MIME types.
|
|
184
223
|
"""
|
|
185
224
|
|
|
186
225
|
command: str
|
|
187
226
|
output_type: str = "text/plain"
|
|
188
227
|
output: str | None = None
|
|
189
228
|
expected: str | None = None
|
|
229
|
+
outputs: list[ReplOutput] | None = None
|
|
190
230
|
|
|
191
231
|
|
|
192
232
|
class ReplSession(msgspec.Struct):
|
|
193
233
|
"""A REPL session.
|
|
194
234
|
|
|
195
235
|
Attributes:
|
|
196
|
-
config (ReplConfig): Config for setting up a REPL
|
|
236
|
+
config (ReplConfig | JupyterConfig): Config for setting up a REPL
|
|
237
|
+
session, the `backend` field selects which.
|
|
197
238
|
commands (list[ReplCommand]): List of commands in the session.
|
|
198
239
|
"""
|
|
199
240
|
|
|
200
|
-
config: ReplConfig
|
|
241
|
+
config: ReplConfig | JupyterConfig
|
|
201
242
|
commands: list[ReplCommand]
|
|
202
|
-
|
|
203
|
-
|
|
204
243
|
```
|
|
205
244
|
|
|
206
245
|
## License and contribution
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "repl-session"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.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 = [
|
|
@@ -13,7 +13,7 @@ classifiers = [
|
|
|
13
13
|
]
|
|
14
14
|
requires-python = ">=3.11"
|
|
15
15
|
dependencies = [
|
|
16
|
-
"
|
|
16
|
+
"click>=8.1",
|
|
17
17
|
"msgspec>=0.19.0",
|
|
18
18
|
"pexpect>=4.9.0",
|
|
19
19
|
]
|
|
@@ -22,15 +22,27 @@ dependencies = [
|
|
|
22
22
|
dev = [
|
|
23
23
|
"basedpyright>=1.31.4",
|
|
24
24
|
"entangled-cli>=2.1.13",
|
|
25
|
+
"ipykernel>=7.4.0",
|
|
25
26
|
"mkdocs-macros-plugin>=1.3.9",
|
|
26
27
|
"mkdocs-material>=9.6.20",
|
|
28
|
+
"pytest>=9.2.0",
|
|
27
29
|
"python-lsp-server>=1.13.1",
|
|
28
30
|
"ruff>=0.13.2",
|
|
29
31
|
"ty>=0.0.1a21",
|
|
32
|
+
"types-pexpect>=4.9.0.20250916",
|
|
30
33
|
]
|
|
31
34
|
|
|
32
35
|
[project.scripts]
|
|
33
36
|
repl-session = "repl_session:main"
|
|
34
37
|
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
jupyter = [
|
|
40
|
+
"jupyter-client>=8.6",
|
|
41
|
+
]
|
|
42
|
+
|
|
35
43
|
[tool.uv]
|
|
36
44
|
package = true
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
# Imports are grouped by theme (stdlib, dependencies, internal), not sorted.
|
|
48
|
+
ignore = ["I001"]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# ~/~ begin <<docs/index.md#src/repl_session/__init__.py>>[init]
|
|
2
|
+
"""
|
|
3
|
+
`repl-session` is a command-line tool to evaluate a given session
|
|
4
|
+
in any REPL, and store the results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# ~/~ begin <<docs/index.md#imports>>[init]
|
|
8
|
+
# from datetime import datetime, tzinfo
|
|
9
|
+
from typing import IO, Any
|
|
10
|
+
|
|
11
|
+
# import re
|
|
12
|
+
from contextlib import AbstractContextManager
|
|
13
|
+
import sys
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
import msgspec
|
|
17
|
+
import click
|
|
18
|
+
import importlib.metadata
|
|
19
|
+
|
|
20
|
+
from .schema import (
|
|
21
|
+
Evaluator,
|
|
22
|
+
JupyterConfig,
|
|
23
|
+
OutputKind,
|
|
24
|
+
ReplCommand,
|
|
25
|
+
ReplConfig,
|
|
26
|
+
ReplOutput,
|
|
27
|
+
ReplSession,
|
|
28
|
+
)
|
|
29
|
+
from .repl import repl
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Evaluator",
|
|
33
|
+
"JupyterConfig",
|
|
34
|
+
"OutputKind",
|
|
35
|
+
"ReplCommand",
|
|
36
|
+
"ReplConfig",
|
|
37
|
+
"ReplOutput",
|
|
38
|
+
"ReplSession",
|
|
39
|
+
"evaluator",
|
|
40
|
+
"read_session",
|
|
41
|
+
"repl",
|
|
42
|
+
"run_session",
|
|
43
|
+
"select_output",
|
|
44
|
+
"write_session",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
__version__ = importlib.metadata.version("repl-session")
|
|
48
|
+
# ~/~ end
|
|
49
|
+
# ~/~ begin <<docs/index.md#run-session>>[init]
|
|
50
|
+
def evaluator(config: ReplConfig | JupyterConfig) -> AbstractContextManager[Evaluator]:
|
|
51
|
+
"""Start the backend selected by `config`. The Jupyter backend is only
|
|
52
|
+
imported when needed, so its dependencies remain optional."""
|
|
53
|
+
if isinstance(config, JupyterConfig):
|
|
54
|
+
from .jupyter import kernel
|
|
55
|
+
|
|
56
|
+
return kernel(config)
|
|
57
|
+
return repl(config)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def select_output(outputs: list[ReplOutput], output_type: str) -> str | None:
|
|
61
|
+
"""Pick the representation of type `output_type` from the outputs. Text
|
|
62
|
+
is concatenated in order; for other types the last one wins."""
|
|
63
|
+
values = [out.data[output_type] for out in outputs if output_type in out.data]
|
|
64
|
+
if not values:
|
|
65
|
+
return None
|
|
66
|
+
if output_type.startswith("text/"):
|
|
67
|
+
return "\n".join(v.strip("\n") for v in values).strip() or None
|
|
68
|
+
return values[-1]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_session(session: ReplSession):
|
|
72
|
+
with evaluator(session.config) as run:
|
|
73
|
+
for cmd in session.commands:
|
|
74
|
+
expected = cmd.expected or cmd.output
|
|
75
|
+
cmd.outputs = run(cmd)
|
|
76
|
+
cmd.output = select_output(cmd.outputs, cmd.output_type)
|
|
77
|
+
cmd.expected = expected
|
|
78
|
+
|
|
79
|
+
return session
|
|
80
|
+
# ~/~ end
|
|
81
|
+
# ~/~ begin <<docs/index.md#io>>[init]
|
|
82
|
+
def read_session(port: IO[str] = sys.stdin) -> ReplSession:
|
|
83
|
+
data: Any = msgspec.yaml.decode(port.read())
|
|
84
|
+
# Sessions written before there were multiple backends have no `backend` tag.
|
|
85
|
+
if isinstance(data, dict) and isinstance(config := data.get("config"), dict):
|
|
86
|
+
_ = config.setdefault("backend", "repl")
|
|
87
|
+
return msgspec.convert(data, type=ReplSession)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def write_session(session: ReplSession, port: IO[str] = sys.stdout):
|
|
91
|
+
data = msgspec.json.encode(session)
|
|
92
|
+
_ = port.write(data.decode())
|
|
93
|
+
# ~/~ end
|
|
94
|
+
|
|
95
|
+
@click.command()
|
|
96
|
+
@click.version_option(
|
|
97
|
+
__version__, "-v", "--version",
|
|
98
|
+
prog_name="repl-session", message="%(prog)s %(version)s",
|
|
99
|
+
help="show version and exit")
|
|
100
|
+
@click.option("-l", "--log-enable", is_flag=True, help="show debugging output")
|
|
101
|
+
def repl_session(log_enable: bool = False):
|
|
102
|
+
"""
|
|
103
|
+
repl-session runs a REPL session, reading JSON from standard input and
|
|
104
|
+
writing to standard output. Both the input and output follow the same
|
|
105
|
+
schema.
|
|
106
|
+
"""
|
|
107
|
+
if log_enable:
|
|
108
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
109
|
+
|
|
110
|
+
write_session(run_session(read_session()))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main():
|
|
114
|
+
repl_session()
|
|
115
|
+
# ~/~ end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ~/~ begin <<docs/jupyter.md#src/repl_session/jupyter.py>>[init]
|
|
2
|
+
"""
|
|
3
|
+
Jupyter backend for `repl-session`. Requires the `jupyter` extra.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import Any
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
import msgspec
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from jupyter_client.manager import start_new_kernel
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"The Jupyter backend needs the `jupyter` extra: "
|
|
19
|
+
"`pip install repl-session[jupyter]`"
|
|
20
|
+
) from e
|
|
21
|
+
|
|
22
|
+
from .schema import ANSI_ESCAPE, Evaluator, JupyterConfig, ReplCommand, ReplOutput
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# ~/~ begin <<docs/jupyter.md#jupyter-collect>>[init]
|
|
27
|
+
def mime_bundle(data: dict[str, Any]) -> dict[str, str]:
|
|
28
|
+
return {
|
|
29
|
+
mime: value if isinstance(value, str) else msgspec.json.encode(value).decode()
|
|
30
|
+
for mime, value in data.items()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OutputCollector:
|
|
35
|
+
"""Collects IOPub messages into a list of `ReplOutput`."""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.outputs: list[ReplOutput] = []
|
|
39
|
+
self.clear_pending: bool = False
|
|
40
|
+
|
|
41
|
+
def append(self, output: ReplOutput):
|
|
42
|
+
if self.clear_pending:
|
|
43
|
+
self.outputs.clear()
|
|
44
|
+
self.clear_pending = False
|
|
45
|
+
|
|
46
|
+
last = self.outputs[-1] if self.outputs else None
|
|
47
|
+
if (
|
|
48
|
+
last is not None
|
|
49
|
+
and output.kind in ("stdout", "stderr")
|
|
50
|
+
and last.kind == output.kind
|
|
51
|
+
):
|
|
52
|
+
last.data["text/plain"] += output.data["text/plain"]
|
|
53
|
+
else:
|
|
54
|
+
self.outputs.append(output)
|
|
55
|
+
|
|
56
|
+
def __call__(self, msg: dict[str, Any]):
|
|
57
|
+
msg_type = msg["header"]["msg_type"]
|
|
58
|
+
content = msg["content"]
|
|
59
|
+
match msg_type:
|
|
60
|
+
case "stream":
|
|
61
|
+
self.append(ReplOutput(content["name"], {"text/plain": content["text"]}))
|
|
62
|
+
case "execute_result" | "display_data":
|
|
63
|
+
kind = "result" if msg_type == "execute_result" else "display"
|
|
64
|
+
self.append(
|
|
65
|
+
ReplOutput(kind, mime_bundle(content["data"]), content.get("metadata", {}))
|
|
66
|
+
)
|
|
67
|
+
case "error":
|
|
68
|
+
traceback = ANSI_ESCAPE.sub("", "\n".join(content["traceback"]))
|
|
69
|
+
self.append(ReplOutput("error", {"text/plain": traceback}))
|
|
70
|
+
case "clear_output":
|
|
71
|
+
if content.get("wait"):
|
|
72
|
+
self.clear_pending = True
|
|
73
|
+
else:
|
|
74
|
+
self.outputs.clear()
|
|
75
|
+
case _:
|
|
76
|
+
logger.debug("ignoring %s message", msg_type)
|
|
77
|
+
# ~/~ end
|
|
78
|
+
# ~/~ begin <<docs/jupyter.md#jupyter-kernel>>[init]
|
|
79
|
+
@contextmanager
|
|
80
|
+
def kernel(config: JupyterConfig) -> Generator[Evaluator]:
|
|
81
|
+
km, kc = start_new_kernel(
|
|
82
|
+
startup_timeout=config.startup_timeout,
|
|
83
|
+
kernel_name=config.kernel,
|
|
84
|
+
cwd=config.cwd,
|
|
85
|
+
env=dict(os.environ) | config.environment,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def send(cmd: ReplCommand) -> list[ReplOutput]:
|
|
89
|
+
logger.debug("executing: %s", cmd.command)
|
|
90
|
+
collect = OutputCollector()
|
|
91
|
+
_ = kc.execute_interactive(
|
|
92
|
+
cmd.command,
|
|
93
|
+
timeout=config.timeout,
|
|
94
|
+
output_hook=collect,
|
|
95
|
+
allow_stdin=False,
|
|
96
|
+
)
|
|
97
|
+
return collect.outputs
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
yield send
|
|
101
|
+
finally:
|
|
102
|
+
kc.stop_channels()
|
|
103
|
+
km.shutdown_kernel(now=True)
|
|
104
|
+
# ~/~ end
|
|
105
|
+
# ~/~ end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# ~/~ begin <<docs/index.md#src/repl_session/repl.py>>[init]
|
|
2
|
+
"""
|
|
3
|
+
REPL backend for `repl-session`: drives any interactive REPL through `pexpect`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
import uuid
|
|
9
|
+
import re
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import pexpect
|
|
14
|
+
|
|
15
|
+
from .schema import ANSI_ESCAPE, Evaluator, ReplCommand, ReplConfig, ReplOutput
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# ~/~ begin <<docs/index.md#repl-contextmanager>>[init]
|
|
20
|
+
def spawn(config: ReplConfig):
|
|
21
|
+
env = dict(os.environ) | config.environment
|
|
22
|
+
child: pexpect.spawn[str] = pexpect.spawn(
|
|
23
|
+
config.command,
|
|
24
|
+
timeout=config.timeout,
|
|
25
|
+
echo=False,
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
env=env,
|
|
28
|
+
)
|
|
29
|
+
return child
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def clean_output(config: ReplConfig, output: str) -> str | None:
|
|
33
|
+
"""Remove ANSI escapes (if requested), normalize the CRLF line endings
|
|
34
|
+
that the pseudo-terminal produces, and trim surrounding whitespace.
|
|
35
|
+
Returns `None` if nothing is left."""
|
|
36
|
+
if config.strip_ansi:
|
|
37
|
+
output = ANSI_ESCAPE.sub("", output)
|
|
38
|
+
output = output.replace("\r\n", "\n").replace("\r", "").strip()
|
|
39
|
+
return output or None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def repl(config: ReplConfig) -> Generator[Evaluator]:
|
|
44
|
+
key = str(uuid.uuid4())
|
|
45
|
+
change_prompt_cmd = config.change_prompt.format(key=key)
|
|
46
|
+
prompt = config.prompt.format(key=key)
|
|
47
|
+
continuation_prompt = (
|
|
48
|
+
config.continuation_prompt.format(key=key)
|
|
49
|
+
if config.continuation_prompt is not None
|
|
50
|
+
else None
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
child: pexpect.spawn[str]
|
|
54
|
+
with spawn(config) as child:
|
|
55
|
+
_ = child.expect(config.first_prompt)
|
|
56
|
+
_ = child.sendline(change_prompt_cmd)
|
|
57
|
+
# if config.strip_command:
|
|
58
|
+
# child.expect(key)
|
|
59
|
+
_ = child.expect(prompt)
|
|
60
|
+
|
|
61
|
+
if continuation_prompt is not None:
|
|
62
|
+
def send_text(msg: str) -> str | None:
|
|
63
|
+
lines = msg.splitlines()
|
|
64
|
+
answer: list[str] = []
|
|
65
|
+
|
|
66
|
+
if not lines:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
still_waiting: bool = True
|
|
70
|
+
for line in lines:
|
|
71
|
+
logger.debug("sending: '%s'", line)
|
|
72
|
+
_ = child.sendline(line)
|
|
73
|
+
logger.debug("waiting for prompt or continuation")
|
|
74
|
+
_ = child.expect(
|
|
75
|
+
f"(?P<norm>{prompt})|(?P<cont>{continuation_prompt})"
|
|
76
|
+
)
|
|
77
|
+
if not isinstance(child.match, re.Match):
|
|
78
|
+
continue
|
|
79
|
+
if child.match.group("cont") is not None:
|
|
80
|
+
logger.debug("continuation: %s -- %s", child.before, child.after)
|
|
81
|
+
still_waiting = True
|
|
82
|
+
else:
|
|
83
|
+
logger.debug("done: %s", child.before)
|
|
84
|
+
if child.before is not None:
|
|
85
|
+
answer.append(child.before)
|
|
86
|
+
still_waiting = False
|
|
87
|
+
|
|
88
|
+
if still_waiting:
|
|
89
|
+
logger.debug("waiting for last prompt")
|
|
90
|
+
_ = child.sendline("")
|
|
91
|
+
_ = child.expect(prompt)
|
|
92
|
+
logger.debug("got: %s", child.before)
|
|
93
|
+
if child.before:
|
|
94
|
+
answer.append(child.before)
|
|
95
|
+
|
|
96
|
+
if not answer:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
return clean_output(config, answer[-1])
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
def send_text(msg: str) -> str | None:
|
|
103
|
+
logger.debug("sending: %s", msg)
|
|
104
|
+
|
|
105
|
+
_ = child.sendline(msg)
|
|
106
|
+
_ = child.expect(prompt)
|
|
107
|
+
|
|
108
|
+
if not child.before:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
return clean_output(config, child.before)
|
|
112
|
+
|
|
113
|
+
def send(cmd: ReplCommand) -> list[ReplOutput]:
|
|
114
|
+
text = send_text(cmd.command)
|
|
115
|
+
if text is None:
|
|
116
|
+
return []
|
|
117
|
+
return [ReplOutput("result", {cmd.output_type: text})]
|
|
118
|
+
|
|
119
|
+
yield send
|
|
120
|
+
# ~/~ end
|
|
121
|
+
# ~/~ end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ~/~ begin <<docs/index.md#src/repl_session/schema.py>>[init]
|
|
2
|
+
"""
|
|
3
|
+
Data types for `repl-session`: the session format and the interface shared
|
|
4
|
+
by the backends.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
import msgspec
|
|
12
|
+
|
|
13
|
+
# ~/~ begin <<docs/jupyter.md#jupyter-config>>[init]
|
|
14
|
+
class JupyterConfig(msgspec.Struct, tag_field="backend", tag="jupyter"):
|
|
15
|
+
"""Configuration for the Jupyter backend; selected with `backend: jupyter`.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
kernel (str): Name of the Jupyter kernel, see `jupyter kernelspec list`.
|
|
19
|
+
timeout (float): Command timeout in seconds.
|
|
20
|
+
startup_timeout (float): Timeout for starting the kernel in seconds.
|
|
21
|
+
cwd (str | None): Working directory of the kernel.
|
|
22
|
+
environment (dict[str, str]): Extra environment variables for the kernel.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
kernel: str
|
|
26
|
+
timeout: float = 30.0
|
|
27
|
+
startup_timeout: float = 60.0
|
|
28
|
+
cwd: str | None = None
|
|
29
|
+
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
30
|
+
# ~/~ end
|
|
31
|
+
# ~/~ begin <<README.md#input-data>>[init]
|
|
32
|
+
class ReplConfig(msgspec.Struct, tag_field="backend", tag="repl"):
|
|
33
|
+
"""Configuration for the `pexpect` backend; selected with `backend: repl`,
|
|
34
|
+
which is also the default when `backend` is absent.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
command (str): Command to start the REPL
|
|
38
|
+
first_prompt (str): Regex to match the first prompt
|
|
39
|
+
change_prompt (str): Command to change prompt; should contain '{key}' as an
|
|
40
|
+
argument.
|
|
41
|
+
next_prompt (str): Regex to match the changed prompts; should contain '{key}'
|
|
42
|
+
as an argument.
|
|
43
|
+
append_newline (bool): Whether to append a newline to given commands.
|
|
44
|
+
strip_command (bool): Whether to strip the original command from the gotten
|
|
45
|
+
output; useful if the REPL echoes your input before answering.
|
|
46
|
+
timeout (float): Command timeout for this session in seconds.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
command: str
|
|
50
|
+
first_prompt: str
|
|
51
|
+
change_prompt: str
|
|
52
|
+
prompt: str
|
|
53
|
+
continuation_prompt: str | None = None
|
|
54
|
+
strip_ansi: bool = False
|
|
55
|
+
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
56
|
+
timeout: float = 5.0
|
|
57
|
+
# ~/~ end
|
|
58
|
+
# ~/~ begin <<README.md#input-data>>[1]
|
|
59
|
+
OutputKind = Literal["result", "display", "stdout", "stderr", "error"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ReplOutput(msgspec.Struct, omit_defaults=True):
|
|
63
|
+
"""A single piece of output produced by a command.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
kind (str): One of `result` (the value of the command), `display`
|
|
67
|
+
(rich output produced along the way, e.g. a plot), `stdout`,
|
|
68
|
+
`stderr` or `error` (a traceback). The `repl` backend cannot tell
|
|
69
|
+
these apart and reports everything as a single `result`.
|
|
70
|
+
data (dict[str, str]): MIME bundle: the same output in one or more
|
|
71
|
+
representations, keyed by MIME type. Binary data is base64 encoded;
|
|
72
|
+
JSON data is serialized to a string.
|
|
73
|
+
metadata (dict[str, Any]): Per MIME type metadata, e.g. image sizes.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
kind: OutputKind
|
|
77
|
+
data: dict[str, str]
|
|
78
|
+
metadata: dict[str, Any] = msgspec.field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ReplCommand(msgspec.Struct):
|
|
82
|
+
"""A command to be sent to the REPL.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
command (str): the command.
|
|
86
|
+
output_type (str): MIME type of expected output.
|
|
87
|
+
output (str | None): evaluated output for `output_type`.
|
|
88
|
+
expected (str | None): expected output.
|
|
89
|
+
outputs (list[ReplOutput] | None): everything the command produced, in
|
|
90
|
+
order, with all available MIME types.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
command: str
|
|
94
|
+
output_type: str = "text/plain"
|
|
95
|
+
output: str | None = None
|
|
96
|
+
expected: str | None = None
|
|
97
|
+
outputs: list[ReplOutput] | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ReplSession(msgspec.Struct):
|
|
101
|
+
"""A REPL session.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
config (ReplConfig | JupyterConfig): Config for setting up a REPL
|
|
105
|
+
session, the `backend` field selects which.
|
|
106
|
+
commands (list[ReplCommand]): List of commands in the session.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
config: ReplConfig | JupyterConfig
|
|
110
|
+
commands: list[ReplCommand]
|
|
111
|
+
# ~/~ end
|
|
112
|
+
# ~/~ begin <<docs/index.md#schema-shared>>[init]
|
|
113
|
+
Evaluator = Callable[[ReplCommand], list[ReplOutput]]
|
|
114
|
+
|
|
115
|
+
ANSI_ESCAPE = re.compile(r"(\u001b|\x1B)(\[[0-?]*[ -\/]*[@-~]|[\>\=])")
|
|
116
|
+
# ~/~ end
|
|
117
|
+
# ~/~ end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repl-session
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -11,9 +11,11 @@ Classifier: Topic :: Utilities
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: click>=8.1
|
|
15
15
|
Requires-Dist: msgspec>=0.19.0
|
|
16
16
|
Requires-Dist: pexpect>=4.9.0
|
|
17
|
+
Provides-Extra: jupyter
|
|
18
|
+
Requires-Dist: jupyter-client>=8.6; extra == "jupyter"
|
|
17
19
|
Dynamic: license-file
|
|
18
20
|
|
|
19
21
|
# REPL Session
|
|
@@ -33,7 +35,7 @@ This is only really useful if you're hacking together a literate programming env
|
|
|
33
35
|
|
|
34
36
|
The REPL of your choice is started and interacted with through the `pexpect` library. All I/O is dealt with through `msgspec`.
|
|
35
37
|
|
|
36
|
-
The preferred way to solve this is by using `jupyter-client` in combination with an existing Jupyter kernel. However, not all languages have a Jupyter kernel available, and developing one takes a bit more than configuring `pexpect` for an existing REPL.
|
|
38
|
+
The preferred way to solve this is by using `jupyter-client` in combination with an existing Jupyter kernel. However, not all languages have a Jupyter kernel available, and developing one takes a bit more than configuring `pexpect` for an existing REPL. If a kernel is available for your language, you can use it through the optional Jupyter backend: `pip install repl-session[jupyter]`, then see [the Jupyter backend docs](https://entangled.github.io/repl-session/jupyter).
|
|
37
39
|
|
|
38
40
|
## Install
|
|
39
41
|
|
|
@@ -64,7 +66,7 @@ config:
|
|
|
64
66
|
first_prompt: "> "
|
|
65
67
|
change_prompt: '(waiter-prompt-string "{key}>")'
|
|
66
68
|
prompt: "{key}> "
|
|
67
|
-
continuation_prompt: ""
|
|
69
|
+
# continuation_prompt: ""
|
|
68
70
|
commands:
|
|
69
71
|
- command: (* 6 7)
|
|
70
72
|
- command: |
|
|
@@ -75,7 +77,7 @@ commands:
|
|
|
75
77
|
- command: (fac 10 1)
|
|
76
78
|
```
|
|
77
79
|
|
|
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!).
|
|
80
|
+
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!). Running this should give:
|
|
79
81
|
|
|
80
82
|
```bash
|
|
81
83
|
repl-session < test/scheme.yml | jq '.commands.[].output'
|
|
@@ -83,7 +85,7 @@ repl-session < test/scheme.yml | jq '.commands.[].output'
|
|
|
83
85
|
|
|
84
86
|
```
|
|
85
87
|
"42"
|
|
86
|
-
|
|
88
|
+
null
|
|
87
89
|
"3628800"
|
|
88
90
|
```
|
|
89
91
|
|
|
@@ -126,6 +128,8 @@ repl-session < test/lua.yml | jq '.commands.[].output'
|
|
|
126
128
|
```
|
|
127
129
|
"42"
|
|
128
130
|
"Hello, World!"
|
|
131
|
+
null
|
|
132
|
+
"3628800"
|
|
129
133
|
```
|
|
130
134
|
|
|
131
135
|
### Python
|
|
@@ -142,6 +146,7 @@ config:
|
|
|
142
146
|
continuation_prompt: "{key}\\+\\+\\+ "
|
|
143
147
|
environment:
|
|
144
148
|
NO_COLOR: "1"
|
|
149
|
+
PYTHON_BASIC_REPL: "1"
|
|
145
150
|
commands:
|
|
146
151
|
- command: print("Hello, World!")
|
|
147
152
|
- command: 6 * 7
|
|
@@ -153,14 +158,26 @@ commands:
|
|
|
153
158
|
- command: fac(10)
|
|
154
159
|
```
|
|
155
160
|
|
|
161
|
+
```bash
|
|
162
|
+
repl-session < test/python.yml | jq '.commands.[].output'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
"Hello, World!"
|
|
167
|
+
"42"
|
|
168
|
+
null
|
|
169
|
+
"3628800"
|
|
170
|
+
```
|
|
171
|
+
|
|
156
172
|
## Input/Output structure
|
|
157
173
|
|
|
158
|
-
The user can configure how the REPL is called and interpreted.
|
|
174
|
+
The user can configure how the REPL is called and interpreted. The `backend` field picks between this `pexpect` based REPL backend (`backend: repl`, the default) and the Jupyter backend (`backend: jupyter`).
|
|
159
175
|
|
|
160
176
|
```python
|
|
161
177
|
#| id: input-data
|
|
162
|
-
class ReplConfig(msgspec.Struct):
|
|
163
|
-
"""Configuration
|
|
178
|
+
class ReplConfig(msgspec.Struct, tag_field="backend", tag="repl"):
|
|
179
|
+
"""Configuration for the `pexpect` backend; selected with `backend: repl`,
|
|
180
|
+
which is also the default when `backend` is absent.
|
|
164
181
|
|
|
165
182
|
Attributes:
|
|
166
183
|
command (str): Command to start the REPL
|
|
@@ -183,42 +200,66 @@ class ReplConfig(msgspec.Struct):
|
|
|
183
200
|
strip_ansi: bool = False
|
|
184
201
|
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
185
202
|
timeout: float = 5.0
|
|
186
|
-
|
|
187
|
-
|
|
188
203
|
```
|
|
189
204
|
|
|
190
205
|
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.
|
|
191
206
|
|
|
207
|
+
The `output` field only holds the representation matching `output_type`. The `outputs` field records everything a command produced, in order. Each item has a `kind` (`result`, `display`, `stdout`, `stderr` or `error`) and a MIME bundle `data` with one or more representations of the same output. A Jupyter kernel may return `text/plain`, `text/html` and `image/png` together, for example. The REPL backend only sees terminal text, so it always gives a single `result`.
|
|
208
|
+
|
|
192
209
|
```python
|
|
193
210
|
#| id: input-data
|
|
211
|
+
OutputKind = Literal["result", "display", "stdout", "stderr", "error"]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ReplOutput(msgspec.Struct, omit_defaults=True):
|
|
215
|
+
"""A single piece of output produced by a command.
|
|
216
|
+
|
|
217
|
+
Attributes:
|
|
218
|
+
kind (str): One of `result` (the value of the command), `display`
|
|
219
|
+
(rich output produced along the way, e.g. a plot), `stdout`,
|
|
220
|
+
`stderr` or `error` (a traceback). The `repl` backend cannot tell
|
|
221
|
+
these apart and reports everything as a single `result`.
|
|
222
|
+
data (dict[str, str]): MIME bundle: the same output in one or more
|
|
223
|
+
representations, keyed by MIME type. Binary data is base64 encoded;
|
|
224
|
+
JSON data is serialized to a string.
|
|
225
|
+
metadata (dict[str, Any]): Per MIME type metadata, e.g. image sizes.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
kind: OutputKind
|
|
229
|
+
data: dict[str, str]
|
|
230
|
+
metadata: dict[str, Any] = msgspec.field(default_factory=dict)
|
|
231
|
+
|
|
232
|
+
|
|
194
233
|
class ReplCommand(msgspec.Struct):
|
|
195
234
|
"""A command to be sent to the REPL.
|
|
196
235
|
|
|
197
236
|
Attributes:
|
|
198
237
|
command (str): the command.
|
|
199
238
|
output_type (str): MIME type of expected output.
|
|
200
|
-
output (str | None): evaluated output
|
|
239
|
+
output (str | None): evaluated output for `output_type`.
|
|
201
240
|
expected (str | None): expected output.
|
|
241
|
+
outputs (list[ReplOutput] | None): everything the command produced, in
|
|
242
|
+
order, with all available MIME types.
|
|
202
243
|
"""
|
|
203
244
|
|
|
204
245
|
command: str
|
|
205
246
|
output_type: str = "text/plain"
|
|
206
247
|
output: str | None = None
|
|
207
248
|
expected: str | None = None
|
|
249
|
+
outputs: list[ReplOutput] | None = None
|
|
208
250
|
|
|
209
251
|
|
|
210
252
|
class ReplSession(msgspec.Struct):
|
|
211
253
|
"""A REPL session.
|
|
212
254
|
|
|
213
255
|
Attributes:
|
|
214
|
-
config (ReplConfig): Config for setting up a REPL
|
|
256
|
+
config (ReplConfig | JupyterConfig): Config for setting up a REPL
|
|
257
|
+
session, the `backend` field selects which.
|
|
215
258
|
commands (list[ReplCommand]): List of commands in the session.
|
|
216
259
|
"""
|
|
217
260
|
|
|
218
|
-
config: ReplConfig
|
|
261
|
+
config: ReplConfig | JupyterConfig
|
|
219
262
|
commands: list[ReplCommand]
|
|
220
|
-
|
|
221
|
-
|
|
222
263
|
```
|
|
223
264
|
|
|
224
265
|
## License and contribution
|
|
@@ -2,10 +2,14 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/repl_session/__init__.py
|
|
5
|
+
src/repl_session/jupyter.py
|
|
5
6
|
src/repl_session/py.typed
|
|
7
|
+
src/repl_session/repl.py
|
|
8
|
+
src/repl_session/schema.py
|
|
6
9
|
src/repl_session.egg-info/PKG-INFO
|
|
7
10
|
src/repl_session.egg-info/SOURCES.txt
|
|
8
11
|
src/repl_session.egg-info/dependency_links.txt
|
|
9
12
|
src/repl_session.egg-info/entry_points.txt
|
|
10
13
|
src/repl_session.egg-info/requires.txt
|
|
11
|
-
src/repl_session.egg-info/top_level.txt
|
|
14
|
+
src/repl_session.egg-info/top_level.txt
|
|
15
|
+
test/test_examples.py
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Run the example sessions from the README and check their outputs."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from repl_session import ReplConfig, ReplSession, read_session, run_session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TEST_DIR = Path(__file__).parent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EXPECTED = {
|
|
18
|
+
"scheme": ["42", None, "3628800"],
|
|
19
|
+
"lua": ["42", "Hello, World!", None, "3628800"],
|
|
20
|
+
"python": ["Hello, World!", "42", None, "3628800"],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_session(name: str) -> ReplSession:
|
|
25
|
+
with open(TEST_DIR / f"{name}.yml") as f:
|
|
26
|
+
return read_session(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def require(available: bool, what: str):
|
|
30
|
+
if available:
|
|
31
|
+
return
|
|
32
|
+
# In CI everything must be available; locally we allow skipping.
|
|
33
|
+
if os.environ.get("CI"):
|
|
34
|
+
pytest.fail(f"{what} not found")
|
|
35
|
+
pytest.skip(f"{what} not found")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize("name", EXPECTED.keys())
|
|
39
|
+
def test_example(name: str):
|
|
40
|
+
session = load_session(name)
|
|
41
|
+
assert isinstance(session.config, ReplConfig)
|
|
42
|
+
executable = shlex.split(session.config.command)[0]
|
|
43
|
+
require(shutil.which(executable) is not None, f"`{executable}`")
|
|
44
|
+
|
|
45
|
+
result = run_session(session)
|
|
46
|
+
assert [cmd.output for cmd in result.commands] == EXPECTED[name]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_jupyter():
|
|
50
|
+
require(
|
|
51
|
+
importlib.util.find_spec("jupyter_client") is not None
|
|
52
|
+
and importlib.util.find_spec("ipykernel") is not None,
|
|
53
|
+
"`jupyter_client` and `ipykernel`",
|
|
54
|
+
)
|
|
55
|
+
result = run_session(load_session("jupyter"))
|
|
56
|
+
hello, answer, define, fac, html, error = result.commands
|
|
57
|
+
|
|
58
|
+
assert [cmd.output for cmd in (hello, answer, define, fac, html)] == [
|
|
59
|
+
"Hello, World!", "42", None, "3628800", "<b>Hello</b>, World!"
|
|
60
|
+
]
|
|
61
|
+
assert [[out.kind for out in cmd.outputs or []] for cmd in result.commands] == [
|
|
62
|
+
["stdout"], ["result"], [], ["result"], ["result"], ["error"]
|
|
63
|
+
]
|
|
64
|
+
assert html.outputs and set(html.outputs[0].data) == {"text/plain", "text/html"}
|
|
65
|
+
assert error.output and error.output.endswith("ZeroDivisionError: division by zero")
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# ~/~ begin <<docs/index.md#src/repl_session/__init__.py>>[init]
|
|
2
|
-
#| file: src/repl_session/__init__.py
|
|
3
|
-
"""
|
|
4
|
-
`repl-session` is a command-line tool to evaluate a given session
|
|
5
|
-
in any REPL, and store the results.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
# ~/~ begin <<docs/index.md#imports>>[init]
|
|
9
|
-
#| id: imports
|
|
10
|
-
# from datetime import datetime, tzinfo
|
|
11
|
-
from typing import IO, cast
|
|
12
|
-
from collections.abc import Generator, Callable
|
|
13
|
-
|
|
14
|
-
# import re
|
|
15
|
-
from contextlib import contextmanager
|
|
16
|
-
import uuid
|
|
17
|
-
import sys
|
|
18
|
-
import re
|
|
19
|
-
import logging
|
|
20
|
-
|
|
21
|
-
import pexpect
|
|
22
|
-
import msgspec
|
|
23
|
-
import argh
|
|
24
|
-
import importlib.metadata
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
__version__ = importlib.metadata.version("repl-session")
|
|
28
|
-
# ~/~ end
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# ~/~ begin <<README.md#input-data>>[init]
|
|
32
|
-
#| id: input-data
|
|
33
|
-
class ReplConfig(msgspec.Struct):
|
|
34
|
-
"""Configuration
|
|
35
|
-
|
|
36
|
-
Attributes:
|
|
37
|
-
command (str): Command to start the REPL
|
|
38
|
-
first_prompt (str): Regex to match the first prompt
|
|
39
|
-
change_prompt (str): Command to change prompt; should contain '{key}' as an
|
|
40
|
-
argument.
|
|
41
|
-
next_prompt (str): Regex to match the changed prompts; should contain '{key}'
|
|
42
|
-
as an argument.
|
|
43
|
-
append_newline (bool): Whether to append a newline to given commands.
|
|
44
|
-
strip_command (bool): Whether to strip the original command from the gotten
|
|
45
|
-
output; useful if the REPL echoes your input before answering.
|
|
46
|
-
timeout (float): Command timeout for this session in seconds.
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
command: str
|
|
50
|
-
first_prompt: str
|
|
51
|
-
change_prompt: str
|
|
52
|
-
prompt: str
|
|
53
|
-
continuation_prompt: str | None = None
|
|
54
|
-
strip_ansi: bool = False
|
|
55
|
-
environment: dict[str, str] = msgspec.field(default_factory=dict)
|
|
56
|
-
timeout: float = 5.0
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# ~/~ end
|
|
60
|
-
# ~/~ begin <<README.md#input-data>>[1]
|
|
61
|
-
#| id: input-data
|
|
62
|
-
class ReplCommand(msgspec.Struct):
|
|
63
|
-
"""A command to be sent to the REPL.
|
|
64
|
-
|
|
65
|
-
Attributes:
|
|
66
|
-
command (str): the command.
|
|
67
|
-
output_type (str): MIME type of expected output.
|
|
68
|
-
output (str | None): evaluated output.
|
|
69
|
-
expected (str | None): expected output.
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
command: str
|
|
73
|
-
output_type: str = "text/plain"
|
|
74
|
-
output: str | None = None
|
|
75
|
-
expected: str | None = None
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class ReplSession(msgspec.Struct):
|
|
79
|
-
"""A REPL session.
|
|
80
|
-
|
|
81
|
-
Attributes:
|
|
82
|
-
config (ReplConfig): Config for setting up a REPL session.
|
|
83
|
-
commands (list[ReplCommand]): List of commands in the session.
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
config: ReplConfig
|
|
87
|
-
commands: list[ReplCommand]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# ~/~ end
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# ~/~ begin <<docs/index.md#repl-contextmanager>>[init]
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
@contextmanager
|
|
107
|
-
def repl(config: ReplConfig) -> Generator[Callable[[str], str | None]]:
|
|
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
|
|
164
|
-
|
|
165
|
-
if config.strip_ansi:
|
|
166
|
-
ansi_escape = re.compile(r"(\u001b\[|\x1B\[)[0-?]*[ -\/]*[@-~]")
|
|
167
|
-
return ansi_escape.sub("", answer[-1].strip())
|
|
168
|
-
|
|
169
|
-
return answer[-1].strip()
|
|
170
|
-
|
|
171
|
-
yield send
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# ~/~ end
|
|
175
|
-
# ~/~ begin <<docs/index.md#run-session>>[init]
|
|
176
|
-
#| id: run-session
|
|
177
|
-
def run_session(session: ReplSession):
|
|
178
|
-
with repl(session.config) as run:
|
|
179
|
-
for cmd in session.commands:
|
|
180
|
-
expected = cmd.expected or cmd.output
|
|
181
|
-
output = run(cmd.command)
|
|
182
|
-
cmd.output = output
|
|
183
|
-
cmd.expected = expected
|
|
184
|
-
|
|
185
|
-
return session
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# ~/~ end
|
|
189
|
-
# ~/~ begin <<docs/index.md#io>>[init]
|
|
190
|
-
#| id: io
|
|
191
|
-
def read_session(port: IO[str] = sys.stdin) -> ReplSession:
|
|
192
|
-
data: str = port.read()
|
|
193
|
-
return msgspec.yaml.decode(data, type=ReplSession)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def write_session(session: ReplSession, port: IO[str] = sys.stdout):
|
|
197
|
-
data = msgspec.json.encode(session)
|
|
198
|
-
_ = port.write(data.decode())
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# ~/~ end
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
@argh.arg("-v", "--version", help="show version and exit")
|
|
205
|
-
@argh.arg("-l", "--log-enable", help="show debugging output")
|
|
206
|
-
def repl_session(version: bool = False, log_enable: bool = False):
|
|
207
|
-
"""
|
|
208
|
-
repl-session runs a REPL session, reading JSON from standard input and
|
|
209
|
-
writing to standard output. Both the input and output follow the same
|
|
210
|
-
schema.
|
|
211
|
-
"""
|
|
212
|
-
if version:
|
|
213
|
-
print(f"repl-session {__version__}")
|
|
214
|
-
sys.exit(0)
|
|
215
|
-
|
|
216
|
-
if log_enable:
|
|
217
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
218
|
-
|
|
219
|
-
write_session(run_session(read_session()))
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def main():
|
|
223
|
-
argh.dispatch_command(repl_session)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
# ~/~ end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|