zallery 0.0.1__py3-none-any.whl
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.
- zallery/__init__.py +11 -0
- zallery/__main__.py +5 -0
- zallery/_types.py +43 -0
- zallery/cache.py +58 -0
- zallery/cli.py +96 -0
- zallery/config.py +204 -0
- zallery/discover.py +54 -0
- zallery/execute.py +245 -0
- zallery/index.py +155 -0
- zallery/pipeline.py +418 -0
- zallery/py.typed +0 -0
- zallery/render.py +405 -0
- zallery/theme.py +274 -0
- zallery-0.0.1.dist-info/METADATA +232 -0
- zallery-0.0.1.dist-info/RECORD +18 -0
- zallery-0.0.1.dist-info/WHEEL +4 -0
- zallery-0.0.1.dist-info/entry_points.txt +3 -0
- zallery-0.0.1.dist-info/licenses/LICENSE +27 -0
zallery/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Build a light-and-dark example gallery for Zensical docs from jupytext scripts.
|
|
2
|
+
|
|
3
|
+
Examples are percent-format `.py` files executed once in an ipykernel; each
|
|
4
|
+
displayed matplotlib figure is rendered for both themes from that single run
|
|
5
|
+
(see `zallery.theme`). [`build_gallery`][zallery.build_gallery] runs the whole
|
|
6
|
+
pipeline: discover, cache, execute, render, index.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from zallery.pipeline import build_gallery
|
|
10
|
+
|
|
11
|
+
__all__ = ["build_gallery"]
|
zallery/__main__.py
ADDED
zallery/_types.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shared dataclasses for the gallery builder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ExampleSpec:
|
|
11
|
+
"""A single discovered example file.
|
|
12
|
+
|
|
13
|
+
Attributes
|
|
14
|
+
----------
|
|
15
|
+
source : pathlib.Path
|
|
16
|
+
Absolute path to the percent-format `.py` file.
|
|
17
|
+
base_name : str
|
|
18
|
+
Output stem used for the rendered Markdown, downloads, and cache entry.
|
|
19
|
+
Derived from `source.stem` with any leading numeric ordering prefix
|
|
20
|
+
(e.g. `01_`) stripped, so the source filename can control card order
|
|
21
|
+
without leaking the prefix into URLs.
|
|
22
|
+
section : str
|
|
23
|
+
Section folder name (e.g. `"io"`).
|
|
24
|
+
section_intro : str
|
|
25
|
+
Contents of the sibling `_section.md` file, or empty string if none.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
source: Path
|
|
29
|
+
base_name: str
|
|
30
|
+
section: str
|
|
31
|
+
section_intro: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class RenderedExample:
|
|
36
|
+
"""An example after the renderer has produced its artifacts."""
|
|
37
|
+
|
|
38
|
+
spec: ExampleSpec
|
|
39
|
+
title: str
|
|
40
|
+
summary: str
|
|
41
|
+
md_path: Path
|
|
42
|
+
thumbnail_light: Path | None
|
|
43
|
+
thumbnail_dark: Path | None
|
zallery/cache.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Cache key + cache directory helpers for the gallery builder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from zallery._types import ExampleSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cache_key(
|
|
12
|
+
spec: ExampleSpec,
|
|
13
|
+
*,
|
|
14
|
+
deps_fingerprint: str,
|
|
15
|
+
python_version: str,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Compute a deterministic cache key for one example.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
spec : ExampleSpec
|
|
22
|
+
The example; its source file content is part of the key.
|
|
23
|
+
deps_fingerprint : str
|
|
24
|
+
Text identifying everything else that shapes the render (lockfile,
|
|
25
|
+
builder sources, configuration).
|
|
26
|
+
python_version : str
|
|
27
|
+
Version of the Python interpreter the example runs under.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
str
|
|
32
|
+
SHA-256 hex digest of the three inputs.
|
|
33
|
+
"""
|
|
34
|
+
digest = hashlib.sha256()
|
|
35
|
+
digest.update(spec.source.read_bytes())
|
|
36
|
+
digest.update(b"\x00")
|
|
37
|
+
digest.update(deps_fingerprint.encode("utf-8"))
|
|
38
|
+
digest.update(b"\x00")
|
|
39
|
+
digest.update(python_version.encode("utf-8"))
|
|
40
|
+
return digest.hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cache_dir(root: Path, key: str) -> Path:
|
|
44
|
+
"""Return the cache directory for `key` rooted at `root`.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
root : pathlib.Path
|
|
49
|
+
Cache root directory.
|
|
50
|
+
key : str
|
|
51
|
+
Cache key of one example, see `cache_key`.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
pathlib.Path
|
|
56
|
+
Directory holding that example's cached artifacts.
|
|
57
|
+
"""
|
|
58
|
+
return root / key
|
zallery/cli.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Command line interface: `zallery [--no-run] [--no-cache] [EXAMPLE ...]`.
|
|
2
|
+
|
|
3
|
+
Run from anywhere inside a project whose `pyproject.toml` has a `[tool.zallery]`
|
|
4
|
+
table (see `zallery.config`). With no arguments every example is executed; name
|
|
5
|
+
example scripts to run only those. The whole gallery is rendered either way: the
|
|
6
|
+
examples not run are restored from cache if present, or rendered without outputs
|
|
7
|
+
(code and Markdown only).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from zallery.config import ConfigError, deps_fingerprint, load_config
|
|
19
|
+
from zallery.pipeline import build_gallery
|
|
20
|
+
|
|
21
|
+
BINDER_REF_ENV = "ZALLERY_BINDER_REF"
|
|
22
|
+
"""Environment variable giving the git ref stamped into Binder URLs (default `main`)."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
26
|
+
"""Build the gallery of the project the working directory belongs to.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
argv : Sequence[str], optional
|
|
31
|
+
Command line arguments. If not provided, `sys.argv[1:]` is used.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
int
|
|
36
|
+
Exit status: 0 on success, 1 when the examples directory is missing or an
|
|
37
|
+
argument names no example, 2 when no configuration is found.
|
|
38
|
+
"""
|
|
39
|
+
parser = argparse.ArgumentParser(prog="zallery", description=__doc__)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"examples",
|
|
42
|
+
nargs="*",
|
|
43
|
+
type=Path,
|
|
44
|
+
help=(
|
|
45
|
+
"Example scripts to run. The whole gallery is still rendered; unnamed "
|
|
46
|
+
"examples are taken from cache if present, or rendered without outputs. "
|
|
47
|
+
"If not provided, all examples are run."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--no-run",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help=(
|
|
54
|
+
"Run no example at all: every page is taken from cache if present, or "
|
|
55
|
+
"rendered without outputs. Cannot be combined with example paths."
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--no-cache",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help=(
|
|
62
|
+
"Execute the selected examples even if a cached result exists, and "
|
|
63
|
+
"replace it. Examples not selected still come from cache."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
args = parser.parse_args(argv)
|
|
67
|
+
if args.no_run and args.examples:
|
|
68
|
+
parser.error("--no-run cannot be combined with example paths.")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
config = load_config(Path.cwd())
|
|
72
|
+
except ConfigError as exc:
|
|
73
|
+
print(f"zallery: {exc}", file=sys.stderr)
|
|
74
|
+
return 2
|
|
75
|
+
if not config.examples.is_dir():
|
|
76
|
+
print(f"No examples directory at {config.examples}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
build_gallery(
|
|
81
|
+
examples_root=config.examples,
|
|
82
|
+
built_dir=config.built,
|
|
83
|
+
cache_root=config.cache,
|
|
84
|
+
deps_fingerprint=deps_fingerprint(config),
|
|
85
|
+
repo_root=config.root,
|
|
86
|
+
binder_repo=config.binder_repo,
|
|
87
|
+
binder_ref=os.environ.get(BINDER_REF_ENV, "main"),
|
|
88
|
+
only=[] if args.no_run else (args.examples or None),
|
|
89
|
+
use_cache=not args.no_cache,
|
|
90
|
+
hook_module=config.hook,
|
|
91
|
+
hook_root=config.root,
|
|
92
|
+
)
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
print(exc, file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
return 0
|
zallery/config.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Project configuration, read from the `[tool.zallery]` table of `pyproject.toml`.
|
|
2
|
+
|
|
3
|
+
The table is looked up from the working directory upwards, so `zallery` can run
|
|
4
|
+
from anywhere inside the project. Relative paths in it are resolved against the
|
|
5
|
+
directory holding that `pyproject.toml`, which is also the project root the kernel
|
|
6
|
+
gets on its `sys.path` so the theme hook module imports.
|
|
7
|
+
|
|
8
|
+
Keys (all optional):
|
|
9
|
+
|
|
10
|
+
- `examples`: directory of example sections, default `docs/examples`.
|
|
11
|
+
- `built`: where rendered pages go, default `<examples>/_built`.
|
|
12
|
+
- `cache`: execution/render cache directory, default `.gallery-cache`.
|
|
13
|
+
- `binder_repo`: `owner/repo` slug for the Binder launch buttons; no buttons if unset.
|
|
14
|
+
- `hook`: dotted name of the module defining `gallery_retheme(fig, theme)`.
|
|
15
|
+
- `fingerprint`: files whose content invalidates the cache when it changes,
|
|
16
|
+
default `["uv.lock"]`. The `zallery` sources, the hook module and the table
|
|
17
|
+
itself are always part of the fingerprint.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import tomllib
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import zallery
|
|
28
|
+
|
|
29
|
+
_TABLE = "zallery"
|
|
30
|
+
"""Name of the table under `[tool]`."""
|
|
31
|
+
|
|
32
|
+
_KNOWN_KEYS = frozenset(
|
|
33
|
+
{"examples", "built", "cache", "binder_repo", "hook", "fingerprint"}
|
|
34
|
+
)
|
|
35
|
+
"""Keys the table may contain; anything else is a typo and rejected."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ConfigError(ValueError):
|
|
39
|
+
"""Raised when no usable `[tool.zallery]` table is found or it is malformed."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Config:
|
|
44
|
+
"""Resolved project configuration.
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
root : pathlib.Path
|
|
49
|
+
Directory holding the `pyproject.toml` the table was read from.
|
|
50
|
+
examples : pathlib.Path
|
|
51
|
+
Directory of example sections.
|
|
52
|
+
built : pathlib.Path
|
|
53
|
+
Directory rendered pages and assets are written to.
|
|
54
|
+
cache : pathlib.Path
|
|
55
|
+
Execution/render cache directory.
|
|
56
|
+
binder_repo : str, optional
|
|
57
|
+
`owner/repo` slug for Binder launch buttons. If not provided, no buttons.
|
|
58
|
+
hook : str, optional
|
|
59
|
+
Dotted name of the project's theme hook module. If not provided, only the
|
|
60
|
+
built-in swap runs.
|
|
61
|
+
fingerprint_files : tuple[pathlib.Path, ...]
|
|
62
|
+
Files whose content is part of the cache fingerprint.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
root: Path
|
|
66
|
+
examples: Path
|
|
67
|
+
built: Path
|
|
68
|
+
cache: Path
|
|
69
|
+
binder_repo: str | None
|
|
70
|
+
hook: str | None
|
|
71
|
+
fingerprint_files: tuple[Path, ...]
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def hook_file(self) -> Path | None:
|
|
75
|
+
"""Source file of the hook module under `root`, if the module is configured.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
pathlib.Path or None
|
|
80
|
+
`<root>/a/b.py` or `<root>/a/b/__init__.py` for `hook = "a.b"`, whichever
|
|
81
|
+
exists, else the module path with `.py`; `None` without a hook.
|
|
82
|
+
"""
|
|
83
|
+
if self.hook is None:
|
|
84
|
+
return None
|
|
85
|
+
base = self.root.joinpath(*self.hook.split("."))
|
|
86
|
+
package_init = base / "__init__.py"
|
|
87
|
+
return package_init if package_init.is_file() else base.with_suffix(".py")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_pyproject(start: Path) -> Path | None:
|
|
91
|
+
"""Return the nearest `pyproject.toml` with a `[tool.zallery]` table.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
start : pathlib.Path
|
|
96
|
+
Directory to search from; its parents are searched in turn.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
pathlib.Path or None
|
|
101
|
+
The file, or `None` if no ancestor holds one with the table.
|
|
102
|
+
"""
|
|
103
|
+
for directory in (start.resolve(), *start.resolve().parents):
|
|
104
|
+
candidate = directory / "pyproject.toml"
|
|
105
|
+
if candidate.is_file():
|
|
106
|
+
with candidate.open("rb") as handle:
|
|
107
|
+
data = tomllib.load(handle)
|
|
108
|
+
if _TABLE in data.get("tool", {}):
|
|
109
|
+
return candidate
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_config(start: Path) -> Config:
|
|
114
|
+
"""Load the configuration that applies to `start`.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
start : pathlib.Path
|
|
119
|
+
Directory to search from, typically the working directory.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
Config
|
|
124
|
+
The resolved configuration, with relative paths made absolute under the
|
|
125
|
+
project root.
|
|
126
|
+
|
|
127
|
+
Raises
|
|
128
|
+
------
|
|
129
|
+
ConfigError
|
|
130
|
+
If no `pyproject.toml` with a `[tool.zallery]` table is found, or the table
|
|
131
|
+
has unknown keys.
|
|
132
|
+
"""
|
|
133
|
+
pyproject = find_pyproject(start)
|
|
134
|
+
if pyproject is None:
|
|
135
|
+
raise ConfigError(
|
|
136
|
+
f"No pyproject.toml with a [tool.zallery] table found in {start} or its "
|
|
137
|
+
"parents."
|
|
138
|
+
)
|
|
139
|
+
with pyproject.open("rb") as handle:
|
|
140
|
+
table = tomllib.load(handle)["tool"][_TABLE]
|
|
141
|
+
unknown = sorted(set(table) - _KNOWN_KEYS)
|
|
142
|
+
if unknown:
|
|
143
|
+
raise ConfigError(
|
|
144
|
+
f"Unknown keys in [tool.zallery] of {pyproject}: {', '.join(unknown)}. "
|
|
145
|
+
f"Known keys: {', '.join(sorted(_KNOWN_KEYS))}."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
root = pyproject.parent
|
|
149
|
+
examples = root / table.get("examples", "docs/examples")
|
|
150
|
+
return Config(
|
|
151
|
+
root=root,
|
|
152
|
+
examples=examples,
|
|
153
|
+
built=root / table["built"] if "built" in table else examples / "_built",
|
|
154
|
+
cache=root / table.get("cache", ".gallery-cache"),
|
|
155
|
+
binder_repo=table.get("binder_repo"),
|
|
156
|
+
hook=table.get("hook"),
|
|
157
|
+
fingerprint_files=tuple(
|
|
158
|
+
root / p for p in table.get("fingerprint", ["uv.lock"])
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def deps_fingerprint(config: Config) -> str:
|
|
164
|
+
"""Return a string identifying everything that shapes an example's render.
|
|
165
|
+
|
|
166
|
+
Any change to it makes every cached example miss: the configured fingerprint
|
|
167
|
+
files (the lockfile by default), the `zallery` sources, the hook module, and
|
|
168
|
+
the configuration values themselves. The Binder ref is deliberately not part of
|
|
169
|
+
it, so renders are reused across branches.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
config : Config
|
|
174
|
+
The project configuration.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
str
|
|
179
|
+
Concatenated, locale-independent text of all the inputs.
|
|
180
|
+
"""
|
|
181
|
+
parts: list[str] = [
|
|
182
|
+
json.dumps(
|
|
183
|
+
{
|
|
184
|
+
"examples": str(config.examples.relative_to(config.root)),
|
|
185
|
+
"built": str(config.built.relative_to(config.root)),
|
|
186
|
+
"binder_repo": config.binder_repo,
|
|
187
|
+
"hook": config.hook,
|
|
188
|
+
},
|
|
189
|
+
sort_keys=True,
|
|
190
|
+
)
|
|
191
|
+
]
|
|
192
|
+
# Read everything as UTF-8 so the text does not depend on the platform's locale.
|
|
193
|
+
for path in config.fingerprint_files:
|
|
194
|
+
if path.is_file():
|
|
195
|
+
parts.append(f"\n# {path.relative_to(config.root)}\n")
|
|
196
|
+
parts.append(path.read_text(encoding="utf-8"))
|
|
197
|
+
for path in sorted(Path(zallery.__file__).resolve().parent.glob("*.py")):
|
|
198
|
+
parts.append(f"\n# zallery/{path.name}\n")
|
|
199
|
+
parts.append(path.read_text(encoding="utf-8"))
|
|
200
|
+
hook_file = config.hook_file
|
|
201
|
+
if hook_file is not None and hook_file.is_file():
|
|
202
|
+
parts.append(f"\n# {hook_file.relative_to(config.root)}\n")
|
|
203
|
+
parts.append(hook_file.read_text(encoding="utf-8"))
|
|
204
|
+
return "".join(parts)
|
zallery/discover.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Discover example source files under the examples root."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from zallery._types import ExampleSpec
|
|
9
|
+
|
|
10
|
+
# Strip leading "01_", "02_" etc. from section directory names and example file
|
|
11
|
+
# names so the built output uses clean names ("io", "decomposition",
|
|
12
|
+
# "pca_single_recording") while the source paths can be prefixed for explicit
|
|
13
|
+
# ordering.
|
|
14
|
+
_NUMERIC_PREFIX_RE = re.compile(r"^\d+_")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def discover(root: Path) -> list[ExampleSpec]:
|
|
18
|
+
"""Return one example spec per example file.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
root : pathlib.Path
|
|
23
|
+
Examples root; each subdirectory not starting with `_` is a section.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
list[ExampleSpec]
|
|
28
|
+
One spec per `.py` file not starting with `_`, sorted by section folder
|
|
29
|
+
then file name. Empty if `root` is not a directory.
|
|
30
|
+
"""
|
|
31
|
+
specs: list[ExampleSpec] = []
|
|
32
|
+
if not root.is_dir():
|
|
33
|
+
return specs
|
|
34
|
+
|
|
35
|
+
for section_dir in sorted(
|
|
36
|
+
p for p in root.iterdir() if p.is_dir() and not p.name.startswith("_")
|
|
37
|
+
):
|
|
38
|
+
intro_path = section_dir / "_section.md"
|
|
39
|
+
intro = intro_path.read_text(encoding="utf-8") if intro_path.is_file() else ""
|
|
40
|
+
section_name = _NUMERIC_PREFIX_RE.sub("", section_dir.name)
|
|
41
|
+
|
|
42
|
+
for source in sorted(section_dir.glob("*.py")):
|
|
43
|
+
if source.name.startswith("_"):
|
|
44
|
+
continue
|
|
45
|
+
specs.append(
|
|
46
|
+
ExampleSpec(
|
|
47
|
+
source=source,
|
|
48
|
+
base_name=_NUMERIC_PREFIX_RE.sub("", source.stem),
|
|
49
|
+
section=section_name,
|
|
50
|
+
section_intro=intro,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return specs
|