sphinx-examples-as-code 0.2.1__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.
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/.github/workflows/ci.yml +1 -1
- sphinx_examples_as_code-0.3.0/LICENSE +21 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/PKG-INFO +42 -49
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/README.md +39 -48
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/pyproject.toml +2 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code/__init__.py +102 -45
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code/_version.py +3 -3
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code.egg-info/PKG-INFO +42 -49
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code.egg-info/SOURCES.txt +6 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code.egg-info/scm_file_list.json +19 -13
- sphinx_examples_as_code-0.3.0/sphinx_examples_as_code.egg-info/scm_version.json +8 -0
- sphinx_examples_as_code-0.3.0/tests/heading_level_fixture/conf.py +23 -0
- sphinx_examples_as_code-0.3.0/tests/heading_level_fixture/examples/GALLERY_HEADER.rst +2 -0
- sphinx_examples_as_code-0.3.0/tests/heading_level_fixture/examples/plot_reused_level.py +19 -0
- sphinx_examples_as_code-0.3.0/tests/heading_level_fixture/index.rst +7 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/test_gallery_downloads.py +16 -7
- sphinx_examples_as_code-0.3.0/tests/test_heading_level_reuse.py +41 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/test_sphinx_examples_as_code.py +303 -62
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/test_tinypages.py +28 -21
- sphinx_examples_as_code-0.2.1/sphinx_examples_as_code.egg-info/scm_version.json +0 -8
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/.github/dependabot.yml +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/.github/release.yml +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/.gitignore +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/.pre-commit-config.yaml +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/setup.cfg +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code.egg-info/dependency_links.txt +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code.egg-info/requires.txt +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code.egg-info/top_level.txt +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/gallery_fixture/conf.py +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/gallery_fixture/examples/GALLERY_HEADER.rst +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/gallery_fixture/examples/plot_minimal.py +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/gallery_fixture/index.rst +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/single_function_fixture/conf.py +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/single_function_fixture/index.rst +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/single_function_fixture/mymodule.py +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/test_single_function_page.py +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/tinypages/conf.py +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/tinypages/docstring_cases.py +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/tinypages/docstring_cases.rst +0 -0
- {sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/tests/tinypages/index.rst +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The PyVista Developers
|
|
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
|
|
7
|
+
deal in the Software without restriction, including without limitation the
|
|
8
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
9
|
+
sell 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
|
|
13
|
+
all 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
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinx-examples-as-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -21,7 +21,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
24
25
|
Requires-Dist: pyvista>=0.48
|
|
26
|
+
Dynamic: license-file
|
|
25
27
|
|
|
26
28
|
# sphinx-examples-as-code
|
|
27
29
|
|
|
@@ -55,12 +57,15 @@ the keys you want to change from their default:
|
|
|
55
57
|
sphinx_examples_as_code_conf = {
|
|
56
58
|
'link_position': 'top',
|
|
57
59
|
'formats': ['py', 'ipynb'],
|
|
58
|
-
'base_url': None,
|
|
59
60
|
'gallery_downloads': False,
|
|
60
61
|
'footer': (
|
|
61
62
|
'Generated by `sphinx-examples-as-code '
|
|
62
63
|
'<https://github.com/pyvista/sphinx-examples-as-code>`_'
|
|
63
64
|
),
|
|
65
|
+
'link_labels': {
|
|
66
|
+
'py': 'Download Python source code',
|
|
67
|
+
'ipynb': 'Download Jupyter notebook',
|
|
68
|
+
},
|
|
64
69
|
}
|
|
65
70
|
```
|
|
66
71
|
|
|
@@ -68,29 +73,26 @@ sphinx_examples_as_code_conf = {
|
|
|
68
73
|
(default) or `'bottom'`.
|
|
69
74
|
- `formats`: which downloads to generate. A list containing `'py'`, `'ipynb'`, or both
|
|
70
75
|
(default). Always offered in that order regardless of how the list is written.
|
|
71
|
-
- `
|
|
72
|
-
to
|
|
73
|
-
actually use. `None` (default) means no links are generated anywhere. A missing
|
|
74
|
-
trailing slash is added automatically; a value with no scheme or host raises a
|
|
75
|
-
configuration error at build start.
|
|
76
|
+
- `link_labels`: the text of the download link(s) themselves, per format. Set only the
|
|
77
|
+
format(s) you want to change; any left unset keep reading their own default shown above.
|
|
76
78
|
- `gallery_downloads`: opt-in takeover of
|
|
77
79
|
[sphinx-gallery](https://sphinx-gallery.github.io)'s own per-example downloads.
|
|
78
80
|
`False` (default) leaves sphinx-gallery pages untouched. See
|
|
79
81
|
[Sphinx-Gallery integration](#sphinx-gallery-integration) below.
|
|
80
82
|
- `footer`: a string appended to the end of every generated file. Defaults to a one-line
|
|
81
83
|
"generated file" credit linking back to this project; set to `None` to omit it
|
|
82
|
-
entirely. Preceded by a blank line and a `-`-only divider line,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
entirely. Preceded by a blank line and a `-`-only divider line, which also renders as a
|
|
85
|
+
real horizontal rule in `.ipynb`; the footer always gets its own dedicated cell there.
|
|
86
|
+
Parsed as RST: a hyperlink written as `` `text <url>`_ `` becomes a real clickable
|
|
87
|
+
Markdown link in `.ipynb`, and renders inline as `text url` in `.py`. Plain text with no
|
|
88
|
+
markup at all becomes one comment line per line of text, and blank-line-separated
|
|
89
|
+
paragraphs stay separated.
|
|
90
|
+
|
|
91
|
+
An unrecognized key raises a configuration error at build start.
|
|
92
|
+
|
|
93
|
+
Cross-references and hyperlinks resolve into absolute links using Sphinx's own
|
|
94
|
+
[`html_baseurl`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_baseurl).
|
|
95
|
+
Leave it unset and no links are generated anywhere.
|
|
94
96
|
|
|
95
97
|
Overriding a single key from the command line works via a dotted `-D` flag, e.g.
|
|
96
98
|
`-D sphinx_examples_as_code_conf.link_position=bottom` -- this only touches that one
|
|
@@ -103,16 +105,14 @@ What happens to the content of an Examples section:
|
|
|
103
105
|
- Doctest blocks (`>>> ...` / `... ...`) keep their input lines, prompts stripped, as
|
|
104
106
|
real Python source. Doctest *output* lines are dropped — only the input code matters.
|
|
105
107
|
- `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
|
|
106
|
-
comments, set off with blank lines on both sides like any other directive
|
|
107
|
-
preformatted block (e.g. an RST `::`-block illustrating a data format) still reads as
|
|
108
|
-
a distinct block rather than melting into the surrounding prose.
|
|
108
|
+
comments, set off with blank lines on both sides like any other directive.
|
|
109
109
|
- Admonitions (`.. note::`, `.. warning::`, `.. seealso::`, ...) become a `# LABEL:`
|
|
110
110
|
comment followed by their content as comments. "See Also" is recognized in any of its
|
|
111
111
|
three forms (`.. seealso::`, a bare `.. rubric:: See Also`, or a hand-written `See
|
|
112
112
|
Also` heading) and always renders the same way.
|
|
113
113
|
- Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
|
|
114
114
|
double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
|
|
115
|
-
``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `
|
|
115
|
+
``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
|
|
116
116
|
the reference resolves: `.ipynb` turns it into a clickable link everywhere; `.py` only
|
|
117
117
|
writes the link inside a "See Also" part (as `name url` on its own line) — everywhere
|
|
118
118
|
else in `.py` the link is simply omitted.
|
|
@@ -121,15 +121,13 @@ What happens to the content of an Examples section:
|
|
|
121
121
|
- Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
|
|
122
122
|
`#` comment.
|
|
123
123
|
- Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
|
|
124
|
-
line are dropped entirely
|
|
125
|
-
standalone file has any use for.
|
|
124
|
+
line are dropped entirely.
|
|
126
125
|
|
|
127
126
|
Generated `.py` files start with a `# Examples from <qualified name>` title header
|
|
128
|
-
(gallery mode uses the page's own title instead -- see below)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
blank
|
|
132
|
-
sides.
|
|
127
|
+
(gallery mode uses the page's own title instead -- see below), with a few whitespace
|
|
128
|
+
conventions: prose directly above a code block stays attached to it, a code block is
|
|
129
|
+
always followed by a blank line, and a directive (header, `# NOTE:`-style block) gets
|
|
130
|
+
blank lines on both sides.
|
|
133
131
|
|
|
134
132
|
Generated `.ipynb` notebooks use the same content, split into alternating code/markdown
|
|
135
133
|
cells instead.
|
|
@@ -146,35 +144,30 @@ full example code" note and the `.py`/`.ipynb`/`.zip` download footer are remove
|
|
|
146
144
|
the page, replaced with download link(s) built by this extension instead — same
|
|
147
145
|
conversion rules as above, applied to the whole page rather than one Examples section.
|
|
148
146
|
The generated file's header uses the page's own title (e.g. `# Create Circular Arcs`)
|
|
149
|
-
rather than the generic `# Examples from <docname
|
|
150
|
-
page, not an Examples section carved out of a larger docstring, so there's no "from"
|
|
151
|
-
framing to make.
|
|
147
|
+
rather than the generic `# Examples from <docname>`.
|
|
152
148
|
|
|
153
149
|
The "Total running time" line and the "Gallery generated by Sphinx-Gallery" credit line
|
|
154
150
|
stay on the rendered page untouched, but never make it into the generated download
|
|
155
|
-
itself
|
|
156
|
-
yourself would read like it's timing *your* run, not the build's, and a "Generated by
|
|
157
|
-
Sphinx-Gallery" credit is simply wrong inside a file this extension generated.
|
|
151
|
+
itself.
|
|
158
152
|
|
|
159
153
|
Detection is automatic and per-page — any page without a sphinx-gallery download footer
|
|
160
|
-
is left completely untouched
|
|
161
|
-
|
|
162
|
-
above.
|
|
154
|
+
is left completely untouched. Gallery pages and ordinary docstring/prose pages (using
|
|
155
|
+
the Examples-section behavior above) can coexist on the same site.
|
|
163
156
|
|
|
164
157
|
A `# %%` cell with its own RST heading gets the same title-plus-underline treatment as
|
|
165
|
-
the file's own header,
|
|
166
|
-
|
|
158
|
+
the file's own header, and renders as a real Markdown heading in `.ipynb` (`<h1>` for the
|
|
159
|
+
file's own header, `# Title` + `# ====`; `<h2>` for a cell heading, `# Title` + `# ----`).
|
|
160
|
+
A cell heading nested under the page's own title (the common case) is level 2; one that
|
|
161
|
+
reuses the page title's own underline character -- a real RST heading, one level up --
|
|
162
|
+
is level 1, matching the file's own header.
|
|
167
163
|
|
|
168
164
|
Two things worth knowing before turning this on:
|
|
169
165
|
|
|
170
|
-
- It's built against sphinx-gallery's own
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
`_downloads/` before this extension gets a chance to remove the links pointing to
|
|
176
|
-
them, so those files still end up in the build output even though nothing on the page
|
|
177
|
-
links to them anymore.
|
|
166
|
+
- It's built against sphinx-gallery's own RST/HTML output (the same `sphx-glr-*` CSS
|
|
167
|
+
classes its own theming depends on), not a documented extension API. A future
|
|
168
|
+
sphinx-gallery release could shift that structure without warning.
|
|
169
|
+
- sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
|
|
170
|
+
`_downloads/`, even though nothing on the page links to them anymore.
|
|
178
171
|
|
|
179
172
|
## Development
|
|
180
173
|
|
|
@@ -30,12 +30,15 @@ the keys you want to change from their default:
|
|
|
30
30
|
sphinx_examples_as_code_conf = {
|
|
31
31
|
'link_position': 'top',
|
|
32
32
|
'formats': ['py', 'ipynb'],
|
|
33
|
-
'base_url': None,
|
|
34
33
|
'gallery_downloads': False,
|
|
35
34
|
'footer': (
|
|
36
35
|
'Generated by `sphinx-examples-as-code '
|
|
37
36
|
'<https://github.com/pyvista/sphinx-examples-as-code>`_'
|
|
38
37
|
),
|
|
38
|
+
'link_labels': {
|
|
39
|
+
'py': 'Download Python source code',
|
|
40
|
+
'ipynb': 'Download Jupyter notebook',
|
|
41
|
+
},
|
|
39
42
|
}
|
|
40
43
|
```
|
|
41
44
|
|
|
@@ -43,29 +46,26 @@ sphinx_examples_as_code_conf = {
|
|
|
43
46
|
(default) or `'bottom'`.
|
|
44
47
|
- `formats`: which downloads to generate. A list containing `'py'`, `'ipynb'`, or both
|
|
45
48
|
(default). Always offered in that order regardless of how the list is written.
|
|
46
|
-
- `
|
|
47
|
-
to
|
|
48
|
-
actually use. `None` (default) means no links are generated anywhere. A missing
|
|
49
|
-
trailing slash is added automatically; a value with no scheme or host raises a
|
|
50
|
-
configuration error at build start.
|
|
49
|
+
- `link_labels`: the text of the download link(s) themselves, per format. Set only the
|
|
50
|
+
format(s) you want to change; any left unset keep reading their own default shown above.
|
|
51
51
|
- `gallery_downloads`: opt-in takeover of
|
|
52
52
|
[sphinx-gallery](https://sphinx-gallery.github.io)'s own per-example downloads.
|
|
53
53
|
`False` (default) leaves sphinx-gallery pages untouched. See
|
|
54
54
|
[Sphinx-Gallery integration](#sphinx-gallery-integration) below.
|
|
55
55
|
- `footer`: a string appended to the end of every generated file. Defaults to a one-line
|
|
56
56
|
"generated file" credit linking back to this project; set to `None` to omit it
|
|
57
|
-
entirely. Preceded by a blank line and a `-`-only divider line,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
entirely. Preceded by a blank line and a `-`-only divider line, which also renders as a
|
|
58
|
+
real horizontal rule in `.ipynb`; the footer always gets its own dedicated cell there.
|
|
59
|
+
Parsed as RST: a hyperlink written as `` `text <url>`_ `` becomes a real clickable
|
|
60
|
+
Markdown link in `.ipynb`, and renders inline as `text url` in `.py`. Plain text with no
|
|
61
|
+
markup at all becomes one comment line per line of text, and blank-line-separated
|
|
62
|
+
paragraphs stay separated.
|
|
63
|
+
|
|
64
|
+
An unrecognized key raises a configuration error at build start.
|
|
65
|
+
|
|
66
|
+
Cross-references and hyperlinks resolve into absolute links using Sphinx's own
|
|
67
|
+
[`html_baseurl`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_baseurl).
|
|
68
|
+
Leave it unset and no links are generated anywhere.
|
|
69
69
|
|
|
70
70
|
Overriding a single key from the command line works via a dotted `-D` flag, e.g.
|
|
71
71
|
`-D sphinx_examples_as_code_conf.link_position=bottom` -- this only touches that one
|
|
@@ -78,16 +78,14 @@ What happens to the content of an Examples section:
|
|
|
78
78
|
- Doctest blocks (`>>> ...` / `... ...`) keep their input lines, prompts stripped, as
|
|
79
79
|
real Python source. Doctest *output* lines are dropped — only the input code matters.
|
|
80
80
|
- `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
|
|
81
|
-
comments, set off with blank lines on both sides like any other directive
|
|
82
|
-
preformatted block (e.g. an RST `::`-block illustrating a data format) still reads as
|
|
83
|
-
a distinct block rather than melting into the surrounding prose.
|
|
81
|
+
comments, set off with blank lines on both sides like any other directive.
|
|
84
82
|
- Admonitions (`.. note::`, `.. warning::`, `.. seealso::`, ...) become a `# LABEL:`
|
|
85
83
|
comment followed by their content as comments. "See Also" is recognized in any of its
|
|
86
84
|
three forms (`.. seealso::`, a bare `.. rubric:: See Also`, or a hand-written `See
|
|
87
85
|
Also` heading) and always renders the same way.
|
|
88
86
|
- Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
|
|
89
87
|
double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
|
|
90
|
-
``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `
|
|
88
|
+
``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
|
|
91
89
|
the reference resolves: `.ipynb` turns it into a clickable link everywhere; `.py` only
|
|
92
90
|
writes the link inside a "See Also" part (as `name url` on its own line) — everywhere
|
|
93
91
|
else in `.py` the link is simply omitted.
|
|
@@ -96,15 +94,13 @@ What happens to the content of an Examples section:
|
|
|
96
94
|
- Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
|
|
97
95
|
`#` comment.
|
|
98
96
|
- Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
|
|
99
|
-
line are dropped entirely
|
|
100
|
-
standalone file has any use for.
|
|
97
|
+
line are dropped entirely.
|
|
101
98
|
|
|
102
99
|
Generated `.py` files start with a `# Examples from <qualified name>` title header
|
|
103
|
-
(gallery mode uses the page's own title instead -- see below)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
blank
|
|
107
|
-
sides.
|
|
100
|
+
(gallery mode uses the page's own title instead -- see below), with a few whitespace
|
|
101
|
+
conventions: prose directly above a code block stays attached to it, a code block is
|
|
102
|
+
always followed by a blank line, and a directive (header, `# NOTE:`-style block) gets
|
|
103
|
+
blank lines on both sides.
|
|
108
104
|
|
|
109
105
|
Generated `.ipynb` notebooks use the same content, split into alternating code/markdown
|
|
110
106
|
cells instead.
|
|
@@ -121,35 +117,30 @@ full example code" note and the `.py`/`.ipynb`/`.zip` download footer are remove
|
|
|
121
117
|
the page, replaced with download link(s) built by this extension instead — same
|
|
122
118
|
conversion rules as above, applied to the whole page rather than one Examples section.
|
|
123
119
|
The generated file's header uses the page's own title (e.g. `# Create Circular Arcs`)
|
|
124
|
-
rather than the generic `# Examples from <docname
|
|
125
|
-
page, not an Examples section carved out of a larger docstring, so there's no "from"
|
|
126
|
-
framing to make.
|
|
120
|
+
rather than the generic `# Examples from <docname>`.
|
|
127
121
|
|
|
128
122
|
The "Total running time" line and the "Gallery generated by Sphinx-Gallery" credit line
|
|
129
123
|
stay on the rendered page untouched, but never make it into the generated download
|
|
130
|
-
itself
|
|
131
|
-
yourself would read like it's timing *your* run, not the build's, and a "Generated by
|
|
132
|
-
Sphinx-Gallery" credit is simply wrong inside a file this extension generated.
|
|
124
|
+
itself.
|
|
133
125
|
|
|
134
126
|
Detection is automatic and per-page — any page without a sphinx-gallery download footer
|
|
135
|
-
is left completely untouched
|
|
136
|
-
|
|
137
|
-
above.
|
|
127
|
+
is left completely untouched. Gallery pages and ordinary docstring/prose pages (using
|
|
128
|
+
the Examples-section behavior above) can coexist on the same site.
|
|
138
129
|
|
|
139
130
|
A `# %%` cell with its own RST heading gets the same title-plus-underline treatment as
|
|
140
|
-
the file's own header,
|
|
141
|
-
|
|
131
|
+
the file's own header, and renders as a real Markdown heading in `.ipynb` (`<h1>` for the
|
|
132
|
+
file's own header, `# Title` + `# ====`; `<h2>` for a cell heading, `# Title` + `# ----`).
|
|
133
|
+
A cell heading nested under the page's own title (the common case) is level 2; one that
|
|
134
|
+
reuses the page title's own underline character -- a real RST heading, one level up --
|
|
135
|
+
is level 1, matching the file's own header.
|
|
142
136
|
|
|
143
137
|
Two things worth knowing before turning this on:
|
|
144
138
|
|
|
145
|
-
- It's built against sphinx-gallery's own
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`_downloads/` before this extension gets a chance to remove the links pointing to
|
|
151
|
-
them, so those files still end up in the build output even though nothing on the page
|
|
152
|
-
links to them anymore.
|
|
139
|
+
- It's built against sphinx-gallery's own RST/HTML output (the same `sphx-glr-*` CSS
|
|
140
|
+
classes its own theming depends on), not a documented extension API. A future
|
|
141
|
+
sphinx-gallery release could shift that structure without warning.
|
|
142
|
+
- sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
|
|
143
|
+
`_downloads/`, even though nothing on the page links to them anymore.
|
|
153
144
|
|
|
154
145
|
## Development
|
|
155
146
|
|
|
@@ -96,6 +96,7 @@ ignore = ['D203', 'D211', 'D213']
|
|
|
96
96
|
# sphinx-gallery example scripts: the module docstring is an RST page title
|
|
97
97
|
# (title line + underline), not prose -- pydocstyle's summary-line rules don't apply.
|
|
98
98
|
'tests/gallery_fixture/examples/*' = ['D205', 'D400', 'D415', 'T201']
|
|
99
|
+
'tests/heading_level_fixture/examples/*' = ['D205', 'D400', 'D415', 'T201']
|
|
99
100
|
|
|
100
101
|
[tool.ruff.lint.isort]
|
|
101
102
|
combine-as-imports = true
|
|
@@ -104,6 +105,7 @@ force-sort-within-sections = true
|
|
|
104
105
|
required-imports = ['from __future__ import annotations']
|
|
105
106
|
|
|
106
107
|
[tool.setuptools]
|
|
108
|
+
license-files = ['LICENSE']
|
|
107
109
|
packages = ['sphinx_examples_as_code']
|
|
108
110
|
|
|
109
111
|
[tool.setuptools_scm]
|
{sphinx_examples_as_code-0.2.1 → sphinx_examples_as_code-0.3.0}/sphinx_examples_as_code/__init__.py
RENAMED
|
@@ -169,6 +169,21 @@ class _RenderContext:
|
|
|
169
169
|
in_footer: bool = False
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
def _normalized_html_baseurl(app: Sphinx) -> str | None:
|
|
173
|
+
"""Sphinx's own ``html_baseurl`` config, normalized for use with ``urljoin``.
|
|
174
|
+
|
|
175
|
+
``None`` if unset or not a usable absolute URL. Adds a missing trailing
|
|
176
|
+
slash.
|
|
177
|
+
"""
|
|
178
|
+
base_url = app.config.html_baseurl
|
|
179
|
+
if not base_url:
|
|
180
|
+
return None
|
|
181
|
+
parsed = urlsplit(base_url)
|
|
182
|
+
if parsed.scheme not in ('http', 'https') or not parsed.netloc:
|
|
183
|
+
return None
|
|
184
|
+
return base_url if base_url.endswith('/') else base_url + '/'
|
|
185
|
+
|
|
186
|
+
|
|
172
187
|
def _resolve_link_url(node: nodes.reference, ctx: _RenderContext) -> str | None:
|
|
173
188
|
"""Resolve a reference node's target to an absolute URL, if possible.
|
|
174
189
|
|
|
@@ -181,7 +196,7 @@ def _resolve_link_url(node: nodes.reference, ctx: _RenderContext) -> str | None:
|
|
|
181
196
|
if refuri and urlsplit(refuri).netloc:
|
|
182
197
|
return refuri # already absolute (an external hyperlink)
|
|
183
198
|
|
|
184
|
-
base_url = ctx.app
|
|
199
|
+
base_url = _normalized_html_baseurl(ctx.app)
|
|
185
200
|
if not base_url:
|
|
186
201
|
return None
|
|
187
202
|
current_page_url = urljoin(base_url, ctx.app.builder.get_target_uri(ctx.docname))
|
|
@@ -353,18 +368,42 @@ def _convert_literal_block(node: nodes.literal_block) -> list[Segment]:
|
|
|
353
368
|
return [('directive', comment_lines)]
|
|
354
369
|
|
|
355
370
|
|
|
356
|
-
|
|
357
|
-
|
|
371
|
+
#: Setext underline character per heading level -- ``=`` for the file's own
|
|
372
|
+
#: top-level header (level 1), ``-`` for a subheading (level 2). CommonMark's
|
|
373
|
+
#: setext syntax only supports these two levels.
|
|
374
|
+
_TITLE_UNDERLINE_CHARS = {1: '=', 2: '-'}
|
|
375
|
+
|
|
358
376
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
377
|
+
def _title_underline_segment(title: str, level: int = 1) -> Segment:
|
|
378
|
+
"""Build a title + underline directive segment, e.g. ``# Title`` + ``# =====``.
|
|
379
|
+
|
|
380
|
+
Shared by the file's own header (see ``_header_segment``), which is
|
|
381
|
+
always level 1, and gallery mode's sibling-section headings, which are
|
|
382
|
+
level 2. The underline is what makes this render as a real Markdown
|
|
383
|
+
heading in ``.ipynb`` (setext-style, once the ``#`` comment prefix is
|
|
384
|
+
stripped for markdown cells) rather than plain text.
|
|
363
385
|
"""
|
|
364
|
-
underline =
|
|
386
|
+
underline = _TITLE_UNDERLINE_CHARS[level] * len(title)
|
|
365
387
|
return ('directive', [f'# {title}', f'# {underline}'])
|
|
366
388
|
|
|
367
389
|
|
|
390
|
+
def _heading_level(title: nodes.title) -> int:
|
|
391
|
+
"""Compute a title's heading level from its ``nodes.section`` nesting depth.
|
|
392
|
+
|
|
393
|
+
Counts the title's own section and each ancestor ``nodes.section``.
|
|
394
|
+
Running this on the file's own header would give level 1; a title
|
|
395
|
+
nested inside that comes out level 2, and so does anything nested
|
|
396
|
+
deeper (clamped -- the deepest level CommonMark's setext syntax can
|
|
397
|
+
represent).
|
|
398
|
+
"""
|
|
399
|
+
level = 0
|
|
400
|
+
node: nodes.Node | None = title.parent
|
|
401
|
+
while isinstance(node, nodes.section):
|
|
402
|
+
level += 1
|
|
403
|
+
node = node.parent
|
|
404
|
+
return min(level, 2)
|
|
405
|
+
|
|
406
|
+
|
|
368
407
|
def _convert_admonition(
|
|
369
408
|
node: nodes.Element, label: str, ctx: _RenderContext, *, skip_first_title: bool = False
|
|
370
409
|
) -> list[Segment]:
|
|
@@ -403,12 +442,12 @@ def _convert_node(node: nodes.Node, ctx: _RenderContext) -> list[Segment]:
|
|
|
403
442
|
if isinstance(node, nodes.title):
|
|
404
443
|
# Gallery mode only: a sibling section's own heading (the page's own
|
|
405
444
|
# outer title is filtered out before reaching here -- see
|
|
406
|
-
# _process_gallery_page).
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
# whatever prose follows it with no visual break.
|
|
445
|
+
# _process_gallery_page). Renders as a real Markdown heading in
|
|
446
|
+
# .ipynb, at a level relative to its own section nesting (see
|
|
447
|
+
# _heading_level).
|
|
410
448
|
title_text = _render_inline(node, ctx).strip()
|
|
411
|
-
|
|
449
|
+
level = _heading_level(node)
|
|
450
|
+
return [_title_underline_segment(title_text, level=level)] if title_text else []
|
|
412
451
|
if isinstance(node, (*_CONTAINER_TYPES, nodes.section)):
|
|
413
452
|
# nodes.section (gallery mode only): a sphinx-gallery ``# %%`` cell
|
|
414
453
|
# with its own RST heading becomes a *sibling* section at the
|
|
@@ -699,12 +738,13 @@ def _write_notebook(app: Sphinx, name: str, notebook: dict) -> str:
|
|
|
699
738
|
return _write_download_file(app, name, 'ipynb', json.dumps(notebook, indent=1))
|
|
700
739
|
|
|
701
740
|
|
|
702
|
-
#:
|
|
703
|
-
|
|
704
|
-
_FORMAT_LABELS = {
|
|
741
|
+
#: Default download link text per format -- see ``sphinx_examples_as_code_conf['link_labels']``.
|
|
742
|
+
_DEFAULT_LINK_LABELS = {
|
|
705
743
|
'py': 'Download Python source code',
|
|
706
744
|
'ipynb': 'Download Jupyter notebook',
|
|
707
745
|
}
|
|
746
|
+
#: The fixed order formats are offered in, regardless of how
|
|
747
|
+
#: ``sphinx_examples_as_code_conf['formats']`` lists them.
|
|
708
748
|
_FORMAT_ORDER = ('py', 'ipynb')
|
|
709
749
|
|
|
710
750
|
|
|
@@ -749,6 +789,7 @@ def _build_download_entries(
|
|
|
749
789
|
nodes_in_span: list[nodes.Node],
|
|
750
790
|
formats: list[str],
|
|
751
791
|
footer: str | None,
|
|
792
|
+
link_labels: dict[str, str],
|
|
752
793
|
) -> list[tuple[str, str]]:
|
|
753
794
|
"""Convert a span of nodes into written ``.py``/``.ipynb`` files, per ``formats``.
|
|
754
795
|
|
|
@@ -775,7 +816,7 @@ def _build_download_entries(
|
|
|
775
816
|
for fmt in _FORMAT_ORDER:
|
|
776
817
|
if fmt not in formats:
|
|
777
818
|
continue
|
|
778
|
-
label =
|
|
819
|
+
label = link_labels[fmt]
|
|
779
820
|
if fmt == 'py':
|
|
780
821
|
rel_path = _write_source(app, name, source)
|
|
781
822
|
else:
|
|
@@ -807,6 +848,7 @@ def _process_span(
|
|
|
807
848
|
position: str,
|
|
808
849
|
formats: list[str],
|
|
809
850
|
footer: str | None,
|
|
851
|
+
link_labels: dict[str, str],
|
|
810
852
|
) -> None:
|
|
811
853
|
"""Convert one Examples span and insert download link(s) if it has real code."""
|
|
812
854
|
nodes_in_span = list(parent.children[start:end])
|
|
@@ -816,7 +858,7 @@ def _process_span(
|
|
|
816
858
|
|
|
817
859
|
name = _qualified_name_for(heading, docname, counter)
|
|
818
860
|
entries = _build_download_entries(
|
|
819
|
-
app, docname, name, _header_segment(name), nodes_in_span, formats, footer
|
|
861
|
+
app, docname, name, _header_segment(name), nodes_in_span, formats, footer, link_labels
|
|
820
862
|
)
|
|
821
863
|
if not entries:
|
|
822
864
|
return
|
|
@@ -900,6 +942,7 @@ def _process_gallery_page(
|
|
|
900
942
|
position: str,
|
|
901
943
|
formats: list[str],
|
|
902
944
|
footer: str | None,
|
|
945
|
+
link_labels: dict[str, str],
|
|
903
946
|
) -> None:
|
|
904
947
|
"""Replace a sphinx-gallery page's own download footer with converted downloads.
|
|
905
948
|
|
|
@@ -936,7 +979,9 @@ def _process_gallery_page(
|
|
|
936
979
|
# no "from" framing to make. Falls back to the usual header on the off
|
|
937
980
|
# chance the page has no title of its own.
|
|
938
981
|
header = _title_underline_segment(title) if title else _header_segment(name)
|
|
939
|
-
entries = _build_download_entries(
|
|
982
|
+
entries = _build_download_entries(
|
|
983
|
+
app, docname, name, header, nodes_in_span, formats, footer, link_labels
|
|
984
|
+
)
|
|
940
985
|
if not entries:
|
|
941
986
|
return
|
|
942
987
|
|
|
@@ -960,9 +1005,10 @@ def _process_doctree(app: Sphinx, doctree: nodes.document, docname: str) -> None
|
|
|
960
1005
|
position = conf['link_position']
|
|
961
1006
|
formats = conf['formats']
|
|
962
1007
|
footer = conf['footer']
|
|
1008
|
+
link_labels = conf['link_labels']
|
|
963
1009
|
|
|
964
1010
|
if conf['gallery_downloads']:
|
|
965
|
-
_process_gallery_page(app, docname, doctree, position, formats, footer)
|
|
1011
|
+
_process_gallery_page(app, docname, doctree, position, formats, footer, link_labels)
|
|
966
1012
|
|
|
967
1013
|
# Process spans per shared parent, last to first: inserting a download
|
|
968
1014
|
# node shifts every later sibling index by one, so this stays correct
|
|
@@ -972,7 +1018,19 @@ def _process_doctree(app: Sphinx, doctree: nodes.document, docname: str) -> None
|
|
|
972
1018
|
for parent, start, end, heading, counter in sorted(
|
|
973
1019
|
numbered_spans, key=lambda s: (id(s[0]), -s[1])
|
|
974
1020
|
):
|
|
975
|
-
_process_span(
|
|
1021
|
+
_process_span(
|
|
1022
|
+
app,
|
|
1023
|
+
docname,
|
|
1024
|
+
parent,
|
|
1025
|
+
start,
|
|
1026
|
+
end,
|
|
1027
|
+
heading,
|
|
1028
|
+
counter,
|
|
1029
|
+
position,
|
|
1030
|
+
formats,
|
|
1031
|
+
footer,
|
|
1032
|
+
link_labels,
|
|
1033
|
+
)
|
|
976
1034
|
|
|
977
1035
|
|
|
978
1036
|
# One line, one link: a second sentence pointing at the issue tracker read
|
|
@@ -989,33 +1047,12 @@ _DEFAULT_FOOTER = (
|
|
|
989
1047
|
_CONF_DEFAULTS: dict[str, object] = {
|
|
990
1048
|
'link_position': 'top',
|
|
991
1049
|
'formats': ['py', 'ipynb'],
|
|
992
|
-
'base_url': None,
|
|
993
1050
|
'gallery_downloads': False,
|
|
994
1051
|
'footer': _DEFAULT_FOOTER,
|
|
1052
|
+
'link_labels': _DEFAULT_LINK_LABELS,
|
|
995
1053
|
}
|
|
996
1054
|
|
|
997
1055
|
|
|
998
|
-
def _normalize_base_url(base_url: str | None) -> str | None:
|
|
999
|
-
"""Validate and normalize a ``base_url`` value.
|
|
1000
|
-
|
|
1001
|
-
Catches two common typos loudly instead of silently generating wrong
|
|
1002
|
-
links: a missing scheme (parses with no netloc at all), and a subpath
|
|
1003
|
-
with no trailing slash (``urljoin`` would drop the last segment).
|
|
1004
|
-
"""
|
|
1005
|
-
if not base_url:
|
|
1006
|
-
return None
|
|
1007
|
-
|
|
1008
|
-
parsed = urlsplit(base_url)
|
|
1009
|
-
if parsed.scheme not in ('http', 'https') or not parsed.netloc:
|
|
1010
|
-
msg = (
|
|
1011
|
-
f"sphinx_examples_as_code_conf['base_url']={base_url!r} does not look "
|
|
1012
|
-
"like a valid absolute URL (expected something like 'https://docs.example.com/')."
|
|
1013
|
-
)
|
|
1014
|
-
raise ConfigError(msg)
|
|
1015
|
-
|
|
1016
|
-
return base_url if base_url.endswith('/') else base_url + '/'
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
1056
|
def _coerce_conf_value(key: str, value: object) -> object:
|
|
1020
1057
|
"""Coerce a value that may have arrived as a raw string; pass anything else through.
|
|
1021
1058
|
|
|
@@ -1039,7 +1076,27 @@ def _coerce_conf_value(key: str, value: object) -> object:
|
|
|
1039
1076
|
f"sphinx_examples_as_code_conf['gallery_downloads'] must be '0' or '1', got {value!r}."
|
|
1040
1077
|
)
|
|
1041
1078
|
raise ConfigError(msg)
|
|
1042
|
-
return value # link_position,
|
|
1079
|
+
return value # link_position, footer: a string is already the real type
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _merged_link_labels(value: object) -> dict[str, str]:
|
|
1083
|
+
"""Merge a user-provided ``link_labels`` override over the defaults.
|
|
1084
|
+
|
|
1085
|
+
A partial override (e.g. just ``{'py': 'Get the script'}``) only changes
|
|
1086
|
+
that one format's label; any not given keep their default.
|
|
1087
|
+
"""
|
|
1088
|
+
if not isinstance(value, dict):
|
|
1089
|
+
msg = f"sphinx_examples_as_code_conf['link_labels'] must be a dict, got {value!r}."
|
|
1090
|
+
raise ConfigError(msg)
|
|
1091
|
+
unknown = sorted(set(value) - set(_DEFAULT_LINK_LABELS))
|
|
1092
|
+
if unknown:
|
|
1093
|
+
valid = ', '.join(sorted(_DEFAULT_LINK_LABELS))
|
|
1094
|
+
msg = (
|
|
1095
|
+
f"sphinx_examples_as_code_conf['link_labels'] has unknown key(s) {unknown} "
|
|
1096
|
+
f'(valid keys: {valid}).'
|
|
1097
|
+
)
|
|
1098
|
+
raise ConfigError(msg)
|
|
1099
|
+
return {**_DEFAULT_LINK_LABELS, **value}
|
|
1043
1100
|
|
|
1044
1101
|
|
|
1045
1102
|
def _finalize_conf(_app: Sphinx, config: Config) -> None:
|
|
@@ -1059,7 +1116,7 @@ def _finalize_conf(_app: Sphinx, config: Config) -> None:
|
|
|
1059
1116
|
|
|
1060
1117
|
merged = dict(_CONF_DEFAULTS)
|
|
1061
1118
|
merged.update((key, _coerce_conf_value(key, value)) for key, value in user_conf.items())
|
|
1062
|
-
merged['
|
|
1119
|
+
merged['link_labels'] = _merged_link_labels(merged['link_labels'])
|
|
1063
1120
|
config.sphinx_examples_as_code_conf = merged
|
|
1064
1121
|
|
|
1065
1122
|
|