forked from tpvasconcelos/ridgeplot
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add automatic RST generator for internal API docs
This commit adds functionality to automatically generate .rst files for internal API documentation, addressing tpvasconcelos#290. Changes include: - Add script to automatically generate RST files - Support hierarchical module structure - Maintain consistent module descriptions - Add basic tests for core functionality The script detects all internal modules and generates appropriate .rst files with consistent formatting and structure.
- Loading branch information
Showing
2 changed files
with
232 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
#!/usr/bin/env python3 | ||
"""Generate RST files for internal API documentation. | ||
Execution steps: | ||
- Scan the ridgeplot source directory for internal modules (prefixed with _) | ||
- Generate organized hierarchical RST documentation structure | ||
- Each RST file includes module description and appropriate directives | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
import importlib | ||
from collections import defaultdict | ||
from pathlib import Path | ||
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from collections.abc import Iterator | ||
|
||
PATH_ROOT_DIR = Path(__file__).parents[3] | ||
PATH_TO_SRC = PATH_ROOT_DIR / "src/ridgeplot" | ||
PATH_TO_DOCS = PATH_ROOT_DIR / "docs/api/internal" | ||
|
||
# Descriptions for all modules | ||
MODULE_DESCRIPTIONS = { | ||
# Main modules | ||
"ridgeplot": "Main ridgeline plotting module.", | ||
"ridgeplot._ridgeplot": "Core implementation of ridgeline plots.", | ||
"ridgeplot._figure_factory": "Factory functions for creating ridgeline plots.", | ||
"ridgeplot._hist": "Histogram generation and binning utilities.", | ||
"ridgeplot._kde": "Kernel density estimation implementation.", | ||
"ridgeplot._types": "Type definitions and validation.", | ||
"ridgeplot._utils": "General utility functions.", | ||
"ridgeplot._missing": "Missing value handling utilities.", | ||
"ridgeplot._version": "Version information.", | ||
|
||
# Color module and submodules | ||
"ridgeplot._color": "Color management and utilities.", | ||
"ridgeplot._color.colorscale": "Continuous colorscale generation and handling.", | ||
"ridgeplot._color.css_colors": "Standard CSS color definitions and mappings.", | ||
"ridgeplot._color.interpolation": "Color interpolation and gradient utilities.", | ||
"ridgeplot._color.utils": "Color manipulation and conversion functions.", | ||
|
||
# Object module and submodules | ||
"ridgeplot._obj": "Object-oriented implementations.", | ||
"ridgeplot._obj.traces": "Trace implementations for different plot types.", | ||
"ridgeplot._obj.traces.area": "Area trace for density visualizations.", | ||
"ridgeplot._obj.traces.bar": "Bar trace for histogram visualizations.", | ||
"ridgeplot._obj.traces.base": "Base classes for trace implementations.", | ||
|
||
# Vendor modules | ||
"ridgeplot._vendor": "Third-party vendored utilities.", | ||
"ridgeplot._vendor.more_itertools": "Additional iteration utilities.", | ||
} | ||
|
||
def find_internal_modules(base_path: Path) -> Iterator[tuple[str, Path]]: | ||
"""Find all internal modules and their paths.""" | ||
for item in base_path.rglob("*.py"): | ||
if item.name.startswith("__"): | ||
continue | ||
|
||
rel_path = item.relative_to(base_path) | ||
mod_parts = list(rel_path.parent.parts) | ||
if rel_path.name != "__init__.py": | ||
mod_parts.append(rel_path.stem) | ||
|
||
if not any(part.startswith("_") and not part.startswith("__") | ||
for part in mod_parts): | ||
continue | ||
|
||
module_name = ".".join(mod_parts) | ||
yield module_name, item | ||
|
||
def get_module_description(full_module_name: str) -> str: | ||
"""Get the module description.""" | ||
try: | ||
module = importlib.import_module(full_module_name) | ||
if module.__doc__ and module.__doc__.strip(): | ||
return module.__doc__.strip().split("\n")[0] | ||
except ImportError: | ||
pass | ||
|
||
return MODULE_DESCRIPTIONS.get( | ||
full_module_name, | ||
"Internal module utilities." | ||
) | ||
|
||
def generate_module_rst(module_name: str, submodules: list[str] | None = None) -> str: | ||
"""Generate RST content for a module.""" | ||
full_module_name = f"ridgeplot.{module_name}" | ||
description = get_module_description(full_module_name) | ||
|
||
content = [ | ||
full_module_name, | ||
"=" * len(full_module_name), | ||
"", | ||
description, | ||
"", | ||
] | ||
|
||
if submodules: | ||
content.extend([ | ||
".. toctree::", | ||
" :maxdepth: 1", | ||
"", | ||
]) | ||
for submod in sorted(submodules): | ||
rel_path = submod.replace(module_name + ".", "") | ||
content.append(f" {rel_path}") | ||
content.append("") | ||
|
||
content.extend([ | ||
f".. automodule:: {full_module_name}", | ||
" :private-members:", | ||
"" | ||
]) | ||
|
||
return "\n".join(content) | ||
|
||
def organize_modules(modules: list[str]) -> dict[str, list[str]]: | ||
"""Organize modules into a hierarchical structure.""" | ||
hierarchy = defaultdict(list) | ||
|
||
for module in modules: | ||
parts = module.split(".") | ||
if len(parts) > 1: | ||
parent = parts[0].lstrip("_") | ||
hierarchy[parent].append(module) | ||
else: | ||
clean_name = module.lstrip("_") | ||
hierarchy[clean_name] = [] | ||
|
||
return dict(hierarchy) | ||
|
||
def write_rst_file(output_dir: Path, module_name: str, content: str) -> None: | ||
"""Write RST content to file.""" | ||
output_dir.mkdir(parents=True, exist_ok=True) | ||
|
||
clean_name = module_name.lstrip("_") | ||
parts = clean_name.split(".") | ||
|
||
if len(parts) > 1: | ||
*dir_parts, filename = parts | ||
current_dir = output_dir | ||
for part in dir_parts: | ||
current_dir = current_dir / part | ||
current_dir.mkdir(exist_ok=True) | ||
filepath = current_dir / f"{filename}.rst" | ||
else: | ||
filepath = output_dir / f"{clean_name}.rst" | ||
|
||
filepath.write_text(content) | ||
print(f"Generated {filepath.relative_to(PATH_TO_DOCS)}") | ||
|
||
def clean_directory(path: Path) -> None: | ||
"""Clean directory recursively.""" | ||
if path.exists(): | ||
for item in sorted(path.glob('**/*'), reverse=True): | ||
if item.is_file(): | ||
item.unlink() | ||
elif item.is_dir(): | ||
item.rmdir() | ||
|
||
def main() -> None: | ||
"""Generate RST files for all internal modules.""" | ||
# Clean up existing directories | ||
for dir_name in ['color', 'obj', 'vendor', '_color', '_obj', '_vendor']: | ||
dir_path = PATH_TO_DOCS / dir_name | ||
if dir_path.exists(): | ||
clean_directory(dir_path) | ||
dir_path.rmdir() | ||
|
||
# Clean up RST files in root | ||
for rst_file in PATH_TO_DOCS.glob('*.rst'): | ||
rst_file.unlink() | ||
|
||
# Create output directory | ||
PATH_TO_DOCS.mkdir(parents=True, exist_ok=True) | ||
|
||
# Generate new files | ||
modules = [name for name, _ in find_internal_modules(PATH_TO_SRC)] | ||
hierarchy = organize_modules(modules) | ||
|
||
for module_name, submodules in hierarchy.items(): | ||
content = generate_module_rst(module_name, submodules) | ||
write_rst_file(PATH_TO_DOCS, module_name, content) | ||
|
||
for submod in submodules: | ||
subcontent = generate_module_rst(submod) | ||
write_rst_file(PATH_TO_DOCS, submod, subcontent) | ||
|
||
if __name__ == "__main__": | ||
main() |
39 changes: 39 additions & 0 deletions
39
tests/cicd_utils/test_scripts/test_generate_internal_api_rst.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
#!/usr/bin/env python3 | ||
"""Tests for generate_internal_api_rst.py script.""" | ||
|
||
from __future__ import annotations | ||
|
||
import sys | ||
from pathlib import Path | ||
|
||
import pytest | ||
|
||
# Import script directly | ||
SCRIPT_PATH = Path(__file__).parents[3] / "cicd_utils/cicd/scripts/generate_internal_api_rst.py" | ||
sys.path.append(str(SCRIPT_PATH.parent)) | ||
|
||
from generate_internal_api_rst import ( | ||
generate_module_rst, | ||
organize_modules, | ||
) | ||
|
||
|
||
def test_organize_modules() -> None: | ||
"""Test basic module organization.""" | ||
modules = ["_color.utils", "_color.css_colors", "_hist", "_kde"] | ||
hierarchy = organize_modules(modules) | ||
|
||
assert "color" in hierarchy | ||
assert len(hierarchy["color"]) == 2 | ||
assert "_color.utils" in hierarchy["color"] | ||
assert "hist" in hierarchy | ||
|
||
|
||
def test_generate_module_rst() -> None: | ||
"""Test RST content generation.""" | ||
content = generate_module_rst("_color", ["_color.utils", "_color.css_colors"]) | ||
|
||
assert "ridgeplot._color" in content | ||
assert ".. toctree::" in content | ||
assert "utils" in content | ||
assert "css_colors" in content |