|  | 
|  | 1 | +"""Generate the API index and autosummary pages for ``movement`` modules. | 
|  | 2 | +
 | 
|  | 3 | +This script generates the top-level API index file (``api_index.rst``) | 
|  | 4 | +for all modules in the `movement` package, except for those specified | 
|  | 5 | +in ``EXCLUDE_MODULES``. | 
|  | 6 | +This script also allows "package modules" that aggregate submodules | 
|  | 7 | +via their ``__init__.py`` files (e.g. ``movement.kinematics``) to be added | 
|  | 8 | +to the API index, rather than listing each submodule separately. | 
|  | 9 | +These modules are specified in ``PACKAGE_MODULES`` and will have their | 
|  | 10 | +autosummary pages generated. | 
|  | 11 | +""" | 
|  | 12 | + | 
|  | 13 | +import importlib | 
|  | 14 | +import inspect | 
|  | 15 | +import os | 
|  | 16 | +import sys | 
|  | 17 | +from pathlib import Path | 
|  | 18 | + | 
|  | 19 | +from jinja2 import FileSystemLoader | 
|  | 20 | +from jinja2.sandbox import SandboxedEnvironment | 
|  | 21 | +from sphinx.ext.autosummary.generate import _underline | 
|  | 22 | +from sphinx.util import rst | 
|  | 23 | + | 
|  | 24 | +# Single-file modules to exclude from the API index | 
|  | 25 | +EXCLUDE_MODULES = { | 
|  | 26 | +    "movement.cli_entrypoint", | 
|  | 27 | +    "movement.napari.loader_widgets", | 
|  | 28 | +    "movement.napari.meta_widget", | 
|  | 29 | +} | 
|  | 30 | + | 
|  | 31 | +# Modules with __init__.py that expose submodules explicitly | 
|  | 32 | +PACKAGE_MODULES = {"movement.kinematics", "movement.plots", "movement.roi"} | 
|  | 33 | + | 
|  | 34 | +# Configure paths | 
|  | 35 | +SCRIPT_DIR = Path(__file__).resolve().parent | 
|  | 36 | +MOVEMENT_ROOT = SCRIPT_DIR.parent | 
|  | 37 | +SOURCE_PATH = Path("source") | 
|  | 38 | +TEMPLATES_PATH = SOURCE_PATH / "_templates" | 
|  | 39 | + | 
|  | 40 | +os.chdir(SCRIPT_DIR) | 
|  | 41 | +sys.path.insert(0, str(MOVEMENT_ROOT)) | 
|  | 42 | + | 
|  | 43 | + | 
|  | 44 | +def get_modules(): | 
|  | 45 | +    """Return all modules to be documented.""" | 
|  | 46 | +    # Gather all modules and their paths | 
|  | 47 | +    module_names = set() | 
|  | 48 | +    for path in sorted((MOVEMENT_ROOT / "movement").rglob("*.py")): | 
|  | 49 | +        module_name = str( | 
|  | 50 | +            path.relative_to(MOVEMENT_ROOT).with_suffix("") | 
|  | 51 | +        ).replace(os.sep, ".") | 
|  | 52 | +        if path.name == "__init__.py": | 
|  | 53 | +            parent = module_name.rsplit(".", 1)[0] | 
|  | 54 | +            if parent in PACKAGE_MODULES: | 
|  | 55 | +                module_names.add(parent) | 
|  | 56 | +        else: | 
|  | 57 | +            module_names.add(module_name) | 
|  | 58 | +    # Determine submodules of package modules to exclude | 
|  | 59 | +    PACKAGE_MODULE_CHILDREN = { | 
|  | 60 | +        name | 
|  | 61 | +        for name in module_names | 
|  | 62 | +        for parent in PACKAGE_MODULES | 
|  | 63 | +        if name.startswith(parent + ".") | 
|  | 64 | +    } | 
|  | 65 | +    return module_names - EXCLUDE_MODULES - PACKAGE_MODULE_CHILDREN | 
|  | 66 | + | 
|  | 67 | + | 
|  | 68 | +def get_members(module_name): | 
|  | 69 | +    """Return all functions and classes in a module.""" | 
|  | 70 | +    mod = importlib.import_module(module_name) | 
|  | 71 | +    functions = [] | 
|  | 72 | +    classes = [] | 
|  | 73 | +    for name in getattr(mod, "__all__", dir(mod)): | 
|  | 74 | +        obj = getattr(mod, name, None) | 
|  | 75 | +        if inspect.isfunction(obj): | 
|  | 76 | +            functions.append(f"{name}") | 
|  | 77 | +        elif inspect.isclass(obj): | 
|  | 78 | +            classes.append(f"{name}") | 
|  | 79 | +    return sorted(functions), sorted(classes) | 
|  | 80 | + | 
|  | 81 | + | 
|  | 82 | +def write_autosummary_module_page(module_name, output_path): | 
|  | 83 | +    """Generate an .rst file with autosummary listing for the given module.""" | 
|  | 84 | +    functions, classes = get_members(module_name) | 
|  | 85 | +    env = SandboxedEnvironment(loader=FileSystemLoader(TEMPLATES_PATH)) | 
|  | 86 | +    # Add custom autosummary filters | 
|  | 87 | +    env.filters["escape"] = rst.escape | 
|  | 88 | +    env.filters["underline"] = _underline | 
|  | 89 | +    template = env.get_template("autosummary/module.rst") | 
|  | 90 | +    content = template.render( | 
|  | 91 | +        fullname=module_name, | 
|  | 92 | +        underline="=" * len(module_name), | 
|  | 93 | +        classes=classes, | 
|  | 94 | +        functions=functions, | 
|  | 95 | +    ) | 
|  | 96 | +    output_path.parent.mkdir(parents=True, exist_ok=True) | 
|  | 97 | +    output_path.write_text(content) | 
|  | 98 | + | 
|  | 99 | + | 
|  | 100 | +def make_api_index(module_names): | 
|  | 101 | +    """Create a top-level API index file listing the specified modules.""" | 
|  | 102 | +    doctree_lines = [ | 
|  | 103 | +        f"    {module_name}" for module_name in sorted(module_names) | 
|  | 104 | +    ] | 
|  | 105 | +    api_head = (TEMPLATES_PATH / "api_index_head.rst").read_text() | 
|  | 106 | +    output_path = SOURCE_PATH / "api_index.rst" | 
|  | 107 | +    output_path.write_text(api_head + "\n" + "\n".join(doctree_lines)) | 
|  | 108 | + | 
|  | 109 | + | 
|  | 110 | +if __name__ == "__main__": | 
|  | 111 | +    # Generate autosummary pages for manual modules | 
|  | 112 | +    for module_name in PACKAGE_MODULES: | 
|  | 113 | +        output_path = SOURCE_PATH / "api" / f"{module_name}.rst" | 
|  | 114 | +        write_autosummary_module_page(module_name, output_path) | 
|  | 115 | +    # Generate the API index | 
|  | 116 | +    make_api_index(get_modules()) | 
0 commit comments