|
1 | 1 | import importlib
|
| 2 | +import os |
| 3 | +import re |
2 | 4 | import shutil
|
3 | 5 | from pathlib import Path
|
| 6 | +from typing import TypeVar |
| 7 | + |
| 8 | +import requests |
| 9 | +import yaml |
| 10 | +from docutils import nodes |
| 11 | +from docutils.statemachine import StringList |
| 12 | +from sphinx.application import Sphinx |
| 13 | +from sphinx.directives.other import TocTree |
| 14 | +from sphinx.util.docutils import SphinxDirective |
4 | 15 |
|
5 | 16 | author = "dynamicslab"
|
6 | 17 | project = "pysindy" # package name
|
|
42 | 53 |
|
43 | 54 | here = Path(__file__).parent.resolve()
|
44 | 55 |
|
45 |
| -if (here / "static/custom.css").exists(): |
46 |
| - html_static_path = ["static"] |
47 |
| - |
48 | 56 | exclude_patterns = ["build", "_build", "Youtube"]
|
49 | 57 | # pygments_style = "sphinx"
|
50 | 58 |
|
@@ -105,7 +113,7 @@ def patched_parse(self):
|
105 | 113 | GoogleDocstring._parse = patched_parse
|
106 | 114 |
|
107 | 115 |
|
108 |
| -def setup(app): |
| 116 | +def setup(app: Sphinx): |
109 | 117 | """Our sphinx extension for copying from examples/ to docs/examples
|
110 | 118 |
|
111 | 119 | Since nbsphinx does not handle glob/regex paths, we need to
|
@@ -135,3 +143,113 @@ def setup(app):
|
135 | 143 | )
|
136 | 144 | if (here / "static/custom.css").exists():
|
137 | 145 | app.add_css_file("custom.css")
|
| 146 | + |
| 147 | + _grab_external_examples(example_source) |
| 148 | + app.add_directive("pysindy-example", PysindyExample) |
| 149 | + |
| 150 | + |
| 151 | +EXTERNAL_EXAMPLES: dict[str, list[tuple[str, str]]] = {} |
| 152 | + |
| 153 | + |
| 154 | +def _load_ext_config(example_source: Path) -> list[dict[str, str]]: |
| 155 | + ext_config = example_source / "external.yml" |
| 156 | + with open(ext_config) as f: |
| 157 | + ext_examples = yaml.safe_load(f) |
| 158 | + return ext_examples |
| 159 | + |
| 160 | + |
| 161 | +def _grab_external_examples(example_source: Path): |
| 162 | + ext_examples = _load_ext_config(example_source) |
| 163 | + for example in ext_examples: |
| 164 | + ex_name = example["name"] |
| 165 | + user = example["user"] |
| 166 | + repo = example["repo"] |
| 167 | + ref = example["ref"] |
| 168 | + dir = example["dir"] |
| 169 | + base = f"https://raw.githubusercontent.com/{user}/{repo}/{ref}/{dir}/" |
| 170 | + notebooks = fetch_notebook_list(base) |
| 171 | + base = f"https://raw.githubusercontent.com/{user}/{repo}/{ref}/" |
| 172 | + local_nbs = [(name, copy_nb(base, pth, repo)) for name, pth in notebooks] |
| 173 | + EXTERNAL_EXAMPLES[ex_name] = local_nbs |
| 174 | + |
| 175 | + |
| 176 | +class PysindyExample(SphinxDirective): |
| 177 | + required_arguments = 0 |
| 178 | + optional_arguments = 0 |
| 179 | + option_spec = {"key": str, "title": str} |
| 180 | + has_content = True |
| 181 | + |
| 182 | + def run(self) -> list[nodes.Node]: |
| 183 | + key = self.options["key"] |
| 184 | + example_config = _load_ext_config((here / "../examples").resolve()) |
| 185 | + try: |
| 186 | + this_example = [ex for ex in example_config if ex["name"] == key][0] |
| 187 | + except IndexError: |
| 188 | + RuntimeError("Unknown configuration key for external example") |
| 189 | + heading_text: str = self.options.get("title") |
| 190 | + base_repo = f"https://github.com/{this_example['user']}/{this_example['repo']}" |
| 191 | + repo_ref = nodes.reference(name="Source repo", refuri=base_repo) |
| 192 | + ref_text = nodes.Text("Source repo") |
| 193 | + repo_ref += ref_text |
| 194 | + repo_par = nodes.paragraph() |
| 195 | + repo_par += repo_ref |
| 196 | + normalized_text = re.sub(r"\s", "_", heading_text) |
| 197 | + tgt_node = nodes.target(refid=normalized_text) |
| 198 | + title_node = nodes.title() |
| 199 | + title_text = nodes.Text(heading_text) |
| 200 | + title_node += [title_text, tgt_node] |
| 201 | + content_nodes = self.parse_content_to_nodes() |
| 202 | + toc_items = [] |
| 203 | + for name, relpath in EXTERNAL_EXAMPLES[key]: |
| 204 | + if name: |
| 205 | + toc_str = f"{name} <{relpath}>" |
| 206 | + if not name: |
| 207 | + toc_str = relpath |
| 208 | + toc_items.append(toc_str) |
| 209 | + toc_nodes = TocTree( |
| 210 | + name="PysindyExample", |
| 211 | + options={"maxdepth": 1}, |
| 212 | + arguments=[], |
| 213 | + content=StringList(initlist=toc_items), |
| 214 | + lineno=self.lineno, |
| 215 | + block_text="", |
| 216 | + content_offset=self.content_offset, |
| 217 | + state=self.state, |
| 218 | + state_machine=self.state_machine, |
| 219 | + ).run() |
| 220 | + section_node = nodes.section(ids=[heading_text], names=[heading_text]) |
| 221 | + section_node += [title_node, *content_nodes, *toc_nodes, repo_par] |
| 222 | + return [section_node] |
| 223 | + |
| 224 | + |
| 225 | +def fetch_notebook_list(base: str) -> list[tuple[str, str]]: |
| 226 | + """Gets the list of example notebooks from a repo's index.html |
| 227 | +
|
| 228 | + Each entry is a tuple of the title name of a link and the address |
| 229 | + """ |
| 230 | + index = requests.get(base + "index.rst") |
| 231 | + if index.status_code != 200: |
| 232 | + raise RuntimeError("Unable to locate external example directory") |
| 233 | + text = str(index.content, encoding="utf-8") |
| 234 | + link_line = r"^\s+(.*)[^\S\r\n]+(\S+.ipynb)" |
| 235 | + T = TypeVar("T") |
| 236 | + |
| 237 | + def deduplicate(mylist: list[T]) -> list[T]: |
| 238 | + return list(set(mylist)) |
| 239 | + |
| 240 | + rellinks = deduplicate(re.findall(link_line, text, flags=re.MULTILINE)) |
| 241 | + return rellinks |
| 242 | + |
| 243 | + |
| 244 | +def copy_nb(base: str, relpath: str, repo: str) -> str: |
| 245 | + """Create a local copy of external file, modifying relative reference""" |
| 246 | + example_dir = Path(__file__).parent / "examples" |
| 247 | + repo_local_dir = example_dir / repo |
| 248 | + repo_local_dir.mkdir(exist_ok=True) |
| 249 | + page = requests.get(base + relpath) |
| 250 | + if page.status_code != 200: |
| 251 | + raise RuntimeError(f"Unable to locate external notebook at {base + relpath}") |
| 252 | + filename = repo_local_dir / relpath.rsplit("/", 1)[1] |
| 253 | + with open(filename, "wb") as f: |
| 254 | + f.write(page.content) |
| 255 | + return os.path.relpath(filename, start=example_dir) |
0 commit comments