diff --git a/docforge/__init__.py b/docforge/__init__.py index 3f93efa..fcd0c8b 100644 --- a/docforge/__init__.py +++ b/docforge/__init__.py @@ -6,13 +6,14 @@ All the rendering, exporting, and serving APIs are intentionally private until their contracts are finalized. """ -from .loader import GriffeLoader +from .loader import GriffeLoader, discover_module_paths from .renderers import MkDocsRenderer from .cli import main from . import model __all__ = [ "GriffeLoader", + "discover_module_paths", "MkDocsRenderer", "model", "main", diff --git a/docforge/cli/main.py b/docforge/cli/main.py index 90ef261..72c46c9 100644 --- a/docforge/cli/main.py +++ b/docforge/cli/main.py @@ -5,7 +5,7 @@ from typing import Sequence, Optional import click -from docforge.loader import GriffeLoader +from docforge.loader import GriffeLoader, discover_module_paths from docforge.renderers.mkdocs import MkDocsRenderer from docforge.cli.mkdocs import mkdocs_cmd @@ -83,7 +83,14 @@ def generate( ) -> None: """Generate documentation source files using MkDocs renderer.""" loader = GriffeLoader() - project = loader.load_project(list(modules), project_name) + discovered_paths = discover_module_paths( + "docforge", + Path(r"C:\Users\vishe\WorkSpace\code\aetos\doc-forge") + ) + project = loader.load_project( + discovered_paths, + project_name + ) renderer = MkDocsRenderer() renderer.generate_sources(project, docs_dir) diff --git a/docforge/loader/__init__.py b/docforge/loader/__init__.py index f896c99..10ac315 100644 --- a/docforge/loader/__init__.py +++ b/docforge/loader/__init__.py @@ -1,5 +1,6 @@ -from .griffe_loader import GriffeLoader +from .griffe_loader import GriffeLoader, discover_module_paths __all__ = [ - "GriffeLoader" + "GriffeLoader", + "discover_module_paths", ] diff --git a/docforge/loader/__init__.pyi b/docforge/loader/__init__.pyi index f896c99..10ac315 100644 --- a/docforge/loader/__init__.pyi +++ b/docforge/loader/__init__.pyi @@ -1,5 +1,6 @@ -from .griffe_loader import GriffeLoader +from .griffe_loader import GriffeLoader, discover_module_paths __all__ = [ - "GriffeLoader" + "GriffeLoader", + "discover_module_paths", ] diff --git a/docforge/loader/griffe_loader.py b/docforge/loader/griffe_loader.py index 04d0095..aff0316 100644 --- a/docforge/loader/griffe_loader.py +++ b/docforge/loader/griffe_loader.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from pathlib import Path from typing import List, Optional from griffe import ( @@ -16,6 +17,41 @@ from docforge.model import Module, Project, DocObject logger = logging.getLogger(__name__) +def discover_module_paths( + module_name: str, + project_root: Path | None = None, +) -> List[str]: + """ + Discover all Python modules under a package via filesystem traversal. + + Rules: + - Directory with __init__.py => package + - .py file => module + - Paths converted to dotted module paths + """ + + if project_root is None: + project_root = Path.cwd() + + pkg_dir = project_root / module_name + if not pkg_dir.exists(): + raise FileNotFoundError(f"Package not found: {pkg_dir}") + + module_paths: List[str] = [] + + for path in pkg_dir.rglob("*.py"): + if path.name == "__init__.py": + module_path = path.parent + else: + module_path = path + + rel = module_path.relative_to(project_root) + dotted = ".".join(rel.with_suffix("").parts) + module_paths.append(dotted) + + return sorted(set(module_paths)) + + class GriffeLoader: """Loads Python modules using Griffe introspection.""" diff --git a/docforge/loader/griffe_loader.pyi b/docforge/loader/griffe_loader.pyi index ebac3e4..d272562 100644 --- a/docforge/loader/griffe_loader.pyi +++ b/docforge/loader/griffe_loader.pyi @@ -1,8 +1,16 @@ from typing import List, Optional +from pathlib import Path from docforge.model import Module, Project +def discover_module_paths( + module_name: str, + project_root: Path | None = None, +) -> List[str]: + ... + + class GriffeLoader: """Griffe-based introspection loader. diff --git a/docforge/renderers/mkdocs.py b/docforge/renderers/mkdocs.py index acd683c..7be0db4 100644 --- a/docforge/renderers/mkdocs.py +++ b/docforge/renderers/mkdocs.py @@ -11,43 +11,43 @@ class MkDocsRenderer: name = "mkdocs" def generate_sources(self, project: Project, out_dir: Path) -> None: - """ - Generate Markdown files with mkdocstrings directives. + modules = list(project.get_all_modules()) + paths = {m.path for m in modules} - Structure rules: - - Each top-level package gets a directory - - Modules become .md files - - Packages (__init__) become index.md - """ - for module in project.get_all_modules(): - self._write_module(project, module, out_dir) + # Package detection (level-agnostic) + packages = { + p for p in paths + if any(other.startswith(p + ".") for other in paths) + } + + for module in modules: + self._write_module(module, packages, out_dir) # ------------------------- # Internal helpers # ------------------------- - - def _write_module(self, project: Project, module, out_dir: Path) -> None: + def _write_module(self, module, packages: set[str], out_dir: Path) -> None: parts = module.path.split(".") - # Root package directory - pkg_dir = out_dir / parts[0] - pkg_dir.mkdir(parents=True, exist_ok=True) + if module.path in packages: + # package → index.md + dir_path = out_dir.joinpath(*parts) + dir_path.mkdir(parents=True, exist_ok=True) - # Package (__init__.py) → index.md - if module.path == parts[0]: - md_path = pkg_dir / "index.md" - title = parts[0].replace("_", " ").title() + md_path = dir_path / "index.md" + title = parts[-1].replace("_", " ").title() else: - # Submodule → .md - md_path = pkg_dir / f"{parts[-1]}.md" + # leaf module → .md + dir_path = out_dir.joinpath(*parts[:-1]) + dir_path.mkdir(parents=True, exist_ok=True) + + md_path = dir_path / f"{parts[-1]}.md" title = parts[-1].replace("_", " ").title() content = self._render_markdown(title, module.path) - # Idempotent write - if md_path.exists(): - if md_path.read_text(encoding="utf-8") == content: - return + if md_path.exists() and md_path.read_text(encoding="utf-8") == content: + return md_path.write_text(content, encoding="utf-8") diff --git a/docforge/renderers/mkdocs.pyi b/docforge/renderers/mkdocs.pyi index d1b3894..8286938 100644 --- a/docforge/renderers/mkdocs.pyi +++ b/docforge/renderers/mkdocs.pyi @@ -1,17 +1,17 @@ from pathlib import Path +from typing import Set -from docforge.model import Project -from docforge.renderers.base import DocRenderer +from docforge.model import Project, Module class MkDocsRenderer: - """MkDocs source generator using mkdocstrings.""" - name: str - def generate_sources( + def generate_sources(self, project: Project, out_dir: Path) -> None: ... + def _write_module( self, - project: Project, + module: Module, + packages: Set[str], out_dir: Path, - ) -> None: - """Generate Markdown files with mkdocstrings directives.""" + ) -> None: ... + def _render_markdown(self, title: str, module_path: str) -> str: ... diff --git a/tests/renderers/test_mkdocs_module_coverage.py b/tests/renderers/test_mkdocs_module_coverage.py index e5639ff..89e5dec 100644 --- a/tests/renderers/test_mkdocs_module_coverage.py +++ b/tests/renderers/test_mkdocs_module_coverage.py @@ -1,12 +1,18 @@ from pathlib import Path -from docforge.loader import GriffeLoader +from docforge.loader import GriffeLoader, discover_module_paths from docforge.renderers.mkdocs import MkDocsRenderer def test_mkdocs_emits_all_modules(tmp_path: Path) -> None: loader = GriffeLoader() - project = loader.load_project(["docforge"]) + discovered_paths = discover_module_paths( + "docforge", + Path(r"C:\Users\vishe\WorkSpace\code\aetos\doc-forge") + ) + project = loader.load_project( + discovered_paths + ) renderer = MkDocsRenderer() renderer.generate_sources(project, tmp_path) @@ -32,5 +38,24 @@ def test_mkdocs_emits_all_modules(tmp_path: Path) -> None: else: expected.add("/".join(parts) + ".md") + # expected = { + # 'docforge/cli/main.md', + # 'docforge/renderers/index.md', + # 'docforge/loader/index.md', + # 'docforge/model/index.md', + # 'docforge/nav/index.md', + # 'docforge/renderers/mkdocs.md', + # 'docforge/index.md', + # 'docforge/loader/griffe_loader.md', + # 'docforge/model/object.md', + # 'docforge/cli/index.md', + # 'docforge/nav/resolver.md', + # 'docforge/renderers/base.md', + # 'docforge/nav/mkdocs.md', + # 'docforge/nav/spec.md', + # 'docforge/model/module.md', + # 'docforge/cli/mkdocs.md', + # 'docforge/model/project.md' + # } missing = expected - emitted assert not missing, f"Missing markdown files for modules: {missing}"