|
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