|
2 | 2 |
|
3 | 3 | import sys |
4 | 4 | from pathlib import Path |
| 5 | +from typing import Collection, Iterator |
5 | 6 |
|
6 | 7 | from sphinx.application import Sphinx |
7 | 8 |
|
8 | 9 |
|
9 | 10 | HERE = Path(__file__).parent |
10 | | -PACKAGE_SRC = HERE.parent.parent.parent / "src" |
| 11 | +SRC = HERE.parent.parent.parent / "src" |
| 12 | +PYTHON_PACKAGE = SRC / "idom" |
11 | 13 |
|
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) |
14 | 16 |
|
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" |
17 | 18 |
|
| 19 | +# All valid RST section symbols - it shouldn't be realistically possible to exhaust them |
| 20 | +SECTION_SYMBOLS = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" |
18 | 21 |
|
19 | | -PUBLIC_TITLE = """\ |
20 | | -User API |
21 | | -======== |
| 22 | +AUTODOC_TEMPLATE_WITH_MEMBERS = """\ |
| 23 | +.. automodule:: {module} |
| 24 | + :members: |
22 | 25 | """ |
23 | 26 |
|
24 | | -PUBLIC_MISC_TITLE = """\ |
25 | | -Misc Modules |
26 | | ------------- |
| 27 | +AUTODOC_TEMPLATE_WITHOUT_MEMBERS = """\ |
| 28 | +.. automodule:: {module} |
27 | 29 | """ |
28 | 30 |
|
29 | | -PRIVATE_TITLE = """\ |
30 | | -Dev API |
31 | | -======= |
| 31 | +TITLE = """\ |
| 32 | +========== |
| 33 | +Python API |
| 34 | +========== |
32 | 35 | """ |
33 | 36 |
|
34 | | -PRIVATE_MISC_TITLE = """\ |
35 | | -Misc Dev Modules |
36 | | ----------------- |
37 | | -""" |
38 | | - |
39 | | -AUTODOC_TEMPLATE = ".. automodule:: {module}\n :members:\n" |
40 | | - |
41 | 37 |
|
42 | 38 | 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] |
80 | 40 |
|
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)) |
97 | 45 | 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 |
100 | 136 |
|
101 | 137 |
|
102 | 138 | def setup(app: Sphinx) -> None: |
|
0 commit comments