md-babel-py 0.1.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.
- md_babel_py-0.1.0/LICENSE +21 -0
- md_babel_py-0.1.0/PKG-INFO +16 -0
- md_babel_py-0.1.0/README.md +406 -0
- md_babel_py-0.1.0/md_babel_py/__init__.py +3 -0
- md_babel_py-0.1.0/md_babel_py/cli.py +262 -0
- md_babel_py-0.1.0/md_babel_py/config.py +268 -0
- md_babel_py-0.1.0/md_babel_py/exceptions.py +39 -0
- md_babel_py-0.1.0/md_babel_py/executor.py +185 -0
- md_babel_py-0.1.0/md_babel_py/parser.py +220 -0
- md_babel_py-0.1.0/md_babel_py/session.py +359 -0
- md_babel_py-0.1.0/md_babel_py/types.py +19 -0
- md_babel_py-0.1.0/md_babel_py/writer.py +95 -0
- md_babel_py-0.1.0/md_babel_py.egg-info/PKG-INFO +16 -0
- md_babel_py-0.1.0/md_babel_py.egg-info/SOURCES.txt +20 -0
- md_babel_py-0.1.0/md_babel_py.egg-info/dependency_links.txt +1 -0
- md_babel_py-0.1.0/md_babel_py.egg-info/entry_points.txt +2 -0
- md_babel_py-0.1.0/md_babel_py.egg-info/requires.txt +9 -0
- md_babel_py-0.1.0/md_babel_py.egg-info/top_level.txt +1 -0
- md_babel_py-0.1.0/pyproject.toml +30 -0
- md_babel_py-0.1.0/setup.cfg +4 -0
- md_babel_py-0.1.0/tests/test_exit_codes.py +133 -0
- md_babel_py-0.1.0/tests/test_integration.py +29 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ivan Nikolic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: md-babel-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Execute code blocks in markdown files with session support
|
|
5
|
+
Author-email: Ivan Nikolic <lesh@mm.st>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: ruff; extra == "dev"
|
|
12
|
+
Requires-Dist: mypy; extra == "dev"
|
|
13
|
+
Provides-Extra: matplotlib
|
|
14
|
+
Requires-Dist: matplotlib; extra == "matplotlib"
|
|
15
|
+
Requires-Dist: numpy; extra == "matplotlib"
|
|
16
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# md-babel-py
|
|
2
|
+
|
|
3
|
+
Execute code blocks in markdown files and insert the results.
|
|
4
|
+
|
|
5
|
+
**Use cases:**
|
|
6
|
+
- Keep documentation examples up-to-date automatically
|
|
7
|
+
- Validate code snippets in docs actually work
|
|
8
|
+
- Generate diagrams and charts from code in markdown
|
|
9
|
+
- Literate programming with executable documentation
|
|
10
|
+
|
|
11
|
+
## Languages
|
|
12
|
+
|
|
13
|
+
### Shell
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
echo "cwd: $(pwd)"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
<!--Result:-->
|
|
20
|
+
```
|
|
21
|
+
cwd: /work
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Python
|
|
25
|
+
|
|
26
|
+
```python session=example
|
|
27
|
+
a = "hello world"
|
|
28
|
+
print(a)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
<!--Result:-->
|
|
32
|
+
```
|
|
33
|
+
hello world
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Sessions preserve state between code blocks:
|
|
37
|
+
|
|
38
|
+
```python session=example
|
|
39
|
+
print(a, "again")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
<!--Result:-->
|
|
43
|
+
```
|
|
44
|
+
hello world again
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Node.js
|
|
48
|
+
|
|
49
|
+
```node
|
|
50
|
+
console.log("Hello from Node.js");
|
|
51
|
+
console.log(`Node version: ${process.version}`);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
<!--Result:-->
|
|
55
|
+
```
|
|
56
|
+
Hello from Node.js
|
|
57
|
+
Node version: v22.21.1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Matplotlib
|
|
61
|
+
|
|
62
|
+
```python output=assets/matplotlib-demo.svg
|
|
63
|
+
import matplotlib.pyplot as plt
|
|
64
|
+
import numpy as np
|
|
65
|
+
plt.style.use('dark_background')
|
|
66
|
+
x = np.linspace(0, 4 * np.pi, 200)
|
|
67
|
+
plt.figure(figsize=(8, 4))
|
|
68
|
+
plt.plot(x, np.sin(x), label='sin(x)', linewidth=2)
|
|
69
|
+
plt.plot(x, np.cos(x), label='cos(x)', linewidth=2)
|
|
70
|
+
plt.xlabel('x')
|
|
71
|
+
plt.ylabel('y')
|
|
72
|
+
plt.legend()
|
|
73
|
+
plt.grid(alpha=0.3)
|
|
74
|
+
plt.savefig('{output}', transparent=True)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
<!--Result:-->
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
### Pikchr
|
|
81
|
+
|
|
82
|
+
SQLite's diagram language:
|
|
83
|
+
|
|
84
|
+
```pikchr output=assets/pikchr-demo.svg
|
|
85
|
+
color = white
|
|
86
|
+
fill = none
|
|
87
|
+
linewid = 0.4in
|
|
88
|
+
|
|
89
|
+
# Input file
|
|
90
|
+
In: file "README.md" fit
|
|
91
|
+
arrow
|
|
92
|
+
|
|
93
|
+
# Processing
|
|
94
|
+
Parse: box "Parse" rad 5px fit
|
|
95
|
+
arrow
|
|
96
|
+
Exec: box "Execute" rad 5px fit
|
|
97
|
+
|
|
98
|
+
# Fan out to languages
|
|
99
|
+
arrow from Exec.e right 0.3in then up 0.4in then right 0.3in
|
|
100
|
+
Sh: oval "Shell" fit
|
|
101
|
+
arrow from Exec.e right 0.3in then right 0.3in
|
|
102
|
+
Node: oval "Node" fit
|
|
103
|
+
arrow from Exec.e right 0.3in then down 0.4in then right 0.3in
|
|
104
|
+
Py: oval "Python" fit
|
|
105
|
+
|
|
106
|
+
# Merge back
|
|
107
|
+
arrow from Sh.e right 0.3in then down 0.4in then right 0.3in
|
|
108
|
+
arrow from Node.e right 0.6in
|
|
109
|
+
Out: file "README.md" fit with .w at last arrow.e
|
|
110
|
+
arrow from Py.e right 0.3in then up 0.4in then to Out.w
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
<!--Result:-->
|
|
114
|
+

|
|
115
|
+
|
|
116
|
+
### Asymptote
|
|
117
|
+
|
|
118
|
+
Vector graphics:
|
|
119
|
+
|
|
120
|
+
```asymptote output=assets/histogram.svg
|
|
121
|
+
import graph;
|
|
122
|
+
import stats;
|
|
123
|
+
|
|
124
|
+
size(400,200,IgnoreAspect);
|
|
125
|
+
defaultpen(white);
|
|
126
|
+
|
|
127
|
+
int n=10000;
|
|
128
|
+
real[] a=new real[n];
|
|
129
|
+
for(int i=0; i < n; ++i) a[i]=Gaussrand();
|
|
130
|
+
|
|
131
|
+
draw(graph(Gaussian,min(a),max(a)),orange);
|
|
132
|
+
|
|
133
|
+
int N=bins(a);
|
|
134
|
+
|
|
135
|
+
histogram(a,min(a),max(a),N,normalize=true,low=0,rgb(0.4,0.6,0.8),rgb(0.2,0.4,0.6),bars=true);
|
|
136
|
+
|
|
137
|
+
xaxis("$x$",BottomTop,LeftTicks,p=white);
|
|
138
|
+
yaxis("$dP/dx$",LeftRight,RightTicks(trailingzero),p=white);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
<!--Result:-->
|
|
142
|
+

|
|
143
|
+
|
|
144
|
+
### Graphviz
|
|
145
|
+
|
|
146
|
+
```dot output=assets/graph.svg
|
|
147
|
+
A -> B -> C
|
|
148
|
+
A -> C
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
<!--Result:-->
|
|
152
|
+

|
|
153
|
+
|
|
154
|
+
### OpenSCAD
|
|
155
|
+
|
|
156
|
+
```openscad output=assets/cube-sphere.png
|
|
157
|
+
cube([10, 10, 10]);
|
|
158
|
+
sphere(r=7);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
<!--Result:-->
|
|
162
|
+

|
|
163
|
+
|
|
164
|
+
### Diagon
|
|
165
|
+
|
|
166
|
+
ASCII art diagrams:
|
|
167
|
+
|
|
168
|
+
```diagon mode=Math
|
|
169
|
+
1 + 1/2 + sum(i,0,10)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
<!--Result:-->
|
|
173
|
+
```
|
|
174
|
+
10
|
|
175
|
+
___
|
|
176
|
+
1 ╲
|
|
177
|
+
1 + ─ + ╱ i
|
|
178
|
+
2 ‾‾‾
|
|
179
|
+
0
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```diagon mode=GraphDAG
|
|
183
|
+
A -> B -> C
|
|
184
|
+
A -> C
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
<!--Result:-->
|
|
188
|
+
```
|
|
189
|
+
┌───┐
|
|
190
|
+
│A │
|
|
191
|
+
└┬─┬┘
|
|
192
|
+
│┌▽┐
|
|
193
|
+
││B│
|
|
194
|
+
│└┬┘
|
|
195
|
+
┌▽─▽┐
|
|
196
|
+
│C │
|
|
197
|
+
└───┘
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Install
|
|
201
|
+
|
|
202
|
+
### Nix (recommended)
|
|
203
|
+
|
|
204
|
+
```sh skip
|
|
205
|
+
# Run directly from GitHub
|
|
206
|
+
nix run github:leshy/md-babel-py -- run README.md --stdout
|
|
207
|
+
|
|
208
|
+
# Or clone and run locally
|
|
209
|
+
nix run . -- run README.md --stdout
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Docker
|
|
213
|
+
|
|
214
|
+
```sh skip
|
|
215
|
+
# Pull from Docker Hub
|
|
216
|
+
docker run -v $(pwd):/work lesh/md-babel-py:main run /work/README.md --stdout
|
|
217
|
+
|
|
218
|
+
# Or build locally via Nix
|
|
219
|
+
nix build .#docker # builds tarball to ./result
|
|
220
|
+
docker load < result # loads image from tarball
|
|
221
|
+
docker run -v $(pwd):/work md-babel-py:latest run /work/file.md --stdout
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### pipx
|
|
225
|
+
|
|
226
|
+
```sh skip
|
|
227
|
+
pipx install md-babel-py
|
|
228
|
+
# or: uv pip install md-babel-py
|
|
229
|
+
md-babel-py run README.md --stdout
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
If not using nix or docker, evaluators require system dependencies:
|
|
233
|
+
|
|
234
|
+
| Language | System packages |
|
|
235
|
+
|-----------|-----------------------------|
|
|
236
|
+
| python | python3 |
|
|
237
|
+
| node | nodejs |
|
|
238
|
+
| dot | graphviz |
|
|
239
|
+
| asymptote | asymptote, texlive, dvisvgm |
|
|
240
|
+
| pikchr | pikchr |
|
|
241
|
+
| openscad | openscad, xvfb, imagemagick |
|
|
242
|
+
| diagon | diagon |
|
|
243
|
+
|
|
244
|
+
```sh skip
|
|
245
|
+
# Arch Linux
|
|
246
|
+
sudo pacman -S python nodejs graphviz asymptote texlive-basic openscad xorg-server-xvfb imagemagick
|
|
247
|
+
|
|
248
|
+
# Debian/Ubuntu
|
|
249
|
+
sudo apt-get install python3 nodejs graphviz asymptote texlive xvfb imagemagick openscad
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Note: pikchr and diagon may need to be built from source. Use Docker or Nix for full evaluator support.
|
|
253
|
+
|
|
254
|
+
## Usage
|
|
255
|
+
|
|
256
|
+
```sh skip
|
|
257
|
+
# Edit file in-place
|
|
258
|
+
md-babel-py run document.md
|
|
259
|
+
|
|
260
|
+
# Output to separate file
|
|
261
|
+
md-babel-py run document.md --output result.md
|
|
262
|
+
|
|
263
|
+
# Print to stdout
|
|
264
|
+
md-babel-py run document.md --stdout
|
|
265
|
+
|
|
266
|
+
# Only run specific languages
|
|
267
|
+
md-babel-py run document.md --lang python,sh
|
|
268
|
+
|
|
269
|
+
# Dry run - show what would execute
|
|
270
|
+
md-babel-py run document.md --dry-run
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Code Block Syntax
|
|
274
|
+
|
|
275
|
+
````markdown
|
|
276
|
+
```python session=main
|
|
277
|
+
x = 42
|
|
278
|
+
```
|
|
279
|
+
````
|
|
280
|
+
|
|
281
|
+
### Flags
|
|
282
|
+
|
|
283
|
+
| Flag | Description |
|
|
284
|
+
|------------------|-----------------------------------------------------------|
|
|
285
|
+
| `session=NAME` | Share state with other blocks using the same session name |
|
|
286
|
+
| `output=PATH` | Write output to file (for images/diagrams) |
|
|
287
|
+
| `expected-error` | Expect this block to fail; test fails if it succeeds |
|
|
288
|
+
| `skip` | Don't execute this block |
|
|
289
|
+
| `no-result` | Execute but don't insert result block |
|
|
290
|
+
|
|
291
|
+
### Custom Parameters
|
|
292
|
+
|
|
293
|
+
Any `key=value` pair becomes a parameter for the evaluator command:
|
|
294
|
+
|
|
295
|
+
````markdown
|
|
296
|
+
```diagon mode=GraphDAG
|
|
297
|
+
A -> B
|
|
298
|
+
```
|
|
299
|
+
````
|
|
300
|
+
|
|
301
|
+
With config `"defaultArguments": ["{mode}"]`, the `{mode}` placeholder is replaced with `GraphDAG`.
|
|
302
|
+
|
|
303
|
+
## GitHub Action
|
|
304
|
+
|
|
305
|
+
```yaml skip
|
|
306
|
+
- uses: leshy/md-babel-py@main
|
|
307
|
+
with:
|
|
308
|
+
files: 'README.md docs/*.md'
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
| Input | Description | Default |
|
|
312
|
+
|------------------|-------------------------------------------|----------|
|
|
313
|
+
| `files` | Markdown files to process (glob patterns) | required |
|
|
314
|
+
| `args` | Additional arguments | `''` |
|
|
315
|
+
| `fail-on-change` | Fail if files were modified (CI check) | `false` |
|
|
316
|
+
|
|
317
|
+
Example with auto-commit:
|
|
318
|
+
|
|
319
|
+
```yaml skip
|
|
320
|
+
- uses: leshy/md-babel-py@main
|
|
321
|
+
with:
|
|
322
|
+
files: '*.md docs/**/*.md'
|
|
323
|
+
|
|
324
|
+
- uses: stefanzweifel/git-auto-commit-action@v5
|
|
325
|
+
with:
|
|
326
|
+
commit_message: 'Update markdown code block results'
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Configuration
|
|
330
|
+
|
|
331
|
+
Create `config.json` in your project or `~/.config/md-babel/config.json`:
|
|
332
|
+
|
|
333
|
+
```json skip
|
|
334
|
+
{
|
|
335
|
+
"evaluators": {
|
|
336
|
+
"codeBlock": {
|
|
337
|
+
"python": {
|
|
338
|
+
"path": "/usr/bin/env",
|
|
339
|
+
"defaultArguments": ["python3"],
|
|
340
|
+
"session": {
|
|
341
|
+
"command": ["python3", "-i"],
|
|
342
|
+
"prompts": [">>> ", "... "]
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### File-based Evaluators
|
|
351
|
+
|
|
352
|
+
For tools that use input/output files:
|
|
353
|
+
|
|
354
|
+
```json skip
|
|
355
|
+
{
|
|
356
|
+
"openscad": {
|
|
357
|
+
"path": "xvfb-run",
|
|
358
|
+
"defaultArguments": ["-a", "openscad", "-o", "{output_file}", "{input_file}"],
|
|
359
|
+
"inputExtension": ".scad"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Development
|
|
365
|
+
|
|
366
|
+
### direnv Setup
|
|
367
|
+
|
|
368
|
+
Two `.envrc` files are provided:
|
|
369
|
+
|
|
370
|
+
| File | Description |
|
|
371
|
+
|---------------|-------------------------------------------------|
|
|
372
|
+
| `.envrc.nix` | Nix flake devShell (all evaluators + dev tools) |
|
|
373
|
+
| `.envrc.venv` | Python venv only |
|
|
374
|
+
|
|
375
|
+
```sh skip
|
|
376
|
+
ln -s .envrc.nix .envrc
|
|
377
|
+
direnv allow
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Nix Development Shell
|
|
381
|
+
|
|
382
|
+
```sh skip
|
|
383
|
+
nix develop
|
|
384
|
+
# Provides: md-babel-py, pytest, mypy, ruff, and all evaluators
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Manual Setup
|
|
388
|
+
|
|
389
|
+
```sh skip
|
|
390
|
+
pip install -e ".[dev]"
|
|
391
|
+
pytest tests/ -v
|
|
392
|
+
mypy md_babel_py/
|
|
393
|
+
ruff check md_babel_py/
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Nix Packages
|
|
397
|
+
|
|
398
|
+
| Package | Description |
|
|
399
|
+
|-----------|--------------------------------------------|
|
|
400
|
+
| `default` | Full package with all evaluators in PATH |
|
|
401
|
+
| `minimal` | Just Python package, no bundled evaluators |
|
|
402
|
+
| `docker` | Docker image tarball |
|
|
403
|
+
|
|
404
|
+
## License
|
|
405
|
+
|
|
406
|
+
MIT
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Command-line interface for md-babel-py."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import load_config, get_evaluator, Config
|
|
9
|
+
from .exceptions import ConfigError, MdBabelError
|
|
10
|
+
from .executor import Executor
|
|
11
|
+
from .parser import find_code_blocks, CodeBlock
|
|
12
|
+
from .types import ExecutionResult
|
|
13
|
+
from .writer import apply_results, BlockResult
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> int:
|
|
19
|
+
"""Main entry point for md-babel-py CLI."""
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="md-babel-py",
|
|
22
|
+
description="Execute code blocks in markdown files",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-v", "--verbose",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help="Enable verbose/debug output",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
31
|
+
|
|
32
|
+
# run command
|
|
33
|
+
run_parser = subparsers.add_parser("run", help="Execute code blocks in a markdown file")
|
|
34
|
+
run_parser.add_argument("file", type=Path, help="Markdown file to process")
|
|
35
|
+
run_parser.add_argument("--output", "-o", type=Path, help="Output file (default: edit in-place)")
|
|
36
|
+
run_parser.add_argument("--stdout", action="store_true", help="Print result to stdout instead of writing file")
|
|
37
|
+
run_parser.add_argument("--config", "-c", type=Path, help="Config file path")
|
|
38
|
+
run_parser.add_argument("--lang", help="Only execute these languages (comma-separated)")
|
|
39
|
+
run_parser.add_argument("--dry-run", action="store_true", help="Show what would be executed")
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
# Setup logging
|
|
44
|
+
setup_logging(verbose=args.verbose)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
if args.command == "run":
|
|
48
|
+
return cmd_run(args)
|
|
49
|
+
return 0
|
|
50
|
+
except ConfigError as e:
|
|
51
|
+
logger.error(f"Configuration error: {e}")
|
|
52
|
+
return 1
|
|
53
|
+
except MdBabelError as e:
|
|
54
|
+
logger.error(f"Error: {e}")
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def setup_logging(verbose: bool = False) -> None:
|
|
59
|
+
"""Configure logging for the application.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
verbose: If True, enable DEBUG level logging.
|
|
63
|
+
"""
|
|
64
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
65
|
+
logging.basicConfig(
|
|
66
|
+
level=level,
|
|
67
|
+
format="%(message)s",
|
|
68
|
+
stream=sys.stderr,
|
|
69
|
+
)
|
|
70
|
+
# Quieter format for non-verbose
|
|
71
|
+
if not verbose:
|
|
72
|
+
logging.getLogger("md_babel_py").setLevel(logging.INFO)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_block_flags(block: CodeBlock) -> str:
|
|
76
|
+
"""Format block flags for display.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
block: The code block to format flags for.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A formatted string like " [session=main, expected-error]" or empty string.
|
|
83
|
+
"""
|
|
84
|
+
flags = []
|
|
85
|
+
if block.session:
|
|
86
|
+
flags.append(f"session={block.session}")
|
|
87
|
+
if block.expected_error:
|
|
88
|
+
flags.append("expected-error")
|
|
89
|
+
if block.no_result:
|
|
90
|
+
flags.append("no-result")
|
|
91
|
+
return f" [{', '.join(flags)}]" if flags else ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def filter_blocks(
|
|
95
|
+
blocks: list[CodeBlock],
|
|
96
|
+
config: Config,
|
|
97
|
+
lang_filter: set[str] | None,
|
|
98
|
+
) -> tuple[list[CodeBlock], set[str]]:
|
|
99
|
+
"""Filter blocks by language and configuration.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
blocks: All parsed code blocks.
|
|
103
|
+
config: The loaded configuration.
|
|
104
|
+
lang_filter: Optional set of languages to include.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple of (configured_blocks, unconfigured_languages).
|
|
108
|
+
"""
|
|
109
|
+
# Filter by language if specified
|
|
110
|
+
if lang_filter:
|
|
111
|
+
blocks = [b for b in blocks if b.language in lang_filter]
|
|
112
|
+
|
|
113
|
+
# Separate configured from unconfigured
|
|
114
|
+
unconfigured: set[str] = set()
|
|
115
|
+
configured: list[CodeBlock] = []
|
|
116
|
+
|
|
117
|
+
for block in blocks:
|
|
118
|
+
if get_evaluator(config, block.language):
|
|
119
|
+
configured.append(block)
|
|
120
|
+
else:
|
|
121
|
+
unconfigured.add(block.language)
|
|
122
|
+
|
|
123
|
+
return configured, unconfigured
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def execute_blocks(
|
|
127
|
+
executor: Executor,
|
|
128
|
+
blocks: list[CodeBlock],
|
|
129
|
+
) -> tuple[list[BlockResult], list[str], bool]:
|
|
130
|
+
"""Execute code blocks and collect results.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
executor: The executor instance.
|
|
134
|
+
blocks: The blocks to execute.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Tuple of (results, test_failures, stopped_early).
|
|
138
|
+
"""
|
|
139
|
+
results: list[BlockResult] = []
|
|
140
|
+
test_failures: list[str] = []
|
|
141
|
+
stopped_early = False
|
|
142
|
+
|
|
143
|
+
for i, block in enumerate(blocks, 1):
|
|
144
|
+
flags_str = format_block_flags(block)
|
|
145
|
+
logger.info(f"[{i}/{len(blocks)}] Executing {block.language}{flags_str} block at line {block.start_line}...")
|
|
146
|
+
|
|
147
|
+
result = executor.execute(block)
|
|
148
|
+
|
|
149
|
+
# Only add to results if we want to write output (not no-result)
|
|
150
|
+
if not block.no_result:
|
|
151
|
+
results.append(BlockResult(block=block, result=result))
|
|
152
|
+
|
|
153
|
+
# Check expected-error logic
|
|
154
|
+
if block.expected_error:
|
|
155
|
+
if result.success:
|
|
156
|
+
msg = f"Line {block.start_line}: expected error but block succeeded"
|
|
157
|
+
test_failures.append(msg)
|
|
158
|
+
logger.error(f"FAIL: {msg}")
|
|
159
|
+
# Don't stop on expected errors
|
|
160
|
+
else:
|
|
161
|
+
if not result.success:
|
|
162
|
+
msg = f"Line {block.start_line}: {result.error_message or 'Execution failed'}"
|
|
163
|
+
test_failures.append(msg)
|
|
164
|
+
logger.error(f"Error: {result.error_message or 'Execution failed'}")
|
|
165
|
+
if result.stderr:
|
|
166
|
+
logger.error(result.stderr)
|
|
167
|
+
# Stop on first unexpected error
|
|
168
|
+
stopped_early = True
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
return results, test_failures, stopped_early
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
175
|
+
"""Execute the run command.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
args: Parsed command-line arguments.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Exit code (0 for success, non-zero for failure).
|
|
182
|
+
"""
|
|
183
|
+
# Load config
|
|
184
|
+
config = load_config(args.config)
|
|
185
|
+
|
|
186
|
+
# Read input file
|
|
187
|
+
if not args.file.exists():
|
|
188
|
+
logger.error(f"Error: File not found: {args.file}")
|
|
189
|
+
return 1
|
|
190
|
+
|
|
191
|
+
content = args.file.read_text()
|
|
192
|
+
|
|
193
|
+
# Parse code blocks
|
|
194
|
+
blocks = find_code_blocks(content)
|
|
195
|
+
|
|
196
|
+
if not blocks:
|
|
197
|
+
logger.info("No code blocks found.")
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
# Parse language filter
|
|
201
|
+
lang_filter = set(args.lang.split(",")) if args.lang else None
|
|
202
|
+
|
|
203
|
+
# Filter blocks
|
|
204
|
+
configured_blocks, unconfigured = filter_blocks(blocks, config, lang_filter)
|
|
205
|
+
|
|
206
|
+
if unconfigured:
|
|
207
|
+
logger.warning(f"Warning: No evaluators configured for: {', '.join(sorted(unconfigured))}")
|
|
208
|
+
|
|
209
|
+
if not configured_blocks:
|
|
210
|
+
logger.info("No executable code blocks found.")
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
# Filter out skipped blocks
|
|
214
|
+
executable_blocks = [b for b in configured_blocks if not b.skip]
|
|
215
|
+
skipped_count = len(configured_blocks) - len(executable_blocks)
|
|
216
|
+
|
|
217
|
+
# Dry run - just show what would execute
|
|
218
|
+
if args.dry_run:
|
|
219
|
+
logger.info(f"Would execute {len(executable_blocks)} code block(s):\n")
|
|
220
|
+
for i, block in enumerate(executable_blocks, 1):
|
|
221
|
+
flags_str = format_block_flags(block)
|
|
222
|
+
logger.info(f"{i}. {block.language}{flags_str} (lines {block.start_line}-{block.end_line})")
|
|
223
|
+
logger.info(f" {block.code[:50]}{'...' if len(block.code) > 50 else ''}")
|
|
224
|
+
logger.info("")
|
|
225
|
+
if skipped_count:
|
|
226
|
+
logger.info(f"({skipped_count} block(s) marked as skip)")
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
# Execute blocks
|
|
230
|
+
executor = Executor(config)
|
|
231
|
+
try:
|
|
232
|
+
results, test_failures, _ = execute_blocks(executor, executable_blocks)
|
|
233
|
+
finally:
|
|
234
|
+
executor.cleanup()
|
|
235
|
+
|
|
236
|
+
# Apply results to content
|
|
237
|
+
new_content = apply_results(content, results)
|
|
238
|
+
|
|
239
|
+
# Write output
|
|
240
|
+
if args.stdout:
|
|
241
|
+
print(new_content)
|
|
242
|
+
else:
|
|
243
|
+
output_path = args.output or args.file
|
|
244
|
+
output_path.write_text(new_content)
|
|
245
|
+
|
|
246
|
+
success_count = sum(1 for r in results if r.result.success)
|
|
247
|
+
logger.info(f"\nDone: {success_count}/{len(results)} blocks executed successfully.")
|
|
248
|
+
|
|
249
|
+
if not args.stdout and args.output and args.output != args.file:
|
|
250
|
+
logger.info(f"Output written to: {args.output}")
|
|
251
|
+
|
|
252
|
+
if test_failures:
|
|
253
|
+
logger.error(f"\n{len(test_failures)} test failure(s):")
|
|
254
|
+
for f in test_failures:
|
|
255
|
+
logger.error(f" - {f}")
|
|
256
|
+
return 1
|
|
257
|
+
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
sys.exit(main())
|