New forge coding added
This commit is contained in:
+211
-72
@@ -1,91 +1,230 @@
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import inspect
|
||||
import ast
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
def generate_markdown_handbook():
|
||||
print("📝 Starting mxPIC Markdown Generation...")
|
||||
|
||||
# Define paths
|
||||
src_root = Path("mxpic/components")
|
||||
# This should point to where your Sphinx .md files are stored
|
||||
docs_root = Path("docs/source/mxpic/components")
|
||||
|
||||
# 1. Delete the directory and all its contents
|
||||
if docs_root.exists() and docs_root.is_dir():
|
||||
shutil.rmtree(docs_root)
|
||||
DEFAULT_SRC_ROOT = Path("mxpic/components")
|
||||
DEFAULT_DOCS_ROOT = Path("docs/source/mxpic/components")
|
||||
DEFAULT_PACKAGE_ROOT = "mxpic.components"
|
||||
|
||||
# 2. Recreate the directory
|
||||
docs_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
success_count = 0
|
||||
@dataclass(frozen=True)
|
||||
class ModuleDoc:
|
||||
"""Documentation targets discovered in one Python module."""
|
||||
|
||||
for py_file in src_root.rglob("*.py"):
|
||||
source_path: Path
|
||||
relative_path: Path
|
||||
module_name: str
|
||||
classes: tuple[str, ...]
|
||||
functions: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def output_path(self) -> Path:
|
||||
return self.relative_path.with_suffix(".md")
|
||||
|
||||
|
||||
def public_top_level_members(py_file: Path) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
||||
"""Return public top-level classes and functions without importing a module."""
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
classes: list[str] = []
|
||||
functions: list[str] = []
|
||||
seen_classes: set[str] = set()
|
||||
seen_functions: set[str] = set()
|
||||
|
||||
for node in tree.body:
|
||||
if (
|
||||
isinstance(node, ast.ClassDef)
|
||||
and not node.name.startswith("_")
|
||||
and node.name not in seen_classes
|
||||
):
|
||||
classes.append(node.name)
|
||||
seen_classes.add(node.name)
|
||||
continue
|
||||
|
||||
if (
|
||||
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
and not node.name.startswith("_")
|
||||
and node.name not in seen_functions
|
||||
):
|
||||
functions.append(node.name)
|
||||
seen_functions.add(node.name)
|
||||
|
||||
return tuple(classes), tuple(functions)
|
||||
|
||||
|
||||
def discover_modules(
|
||||
src_root: Path = DEFAULT_SRC_ROOT,
|
||||
package_root: str = DEFAULT_PACKAGE_ROOT,
|
||||
) -> list[ModuleDoc]:
|
||||
"""Discover Python modules that should receive generated Markdown pages."""
|
||||
src_root = Path(src_root)
|
||||
modules: list[ModuleDoc] = []
|
||||
|
||||
for py_file in sorted(src_root.rglob("*.py")):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
|
||||
# Convert path to module name (e.g., mxpic.components.primitives.beam_splitters)
|
||||
rel_path = py_file.relative_to(src_root)
|
||||
module_name = "mxpic.components." + str(rel_path.with_suffix("")).replace(os.sep, ".")
|
||||
relative_path = py_file.relative_to(src_root)
|
||||
module_suffix = ".".join(relative_path.with_suffix("").parts)
|
||||
classes, functions = public_top_level_members(py_file)
|
||||
modules.append(
|
||||
ModuleDoc(
|
||||
source_path=py_file,
|
||||
relative_path=relative_path,
|
||||
module_name=f"{package_root}.{module_suffix}",
|
||||
classes=classes,
|
||||
functions=functions,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not import {module_name}: {e}")
|
||||
continue
|
||||
return modules
|
||||
|
||||
# Find all classes defined inside this specific module
|
||||
classes = []
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if obj.__module__ == module_name:
|
||||
classes.append(name)
|
||||
|
||||
# If the file has no classes, skip it
|
||||
if not classes:
|
||||
continue
|
||||
def remove_generated_markdown(docs_root: Path) -> None:
|
||||
"""Remove generated Markdown while preserving images and other assets."""
|
||||
docs_root = Path(docs_root)
|
||||
if not docs_root.exists():
|
||||
return
|
||||
|
||||
# Define where to save the .md file
|
||||
md_file_path = docs_root / rel_path.with_suffix(".md")
|
||||
md_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
for md_file in docs_root.rglob("*.md"):
|
||||
md_file.unlink()
|
||||
|
||||
# --- WRITE THE MARKDOWN FILE ---
|
||||
with open(md_file_path, "w", encoding="utf-8") as f:
|
||||
# 1. Write the Module Header
|
||||
f.write(f"# {module_name}\n\n")
|
||||
|
||||
# 2. Document any module-level docstrings (skipping classes)
|
||||
f.write("```{eval-rst}\n")
|
||||
f.write(f".. automodule:: {module_name}\n")
|
||||
f.write(" :no-members:\n") # This prevents duplicating the classes!
|
||||
f.write("```\n\n")
|
||||
|
||||
# 3. Loop through and write each class with its image
|
||||
for class_name in classes:
|
||||
f.write(f"## {class_name}\n\n")
|
||||
|
||||
# Point to the image path in Sphinx
|
||||
# img_path = f"{sphinx_image_root}/{rel_path.parent.as_posix()}/{class_name}.png"
|
||||
img_path = f"{class_name}.png"
|
||||
|
||||
f.write("```{eval-rst}\n")
|
||||
# Insert the Sphinx image directive
|
||||
f.write(f".. image:: {img_path}\n")
|
||||
f.write(" :align: center\n")
|
||||
f.write(" :width: 600px\n\n")
|
||||
|
||||
# Insert the specific class documentation
|
||||
f.write(f".. autoclass:: {module_name}.{class_name}\n")
|
||||
f.write(" :members:\n")
|
||||
f.write(" :undoc-members:\n")
|
||||
f.write(" :show-inheritance:\n")
|
||||
f.write("```\n\n")
|
||||
def write_module_page(module: ModuleDoc, docs_root: Path) -> None:
|
||||
"""Write a Markdown page for one Python module."""
|
||||
md_file_path = Path(docs_root) / module.output_path
|
||||
md_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"✅ Generated docs for: {module_name}")
|
||||
success_count += 1
|
||||
lines = [
|
||||
f"# {module.module_name}",
|
||||
"",
|
||||
"```{eval-rst}",
|
||||
f".. automodule:: {module.module_name}",
|
||||
" :no-members:",
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
|
||||
for class_name in module.classes:
|
||||
image_path = md_file_path.parent / f"{class_name}.png"
|
||||
lines.extend([f"## {class_name}", "", "```{eval-rst}"])
|
||||
|
||||
if image_path.exists():
|
||||
lines.extend(
|
||||
[
|
||||
f".. image:: {class_name}.png",
|
||||
" :align: center",
|
||||
" :width: 600px",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
f".. autoclass:: {module.module_name}.{class_name}",
|
||||
" :members:",
|
||||
" :undoc-members:",
|
||||
" :show-inheritance:",
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for function_name in module.functions:
|
||||
lines.extend(
|
||||
[
|
||||
f"## {function_name}",
|
||||
"",
|
||||
"```{eval-rst}",
|
||||
f".. autofunction:: {module.module_name}.{function_name}",
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
md_file_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def title_for_directory(relative_dir: Path) -> str:
|
||||
"""Build a readable title for a generated directory index."""
|
||||
if relative_dir == Path("."):
|
||||
return "Components"
|
||||
|
||||
return relative_dir.name.replace("_", " ").title()
|
||||
|
||||
|
||||
def write_index_page(docs_root: Path, relative_dir: Path, entries: list[str]) -> None:
|
||||
"""Write a MyST toctree index for one docs directory."""
|
||||
index_path = Path(docs_root) / relative_dir / "index.md"
|
||||
index_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lines = [
|
||||
f"# {title_for_directory(relative_dir)}",
|
||||
"",
|
||||
"```{toctree}",
|
||||
" :maxdepth: 2",
|
||||
"",
|
||||
]
|
||||
lines.extend(entries)
|
||||
lines.extend(["```", ""])
|
||||
index_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def write_index_pages(modules: list[ModuleDoc], docs_root: Path) -> None:
|
||||
"""Create recursive index.md files that include all generated module pages."""
|
||||
directories: set[Path] = {Path(".")}
|
||||
module_entries: dict[Path, list[str]] = {}
|
||||
|
||||
for module in modules:
|
||||
directory = module.output_path.parent
|
||||
directories.add(directory)
|
||||
module_entries.setdefault(directory, []).append(module.output_path.stem)
|
||||
|
||||
current = directory
|
||||
while current != Path("."):
|
||||
current = current.parent
|
||||
directories.add(current)
|
||||
|
||||
for directory in sorted(directories, key=lambda path: (len(path.parts), path.as_posix()), reverse=True):
|
||||
child_dirs = sorted(
|
||||
child
|
||||
for child in directories
|
||||
if child != directory and child.parent == directory
|
||||
)
|
||||
entries = [f"{child.name}/index" for child in child_dirs]
|
||||
entries.extend(sorted(module_entries.get(directory, [])))
|
||||
|
||||
if entries or directory == Path("."):
|
||||
write_index_page(docs_root, directory, entries)
|
||||
|
||||
|
||||
def generate_markdown_handbook(
|
||||
src_root: Path = DEFAULT_SRC_ROOT,
|
||||
docs_root: Path = DEFAULT_DOCS_ROOT,
|
||||
package_root: str = DEFAULT_PACKAGE_ROOT,
|
||||
) -> list[ModuleDoc]:
|
||||
"""Generate Sphinx Markdown pages for mxpic component modules."""
|
||||
print("Starting mxPIC Markdown generation...")
|
||||
|
||||
src_root = Path(src_root)
|
||||
docs_root = Path(docs_root)
|
||||
|
||||
if not src_root.exists():
|
||||
raise FileNotFoundError(f"Source directory not found: {src_root}")
|
||||
|
||||
docs_root.mkdir(parents=True, exist_ok=True)
|
||||
remove_generated_markdown(docs_root)
|
||||
|
||||
modules = discover_modules(src_root=src_root, package_root=package_root)
|
||||
for module in modules:
|
||||
write_module_page(module, docs_root)
|
||||
print(f"Generated docs for: {module.module_name}")
|
||||
|
||||
write_index_pages(modules, docs_root)
|
||||
|
||||
print(f"Markdown generation complete. Updated {len(modules)} module pages.")
|
||||
return modules
|
||||
|
||||
print(f"\n✨ Markdown generation complete! Updated {success_count} files.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_markdown_handbook()
|
||||
generate_markdown_handbook()
|
||||
|
||||
Reference in New Issue
Block a user