sphinx-examples-as-code 0.7.0__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.
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/PKG-INFO +56 -1
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/README.md +54 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/pyproject.toml +1 -1
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code/__init__.py +314 -15
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code/_version.py +3 -3
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/PKG-INFO +56 -1
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/requires.txt +1 -0
- sphinx_examples_as_code-0.8.0/sphinx_examples_as_code.egg-info/scm_version.json +8 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/test_sphinx_examples_as_code.py +446 -0
- sphinx_examples_as_code-0.7.0/sphinx_examples_as_code.egg-info/scm_version.json +0 -8
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/.github/dependabot.yml +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/.github/release.yml +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/.github/workflows/ci.yml +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/.gitignore +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/.pre-commit-config.yaml +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/LICENSE +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/setup.cfg +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/SOURCES.txt +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/dependency_links.txt +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/scm_file_list.json +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code.egg-info/top_level.txt +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/conf.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/examples/GALLERY_HEADER.rst +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/examples/plot_minimal.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/gallery_fixture/index.rst +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/conf.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/examples/GALLERY_HEADER.rst +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/examples/plot_reused_level.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/examples/plot_three_levels.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/heading_level_fixture/index.rst +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/single_function_fixture/conf.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/single_function_fixture/index.rst +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/single_function_fixture/mymodule.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/test_gallery_downloads.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/test_heading_level_reuse.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/test_single_function_page.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/test_tinypages.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/tinypages/conf.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/tinypages/docstring_cases.py +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/tinypages/docstring_cases.rst +0 -0
- {sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/tests/tinypages/index.rst +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinx-examples-as-code
|
|
3
|
-
Version: 0.
|
|
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,6 +22,7 @@ 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"
|
|
@@ -85,6 +86,9 @@ sphinx_examples_as_code_conf = {
|
|
|
85
86
|
'jupyter': 'Download Jupyter notebook',
|
|
86
87
|
'marimo': 'Download marimo notebook',
|
|
87
88
|
},
|
|
89
|
+
'script_metadata': False,
|
|
90
|
+
'dependencies': {},
|
|
91
|
+
'requires_python': None,
|
|
88
92
|
}
|
|
89
93
|
```
|
|
90
94
|
|
|
@@ -111,6 +115,17 @@ sphinx_examples_as_code_conf = {
|
|
|
111
115
|
Markdown link in notebooks, and renders inline as `text url` in Python scripts. Plain
|
|
112
116
|
text with no markup at all becomes one comment line per line of text, and
|
|
113
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.
|
|
114
129
|
|
|
115
130
|
An unrecognized key raises a configuration error at build start.
|
|
116
131
|
|
|
@@ -122,6 +137,46 @@ Overriding a single key from the command line works via a dotted `-D` flag, e.g.
|
|
|
122
137
|
`-D sphinx_examples_as_code_conf.link_position=bottom` -- this only touches that one
|
|
123
138
|
key, leaving the rest (`conf.py`'s values, or the defaults) alone.
|
|
124
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
|
+
|
|
125
180
|
## Conversion rules
|
|
126
181
|
|
|
127
182
|
What happens to the content of an Examples section:
|
|
@@ -54,6 +54,9 @@ sphinx_examples_as_code_conf = {
|
|
|
54
54
|
'jupyter': 'Download Jupyter notebook',
|
|
55
55
|
'marimo': 'Download marimo notebook',
|
|
56
56
|
},
|
|
57
|
+
'script_metadata': False,
|
|
58
|
+
'dependencies': {},
|
|
59
|
+
'requires_python': None,
|
|
57
60
|
}
|
|
58
61
|
```
|
|
59
62
|
|
|
@@ -80,6 +83,17 @@ sphinx_examples_as_code_conf = {
|
|
|
80
83
|
Markdown link in notebooks, and renders inline as `text url` in Python scripts. Plain
|
|
81
84
|
text with no markup at all becomes one comment line per line of text, and
|
|
82
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.
|
|
83
97
|
|
|
84
98
|
An unrecognized key raises a configuration error at build start.
|
|
85
99
|
|
|
@@ -91,6 +105,46 @@ Overriding a single key from the command line works via a dotted `-D` flag, e.g.
|
|
|
91
105
|
`-D sphinx_examples_as_code_conf.link_position=bottom` -- this only touches that one
|
|
92
106
|
key, leaving the rest (`conf.py`'s values, or the defaults) alone.
|
|
93
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
|
+
|
|
94
148
|
## Conversion rules
|
|
95
149
|
|
|
96
150
|
What happens to the content of an Examples section:
|
|
@@ -32,7 +32,7 @@ classifiers = [
|
|
|
32
32
|
'Programming Language :: Python :: 3.14',
|
|
33
33
|
'Topic :: Scientific/Engineering :: Visualization',
|
|
34
34
|
]
|
|
35
|
-
dependencies = ['pyvista>=0.48']
|
|
35
|
+
dependencies = ['packaging>=22', 'pyvista>=0.48']
|
|
36
36
|
description = 'Sphinx extension for converting docstring examples into downloadable code.'
|
|
37
37
|
dynamic = ['version']
|
|
38
38
|
keywords = ['download', 'examples', 'sphinx']
|
{sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code/__init__.py
RENAMED
|
@@ -7,17 +7,27 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import ast
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
+
import functools
|
|
10
11
|
import hashlib
|
|
12
|
+
import importlib.metadata
|
|
11
13
|
import importlib.util
|
|
12
14
|
import json
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
import re
|
|
17
|
+
import shlex
|
|
18
|
+
import sys
|
|
15
19
|
from typing import TYPE_CHECKING
|
|
16
20
|
from urllib.parse import urljoin
|
|
17
21
|
from urllib.parse import urlsplit
|
|
18
22
|
|
|
19
23
|
from docutils import nodes
|
|
20
24
|
from docutils.core import publish_doctree
|
|
25
|
+
from packaging.requirements import Requirement
|
|
26
|
+
from packaging.specifiers import InvalidSpecifier
|
|
27
|
+
from packaging.specifiers import SpecifierSet
|
|
28
|
+
from packaging.utils import canonicalize_name
|
|
29
|
+
from packaging.version import InvalidVersion
|
|
30
|
+
from packaging.version import Version
|
|
21
31
|
from sphinx import addnodes
|
|
22
32
|
from sphinx.errors import ConfigError
|
|
23
33
|
from sphinx.util import logging
|
|
@@ -921,6 +931,12 @@ def _build_notebook(cells: list[tuple[str, list[str]]]) -> dict:
|
|
|
921
931
|
}
|
|
922
932
|
|
|
923
933
|
|
|
934
|
+
def _download_filename(name: str, extension: str) -> str:
|
|
935
|
+
"""Return the file name a download is written under, e.g. ``pyvista_read.py``."""
|
|
936
|
+
safe_name = name.replace('.', '_') if name else 'example'
|
|
937
|
+
return f'{safe_name}.{extension}'
|
|
938
|
+
|
|
939
|
+
|
|
924
940
|
def _write_download_file(app: Sphinx, name: str, extension: str, content: str) -> str:
|
|
925
941
|
"""Write generated content directly into the builder's downloads dir.
|
|
926
942
|
|
|
@@ -932,8 +948,7 @@ def _write_download_file(app: Sphinx, name: str, extension: str, content: str) -
|
|
|
932
948
|
"""
|
|
933
949
|
# 32 hex chars, matching Sphinx's own native _downloads/<digest>/... layout.
|
|
934
950
|
digest = hashlib.sha256(content.encode()).hexdigest()[:32]
|
|
935
|
-
|
|
936
|
-
filename = f'{safe_name}.{extension}'
|
|
951
|
+
filename = _download_filename(name, extension)
|
|
937
952
|
rel_path = f'{digest}/{filename}'
|
|
938
953
|
|
|
939
954
|
out_path = Path(app.outdir) / '_downloads' / digest / filename
|
|
@@ -968,7 +983,7 @@ def _drop_future_imports(source: list[str]) -> list[str]:
|
|
|
968
983
|
return [line for i, line in enumerate(source, start=1) if i not in dropped]
|
|
969
984
|
|
|
970
985
|
|
|
971
|
-
def _write_marimo_notebook(app: Sphinx, name: str, notebook: dict) -> str:
|
|
986
|
+
def _write_marimo_notebook(app: Sphinx, name: str, notebook: dict, header: str = '') -> str:
|
|
972
987
|
"""Convert a generated Jupyter notebook with marimo's own converter and write it."""
|
|
973
988
|
from marimo._convert.converters import MarimoConvert
|
|
974
989
|
|
|
@@ -980,7 +995,191 @@ def _write_marimo_notebook(app: Sphinx, name: str, notebook: dict) -> str:
|
|
|
980
995
|
for cell in notebook['cells']
|
|
981
996
|
]
|
|
982
997
|
source = MarimoConvert.from_ipynb(json.dumps({**notebook, 'cells': cells})).to_py()
|
|
983
|
-
return _write_download_file(app, name, 'marimo.py', source)
|
|
998
|
+
return _write_download_file(app, name, 'marimo.py', header + source)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
@functools.cache
|
|
1002
|
+
def _module_distributions() -> dict[str, list[str]]:
|
|
1003
|
+
"""Map each importable top-level module to the distribution(s) providing it."""
|
|
1004
|
+
return importlib.metadata.packages_distributions()
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _canonical_name(requirement: str) -> str:
|
|
1008
|
+
"""Return a requirement's normalized name, e.g. ``pyvista`` for ``PyVista[all]>=0.4``."""
|
|
1009
|
+
return canonicalize_name(Requirement(requirement).name)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _distribution_for(module: str) -> str | None:
|
|
1013
|
+
"""Return the one installed distribution providing top-level ``module``, if there is one."""
|
|
1014
|
+
dists = sorted(set(_module_distributions().get(module, [])))
|
|
1015
|
+
if len(dists) == 1:
|
|
1016
|
+
return dists[0]
|
|
1017
|
+
if not dists:
|
|
1018
|
+
# Fall back to a distribution named like the module.
|
|
1019
|
+
try:
|
|
1020
|
+
return importlib.metadata.distribution(module).name
|
|
1021
|
+
except importlib.metadata.PackageNotFoundError:
|
|
1022
|
+
pass
|
|
1023
|
+
logger.debug(
|
|
1024
|
+
'sphinx_examples_as_code: no single distribution provides %r (%s), not listed.',
|
|
1025
|
+
module,
|
|
1026
|
+
', '.join(dists) or 'none installed',
|
|
1027
|
+
)
|
|
1028
|
+
return None
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _imported_distributions(source: str) -> list[str]:
|
|
1032
|
+
"""Return the third-party distributions whose modules ``source`` imports."""
|
|
1033
|
+
try:
|
|
1034
|
+
tree = ast.parse(source)
|
|
1035
|
+
except SyntaxError:
|
|
1036
|
+
return []
|
|
1037
|
+
modules = set()
|
|
1038
|
+
for node in ast.walk(tree):
|
|
1039
|
+
if isinstance(node, ast.Import):
|
|
1040
|
+
modules.update(alias.name.split('.')[0] for alias in node.names)
|
|
1041
|
+
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
|
1042
|
+
modules.add(node.module.split('.')[0])
|
|
1043
|
+
modules -= {*sys.stdlib_module_names, '__future__'}
|
|
1044
|
+
dists = (_distribution_for(module) for module in sorted(modules))
|
|
1045
|
+
return [dist for dist in dists if dist is not None]
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _script_dependencies(
|
|
1049
|
+
source: str, extra: list[str], pins: dict[str, str] | None = None
|
|
1050
|
+
) -> list[str]:
|
|
1051
|
+
"""Return ``source``'s imported distributions, pinned per ``pins``, then ``extra`` on top."""
|
|
1052
|
+
pins = pins or {}
|
|
1053
|
+
deps = {_canonical_name(dist): dist for dist in _imported_distributions(source)}
|
|
1054
|
+
deps.update((name, pin) for name, pin in pins.items() if name in deps)
|
|
1055
|
+
for req in extra:
|
|
1056
|
+
name = _canonical_name(req)
|
|
1057
|
+
deps[name] = _keep_pin(req, pins.get(name))
|
|
1058
|
+
return [deps[key] for key in sorted(deps)]
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def _keep_pin(requirement: str, pin: str | None) -> str:
|
|
1062
|
+
"""Give ``requirement`` the version specifier of ``pin`` when it has none of its own."""
|
|
1063
|
+
parsed = Requirement(requirement)
|
|
1064
|
+
if pin is None or parsed.specifier or parsed.url:
|
|
1065
|
+
return requirement
|
|
1066
|
+
parsed.specifier = Requirement(pin).specifier
|
|
1067
|
+
return str(parsed)
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
@dataclass(frozen=True)
|
|
1071
|
+
class _DocumentedProject:
|
|
1072
|
+
"""The installed distribution named by Sphinx's ``project``."""
|
|
1073
|
+
|
|
1074
|
+
name: str # normalized, as _canonical_name returns it
|
|
1075
|
+
pin: str | None # e.g. 'pyvista>=0.49'; None for a pre-release, dev or unparsable version
|
|
1076
|
+
requires_python: str | None
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
@functools.cache
|
|
1080
|
+
def _documented_project(project: str | None) -> _DocumentedProject | None:
|
|
1081
|
+
"""Look up the installed distribution named like Sphinx's ``project``, if there is one."""
|
|
1082
|
+
if not project:
|
|
1083
|
+
return None
|
|
1084
|
+
name = canonicalize_name(re.sub(r'\s+', '-', project.strip()))
|
|
1085
|
+
try:
|
|
1086
|
+
dist = importlib.metadata.distribution(name)
|
|
1087
|
+
except importlib.metadata.PackageNotFoundError:
|
|
1088
|
+
return None
|
|
1089
|
+
try:
|
|
1090
|
+
version = Version(dist.version)
|
|
1091
|
+
except InvalidVersion:
|
|
1092
|
+
version = None
|
|
1093
|
+
pin = None
|
|
1094
|
+
if version is not None and not version.is_prerelease:
|
|
1095
|
+
pin = f'{dist.name}>={version.major}.{version.minor}'
|
|
1096
|
+
return _DocumentedProject(name, pin, dist.metadata['Requires-Python'])
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
_UV_INSTALL_LINE = 'Install uv first: https://docs.astral.sh/uv/getting-started/installation/'
|
|
1100
|
+
|
|
1101
|
+
_PYTHON_CLASSIFIER = re.compile(r'Programming Language :: Python :: 3\.(\d+)$')
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
@functools.cache
|
|
1105
|
+
def _newest_python_minor(name: str) -> int | None:
|
|
1106
|
+
"""Return the newest ``3.N`` minor an installed distribution's classifiers name, if any."""
|
|
1107
|
+
try:
|
|
1108
|
+
classifiers = importlib.metadata.metadata(name).get_all('Classifier')
|
|
1109
|
+
except importlib.metadata.PackageNotFoundError:
|
|
1110
|
+
return None
|
|
1111
|
+
minors = [int(m[1]) for c in classifiers or [] if (m := _PYTHON_CLASSIFIER.match(c))]
|
|
1112
|
+
return max(minors, default=None)
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
@functools.cache
|
|
1116
|
+
def _ships_compiled_wheel(name: str) -> bool:
|
|
1117
|
+
"""Check whether an installed distribution's wheel is tagged for a platform or ABI."""
|
|
1118
|
+
try:
|
|
1119
|
+
wheel = importlib.metadata.distribution(name).read_text('WHEEL') or ''
|
|
1120
|
+
except importlib.metadata.PackageNotFoundError:
|
|
1121
|
+
return False
|
|
1122
|
+
tags = [
|
|
1123
|
+
line.partition(':')[2].strip() for line in wheel.splitlines() if line.startswith('Tag:')
|
|
1124
|
+
]
|
|
1125
|
+
return any(not tag.endswith('-none-any') for tag in tags)
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _python_upper_bound(deps: list[str], project: str | None = None) -> str | None:
|
|
1129
|
+
"""Return ``<3.N`` just above the newest Python ``project`` and every compiled dep name."""
|
|
1130
|
+
names = [_canonical_name(dep) for dep in deps]
|
|
1131
|
+
newest = [_newest_python_minor(n) for n in names if n == project or _ships_compiled_wheel(n)]
|
|
1132
|
+
known = [minor for minor in newest if minor is not None]
|
|
1133
|
+
return f'<3.{min(known) + 1}' if known else None
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _python_range(floor: str | None, cap: str | None) -> str | None:
|
|
1137
|
+
"""Join ``floor`` and ``cap``, dropping ``cap`` when no ``3.N`` Python would be left."""
|
|
1138
|
+
if floor and cap:
|
|
1139
|
+
allowed = SpecifierSet(f'{floor},{cap}')
|
|
1140
|
+
if not any(f'3.{minor}' in allowed for minor in range(100)):
|
|
1141
|
+
cap = None
|
|
1142
|
+
return ','.join(filter(None, (floor, cap))) or None
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _script_metadata_header(deps: list[str], requires_python: str | None, run_line: str) -> str:
|
|
1146
|
+
"""Build a PEP 723 ``# /// script`` block followed by a one-line run instruction."""
|
|
1147
|
+
lines = ['# /// script']
|
|
1148
|
+
if requires_python:
|
|
1149
|
+
lines.append(f'# requires-python = {json.dumps(requires_python)}')
|
|
1150
|
+
if deps:
|
|
1151
|
+
lines.append('# dependencies = [')
|
|
1152
|
+
lines.extend(f'# {json.dumps(dep)},' for dep in deps)
|
|
1153
|
+
lines.append('# ]')
|
|
1154
|
+
else:
|
|
1155
|
+
lines.append('# dependencies = []')
|
|
1156
|
+
lines.extend(['# ///', f'# {run_line}', f'# {_UV_INSTALL_LINE}', ''])
|
|
1157
|
+
return '\n'.join(lines) + '\n'
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
_BARE_SHELL_ARG = re.compile(r'[\w./-][\w./:@+=-]*')
|
|
1161
|
+
#: Characters some shell still expands inside double quotes; ``!=`` is safe everywhere.
|
|
1162
|
+
_DOUBLE_QUOTE_UNSAFE = re.compile(r'["$`\\%]|!(?!=)')
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _quote_shell_arg(arg: str) -> str:
|
|
1166
|
+
"""Quote a command argument for bash, zsh, PowerShell and cmd alike, where possible."""
|
|
1167
|
+
if _BARE_SHELL_ARG.fullmatch(arg):
|
|
1168
|
+
return arg
|
|
1169
|
+
if not _DOUBLE_QUOTE_UNSAFE.search(arg):
|
|
1170
|
+
return f'"{arg}"'
|
|
1171
|
+
return shlex.quote(arg)
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _jupyter_run_line(deps: list[str], requires_python: str | None, filename: str) -> str:
|
|
1175
|
+
"""Build the one-line command that opens a generated notebook with its dependencies."""
|
|
1176
|
+
python_arg = f' --python {_quote_shell_arg(requires_python)}' if requires_python else ''
|
|
1177
|
+
# Swap marker double quotes for single quotes.
|
|
1178
|
+
dep_args = (_quote_shell_arg(dep.replace('"', "'")) for dep in deps)
|
|
1179
|
+
with_args = ''.join(f' --with {arg}' for arg in dep_args)
|
|
1180
|
+
notebook = _quote_shell_arg(filename)
|
|
1181
|
+
command = f'uvx{python_arg} --from jupyterlab{with_args} jupyter-lab {notebook}'
|
|
1182
|
+
return f'Open this notebook with: `{command}`'
|
|
984
1183
|
|
|
985
1184
|
|
|
986
1185
|
#: Default download link text per format -- see ``sphinx_examples_as_code_conf['link_labels']``.
|
|
@@ -1033,6 +1232,7 @@ def _build_download_entries(
|
|
|
1033
1232
|
formats: list[str],
|
|
1034
1233
|
footer: str | None,
|
|
1035
1234
|
link_labels: dict[str, str],
|
|
1235
|
+
script_metadata: dict | None = None,
|
|
1036
1236
|
) -> list[tuple[str, str]]:
|
|
1037
1237
|
"""Convert a span of nodes into written download files, per ``formats``.
|
|
1038
1238
|
|
|
@@ -1062,17 +1262,42 @@ def _build_download_entries(
|
|
|
1062
1262
|
if not _has_real_code(source):
|
|
1063
1263
|
return []
|
|
1064
1264
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1265
|
+
deps: dict[str, list[str]] = {}
|
|
1266
|
+
project = None
|
|
1267
|
+
if script_metadata is not None:
|
|
1268
|
+
implied = {_FMT_MARIMO: ['marimo']}
|
|
1269
|
+
extra = script_metadata['dependencies']
|
|
1270
|
+
project = _documented_project(script_metadata['project'])
|
|
1271
|
+
pins = {project.name: project.pin} if project and project.pin else {}
|
|
1272
|
+
deps = {
|
|
1273
|
+
fmt: _script_dependencies(source, [*implied.get(fmt, []), *extra.get(fmt, [])], pins)
|
|
1274
|
+
for fmt in formats
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
def notebook_for(run_line: str | None) -> dict:
|
|
1067
1278
|
jupyter_ctx = _RenderContext(app=app, docname=docname, fmt=_FMT_JUPYTER)
|
|
1068
1279
|
jupyter_segments = _build_segments(nodes_in_span, jupyter_ctx)
|
|
1069
1280
|
jupyter_header = _title_underline_segment(header_title, 1, _FMT_JUPYTER)
|
|
1281
|
+
run_segments = [('text', [f'# {run_line}', f'# {_UV_INSTALL_LINE}'])] if run_line else []
|
|
1070
1282
|
# Converted separately from the footer and concatenated, rather
|
|
1071
1283
|
# than joined into one segment list and converted together --
|
|
1072
1284
|
# the footer always gets its own dedicated cell this way.
|
|
1073
|
-
cells = _segments_to_cells([jupyter_header, *jupyter_segments])
|
|
1285
|
+
cells = _segments_to_cells([jupyter_header, *run_segments, *jupyter_segments])
|
|
1074
1286
|
cells.extend(_segments_to_cells(_footer_segment(footer, _FMT_JUPYTER, app, docname)))
|
|
1075
|
-
|
|
1287
|
+
return _build_notebook(cells)
|
|
1288
|
+
|
|
1289
|
+
def requires_python_for(fmt: str) -> str | None:
|
|
1290
|
+
if script_metadata['requires_python']:
|
|
1291
|
+
return script_metadata['requires_python']
|
|
1292
|
+
listed = {_canonical_name(dep) for dep in deps[fmt]}
|
|
1293
|
+
floor = project.requires_python if project and project.name in listed else None
|
|
1294
|
+
cap = _python_upper_bound(deps[fmt], project.name if project else None)
|
|
1295
|
+
return _python_range(floor, cap)
|
|
1296
|
+
|
|
1297
|
+
def header_for(fmt: str, run_line: str) -> str:
|
|
1298
|
+
if not deps:
|
|
1299
|
+
return ''
|
|
1300
|
+
return _script_metadata_header(deps[fmt], requires_python_for(fmt), run_line)
|
|
1076
1301
|
|
|
1077
1302
|
entries = []
|
|
1078
1303
|
for fmt in _FORMAT_ORDER:
|
|
@@ -1080,11 +1305,21 @@ def _build_download_entries(
|
|
|
1080
1305
|
continue
|
|
1081
1306
|
label = link_labels[fmt]
|
|
1082
1307
|
if fmt == _FMT_PYTHON:
|
|
1083
|
-
|
|
1308
|
+
filename = _download_filename(name, 'py')
|
|
1309
|
+
header = header_for(fmt, f'Run this script with: uv run {filename}')
|
|
1310
|
+
rel_path = _write_python_script(app, name, header + source)
|
|
1084
1311
|
elif fmt == _FMT_JUPYTER:
|
|
1085
|
-
|
|
1312
|
+
filename = _download_filename(name, 'ipynb')
|
|
1313
|
+
run_line = (
|
|
1314
|
+
_jupyter_run_line(deps[fmt], requires_python_for(fmt), filename) if deps else None
|
|
1315
|
+
)
|
|
1316
|
+
rel_path = _write_jupyter_notebook(app, name, notebook_for(run_line))
|
|
1086
1317
|
else:
|
|
1087
|
-
|
|
1318
|
+
filename = _download_filename(name, 'marimo.py')
|
|
1319
|
+
run_line = f'Open this notebook with: uvx marimo edit --sandbox {filename}'
|
|
1320
|
+
rel_path = _write_marimo_notebook(
|
|
1321
|
+
app, name, notebook_for(None), header_for(fmt, run_line)
|
|
1322
|
+
)
|
|
1088
1323
|
entries.append((label, rel_path))
|
|
1089
1324
|
|
|
1090
1325
|
return entries
|
|
@@ -1102,6 +1337,7 @@ def _process_span(
|
|
|
1102
1337
|
formats: list[str],
|
|
1103
1338
|
footer: str | None,
|
|
1104
1339
|
link_labels: dict[str, str],
|
|
1340
|
+
script_metadata: dict | None = None,
|
|
1105
1341
|
) -> None:
|
|
1106
1342
|
"""Convert one Examples span and insert download link(s) if it has real code."""
|
|
1107
1343
|
nodes_in_span = list(parent.children[start:end])
|
|
@@ -1116,6 +1352,7 @@ def _process_span(
|
|
|
1116
1352
|
formats,
|
|
1117
1353
|
footer,
|
|
1118
1354
|
link_labels,
|
|
1355
|
+
script_metadata,
|
|
1119
1356
|
)
|
|
1120
1357
|
if not entries:
|
|
1121
1358
|
return
|
|
@@ -1196,6 +1433,7 @@ def _process_gallery_page(
|
|
|
1196
1433
|
formats: list[str],
|
|
1197
1434
|
footer: str | None,
|
|
1198
1435
|
link_labels: dict[str, str],
|
|
1436
|
+
script_metadata: dict | None = None,
|
|
1199
1437
|
) -> None:
|
|
1200
1438
|
"""Replace a sphinx-gallery page's own download footer with converted downloads.
|
|
1201
1439
|
|
|
@@ -1238,6 +1476,7 @@ def _process_gallery_page(
|
|
|
1238
1476
|
formats,
|
|
1239
1477
|
footer,
|
|
1240
1478
|
link_labels,
|
|
1479
|
+
script_metadata,
|
|
1241
1480
|
)
|
|
1242
1481
|
if not entries:
|
|
1243
1482
|
return
|
|
@@ -1263,9 +1502,20 @@ def _process_doctree(app: Sphinx, doctree: nodes.document, docname: str) -> None
|
|
|
1263
1502
|
formats = conf['formats']
|
|
1264
1503
|
footer = conf['footer']
|
|
1265
1504
|
link_labels = conf['link_labels']
|
|
1505
|
+
script_metadata = (
|
|
1506
|
+
{
|
|
1507
|
+
'dependencies': conf['dependencies'],
|
|
1508
|
+
'requires_python': conf['requires_python'],
|
|
1509
|
+
'project': app.config.project,
|
|
1510
|
+
}
|
|
1511
|
+
if conf['script_metadata']
|
|
1512
|
+
else None
|
|
1513
|
+
)
|
|
1266
1514
|
|
|
1267
1515
|
if conf['gallery_downloads']:
|
|
1268
|
-
_process_gallery_page(
|
|
1516
|
+
_process_gallery_page(
|
|
1517
|
+
app, docname, doctree, position, formats, footer, link_labels, script_metadata
|
|
1518
|
+
)
|
|
1269
1519
|
|
|
1270
1520
|
# Process spans per shared parent, last to first.
|
|
1271
1521
|
spans = _examples_spans(doctree)
|
|
@@ -1285,6 +1535,7 @@ def _process_doctree(app: Sphinx, doctree: nodes.document, docname: str) -> None
|
|
|
1285
1535
|
formats,
|
|
1286
1536
|
footer,
|
|
1287
1537
|
link_labels,
|
|
1538
|
+
script_metadata,
|
|
1288
1539
|
)
|
|
1289
1540
|
|
|
1290
1541
|
|
|
@@ -1302,6 +1553,9 @@ _CONF_DEFAULTS: dict[str, object] = {
|
|
|
1302
1553
|
'gallery_downloads': False,
|
|
1303
1554
|
'footer': _DEFAULT_FOOTER,
|
|
1304
1555
|
'link_labels': _DEFAULT_LINK_LABELS,
|
|
1556
|
+
'script_metadata': False,
|
|
1557
|
+
'dependencies': {},
|
|
1558
|
+
'requires_python': None,
|
|
1305
1559
|
}
|
|
1306
1560
|
|
|
1307
1561
|
|
|
@@ -1313,20 +1567,20 @@ def _coerce_conf_value(key: str, value: object) -> object:
|
|
|
1313
1567
|
command line always arrives as a plain string -- Sphinx's own type
|
|
1314
1568
|
coercion for ``-D`` overrides (bool ``0``/``1``, comma-split lists, ...)
|
|
1315
1569
|
only applies to a config value's top level, never to a dict's individual
|
|
1316
|
-
keys, so it's redone here for the
|
|
1570
|
+
keys, so it's redone here for the keys that need it.
|
|
1317
1571
|
"""
|
|
1318
1572
|
if not isinstance(value, str):
|
|
1319
1573
|
return value
|
|
1320
1574
|
if key == 'formats':
|
|
1321
1575
|
return value.split(',')
|
|
1322
|
-
if key
|
|
1576
|
+
if key in ('gallery_downloads', 'script_metadata'):
|
|
1323
1577
|
if value == '0':
|
|
1324
1578
|
return False
|
|
1325
1579
|
if value == '1':
|
|
1326
1580
|
return True
|
|
1327
1581
|
msg = f"sphinx_examples_as_code_conf[{key!r}] must be '0' or '1', got {value!r}."
|
|
1328
1582
|
raise ConfigError(msg)
|
|
1329
|
-
return value # link_position, footer: a string is already the real type
|
|
1583
|
+
return value # link_position, footer, requires_python: a string is already the real type
|
|
1330
1584
|
|
|
1331
1585
|
|
|
1332
1586
|
def _merged_link_labels(value: object) -> dict[str, str]:
|
|
@@ -1373,6 +1627,49 @@ def _validate_formats(formats: list[str]) -> None:
|
|
|
1373
1627
|
raise ConfigError(msg + _renamed_hint(unknown))
|
|
1374
1628
|
|
|
1375
1629
|
|
|
1630
|
+
def _validate_dependencies(dependencies: object) -> None:
|
|
1631
|
+
"""Raise a ``ConfigError`` unless ``dependencies`` maps known formats to lists of strings."""
|
|
1632
|
+
key = "sphinx_examples_as_code_conf['dependencies']"
|
|
1633
|
+
if not isinstance(dependencies, dict):
|
|
1634
|
+
msg = f'{key} must be a dict, got {dependencies!r}.'
|
|
1635
|
+
raise ConfigError(msg)
|
|
1636
|
+
unknown = sorted(set(dependencies) - set(_FORMAT_ORDER))
|
|
1637
|
+
if unknown:
|
|
1638
|
+
msg = f'{key} has unknown format(s) {unknown} (valid: {", ".join(_FORMAT_ORDER)}).'
|
|
1639
|
+
raise ConfigError(msg + _renamed_hint(unknown))
|
|
1640
|
+
for fmt, reqs in dependencies.items():
|
|
1641
|
+
if not isinstance(reqs, list) or not all(_is_requirement(req) for req in reqs):
|
|
1642
|
+
msg = f'{key}[{fmt!r}] must be a list of requirement strings, got {reqs!r}.'
|
|
1643
|
+
raise ConfigError(msg)
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
def _is_requirement(value: object) -> bool:
|
|
1647
|
+
"""Check whether ``value`` is a valid PEP 508 requirement string."""
|
|
1648
|
+
try:
|
|
1649
|
+
Requirement(value)
|
|
1650
|
+
except (TypeError, ValueError):
|
|
1651
|
+
return False
|
|
1652
|
+
return True
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def _validate_requires_python(requires_python: object) -> None:
|
|
1656
|
+
"""Raise a ``ConfigError`` unless ``requires_python`` is ``None`` or a non-empty string."""
|
|
1657
|
+
if requires_python is None:
|
|
1658
|
+
return
|
|
1659
|
+
if isinstance(requires_python, str) and requires_python.strip():
|
|
1660
|
+
try:
|
|
1661
|
+
SpecifierSet(requires_python)
|
|
1662
|
+
except InvalidSpecifier:
|
|
1663
|
+
pass
|
|
1664
|
+
else:
|
|
1665
|
+
return
|
|
1666
|
+
msg = (
|
|
1667
|
+
"sphinx_examples_as_code_conf['requires_python'] must be a specifier string such as "
|
|
1668
|
+
f"'>=3.10', or None, got {requires_python!r}."
|
|
1669
|
+
)
|
|
1670
|
+
raise ConfigError(msg)
|
|
1671
|
+
|
|
1672
|
+
|
|
1376
1673
|
def _marimo_converter_available() -> bool:
|
|
1377
1674
|
"""Check whether marimo's notebook converter (marimo 0.14 or newer) is importable."""
|
|
1378
1675
|
try:
|
|
@@ -1399,6 +1696,8 @@ def _finalize_conf(_app: Sphinx, config: Config) -> None:
|
|
|
1399
1696
|
merged = dict(_CONF_DEFAULTS)
|
|
1400
1697
|
merged.update((key, _coerce_conf_value(key, value)) for key, value in user_conf.items())
|
|
1401
1698
|
merged['link_labels'] = _merged_link_labels(merged['link_labels'])
|
|
1699
|
+
_validate_dependencies(merged['dependencies'])
|
|
1700
|
+
_validate_requires_python(merged['requires_python'])
|
|
1402
1701
|
_validate_formats(merged['formats'])
|
|
1403
1702
|
if _FMT_MARIMO in merged['formats'] and not _marimo_converter_available():
|
|
1404
1703
|
if 'formats' in user_conf:
|
{sphinx_examples_as_code-0.7.0 → sphinx_examples_as_code-0.8.0}/sphinx_examples_as_code/_version.py
RENAMED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.8.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 8, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g35e7c7f07'
|