module is source flag to ensure for single source modules it's not treated as a module but root

This commit is contained in:
2026-02-21 21:03:49 +05:30
parent a2ebd7d19b
commit 5149034d19
6 changed files with 69 additions and 17 deletions

View File

@@ -18,6 +18,7 @@ def cli() -> None:
@cli.command() @cli.command()
@click.option("--mcp", is_flag=True, help="Build MCP resources") @click.option("--mcp", is_flag=True, help="Build MCP resources")
@click.option("--mkdocs", is_flag=True, help="Build MkDocs site") @click.option("--mkdocs", is_flag=True, help="Build MkDocs site")
@click.option("--module-is-source", is_flag=True, help="Module is source folder and to be treated as root folder")
@click.option("--module", help="Python module to document") @click.option("--module", help="Python module to document")
@click.option("--project-name", help="Project name override") @click.option("--project-name", help="Project name override")
# MkDocs specific # MkDocs specific
@@ -32,6 +33,7 @@ def cli() -> None:
def build( def build(
mcp: bool, mcp: bool,
mkdocs: bool, mkdocs: bool,
module_is_source: bool,
module: Optional[str], module: Optional[str],
project_name: Optional[str], project_name: Optional[str],
site_name: Optional[str], site_name: Optional[str],
@@ -52,7 +54,8 @@ def build(
Args: Args:
mcp: Use the MCP documentation builder. mcp: Use the MCP documentation builder.
mkdocs: Use the MkDocs documentation builder. mkdocs: Use the MkDocs documentation builder.
module: The dotted path of the module to document. module_is_source: Module is the source folder and to be treated as the root folder.
module: The dotted path of the module to the document.
project_name: Optional override for the project name. project_name: Optional override for the project name.
site_name: (MkDocs) The site display name. Defaults to module name. site_name: (MkDocs) The site display name. Defaults to module name.
docs_dir: (MkDocs) Target directory for Markdown sources. docs_dir: (MkDocs) Target directory for Markdown sources.
@@ -71,7 +74,12 @@ def build(
site_name = module site_name = module
click.echo(f"Generating MkDocs sources in {docs_dir}...") click.echo(f"Generating MkDocs sources in {docs_dir}...")
mkdocs_utils.generate_sources(module, project_name, docs_dir) mkdocs_utils.generate_sources(
module,
docs_dir,
project_name,
module_is_source,
)
click.echo(f"Generating MkDocs config {mkdocs_yml}...") click.echo(f"Generating MkDocs config {mkdocs_yml}...")
mkdocs_utils.generate_config(docs_dir, nav_file, template, mkdocs_yml, site_name) mkdocs_utils.generate_config(docs_dir, nav_file, template, mkdocs_yml, site_name)

View File

@@ -7,6 +7,7 @@ cli: Group
def build( def build(
mcp: bool, mcp: bool,
mkdocs: bool, mkdocs: bool,
module_is_source: bool,
module: Optional[str], module: Optional[str],
project_name: Optional[str], project_name: Optional[str],
site_name: Optional[str], site_name: Optional[str],

View File

@@ -6,7 +6,12 @@ from docforge.loaders import GriffeLoader, discover_module_paths
from docforge.renderers import MkDocsRenderer from docforge.renderers import MkDocsRenderer
from docforge.nav import load_nav_spec, resolve_nav, MkDocsNavEmitter from docforge.nav import load_nav_spec, resolve_nav, MkDocsNavEmitter
def generate_sources(module: str, project_name: str | None, docs_dir: Path) -> None: def generate_sources(
module: str,
docs_dir: Path,
project_name: str | None = None,
module_is_source: bool | None = None,
) -> None:
""" """
Generate Markdown source files for the specified module. Generate Markdown source files for the specified module.
@@ -14,13 +19,18 @@ def generate_sources(module: str, project_name: str | None, docs_dir: Path) -> N
module: The dotted path of the primary module to document. module: The dotted path of the primary module to document.
project_name: Optional override for the project name. project_name: Optional override for the project name.
docs_dir: Directory where the generated Markdown files will be written. docs_dir: Directory where the generated Markdown files will be written.
module_is_source: Module is the source folder and to be treated as the root folder.
""" """
loader = GriffeLoader() loader = GriffeLoader()
discovered_paths = discover_module_paths(module) discovered_paths = discover_module_paths(module)
project = loader.load_project(discovered_paths, project_name) project = loader.load_project(discovered_paths, project_name)
renderer = MkDocsRenderer() renderer = MkDocsRenderer()
renderer.generate_sources(project, docs_dir) renderer.generate_sources(
project,
docs_dir,
module_is_source,
)
def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str) -> None: def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str) -> None:
""" """

View File

@@ -1,6 +1,11 @@
from pathlib import Path from pathlib import Path
def generate_sources(module: str, project_name: str | None, docs_dir: Path) -> None: ... def generate_sources(
module: str,
docs_dir: Path,
project_name: str | None = None,
module_is_source: bool | None = None,
) -> None: ...
def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str) -> None: ... def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str) -> None: ...
def build(mkdocs_yml: Path) -> None: ... def build(mkdocs_yml: Path) -> None: ...
def serve(mkdocs_yml: Path) -> None: ... def serve(mkdocs_yml: Path) -> None: ...

View File

@@ -10,7 +10,7 @@ and mkdocstrings, ensuring:
""" """
from pathlib import Path from pathlib import Path
from docforge.models import Project from docforge.models import Project, Module
class MkDocsRenderer: class MkDocsRenderer:
@@ -24,7 +24,12 @@ class MkDocsRenderer:
# ------------------------- # -------------------------
# Public API # Public API
# ------------------------- # -------------------------
def generate_sources(self, project: Project, out_dir: Path) -> None: def generate_sources(
self,
project: Project,
out_dir: Path,
module_is_source: bool | None = None,
) -> None:
""" """
Produce a set of Markdown files in the output directory based on the Produce a set of Markdown files in the output directory based on the
provided Project models. provided Project models.
@@ -32,6 +37,7 @@ class MkDocsRenderer:
Args: Args:
project: The project models to render. project: The project models to render.
out_dir: Target directory for documentation files. out_dir: Target directory for documentation files.
module_is_source: Module is the source folder and to be treated as the root folder.
""" """
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
self._ensure_root_index(project, out_dir) self._ensure_root_index(project, out_dir)
@@ -46,12 +52,23 @@ class MkDocsRenderer:
} }
for module in modules: for module in modules:
self._write_module(module, packages, out_dir) self._write_module(
module,
packages,
out_dir,
module_is_source,
)
# ------------------------- # -------------------------
# Internal helpers # Internal helpers
# ------------------------- # -------------------------
def _write_module(self, module, packages: set[str], out_dir: Path) -> None: def _write_module(
self,
module: Module,
packages: set[str],
out_dir: Path,
module_is_source: bool | None = None,
) -> None:
""" """
Write a single module's documentation file. Packages are written as Write a single module's documentation file. Packages are written as
'index.md' inside their respective directories. 'index.md' inside their respective directories.
@@ -60,28 +77,35 @@ class MkDocsRenderer:
module: The module to write. module: The module to write.
packages: A set of module paths that are identified as packages. packages: A set of module paths that are identified as packages.
out_dir: The base output directory. out_dir: The base output directory.
module_is_source: Module is the source folder and to be treated as the root folder.
""" """
parts = module.path.split(".") parts = module.path.split(".")
if module_is_source:
module_name, parts = parts[0], parts[1:]
else:
module_name, parts = parts[0], parts
if module.path in packages: if module.path in packages:
# Package → directory/index.md # Package → directory/index.md
dir_path = out_dir.joinpath(*parts) dir_path = out_dir.joinpath(*parts)
dir_path.mkdir(parents=True, exist_ok=True) dir_path.mkdir(parents=True, exist_ok=True)
md_path = dir_path / "index.md" md_path = dir_path / "index.md"
link_target = f"{parts[-1]}/" link_target = f"{parts[-1]}/" if parts else None
else: else:
# Leaf module → parent_dir/<name>.md # Leaf module → parent_dir/<name>.md
dir_path = out_dir.joinpath(*parts[:-1]) dir_path = out_dir.joinpath(*parts[:-1])
dir_path.mkdir(parents=True, exist_ok=True) dir_path.mkdir(parents=True, exist_ok=True)
md_path = dir_path / f"{parts[-1]}.md" md_path = dir_path / f"{parts[-1]}.md"
link_target = f"{parts[-1]}.md" link_target = f"{parts[-1]}.md" if parts else None
title = parts[-1].replace("_", " ").title() title = parts[-1].replace("_", " ").title() if parts else module_name
content = self._render_markdown(title, module.path) content = self._render_markdown(title, module.path)
if not md_path.exists() or md_path.read_text(encoding="utf-8") != content: if not md_path.exists() or md_path.read_text(encoding="utf-8") != content:
md_path.write_text(content, encoding="utf-8") md_path.write_text(content, encoding="utf-8")
if not module_is_source:
self._ensure_parent_index(parts, out_dir, link_target, title) self._ensure_parent_index(parts, out_dir, link_target, title)
def _render_markdown(self, title: str, module_path: str) -> str: def _render_markdown(self, title: str, module_path: str) -> str:

View File

@@ -1,19 +1,23 @@
from pathlib import Path from pathlib import Path
from typing import Set
from docforge.models import Project, Module from docforge.models import Project, Module
class MkDocsRenderer: class MkDocsRenderer:
name: str name: str
def generate_sources(self, project: Project, out_dir: Path) -> None: ... def generate_sources(
self,
project: Project,
out_dir: Path,
module_is_source: bool | None = None,
) -> None: ...
def _write_module( def _write_module(
self, self,
module: Module, module: Module,
packages: Set[str], packages: set[str],
out_dir: Path, out_dir: Path,
module_is_source: bool | None = None,
) -> None: ... ) -> None: ...
def _render_markdown(self, title: str, module_path: str) -> str: ... def _render_markdown(self, title: str, module_path: str) -> str: ...