from __future__ import annotations import sys from pathlib import Path from typing import Collection, Iterator from sphinx.application import Sphinx HERE = Path(__file__).parent SRC = HERE.parent.parent.parent / "src" PYTHON_PACKAGE = SRC / "idom" AUTO_DIR = HERE.parent / "_auto" AUTO_DIR.mkdir(exist_ok=True) API_FILE = AUTO_DIR / "apis.rst" # All valid RST section symbols - it shouldn't be realistically possible to exhaust them SECTION_SYMBOLS = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" AUTODOC_TEMPLATE_WITH_MEMBERS = """\ .. automodule:: {module} :members: :ignore-module-all: """ AUTODOC_TEMPLATE_WITHOUT_MEMBERS = """\ .. automodule:: {module} :ignore-module-all: """ TITLE = """\ ========== Python API ========== """ def generate_api_docs(): content = [TITLE] for file in walk_python_files(PYTHON_PACKAGE, ignore_dirs={"__pycache__"}): if file.name == "__init__.py": if file.parent != PYTHON_PACKAGE: content.append(make_package_section(file)) else: content.append(make_module_section(file)) API_FILE.write_text("\n".join(content)) def make_package_section(file: Path) -> str: parent_dir = file.parent symbol = get_section_symbol(parent_dir) section_name = f"``{parent_dir.name}``" module_name = get_module_name(parent_dir) return ( section_name + "\n" + (symbol * len(section_name)) + "\n" + AUTODOC_TEMPLATE_WITHOUT_MEMBERS.format(module=module_name) ) def make_module_section(file: Path) -> str: symbol = get_section_symbol(file) section_name = f"``{file.stem}``" module_name = get_module_name(file) return ( section_name + "\n" + (symbol * len(section_name)) + "\n" + AUTODOC_TEMPLATE_WITH_MEMBERS.format(module=module_name) ) def get_module_name(path: Path) -> str: return ".".join(path.with_suffix("").relative_to(PYTHON_PACKAGE.parent).parts) def get_section_symbol(path: Path) -> str: rel_path_parts = path.relative_to(PYTHON_PACKAGE).parts assert len(rel_path_parts) < len(SECTION_SYMBOLS), "package structure is too deep" return SECTION_SYMBOLS[len(rel_path_parts)] def walk_python_files(root: Path, ignore_dirs: Collection[str]) -> Iterator[Path]: """Iterate over Python files We yield in a particular order to get the correction title section structure. Given a directory structure of the form:: project/ __init__.py /package __init__.py module_a.py module_b.py We yield the files in this order:: project/__init__.py project/package/__init__.py project/package/module_a.py project/module_b.py In this way we generate the section titles in the appropriate order:: project ======= project.package --------------- project.package.module_a ------------------------ """ for path in sorted( root.iterdir(), key=lambda path: ( # __init__.py files first int(not path.name == "__init__.py"), # then directories int(not path.is_dir()), # sort by file name last path.name, ), ): if path.is_dir(): if (path / "__init__.py").exists() and path.name not in ignore_dirs: yield from walk_python_files(path, ignore_dirs) elif path.suffix == ".py": yield path def setup(app: Sphinx) -> None: if sys.platform == "win32" and sys.version_info[:2] == (3, 7): return None generate_api_docs() return None