sphinx-examples-as-code 0.6.1__tar.gz → 0.8.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.
Files changed (43) hide show
  1. sphinx_examples_as_code-0.8.0/.github/dependabot.yml +22 -0
  2. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/.github/workflows/ci.yml +20 -4
  3. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/PKG-INFO +108 -29
  4. sphinx_examples_as_code-0.8.0/README.md +252 -0
  5. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/pyproject.toml +3 -1
  6. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code/__init__.py +490 -57
  7. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code/_version.py +3 -3
  8. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/PKG-INFO +108 -29
  9. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/requires.txt +4 -0
  10. sphinx_examples_as_code-0.8.0/sphinx_examples_as_code.egg-info/scm_version.json +8 -0
  11. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/test_gallery_downloads.py +84 -11
  12. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/test_heading_level_reuse.py +7 -7
  13. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/test_single_function_page.py +5 -5
  14. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/test_sphinx_examples_as_code.py +735 -127
  15. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/test_tinypages.py +79 -67
  16. sphinx_examples_as_code-0.6.1/.github/dependabot.yml +0 -16
  17. sphinx_examples_as_code-0.6.1/README.md +0 -176
  18. sphinx_examples_as_code-0.6.1/sphinx_examples_as_code.egg-info/scm_version.json +0 -8
  19. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/.github/release.yml +0 -0
  20. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/.gitignore +0 -0
  21. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/.pre-commit-config.yaml +0 -0
  22. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/LICENSE +0 -0
  23. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/setup.cfg +0 -0
  24. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/SOURCES.txt +0 -0
  25. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/dependency_links.txt +0 -0
  26. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/scm_file_list.json +0 -0
  27. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/top_level.txt +0 -0
  28. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/conf.py +0 -0
  29. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/examples/GALLERY_HEADER.rst +0 -0
  30. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/examples/plot_minimal.py +0 -0
  31. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/index.rst +0 -0
  32. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/conf.py +0 -0
  33. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/examples/GALLERY_HEADER.rst +0 -0
  34. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/examples/plot_reused_level.py +0 -0
  35. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/examples/plot_three_levels.py +0 -0
  36. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/index.rst +0 -0
  37. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/single_function_fixture/conf.py +0 -0
  38. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/single_function_fixture/index.rst +0 -0
  39. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/single_function_fixture/mymodule.py +0 -0
  40. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/tinypages/conf.py +0 -0
  41. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/tinypages/docstring_cases.py +0 -0
  42. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/tinypages/docstring_cases.rst +0 -0
  43. {sphinx_examples_as_code-0.6.1 → sphinx_examples_as_code-0.8.0}/tests/tinypages/index.rst +0 -0
@@ -0,0 +1,22 @@
1
+ version: 2
2
+ multi-ecosystem-groups:
3
+ maintenance:
4
+ schedule:
5
+ interval: quarterly
6
+ labels: [maintenance, dependencies]
7
+ updates:
8
+ - package-ecosystem: pip
9
+ directory: /
10
+ schedule:
11
+ interval: weekly
12
+ labels: [maintenance, dependencies]
13
+ allow:
14
+ - dependency-name: pyvista
15
+ - dependency-name: sphinx-gallery
16
+ groups:
17
+ project:
18
+ patterns: ['*']
19
+ - package-ecosystem: github-actions
20
+ directory: /
21
+ patterns: ['*']
22
+ multi-ecosystem-group: maintenance
@@ -22,7 +22,7 @@ jobs:
22
22
  with:
23
23
  persist-credentials: false
24
24
  fetch-depth: 0
25
- - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
25
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
26
26
  with:
27
27
  enable-cache: false
28
28
  - run: uv sync --group dev --extra gallery
@@ -43,7 +43,7 @@ jobs:
43
43
  with:
44
44
  persist-credentials: false
45
45
  fetch-depth: 0
46
- - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
46
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
47
47
  with:
48
48
  enable-cache: false
49
49
  - uses: pyvista/setup-headless-display-action@c103a2ff45650d38cb71684b5dc6cdfeb9442c79 # v5.1.0
@@ -51,7 +51,7 @@ jobs:
51
51
  pyvista: true
52
52
  - run: uv sync --group dev --extra gallery --python ${{ matrix.python-version }}
53
53
  - run: uv run pytest tests/ --cov --cov-report=xml
54
- - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
54
+ - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
55
55
  with:
56
56
  files: ./coverage.xml
57
57
  env:
@@ -65,7 +65,7 @@ jobs:
65
65
  with:
66
66
  persist-credentials: false
67
67
  fetch-depth: 0
68
- - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
68
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
69
69
  with:
70
70
  enable-cache: false
71
71
  - run: uv build
@@ -87,3 +87,19 @@ jobs:
87
87
  name: dist
88
88
  path: dist/
89
89
  - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
90
+
91
+ release:
92
+ needs: publish
93
+ runs-on: ubuntu-latest
94
+ permissions:
95
+ contents: write
96
+ steps:
97
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
98
+ with:
99
+ name: dist
100
+ path: dist/
101
+ # Notes come from the merged pull requests, shaped by .github/release.yml.
102
+ - env:
103
+ GH_TOKEN: ${{ github.token }}
104
+ TAG: ${{ github.ref_name }}
105
+ run: gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --title "$TAG" --generate-notes --verify-tag dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-examples-as-code
3
- Version: 0.6.1
3
+ Version: 0.8.0
4
4
  Summary: Sphinx extension for converting docstring examples into downloadable code.
5
5
  Author-email: The PyVista Developers <info@pyvista.org>
6
6
  License-Expression: MIT
@@ -22,15 +22,19 @@ Classifier: Topic :: Scientific/Engineering :: Visualization
22
22
  Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
+ Requires-Dist: packaging>=22
25
26
  Requires-Dist: pyvista>=0.48
26
27
  Provides-Extra: gallery
27
28
  Requires-Dist: sphinx-gallery<0.23,>=0.22; extra == "gallery"
29
+ Provides-Extra: marimo
30
+ Requires-Dist: marimo>=0.14; extra == "marimo"
28
31
  Dynamic: license-file
29
32
 
30
33
  # sphinx-examples-as-code
31
34
 
32
35
  A Sphinx extension that turns docstring/page "Examples" sections into downloadable,
33
- runnable `.py` and/or `.ipynb` files, with a download link inserted into the section.
36
+ runnable Python scripts, Jupyter notebooks and/or [marimo](https://marimo.io) notebooks,
37
+ with a download link inserted into the section.
34
38
 
35
39
  Pages or docstrings without an Examples section are left completely untouched. Adding
36
40
  `sphinx_examples_as_code` to `conf.py`'s `extensions` is the only on/off switch.
@@ -48,6 +52,12 @@ Sphinx-Gallery integration is opt-in and needs a compatible sphinx-gallery, whic
48
52
  pip install 'sphinx-examples-as-code[gallery]'
49
53
  ```
50
54
 
55
+ marimo notebook downloads need marimo 0.14 or newer, which the `marimo` extra installs:
56
+
57
+ ```bash
58
+ pip install 'sphinx-examples-as-code[marimo]'
59
+ ```
60
+
51
61
  Add it to your Sphinx `conf.py`:
52
62
 
53
63
  ```python
@@ -65,23 +75,32 @@ the keys you want to change from their default:
65
75
  ```python
66
76
  sphinx_examples_as_code_conf = {
67
77
  'link_position': 'top',
68
- 'formats': ['py', 'ipynb'],
78
+ 'formats': ['python', 'jupyter', 'marimo'],
69
79
  'gallery_downloads': False,
70
80
  'footer': (
71
81
  'Generated by `sphinx-examples-as-code '
72
82
  '<https://github.com/pyvista/sphinx-examples-as-code>`_'
73
83
  ),
74
84
  'link_labels': {
75
- 'py': 'Download Python source code',
76
- 'ipynb': 'Download Jupyter notebook',
85
+ 'python': 'Download Python script',
86
+ 'jupyter': 'Download Jupyter notebook',
87
+ 'marimo': 'Download marimo notebook',
77
88
  },
89
+ 'script_metadata': False,
90
+ 'dependencies': {},
91
+ 'requires_python': None,
78
92
  }
79
93
  ```
80
94
 
81
95
  - `link_position`: where the download link(s) land within the Examples section. `'top'`
82
96
  (default) or `'bottom'`.
83
- - `formats`: which downloads to generate. A list containing `'py'`, `'ipynb'`, or both
84
- (default). Always offered in that order regardless of how the list is written.
97
+ - `formats`: which downloads to generate. A list containing any of `'python'` (a plain
98
+ Python script, `<name>.py`), `'jupyter'` (a Jupyter notebook, `<name>.ipynb`) and
99
+ `'marimo'` (a marimo notebook, `<name>.marimo.py`); defaults to all three. Always
100
+ offered in that order regardless of how the list is written. Without marimo 0.14 or
101
+ newer installed, the default skips the marimo download, and a `formats` that names
102
+ `'marimo'` (in `conf.py` or through `-D`) stops the build with a configuration error. An unknown format raises a configuration error too;
103
+ the old `'py'` and `'ipynb'` names say what replaced them.
85
104
  - `link_labels`: the text of the download link(s) themselves, per format. Set only the
86
105
  format(s) you want to change; any left unset keep reading their own default shown above.
87
106
  - `gallery_downloads`: opt-in takeover of
@@ -91,11 +110,22 @@ sphinx_examples_as_code_conf = {
91
110
  - `footer`: a string appended to the end of every generated file. Defaults to a one-line
92
111
  "generated file" credit linking back to this project; set to `None` to omit it
93
112
  entirely. Preceded by a blank line and a `-`-only divider line, which also renders as a
94
- real horizontal rule in `.ipynb`; the footer always gets its own dedicated cell there.
113
+ real horizontal rule in notebooks; the footer always gets its own dedicated cell there.
95
114
  Parsed as RST: a hyperlink written as `` `text <url>`_ `` becomes a real clickable
96
- Markdown link in `.ipynb`, and renders inline as `text url` in `.py`. Plain text with no
97
- markup at all becomes one comment line per line of text, and blank-line-separated
98
- paragraphs stay separated.
115
+ Markdown link in notebooks, and renders inline as `text url` in Python scripts. Plain
116
+ text with no markup at all becomes one comment line per line of text, and
117
+ blank-line-separated paragraphs stay separated.
118
+ - `script_metadata`: `True` adds each download's dependencies and a one-line run
119
+ command. See [Dependencies and run commands](#dependencies-and-run-commands) below.
120
+ `False` (default) adds neither.
121
+ - `dependencies`: extra PEP 508 requirements per format, added to the detected ones, e.g.
122
+ `{'jupyter': ['pyvista[jupyter]']}`. An entry naming a detected project replaces it,
123
+ so `{'python': ['pyvista>=0.46']}` sets its version. An entry without a version keeps
124
+ the automatic pin described below. Only used with `script_metadata` on.
125
+ - `requires_python`: a `requires-python` specifier for the dependency block, e.g.
126
+ `'>=3.10'`. `None` (default) derives one from the dependencies instead; see
127
+ [Dependencies and run commands](#dependencies-and-run-commands). Only used with
128
+ `script_metadata` on.
99
129
 
100
130
  An unrecognized key raises a configuration error at build start.
101
131
 
@@ -107,6 +137,46 @@ Overriding a single key from the command line works via a dotted `-D` flag, e.g.
107
137
  `-D sphinx_examples_as_code_conf.link_position=bottom` -- this only touches that one
108
138
  key, leaving the rest (`conf.py`'s values, or the defaults) alone.
109
139
 
140
+ ## Dependencies and run commands
141
+
142
+ With `script_metadata` on, each download lists the third-party projects its code
143
+ imports, plus any `dependencies` set for its format, and says how to run it:
144
+
145
+ - Python scripts start with a [PEP 723](https://peps.python.org/pep-0723/) `# /// script`
146
+ block, followed by `# Run this script with: uv run <name>.py`.
147
+ - Jupyter notebooks say `` Open this notebook with: `uvx --python "<requires-python>" --from jupyterlab --with <dependency> ... jupyter-lab <name>.ipynb` `` in their first cell,
148
+ with `--python` only when there is a `requires-python`. Arguments are double-quoted where
149
+ needed, so the command works in bash, zsh, PowerShell and cmd. An argument containing
150
+ `"`, `$`, a backtick, `\`, `%` or a `!` not followed by `=` is single-quoted instead,
151
+ which cmd does not understand.
152
+ - marimo notebooks start with the same `# /// script` block, which also lists `marimo`,
153
+ followed by `# Open this notebook with: uvx marimo edit --sandbox <name>.marimo.py`.
154
+ - Each run command is followed by a line linking to
155
+ [uv's installation guide](https://docs.astral.sh/uv/getting-started/installation/).
156
+
157
+ Imports are read from the code, not run. Each imported top-level module maps to the
158
+ project that provides it in the environment building the docs; standard-library
159
+ modules, relative imports and modules no single installed project provides are left
160
+ out. Projects the code needs but never imports, such as an optional backend, go in
161
+ `dependencies`.
162
+
163
+ The project named by Sphinx's own `project` setting (e.g. `project = 'PyVista'`) gets
164
+ extra treatment when an example imports it and it is installed: its dependency becomes
165
+ `<name>>=X.Y`, the installed release's major and minor version, or plain `<name>` for a
166
+ pre-release or dev build.
167
+
168
+ Unless `requires_python` is set, the block's `requires-python` combines:
169
+
170
+ - that project's own `Requires-Python`, e.g. `>=3.10`, when it is listed;
171
+ - `<3.N`, one minor version above the newest Python named in the `Programming Language ::
172
+ Python :: 3.X` classifiers of that project and of every listed dependency installed
173
+ from a platform-specific wheel, as installed in the environment building the docs.
174
+ Pure-Python dependencies and dependencies without such classifiers don't count, and
175
+ the bound is dropped when it would exclude every Python the project's own
176
+ `Requires-Python` allows.
177
+
178
+ With neither, the line is left out.
179
+
110
180
  ## Conversion rules
111
181
 
112
182
  What happens to the content of an Examples section:
@@ -116,39 +186,43 @@ What happens to the content of an Examples section:
116
186
  - `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
117
187
  comments, set off with blank lines on both sides like any other directive.
118
188
  - Admonitions (`.. note::`, `.. warning::`, ...) become a `# LABEL:` comment followed by
119
- their content as comments, indented one level under the label in `.py` only.
189
+ their content as comments, indented one level under the label in Python scripts only.
120
190
  - A bullet/numbered list becomes a `-`/`N.`-marked line per item, set off with a blank
121
191
  line on both sides in either format — a bare `#` instead of a real blank line when
122
- that falls inside an admonition or a definition's own body in `.py`, so the whole
123
- thing still reads as one unbroken comment block. A definition list's term stays at
124
- the surrounding indent; its definition (the body nested under it) is indented one
125
- level further in `.py`, same as an admonition's own content.
192
+ that falls inside an admonition or a definition's own body in a Python script, so the
193
+ whole thing still reads as one unbroken comment block. A definition list's term stays
194
+ at the surrounding indent; its definition (the body nested under it) is indented one
195
+ level further in Python scripts, same as an admonition's own content.
126
196
  - Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
127
197
  double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
128
198
  ``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
129
- the reference resolves, `.ipynb` turns it into a clickable link; `.py` never writes the
130
- link, only the display text.
199
+ the reference resolves, notebooks turn it into a clickable link; Python scripts never
200
+ write the link, only the display text.
131
201
  - Plain prose-style references (`:ref:`, `:doc:`) are treated the same way, minus the
132
202
  backticks.
133
203
  - Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
134
204
  `#` comment.
135
205
  - A table becomes an aligned text table, its caption on the line above: an RST simple
136
- table in `.py`, a Markdown pipe table in `.ipynb`. Each cell is flattened to a single
137
- line, and a column left entirely empty -- one holding only images, say -- is dropped
138
- along with its heading.
206
+ table in Python scripts, a Markdown pipe table in notebooks. Each cell is flattened to a
207
+ single line, and a column left entirely empty -- one holding only images, say -- is
208
+ dropped along with its heading.
139
209
  - Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
140
210
  line are dropped entirely.
141
211
  - "See Also" content is always dropped.
142
212
 
143
- Generated `.py` files start with a `# Examples from <qualified name>` title header
213
+ Generated Python scripts start with a `# Examples from <qualified name>` title header
144
214
  (gallery mode uses the page's own title instead -- see below), with a few whitespace
145
215
  conventions: prose directly above a code block stays attached to it, a code block is
146
216
  always followed by a blank line, and a directive (header, `# NOTE:`-style block) gets
147
217
  blank lines on both sides.
148
218
 
149
- Generated `.ipynb` notebooks use the same content, split into alternating code/markdown
219
+ Generated Jupyter notebooks use the same content, split into alternating code/markdown
150
220
  cells instead.
151
221
 
222
+ Generated marimo notebooks are that same Jupyter notebook converted by marimo's own
223
+ converter, the one behind `marimo convert`. `from __future__` imports are dropped first,
224
+ so the notebook also runs as a plain script (`python <name>.marimo.py`).
225
+
152
226
  A download link is only added if the resulting code contains at least one real
153
227
  executable statement.
154
228
 
@@ -157,7 +231,7 @@ executable statement.
157
231
  With `sphinx_examples_as_code_conf['gallery_downloads'] = True`, this extension takes
158
232
  over the downloads on every page generated by
159
233
  [sphinx-gallery](https://sphinx-gallery.github.io): the "Go to the end to download the
160
- full example code" note and the `.py`/`.ipynb`/`.zip` download footer are removed from
234
+ full example code" note and the `.py`/`.zip` download footer are removed from
161
235
  the page, replaced with download link(s) built by this extension instead — same
162
236
  conversion rules as above, applied to the whole page rather than one Examples section.
163
237
  The generated file's header uses the page's own title (e.g. `# Create Circular Arcs`)
@@ -172,7 +246,7 @@ is left completely untouched. Gallery pages and ordinary docstring/prose pages (
172
246
  the Examples-section behavior above) can coexist on the same site.
173
247
 
174
248
  A `# %%` cell with its own RST heading gets the same header treatment as the file's own
175
- title, and renders as a real Markdown heading in `.ipynb`. Level is relative to actual
249
+ title, and renders as a real Markdown heading in notebooks. Level is relative to actual
176
250
  RST section nesting, not always one below the file's own header: a cell heading nested
177
251
  under the page's own title (the common case) is one level below it; one that reuses the
178
252
  page title's own underline character is level 1, the same as the file's own header; a
@@ -181,10 +255,10 @@ cell heading nested under *that* is level 2 relative to it, and so on.
181
255
  Each format uses one heading style consistently across every level, rather than mixing
182
256
  styles:
183
257
 
184
- - `.py` uses an RST-style title + underline, one character per level -- the same
258
+ - Python scripts use an RST-style title + underline, one character per level -- the same
185
259
  sequence Sphinx's own documentation uses for sections through sub-paragraphs: `=`, `-`,
186
260
  `~`, `^`, `"`, `'` for levels 1 through 6.
187
- - `.ipynb` always uses ATX syntax (`#`, `##`, `###`, ...) at every level.
261
+ - Notebooks always use ATX syntax (`#`, `##`, `###`, ...) at every level.
188
262
 
189
263
  Two things worth knowing before turning this on:
190
264
 
@@ -193,8 +267,13 @@ Two things worth knowing before turning this on:
193
267
  block — not a documented extension API. A future sphinx-gallery release could shift
194
268
  that structure without warning. Only sphinx-gallery 0.22.x is tested, so the `gallery`
195
269
  extra caps there rather than letting the next release quietly produce no downloads.
196
- - sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
197
- `_downloads/`, even though nothing on the page links to them anymore.
270
+ - sphinx-gallery's own `.py`/`.zip` downloads still end up copied into `_downloads/`,
271
+ even though nothing on the page links to them anymore. Its `.ipynb` is never written:
272
+ with `gallery_downloads` on, this extension sets `sphinx_gallery_conf['pypandoc']` to
273
+ `False` and `sphinx_gallery_conf['notebook_extensions']` to an empty set before
274
+ sphinx-gallery reads them, so pandoc is not needed. Setting `pypandoc`,
275
+ `notebook_extensions`, `first_notebook_cell`, `last_notebook_cell` or `notebook_images`
276
+ yourself has no effect and is reported as a warning.
198
277
 
199
278
  ## Development
200
279
 
@@ -0,0 +1,252 @@
1
+ # sphinx-examples-as-code
2
+
3
+ A Sphinx extension that turns docstring/page "Examples" sections into downloadable,
4
+ runnable Python scripts, Jupyter notebooks and/or [marimo](https://marimo.io) notebooks,
5
+ with a download link inserted into the section.
6
+
7
+ Pages or docstrings without an Examples section are left completely untouched. Adding
8
+ `sphinx_examples_as_code` to `conf.py`'s `extensions` is the only on/off switch.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install sphinx-examples-as-code
14
+ ```
15
+
16
+ Sphinx-Gallery integration is opt-in and needs a compatible sphinx-gallery, which the
17
+ `gallery` extra pins:
18
+
19
+ ```bash
20
+ pip install 'sphinx-examples-as-code[gallery]'
21
+ ```
22
+
23
+ marimo notebook downloads need marimo 0.14 or newer, which the `marimo` extra installs:
24
+
25
+ ```bash
26
+ pip install 'sphinx-examples-as-code[marimo]'
27
+ ```
28
+
29
+ Add it to your Sphinx `conf.py`:
30
+
31
+ ```python
32
+ extensions = [
33
+ ...,
34
+ 'sphinx_examples_as_code',
35
+ ]
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ Everything lives in one dict in `conf.py`, `sphinx_examples_as_code_conf` -- set only
41
+ the keys you want to change from their default:
42
+
43
+ ```python
44
+ sphinx_examples_as_code_conf = {
45
+ 'link_position': 'top',
46
+ 'formats': ['python', 'jupyter', 'marimo'],
47
+ 'gallery_downloads': False,
48
+ 'footer': (
49
+ 'Generated by `sphinx-examples-as-code '
50
+ '<https://github.com/pyvista/sphinx-examples-as-code>`_'
51
+ ),
52
+ 'link_labels': {
53
+ 'python': 'Download Python script',
54
+ 'jupyter': 'Download Jupyter notebook',
55
+ 'marimo': 'Download marimo notebook',
56
+ },
57
+ 'script_metadata': False,
58
+ 'dependencies': {},
59
+ 'requires_python': None,
60
+ }
61
+ ```
62
+
63
+ - `link_position`: where the download link(s) land within the Examples section. `'top'`
64
+ (default) or `'bottom'`.
65
+ - `formats`: which downloads to generate. A list containing any of `'python'` (a plain
66
+ Python script, `<name>.py`), `'jupyter'` (a Jupyter notebook, `<name>.ipynb`) and
67
+ `'marimo'` (a marimo notebook, `<name>.marimo.py`); defaults to all three. Always
68
+ offered in that order regardless of how the list is written. Without marimo 0.14 or
69
+ newer installed, the default skips the marimo download, and a `formats` that names
70
+ `'marimo'` (in `conf.py` or through `-D`) stops the build with a configuration error. An unknown format raises a configuration error too;
71
+ the old `'py'` and `'ipynb'` names say what replaced them.
72
+ - `link_labels`: the text of the download link(s) themselves, per format. Set only the
73
+ format(s) you want to change; any left unset keep reading their own default shown above.
74
+ - `gallery_downloads`: opt-in takeover of
75
+ [sphinx-gallery](https://sphinx-gallery.github.io)'s own per-example downloads.
76
+ `False` (default) leaves sphinx-gallery pages untouched. See
77
+ [Sphinx-Gallery integration](#sphinx-gallery-integration) below.
78
+ - `footer`: a string appended to the end of every generated file. Defaults to a one-line
79
+ "generated file" credit linking back to this project; set to `None` to omit it
80
+ entirely. Preceded by a blank line and a `-`-only divider line, which also renders as a
81
+ real horizontal rule in notebooks; the footer always gets its own dedicated cell there.
82
+ Parsed as RST: a hyperlink written as `` `text <url>`_ `` becomes a real clickable
83
+ Markdown link in notebooks, and renders inline as `text url` in Python scripts. Plain
84
+ text with no markup at all becomes one comment line per line of text, and
85
+ blank-line-separated paragraphs stay separated.
86
+ - `script_metadata`: `True` adds each download's dependencies and a one-line run
87
+ command. See [Dependencies and run commands](#dependencies-and-run-commands) below.
88
+ `False` (default) adds neither.
89
+ - `dependencies`: extra PEP 508 requirements per format, added to the detected ones, e.g.
90
+ `{'jupyter': ['pyvista[jupyter]']}`. An entry naming a detected project replaces it,
91
+ so `{'python': ['pyvista>=0.46']}` sets its version. An entry without a version keeps
92
+ the automatic pin described below. Only used with `script_metadata` on.
93
+ - `requires_python`: a `requires-python` specifier for the dependency block, e.g.
94
+ `'>=3.10'`. `None` (default) derives one from the dependencies instead; see
95
+ [Dependencies and run commands](#dependencies-and-run-commands). Only used with
96
+ `script_metadata` on.
97
+
98
+ An unrecognized key raises a configuration error at build start.
99
+
100
+ Cross-references and hyperlinks resolve into absolute links using Sphinx's own
101
+ [`html_baseurl`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_baseurl).
102
+ Leave it unset and no links are generated anywhere.
103
+
104
+ Overriding a single key from the command line works via a dotted `-D` flag, e.g.
105
+ `-D sphinx_examples_as_code_conf.link_position=bottom` -- this only touches that one
106
+ key, leaving the rest (`conf.py`'s values, or the defaults) alone.
107
+
108
+ ## Dependencies and run commands
109
+
110
+ With `script_metadata` on, each download lists the third-party projects its code
111
+ imports, plus any `dependencies` set for its format, and says how to run it:
112
+
113
+ - Python scripts start with a [PEP 723](https://peps.python.org/pep-0723/) `# /// script`
114
+ block, followed by `# Run this script with: uv run <name>.py`.
115
+ - Jupyter notebooks say `` Open this notebook with: `uvx --python "<requires-python>" --from jupyterlab --with <dependency> ... jupyter-lab <name>.ipynb` `` in their first cell,
116
+ with `--python` only when there is a `requires-python`. Arguments are double-quoted where
117
+ needed, so the command works in bash, zsh, PowerShell and cmd. An argument containing
118
+ `"`, `$`, a backtick, `\`, `%` or a `!` not followed by `=` is single-quoted instead,
119
+ which cmd does not understand.
120
+ - marimo notebooks start with the same `# /// script` block, which also lists `marimo`,
121
+ followed by `# Open this notebook with: uvx marimo edit --sandbox <name>.marimo.py`.
122
+ - Each run command is followed by a line linking to
123
+ [uv's installation guide](https://docs.astral.sh/uv/getting-started/installation/).
124
+
125
+ Imports are read from the code, not run. Each imported top-level module maps to the
126
+ project that provides it in the environment building the docs; standard-library
127
+ modules, relative imports and modules no single installed project provides are left
128
+ out. Projects the code needs but never imports, such as an optional backend, go in
129
+ `dependencies`.
130
+
131
+ The project named by Sphinx's own `project` setting (e.g. `project = 'PyVista'`) gets
132
+ extra treatment when an example imports it and it is installed: its dependency becomes
133
+ `<name>>=X.Y`, the installed release's major and minor version, or plain `<name>` for a
134
+ pre-release or dev build.
135
+
136
+ Unless `requires_python` is set, the block's `requires-python` combines:
137
+
138
+ - that project's own `Requires-Python`, e.g. `>=3.10`, when it is listed;
139
+ - `<3.N`, one minor version above the newest Python named in the `Programming Language ::
140
+ Python :: 3.X` classifiers of that project and of every listed dependency installed
141
+ from a platform-specific wheel, as installed in the environment building the docs.
142
+ Pure-Python dependencies and dependencies without such classifiers don't count, and
143
+ the bound is dropped when it would exclude every Python the project's own
144
+ `Requires-Python` allows.
145
+
146
+ With neither, the line is left out.
147
+
148
+ ## Conversion rules
149
+
150
+ What happens to the content of an Examples section:
151
+
152
+ - Doctest blocks (`>>> ...` / `... ...`) keep their input lines, prompts stripped, as
153
+ real Python source. Doctest *output* lines are dropped — only the input code matters.
154
+ - `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
155
+ comments, set off with blank lines on both sides like any other directive.
156
+ - Admonitions (`.. note::`, `.. warning::`, ...) become a `# LABEL:` comment followed by
157
+ their content as comments, indented one level under the label in Python scripts only.
158
+ - A bullet/numbered list becomes a `-`/`N.`-marked line per item, set off with a blank
159
+ line on both sides in either format — a bare `#` instead of a real blank line when
160
+ that falls inside an admonition or a definition's own body in a Python script, so the
161
+ whole thing still reads as one unbroken comment block. A definition list's term stays
162
+ at the surrounding indent; its definition (the body nested under it) is indented one
163
+ level further in Python scripts, same as an admonition's own content.
164
+ - Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
165
+ double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
166
+ ``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
167
+ the reference resolves, notebooks turn it into a clickable link; Python scripts never
168
+ write the link, only the display text.
169
+ - Plain prose-style references (`:ref:`, `:doc:`) are treated the same way, minus the
170
+ backticks.
171
+ - Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
172
+ `#` comment.
173
+ - A table becomes an aligned text table, its caption on the line above: an RST simple
174
+ table in Python scripts, a Markdown pipe table in notebooks. Each cell is flattened to a
175
+ single line, and a column left entirely empty -- one holding only images, say -- is
176
+ dropped along with its heading.
177
+ - Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
178
+ line are dropped entirely.
179
+ - "See Also" content is always dropped.
180
+
181
+ Generated Python scripts start with a `# Examples from <qualified name>` title header
182
+ (gallery mode uses the page's own title instead -- see below), with a few whitespace
183
+ conventions: prose directly above a code block stays attached to it, a code block is
184
+ always followed by a blank line, and a directive (header, `# NOTE:`-style block) gets
185
+ blank lines on both sides.
186
+
187
+ Generated Jupyter notebooks use the same content, split into alternating code/markdown
188
+ cells instead.
189
+
190
+ Generated marimo notebooks are that same Jupyter notebook converted by marimo's own
191
+ converter, the one behind `marimo convert`. `from __future__` imports are dropped first,
192
+ so the notebook also runs as a plain script (`python <name>.marimo.py`).
193
+
194
+ A download link is only added if the resulting code contains at least one real
195
+ executable statement.
196
+
197
+ ## Sphinx-Gallery integration
198
+
199
+ With `sphinx_examples_as_code_conf['gallery_downloads'] = True`, this extension takes
200
+ over the downloads on every page generated by
201
+ [sphinx-gallery](https://sphinx-gallery.github.io): the "Go to the end to download the
202
+ full example code" note and the `.py`/`.zip` download footer are removed from
203
+ the page, replaced with download link(s) built by this extension instead — same
204
+ conversion rules as above, applied to the whole page rather than one Examples section.
205
+ The generated file's header uses the page's own title (e.g. `# Create Circular Arcs`)
206
+ rather than the generic `# Examples from <docname>`.
207
+
208
+ The "Total running time" line and the "Gallery generated by Sphinx-Gallery" credit line
209
+ stay on the rendered page untouched, but never make it into the generated download
210
+ itself.
211
+
212
+ Detection is automatic and per-page — any page without a sphinx-gallery download footer
213
+ is left completely untouched. Gallery pages and ordinary docstring/prose pages (using
214
+ the Examples-section behavior above) can coexist on the same site.
215
+
216
+ A `# %%` cell with its own RST heading gets the same header treatment as the file's own
217
+ title, and renders as a real Markdown heading in notebooks. Level is relative to actual
218
+ RST section nesting, not always one below the file's own header: a cell heading nested
219
+ under the page's own title (the common case) is one level below it; one that reuses the
220
+ page title's own underline character is level 1, the same as the file's own header; a
221
+ cell heading nested under *that* is level 2 relative to it, and so on.
222
+
223
+ Each format uses one heading style consistently across every level, rather than mixing
224
+ styles:
225
+
226
+ - Python scripts use an RST-style title + underline, one character per level -- the same
227
+ sequence Sphinx's own documentation uses for sections through sub-paragraphs: `=`, `-`,
228
+ `~`, `^`, `"`, `'` for levels 1 through 6.
229
+ - Notebooks always use ATX syntax (`#`, `##`, `###`, ...) at every level.
230
+
231
+ Two things worth knowing before turning this on:
232
+
233
+ - It's built against sphinx-gallery's own doctree output — the `sphx-glr-*` CSS classes
234
+ its own theming depends on, and the private node type holding a highlighted code
235
+ block — not a documented extension API. A future sphinx-gallery release could shift
236
+ that structure without warning. Only sphinx-gallery 0.22.x is tested, so the `gallery`
237
+ extra caps there rather than letting the next release quietly produce no downloads.
238
+ - sphinx-gallery's own `.py`/`.zip` downloads still end up copied into `_downloads/`,
239
+ even though nothing on the page links to them anymore. Its `.ipynb` is never written:
240
+ with `gallery_downloads` on, this extension sets `sphinx_gallery_conf['pypandoc']` to
241
+ `False` and `sphinx_gallery_conf['notebook_extensions']` to an empty set before
242
+ sphinx-gallery reads them, so pandoc is not needed. Setting `pypandoc`,
243
+ `notebook_extensions`, `first_notebook_cell`, `last_notebook_cell` or `notebook_images`
244
+ yourself has no effect and is reported as a warning.
245
+
246
+ ## Development
247
+
248
+ ```bash
249
+ uv sync --group dev --extra gallery
250
+ uv run pytest
251
+ uv run pre-commit run --all-files
252
+ ```
@@ -4,6 +4,7 @@ requires = ['setuptools>=64', 'setuptools_scm>=8']
4
4
 
5
5
  [dependency-groups]
6
6
  dev = [
7
+ 'marimo>=0.14',
7
8
  'nbformat',
8
9
  'numpydoc',
9
10
  'pre-commit',
@@ -31,7 +32,7 @@ classifiers = [
31
32
  'Programming Language :: Python :: 3.14',
32
33
  'Topic :: Scientific/Engineering :: Visualization',
33
34
  ]
34
- dependencies = ['pyvista>=0.48']
35
+ dependencies = ['packaging>=22', 'pyvista>=0.48']
35
36
  description = 'Sphinx extension for converting docstring examples into downloadable code.'
36
37
  dynamic = ['version']
37
38
  keywords = ['download', 'examples', 'sphinx']
@@ -42,6 +43,7 @@ requires-python = '>=3.10'
42
43
 
43
44
  [project.optional-dependencies]
44
45
  gallery = ['sphinx-gallery>=0.22,<0.23']
46
+ marimo = ['marimo>=0.14']
45
47
 
46
48
  [project.urls]
47
49
  Homepage = 'https://github.com/pyvista/sphinx-examples-as-code'