Skip to content

Commit a8dd9b9

Browse files
committed
improve api section
1 parent 26c6010 commit a8dd9b9

29 files changed

+207
-289
lines changed

Diff for: docs/.gitignore

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
build
2-
3-
# VS Code RST extension builds here by default
4-
source/_build
2+
source/_auto
53
source/_static/custom.js
64
source/vdom-json-schema.json

Diff for: docs/source/_autogen/dev-apis.rst

-5
This file was deleted.

Diff for: docs/source/_autogen/user-apis.rst

-74
This file was deleted.

Diff for: docs/source/_exts/autogen_api_docs.py

+112-76
Original file line numberDiff line numberDiff line change
@@ -2,101 +2,137 @@
22

33
import sys
44
from pathlib import Path
5+
from typing import Collection, Iterator
56

67
from sphinx.application import Sphinx
78

89

910
HERE = Path(__file__).parent
10-
PACKAGE_SRC = HERE.parent.parent.parent / "src"
11+
SRC = HERE.parent.parent.parent / "src"
12+
PYTHON_PACKAGE = SRC / "idom"
1113

12-
AUTOGEN_DIR = HERE.parent / "_autogen"
13-
AUTOGEN_DIR.mkdir(exist_ok=True)
14+
AUTO_DIR = HERE.parent / "_auto"
15+
AUTO_DIR.mkdir(exist_ok=True)
1416

15-
PUBLIC_API_REFERENCE_FILE = AUTOGEN_DIR / "user-apis.rst"
16-
PRIVATE_API_REFERENCE_FILE = AUTOGEN_DIR / "dev-apis.rst"
17+
API_FILE = AUTO_DIR / "apis.rst"
1718

19+
# All valid RST section symbols - it shouldn't be realistically possible to exhaust them
20+
SECTION_SYMBOLS = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
1821

19-
PUBLIC_TITLE = """\
20-
User API
21-
========
22+
AUTODOC_TEMPLATE_WITH_MEMBERS = """\
23+
.. automodule:: {module}
24+
:members:
2225
"""
2326

24-
PUBLIC_MISC_TITLE = """\
25-
Misc Modules
26-
------------
27+
AUTODOC_TEMPLATE_WITHOUT_MEMBERS = """\
28+
.. automodule:: {module}
2729
"""
2830

29-
PRIVATE_TITLE = """\
30-
Dev API
31-
=======
31+
TITLE = """\
32+
==========
33+
Python API
34+
==========
3235
"""
3336

34-
PRIVATE_MISC_TITLE = """\
35-
Misc Dev Modules
36-
----------------
37-
"""
38-
39-
AUTODOC_TEMPLATE = ".. automodule:: {module}\n :members:\n"
40-
4137

4238
def generate_api_docs():
43-
docs = {
44-
"public.main": [PUBLIC_TITLE],
45-
"public.misc": [PUBLIC_MISC_TITLE],
46-
"private.main": [PRIVATE_TITLE],
47-
"private.misc": [PRIVATE_MISC_TITLE],
48-
}
49-
50-
for file in sorted(pathlib_walk(PACKAGE_SRC, ignore_dirs=["node_modules"])):
51-
if not file.suffix == ".py" or file.stem.startswith("__"):
52-
# skip non-Python files along with __init__ and __main__
53-
continue
54-
public_vs_private = "private" if is_private_module(file) else "public"
55-
main_vs_misc = "main" if file_starts_with_docstring(file) else "misc"
56-
key = f"{public_vs_private}.{main_vs_misc}"
57-
docs[key].append(make_autodoc_section(file, public_vs_private == "private"))
58-
59-
public_content = docs["public.main"]
60-
if len(docs["public.misc"]) > 1:
61-
public_content += docs["public.misc"]
62-
63-
private_content = docs["private.main"]
64-
if len(docs["private.misc"]) > 1:
65-
private_content += docs["private.misc"]
66-
67-
PUBLIC_API_REFERENCE_FILE.write_text("\n".join(public_content))
68-
PRIVATE_API_REFERENCE_FILE.write_text("\n".join(private_content))
69-
70-
71-
def pathlib_walk(root: Path, ignore_dirs: list[str]):
72-
for path in root.iterdir():
73-
if path.is_dir():
74-
if path.name in ignore_dirs:
75-
continue
76-
yield from pathlib_walk(path, ignore_dirs)
77-
else:
78-
yield path
79-
39+
content = [TITLE]
8040

81-
def is_private_module(path: Path) -> bool:
82-
return any(p.startswith("_") for p in path.parts)
83-
84-
85-
def make_autodoc_section(path: Path, is_public) -> str:
86-
rel_path = path.relative_to(PACKAGE_SRC)
87-
module_name = ".".join(rel_path.with_suffix("").parts)
88-
return AUTODOC_TEMPLATE.format(module=module_name, underline="-" * len(module_name))
89-
90-
91-
def file_starts_with_docstring(path: Path) -> bool:
92-
for line in path.read_text().split("\n"):
93-
if line.startswith("#"):
94-
continue
95-
if line.startswith('"""') or line.startswith("'''"):
96-
return True
41+
for file in walk_python_files(PYTHON_PACKAGE, ignore_dirs={"__pycache__"}):
42+
if file.name == "__init__.py":
43+
if file.parent != PYTHON_PACKAGE:
44+
content.append(make_package_section(file))
9745
else:
98-
break
99-
return False
46+
content.append(make_module_section(file))
47+
48+
API_FILE.write_text("\n".join(content))
49+
50+
51+
def make_package_section(file: Path) -> str:
52+
parent_dir = file.parent
53+
symbol = get_section_symbol(parent_dir)
54+
section_name = f"``{parent_dir.name}``"
55+
module_name = get_module_name(parent_dir)
56+
return (
57+
section_name
58+
+ "\n"
59+
+ (symbol * len(section_name))
60+
+ "\n"
61+
+ AUTODOC_TEMPLATE_WITHOUT_MEMBERS.format(module=module_name)
62+
)
63+
64+
65+
def make_module_section(file: Path) -> str:
66+
symbol = get_section_symbol(file)
67+
section_name = f"``{file.stem}``"
68+
module_name = get_module_name(file)
69+
return (
70+
section_name
71+
+ "\n"
72+
+ (symbol * len(section_name))
73+
+ "\n"
74+
+ AUTODOC_TEMPLATE_WITH_MEMBERS.format(module=module_name)
75+
)
76+
77+
78+
def get_module_name(path: Path) -> str:
79+
return ".".join(path.with_suffix("").relative_to(PYTHON_PACKAGE.parent).parts)
80+
81+
82+
def get_section_symbol(path: Path) -> str:
83+
rel_path_parts = path.relative_to(PYTHON_PACKAGE).parts
84+
assert len(rel_path_parts) < len(SECTION_SYMBOLS), "package structure is too deep"
85+
return SECTION_SYMBOLS[len(rel_path_parts)]
86+
87+
88+
def walk_python_files(root: Path, ignore_dirs: Collection[str]) -> Iterator[Path]:
89+
"""Iterate over Python files
90+
91+
We yield in a particular order to get the correction title section structure. Given
92+
a directory structure of the form::
93+
94+
project/
95+
__init__.py
96+
/package
97+
__init__.py
98+
module_a.py
99+
module_b.py
100+
101+
We yield the files in this order::
102+
103+
project/__init__.py
104+
project/package/__init__.py
105+
project/package/module_a.py
106+
project/module_b.py
107+
108+
In this way we generate the section titles in the appropriate order::
109+
110+
project
111+
=======
112+
113+
project.package
114+
---------------
115+
116+
project.package.module_a
117+
------------------------
118+
119+
"""
120+
for path in sorted(
121+
root.iterdir(),
122+
key=lambda path: (
123+
# __init__.py files first
124+
int(not path.name == "__init__.py"),
125+
# then directories
126+
int(not path.is_dir()),
127+
# sort by file name last
128+
path.name,
129+
),
130+
):
131+
if path.is_dir():
132+
if (path / "__init__.py").exists() and path.name not in ignore_dirs:
133+
yield from walk_python_files(path, ignore_dirs)
134+
elif path.suffix == ".py":
135+
yield path
100136

101137

102138
def setup(app: Sphinx) -> None:

Diff for: docs/source/_exts/custom_autosectionlabel.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Mostly copied from sphinx.ext.autosectionlabel
2+
3+
See Sphinx BSD license:
4+
https://github.com/sphinx-doc/sphinx/blob/f9968594206e538f13fa1c27c065027f10d4ea27/LICENSE
5+
"""
6+
7+
from fnmatch import fnmatch
8+
from typing import Any, Dict, cast
9+
10+
from docutils import nodes
11+
from docutils.nodes import Node
12+
from sphinx.application import Sphinx
13+
from sphinx.domains.std import StandardDomain
14+
from sphinx.locale import __
15+
from sphinx.util import logging
16+
from sphinx.util.nodes import clean_astext
17+
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def get_node_depth(node: Node) -> int:
23+
i = 0
24+
cur_node = node
25+
while cur_node.parent != node.document:
26+
cur_node = cur_node.parent
27+
i += 1
28+
return i
29+
30+
31+
def register_sections_as_label(app: Sphinx, document: Node) -> None:
32+
docname = app.env.docname
33+
print(docname)
34+
35+
for pattern in app.config.autosectionlabel_skip_docs:
36+
if fnmatch(docname, pattern):
37+
return None
38+
39+
domain = cast(StandardDomain, app.env.get_domain("std"))
40+
for node in document.traverse(nodes.section):
41+
if (
42+
app.config.autosectionlabel_maxdepth
43+
and get_node_depth(node) >= app.config.autosectionlabel_maxdepth
44+
):
45+
continue
46+
labelid = node["ids"][0]
47+
48+
title = cast(nodes.title, node[0])
49+
ref_name = getattr(title, "rawsource", title.astext())
50+
if app.config.autosectionlabel_prefix_document:
51+
name = nodes.fully_normalize_name(docname + ":" + ref_name)
52+
else:
53+
name = nodes.fully_normalize_name(ref_name)
54+
sectname = clean_astext(title)
55+
56+
if name in domain.labels:
57+
logger.warning(
58+
__("duplicate label %s, other instance in %s"),
59+
name,
60+
app.env.doc2path(domain.labels[name][0]),
61+
location=node,
62+
type="autosectionlabel",
63+
subtype=docname,
64+
)
65+
66+
domain.anonlabels[name] = docname, labelid
67+
domain.labels[name] = docname, labelid, sectname
68+
69+
70+
def setup(app: Sphinx) -> Dict[str, Any]:
71+
app.add_config_value("autosectionlabel_prefix_document", False, "env")
72+
app.add_config_value("autosectionlabel_maxdepth", None, "env")
73+
app.add_config_value("autosectionlabel_skip_docs", [], "env")
74+
app.connect("doctree-read", register_sections_as_label)
75+
76+
return {
77+
"version": "builtin",
78+
"parallel_read_safe": True,
79+
"parallel_write_safe": True,
80+
}

0 commit comments

Comments
 (0)