Skip to content

Commit 3bf66a8

Browse files
committed
make package ready for publishing
1 parent a76a1a7 commit 3bf66a8

11 files changed

Lines changed: 367 additions & 145 deletions

File tree

.github/workflows/pages.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name: build pages
66
on:
77
# Triggers the workflow on push or pull request events but only for the "main" branch
88
push:
9-
branches: [ "main" ]
9+
branches: ["main"]
1010

1111
# Allows you to run this workflow manually from the Actions tab
1212
workflow_dispatch:
@@ -42,14 +42,14 @@ jobs:
4242

4343
- name: Run MkDocs
4444
run: uv run mkdocs build
45-
45+
4646
- name: Configure GitHub Pages
4747
uses: actions/configure-pages@v5
4848

4949
- name: Upload artifact
5050
uses: actions/upload-pages-artifact@v4
5151
with:
52-
path: 'site'
52+
path: "site"
5353

5454
deploy:
5555
environment:

README.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# REPL Session
2+
3+
[![docs](https://github.com/entangled/repl-session/actions/workflows/pages.yml/badge.svg)](https://github.com/entangled/repl-session/actions/workflows/pages.yml)
4+
[![Static Badge](https://img.shields.io/badge/docs-latest-blue)](https://entangled.github.io/repl-session)
5+
6+
This is script that runs a session on any REPL following a description in a JSON file. The output contains the commands entered and the results given. This can be useful to drive documentation tests or literate programming tasks. This way we decouple running the commands from rendering or presenting the corresponding results, leading to better reproducibility, caching output and modularity w.r.t. any other tools that you may use.
7+
8+
This is very similar to running a Jupyter notebook from the console, with the benefit that you don't need a Jupyter kernel available for the language you're using. The downside is that REPLs can be messy to interact with.
9+
10+
## Is this for you?
11+
12+
This is only really useful if you're hacking together a literate programming environment similar to [Entangled](https://entangled.github.io). Suppose you have your documentation written in Markdown, ready for rendering with MkDocs or Pandoc. You want to automatically evaluate some expressions in this document as if they're entered in a REPL and process the results for inclusion in the document generator. Here `repl-session` is a nicely confined command-line tool, so its easy to integrate into a build pipeline.
13+
14+
## How does it work?
15+
16+
The REPL of your choice is started and interacted with through the `pexpect` library. All I/O is dealt with through `msgspec`.
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. I may still include Jupyter support in this application at a later stage.
19+
20+
## Install
21+
22+
Install with,
23+
24+
```bash
25+
pip install repl-session
26+
```
27+
28+
Or equivalent Poetry, Astral Uv, Hatch or Conda commands.
29+
30+
## Documentation
31+
32+
The full documentation is available at [entangled.github.io/repl-session](https://entangled.github.io/repl-session).
33+
34+
## Examples
35+
36+
Here are some examples where we could interact with a REPL successfully. In general, the less intricate the REPL the better the results.
37+
38+
### Chez Scheme
39+
40+
I like to work with [Chez Scheme](https://cisco.github.io/ChezScheme/). Suppose I want to document an interactive session. This can be done:
41+
42+
```yaml
43+
#| file: test/scheme.yml
44+
config:
45+
command: "scheme --eedisable"
46+
first_prompt: "> "
47+
change_prompt: "(waiter-prompt-string \"{key}>\")"
48+
next_prompt: "{key}> "
49+
strip_command: true
50+
commands:
51+
- command: (* 6 7)
52+
- command: |
53+
(define (fac n init)
54+
(if (zero? n)
55+
init
56+
(fac (- n 1) (* init n)))))
57+
- command: (fac 10 1)
58+
```
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!). Since commands are also echoed to standard out, we need to strip them from the resulting output. Running this should give:
61+
62+
```bash
63+
repl-session < test/scheme.yml | jq '.commands.[].output'
64+
```
65+
66+
```
67+
"42"
68+
"(define (fac n init)\n (if (zero? n)\n init\n (fac (- n 1) (* init n)))))"
69+
"3628800"
70+
```
71+
72+
### Lua
73+
74+
This looks very similar to the previous example:
75+
76+
```yaml
77+
#| file: test/lua.yml
78+
config:
79+
command: "lua"
80+
first_prompt: "> "
81+
change_prompt: "_PROMPT = \"{key}> \""
82+
next_prompt: "{key}> "
83+
strip_command: true
84+
strip_ansi: true
85+
commands:
86+
- command: 6 * 7
87+
- command: "\"Hello\" .. \", \" .. \"World!\""
88+
```
89+
90+
The Lua REPL is not so nice. It sends ANSI escape codes and those need to be filtered out.
91+
92+
```bash
93+
repl-session < test/lua.yml | jq '.commands.[].output'
94+
```
95+
96+
```
97+
"42"
98+
"Hello, World!"
99+
```
100+
101+
## Input/Output structure
102+
103+
The user can configure how the REPL is called and interpreted.
104+
105+
```python
106+
#| id: input-data
107+
class ReplConfig(msgspec.Struct):
108+
"""Configuration
109+
110+
Attributes:
111+
command (str): Command to start the REPL
112+
first_prompt (str): Regex to match the first prompt
113+
change_prompt (str): Command to change prompt; should contain '{key}' as an
114+
argument.
115+
next_prompt (str): Regex to match the changed prompts; should contain '{key}'
116+
as an argument.
117+
append_newline (bool): Whether to append a newline to given commands.
118+
strip_command (bool): Whether to strip the original command from the gotten
119+
output; useful if the REPL echoes your input before answering.
120+
timeout (float): Command timeout for this session in seconds.
121+
"""
122+
command: str
123+
first_prompt: str
124+
change_prompt: str
125+
next_prompt: str
126+
append_newline: bool = True
127+
strip_command: bool = False
128+
strip_ansi: bool = False
129+
timeout: float = 5.0
130+
```
131+
132+
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.
133+
134+
```python
135+
#| id: input-data
136+
class ReplCommand(msgspec.Struct):
137+
"""A command to be sent to the REPL.
138+
139+
Attributes:
140+
command (str): the command.
141+
output_type (str): MIME type of expected output.
142+
output (str | None): evaluated output.
143+
expected (str | None): expected output.
144+
"""
145+
command: str
146+
output_type: str = "text/plain"
147+
output: str | None = None
148+
expected: str | None = None
149+
150+
151+
class ReplSession(msgspec.Struct):
152+
"""A REPL session.
153+
154+
Attributes:
155+
config (ReplConfig): Config for setting up a REPL session.
156+
commands (list[ReplCommand]): List of commands in the session.
157+
"""
158+
config: ReplConfig
159+
commands: list[ReplCommand]
160+
```
161+
162+
## License and contribution
163+
164+
Licensed under the Apache 2.0 license. Contributions are welcome: if you've succesfully applied `repl-session` to a REPL not listed in the documentation, consider contributing your configuration to the documentation. If your contribution fixes a bug, please first file an issue.

docs/index.md

Lines changed: 2 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,4 @@
1-
# REPL Session
2-
3-
This is a Python script that runs a session on any REPL following a description in a JSON file. The output contains the commands entered and the results given. This can be useful to drive documentation tests or literate programming tasks. This way we decouple running the commands from rendering or presenting the corresponding results, leading to better reproducibility, caching output and modularity w.r.t. any other tools that you may use.
4-
5-
This is very similar to running a Jupyter notebook, with the benefit that you don't need a Jupyter kernel available for the language you're using.
6-
7-
## Examples
8-
9-
### Chez Scheme
10-
11-
I like to work with [Chez Scheme](https://cisco.github.io/ChezScheme/). Suppose I want to document an interactive session. This can be done:
12-
13-
```yaml
14-
#| file: test/scheme.yml
15-
config:
16-
command: "scheme --eedisable"
17-
first_prompt: "> "
18-
change_prompt: "(waiter-prompt-string \"{key}>\")"
19-
next_prompt: "{key}> "
20-
strip_command: true
21-
commands:
22-
- command: (* 6 7)
23-
- command: |
24-
(define (fac n init)
25-
(if (zero? n)
26-
init
27-
(fac (- n 1) (* init n)))))
28-
- command: (fac 10 1)
29-
```
30-
31-
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:
32-
33-
```bash
34-
repl-session < test/scheme.yml | jq '.commands.[].output'
35-
```
36-
37-
```
38-
"42"
39-
"(define (fac n init)\n (if (zero? n)\n init\n (fac (- n 1) (* init n)))))"
40-
"3628800"
41-
```
42-
43-
### Lua
44-
45-
This looks very similar to the previous example:
46-
47-
```yaml
48-
#| file: test/lua.yml
49-
config:
50-
command: "lua"
51-
first_prompt: "> "
52-
change_prompt: "_PROMPT = \"{key}> \""
53-
next_prompt: "{key}> "
54-
strip_command: true
55-
strip_ansi: true
56-
commands:
57-
- command: 6 * 7
58-
- command: "\"Hello\" .. \", \" .. \"World!\""
59-
```
60-
61-
The Lua REPL is not so nice. It sends ANSI escape codes and those need to be filtered out.
62-
63-
```bash
64-
repl-session < test/lua.yml | jq '.commands.[].output'
65-
```
66-
67-
```
68-
"42"
69-
"Hello, World!"
70-
```
71-
72-
## Input/Output structure
73-
74-
The user can configure how the REPL is called and interpreted.
75-
76-
```python
77-
#| id: input-data
78-
class ReplConfig(msgspec.Struct):
79-
"""Configuration
80-
81-
Attributes:
82-
command (str): Command to start the REPL
83-
first_prompt (str): Regex to match the first prompt
84-
change_prompt (str): Command to change prompt; should contain '{key}' as an
85-
argument.
86-
next_prompt (str): Regex to match the changed prompts; should contain '{key}'
87-
as an argument.
88-
append_newline (bool): Whether to append a newline to given commands.
89-
strip_command (bool): Whether to strip the original command from the gotten
90-
output; useful if the REPL echoes your input before answering.
91-
timeout (float): Command timeout for this session in seconds.
92-
"""
93-
command: str
94-
first_prompt: str
95-
change_prompt: str
96-
next_prompt: str
97-
append_newline: bool = True
98-
strip_command: bool = False
99-
strip_ansi: bool = False
100-
timeout: float = 5.0
101-
```
102-
103-
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.
104-
105-
```python
106-
#| id: input-data
107-
class ReplCommand(msgspec.Struct):
108-
"""A command to be sent to the REPL.
109-
110-
Attributes:
111-
command (str): the command.
112-
output_type (str): MIME type of expected output.
113-
output (str | None): evaluated output.
114-
expected (str | None): expected output.
115-
"""
116-
command: str
117-
output_type: str = "text/plain"
118-
output: str | None = None
119-
expected: str | None = None
120-
121-
122-
class ReplSession(msgspec.Struct):
123-
"""A REPL session.
124-
125-
Attributes:
126-
config (ReplConfig): Config for setting up a REPL session.
127-
commands (list[ReplCommand]): List of commands in the session.
128-
"""
129-
config: ReplConfig
130-
commands: list[ReplCommand]
131-
```
1+
{% include 'README.md' %}
1322

1333
## Implementation
1344

@@ -228,7 +98,7 @@ __version__ = importlib.metadata.version("repl-session")
22898
## Synthesis
22999

230100
```python
231-
#| file: repl_session/__init__.py
101+
#| file: src/repl_session/__init__.py
232102
"""
233103
`repl-session` is a command-line tool to evaluate a given session
234104
in any REPL, and store the results.

entangled.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
version="2.0"
2-
watch_list=["docs/*.md"]
2+
watch_list=["README.md", "docs/*.md"]
33
hooks=["quarto_attributes"]
44

55
[[languages]]

mkdocs.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
site_name: repl-session
2+
markdown_extensions:
3+
- pymdownx.highlight:
4+
anchor_linenums: true
5+
- pymdownx.superfences
6+
plugins:
7+
- macros:
8+
include_dir: .
29
theme:
310
name: material

pyproject.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
[project]
22
name = "repl-session"
33
version = "0.1.0"
4-
description = "Runs a session with a Read-Eval-Print-Loop (REPL). Takes JSON as input gives JSON as output."
4+
description = "Runs a session with a Read-Eval-Print-Loop (REPL). Takes YAML as input gives JSON as output."
55
readme = "README.md"
6-
requires-python = ">=3.13"
6+
classifiers = [
7+
"Development Status :: 3 - Alpha",
8+
"Environment :: Console",
9+
"Intended Audience :: Developers",
10+
"License :: OSI Approved :: Apache Software License",
11+
"Topic :: Software Development",
12+
"Topic :: Utilities"
13+
]
14+
requires-python = ">=3.11"
715
dependencies = [
816
"argh>=0.30.5",
917
"msgspec>=0.19.0",
@@ -14,6 +22,7 @@ dependencies = [
1422
dev = [
1523
"basedpyright>=1.31.4",
1624
"entangled-cli>=2.1.13",
25+
"mkdocs-macros-plugin>=1.3.9",
1726
"mkdocs-material>=9.6.20",
1827
"ty>=0.0.1a21",
1928
]

0 commit comments

Comments
 (0)