standardize packaging, tooling, docs, CI, and licensing

This commit is contained in:
2026-09-10 18:49:04 +05:30
parent 8253c25928
commit 0129268cd3
61 changed files with 429 additions and 273 deletions

View File

@@ -1,7 +1,7 @@
from .loaders import GriffeLoader, discover_module_paths
from .renderers import MkDocsRenderer, MCPRenderer
from .cli import main
from . import models
from .cli import main
from .loaders import GriffeLoader, discover_module_paths
from .renderers import MCPRenderer, MkDocsRenderer
__all__ = [
"GriffeLoader",

View File

@@ -37,6 +37,4 @@ Example:
from .main import main
__all__ = [
"main"
]
__all__ = ["main"]

View File

@@ -1,5 +1,3 @@
from .main import main
__all__ = [
"main"
]
__all__ = ["main"]

View File

@@ -6,12 +6,12 @@ Command definitions for the doc-forge CLI.
Provides the CLI structure using Click, including build, serve, and tree commands.
"""
import click
from pathlib import Path
from typing import Sequence, Optional
import click
from docforge.cli import mcp_utils, mkdocs_utils
from docforge.loaders import GriffeLoader
from docforge.cli import mkdocs_utils
from docforge.cli import mcp_utils
@click.group()
@@ -28,26 +28,52 @@ def cli() -> None:
@cli.command()
@click.option("--mcp", is_flag=True, help="Build MCP resources")
@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-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("--project-name", help="Project name override")
@click.option("--site-name", help="MkDocs site name")
@click.option("--docs-dir", type=click.Path(path_type=Path), default=Path("docs"), help="Directory for MD sources")
@click.option("--nav", "nav_file", type=click.Path(path_type=Path), default=Path("docforge.nav.yml"),
help="Nav spec path")
@click.option("--template", type=click.Path(path_type=Path), help="MkDocs template path")
@click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="Output config path")
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP output directory")
@click.option(
"--docs-dir",
type=click.Path(path_type=Path),
default=Path("docs"),
help="Directory for MD sources",
)
@click.option(
"--nav",
"nav_file",
type=click.Path(path_type=Path),
default=Path("docforge.nav.yml"),
help="Nav spec path",
)
@click.option(
"--template", type=click.Path(path_type=Path), help="MkDocs template path"
)
@click.option(
"--mkdocs-yml",
type=click.Path(path_type=Path),
default=Path("mkdocs.yml"),
help="Output config path",
)
@click.option(
"--out-dir",
type=click.Path(path_type=Path),
default=Path("mcp_docs"),
help="MCP output directory",
)
def build(
mcp: bool,
mkdocs: bool,
module_is_source: bool,
module: Optional[str],
project_name: Optional[str],
site_name: Optional[str],
module: str | None,
project_name: str | None,
site_name: str | None,
docs_dir: Path,
nav_file: Path,
template: Optional[Path],
template: Path | None,
mkdocs_yml: Path,
out_dir: Path,
) -> None:
@@ -121,7 +147,9 @@ def build(
)
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
)
click.echo("Running MkDocs build...")
mkdocs_utils.build(mkdocs_yml)
@@ -140,12 +168,22 @@ def build(
@click.option("--mcp", is_flag=True, help="Serve MCP documentation")
@click.option("--mkdocs", is_flag=True, help="Serve MkDocs site")
@click.option("--module", help="Python module to serve")
@click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="MkDocs config path")
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP root directory")
@click.option(
"--mkdocs-yml",
type=click.Path(path_type=Path),
default=Path("mkdocs.yml"),
help="MkDocs config path",
)
@click.option(
"--out-dir",
type=click.Path(path_type=Path),
default=Path("mcp_docs"),
help="MCP root directory",
)
def serve(
mcp: bool,
mkdocs: bool,
module: Optional[str],
module: str | None,
mkdocs_yml: Path,
out_dir: Path,
) -> None:
@@ -202,7 +240,7 @@ def serve(
)
def tree(
module: str,
project_name: Optional[str],
project_name: str | None,
) -> None:
"""
Display the documentation object tree for a module.

View File

@@ -1,6 +1,7 @@
from click.core import Group
from pathlib import Path
from typing import Sequence, Optional, Any
from typing import Any
from click.core import Group
cli: Group
@@ -8,27 +9,24 @@ def build(
mcp: bool,
mkdocs: bool,
module_is_source: bool,
module: Optional[str],
project_name: Optional[str],
site_name: Optional[str],
module: str | None,
project_name: str | None,
site_name: str | None,
docs_dir: Path,
nav_file: Path,
template: Optional[Path],
template: Path | None,
mkdocs_yml: Path,
out_dir: Path,
) -> None: ...
def serve(
mcp: bool,
mkdocs: bool,
module: Optional[str],
module: str | None,
mkdocs_yml: Path,
out_dir: Path,
) -> None: ...
def tree(
module: str,
project_name: Optional[str],
project_name: str | None,
) -> None: ...
def _print_object(obj: Any, indent: str) -> None: ...

View File

@@ -22,4 +22,4 @@ def main() -> None:
if __name__ == "__main__":
main()
main()

View File

@@ -5,7 +5,9 @@ Utilities for working with MCP in the doc-forge CLI.
"""
from pathlib import Path
import click
from docforge.loaders import GriffeLoader, discover_module_paths
from docforge.renderers import MCPRenderer
from docforge.servers import MCPServer

View File

@@ -1,4 +1,6 @@
from pathlib import Path
def generate_resources(module: str, project_name: str | None, out_dir: Path) -> None: ...
def generate_resources(
module: str, project_name: str | None, out_dir: Path
) -> None: ...
def serve(module: str, mcp_root: Path) -> None: ...

View File

@@ -4,13 +4,15 @@
Utilities for working with MkDocs in the doc-forge CLI.
"""
from pathlib import Path
from importlib import resources
from pathlib import Path
import click
import yaml
from docforge.loaders import GriffeLoader, discover_module_paths
from docforge.nav import MkDocsNavEmitter, load_nav_spec, resolve_nav
from docforge.renderers import MkDocsRenderer
from docforge.nav import load_nav_spec, resolve_nav, MkDocsNavEmitter
def generate_sources(
@@ -138,8 +140,8 @@ def build(mkdocs_yml: Path) -> None:
if not mkdocs_yml.exists():
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
from mkdocs.config import load_config
from mkdocs.commands.build import build as mkdocs_build
from mkdocs.config import load_config
mkdocs_build(load_config(str(mkdocs_yml)))
@@ -163,4 +165,5 @@ def serve(mkdocs_yml: Path) -> None:
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
from mkdocs.commands.serve import serve as mkdocs_serve
mkdocs_serve(config_file=str(mkdocs_yml))

View File

@@ -6,6 +6,8 @@ def generate_sources(
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 serve(mkdocs_yml: Path) -> None: ...

View File

@@ -10,17 +10,18 @@ into doc-forge documentation models.
import logging
from pathlib import Path
from typing import List, Optional
from griffe import (
GriffeLoader as _GriffeLoader,
ModulesCollection,
LinesCollection,
Object,
AliasResolutionError,
LinesCollection,
ModulesCollection,
Object,
)
from griffe import (
GriffeLoader as _GriffeLoader,
)
from docforge.models import Module, Project, DocObject
from docforge.models import DocObject, Module, Project
logger = logging.getLogger(__name__)
@@ -28,7 +29,7 @@ logger = logging.getLogger(__name__)
def discover_module_paths(
module_name: str,
project_root: Path | None = None,
) -> List[str]:
) -> list[str]:
"""
Discover Python modules within a package directory.
@@ -65,7 +66,7 @@ def discover_module_paths(
if not pkg_dir.exists():
raise FileNotFoundError(f"Package not found: {pkg_dir}")
module_paths: List[str] = []
module_paths: list[str] = []
for path in pkg_dir.rglob("*.py"):
if path.name == "__init__.py":
@@ -102,10 +103,10 @@ class GriffeLoader:
)
def load_project(
self,
module_paths: List[str],
project_name: Optional[str] = None,
skip_import_errors: bool = None,
self,
module_paths: list[str],
project_name: str | None = None,
skip_import_errors: bool = None,
) -> Project:
"""
Load multiple modules and assemble them into a Project model.
@@ -250,7 +251,7 @@ class GriffeLoader:
# Safe extractors
# -------------------------
def _safe_docstring(self, obj: Object) -> Optional[str]:
def _safe_docstring(self, obj: Object) -> str | None:
"""
Safely extract a docstring from a Griffe object.
@@ -267,7 +268,7 @@ class GriffeLoader:
except AliasResolutionError:
return None
def _safe_signature(self, obj: Object) -> Optional[str]:
def _safe_signature(self, obj: Object) -> str | None:
"""
Safely extract the signature of a Griffe object.

View File

@@ -1,15 +1,11 @@
from typing import List, Optional
from pathlib import Path
from docforge.models import Module, Project
def discover_module_paths(
module_name: str,
project_root: Path | None = None,
) -> List[str]:
...
) -> list[str]: ...
class GriffeLoader:
"""Griffe-based introspection loaders.
@@ -18,11 +14,10 @@ class GriffeLoader:
"""
def __init__(self) -> None: ...
def load_project(
self,
module_paths: List[str],
project_name: Optional[str] = ...,
module_paths: list[str],
project_name: str | None = ...,
skip_import_errors: bool = ...,
) -> Project:
"""Load a documentation project from Python modules."""

View File

@@ -1,6 +1,6 @@
from .project import Project
from .module import Module
from .object import DocObject
from .project import Project
__all__ = [
"Project",

View File

@@ -9,7 +9,7 @@ model. A `Module` acts as a container for top-level documented objects
introspection.
"""
from typing import Dict, Iterable, Optional
from collections.abc import Iterable
from docforge.models.object import DocObject
@@ -36,7 +36,7 @@ class Module:
def __init__(
self,
path: str,
docstring: Optional[str] = None,
docstring: str | None = None,
) -> None:
"""
Initialize a Module instance.
@@ -50,7 +50,7 @@ class Module:
"""
self.path = path
self.docstring = docstring
self.members: Dict[str, DocObject] = {}
self.members: dict[str, DocObject] = {}
def add_object(self, obj: DocObject) -> None:
"""

View File

@@ -1,23 +1,19 @@
from typing import Dict, Iterable, Optional
from collections.abc import Iterable
from docforge.models.object import DocObject
class Module:
"""Represents a documented Python module."""
path: str
docstring: Optional[str]
members: Dict[str, DocObject]
docstring: str | None
members: dict[str, DocObject]
def __init__(
self,
path: str,
docstring: Optional[str] = ...,
docstring: str | None = ...,
) -> None: ...
def add_object(self, obj: DocObject) -> None: ...
def get_object(self, name: str) -> DocObject: ...
def get_all_objects(self) -> Iterable[DocObject]: ...

View File

@@ -9,7 +9,7 @@ entity such as a class, function, method, or attribute, and may contain nested
members that form a hierarchical documentation structure.
"""
from typing import Dict, Iterable, Optional
from collections.abc import Iterable
class DocObject:
@@ -45,8 +45,8 @@ class DocObject:
name: str,
kind: str,
path: str,
signature: Optional[str] = None,
docstring: Optional[str] = None,
signature: str | None = None,
docstring: str | None = None,
) -> None:
"""
Initialize a DocObject instance.
@@ -72,7 +72,7 @@ class DocObject:
self.path = path
self.signature = signature
self.docstring = docstring
self.members: Dict[str, "DocObject"] = {}
self.members: dict[str, DocObject] = {}
def add_member(self, obj: "DocObject") -> None:
"""

View File

@@ -1,5 +1,4 @@
from typing import Dict, Iterable, Optional
from collections.abc import Iterable
class DocObject:
"""Represents a documented Python object."""
@@ -7,21 +6,18 @@ class DocObject:
name: str
kind: str
path: str
signature: Optional[str]
docstring: Optional[str]
members: Dict[str, "DocObject"]
signature: str | None
docstring: str | None
members: dict[str, DocObject]
def __init__(
self,
name: str,
kind: str,
path: str,
signature: Optional[str] = ...,
docstring: Optional[str] = ...,
signature: str | None = ...,
docstring: str | None = ...,
) -> None: ...
def add_member(self, obj: "DocObject") -> None: ...
def get_member(self, name: str) -> "DocObject": ...
def get_all_members(self) -> Iterable["DocObject"]: ...
def add_member(self, obj: DocObject) -> None: ...
def get_member(self, name: str) -> DocObject: ...
def get_all_members(self) -> Iterable[DocObject]: ...

View File

@@ -8,7 +8,7 @@ doc-forge to represent a documented codebase. A `Project` aggregates multiple
modules and provides access to them through a unified interface.
"""
from typing import Dict, Iterable
from collections.abc import Iterable
from docforge.models.module import Module
@@ -37,7 +37,7 @@ class Project:
Name used to identify the documentation project.
"""
self.name = name
self.modules: Dict[str, Module] = {}
self.modules: dict[str, Module] = {}
def add_module(self, module: Module) -> None:
"""
@@ -85,4 +85,4 @@ class Project:
list[str]:
A list containing the dotted paths of all modules in the project.
"""
return list(self.modules.keys())
return list(self.modules.keys())

View File

@@ -1,20 +1,15 @@
from typing import Dict, Iterable
from collections.abc import Iterable
from docforge.models.module import Module
class Project:
"""Represents a documentation project."""
name: str
modules: Dict[str, Module]
modules: dict[str, Module]
def __init__(self, name: str) -> None: ...
def add_module(self, module: Module) -> None: ...
def get_module(self, path: str) -> Module: ...
def get_all_modules(self) -> Iterable[Module]: ...
def get_module_list(self) -> list[str]: ...

View File

@@ -1,6 +1,6 @@
from .spec import NavSpec, load_nav_spec
from .resolver import ResolvedNav, resolve_nav
from .mkdocs import MkDocsNavEmitter
from .resolver import ResolvedNav, resolve_nav
from .spec import NavSpec, load_nav_spec
__all__ = [
"NavSpec",

View File

@@ -7,7 +7,7 @@ MkDocs ``nav`` configuration.
"""
from pathlib import Path
from typing import List, Dict, Any
from typing import Any
from docforge.nav.resolver import ResolvedNav
@@ -20,7 +20,7 @@ class MkDocsNavEmitter:
list structure expected by the MkDocs ``nav`` configuration field.
"""
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
"""
Generate a navigation structure for ``mkdocs.yml``.
@@ -33,7 +33,7 @@ class MkDocsNavEmitter:
Each dictionary maps a navigation label to a page or a list of
pages.
"""
result: List[Dict[str, Any]] = []
result: list[dict[str, Any]] = []
# Home entry (semantic path)
if nav.home:
@@ -41,7 +41,7 @@ class MkDocsNavEmitter:
# Group entries
for group, paths in nav.groups.items():
entries: List[str] = []
entries: list[str] = []
for p in paths:
# Convert filesystem path back to docs-relative path
rel_path = self._to_relative(p, nav._docs_root)
@@ -75,7 +75,7 @@ class MkDocsNavEmitter:
path_str = path.as_posix()
docs_root_str = docs_root.as_posix()
if path_str.startswith(docs_root_str + "/"):
return path_str[len(docs_root_str) + 1:]
return path_str[len(docs_root_str) + 1 :]
# Fallback for other cases
return path.as_posix().split("/docs/", 1)[-1]

View File

@@ -1,15 +1,14 @@
from typing import Dict, List, Any
from pathlib import Path
from typing import Any
from docforge.nav.resolver import ResolvedNav
class MkDocsNavEmitter:
"""
Converts a ResolvedNav into MkDocs-compatible `nav` data.
"""
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
"""
Emit a structure suitable for insertion into mkdocs.yml.

View File

@@ -5,10 +5,9 @@ This module resolves a ``NavSpec`` against the filesystem by expanding glob
patterns and validating that referenced documentation files exist.
"""
from pathlib import Path
from typing import Dict, Iterable, List
import glob
from collections.abc import Iterable
from pathlib import Path
from docforge.nav.spec import NavSpec
@@ -29,7 +28,7 @@ class ResolvedNav:
def __init__(
self,
home: str | None,
groups: Dict[str, List[Path]],
groups: dict[str, list[Path]],
docs_root: Path | None = None,
) -> None:
"""
@@ -61,8 +60,7 @@ class ResolvedNav:
yield self._docs_root / self.home
for paths in self.groups.values():
for p in paths:
yield p
yield from paths
def resolve_nav(
@@ -89,7 +87,7 @@ def resolve_nav(
if not docs_root.exists():
raise FileNotFoundError(docs_root)
def resolve_pattern(pattern: str) -> List[Path]:
def resolve_pattern(pattern: str) -> list[Path]:
"""
Resolve a glob pattern relative to the documentation root.
@@ -103,9 +101,7 @@ def resolve_nav(
FileNotFoundError: If the pattern does not match any files.
"""
full = docs_root / pattern
matches = sorted(
Path(p) for p in glob.glob(str(full), recursive=True)
)
matches = sorted(Path(p) for p in glob.glob(str(full), recursive=True))
if not matches:
raise FileNotFoundError(pattern)
@@ -121,10 +117,10 @@ def resolve_nav(
home = spec.home
# Resolve navigation groups
resolved_groups: Dict[str, List[Path]] = {}
resolved_groups: dict[str, list[Path]] = {}
for group, patterns in spec.groups.items():
files: List[Path] = []
files: list[Path] = []
for pattern in patterns:
files.extend(resolve_pattern(pattern))
resolved_groups[group] = files

View File

@@ -1,9 +1,8 @@
from collections.abc import Iterable
from pathlib import Path
from typing import Dict, List, Iterable, Optional
from docforge.nav.spec import NavSpec
class ResolvedNav:
"""
Fully-resolved navigation tree.
@@ -13,17 +12,16 @@ class ResolvedNav:
- Order is preserved
"""
home: Optional[str]
groups: Dict[str, List[Path]]
_docs_root: Optional[Path]
home: str | None
groups: dict[str, list[Path]]
_docs_root: Path | None
def __init__(
self,
home: str | None,
groups: Dict[str, List[Path]],
groups: dict[str, list[Path]],
docs_root: Path | None = ...,
) -> None: ...
def all_files(self) -> Iterable[Path]:
"""
Return all resolved documentation files in nav order.
@@ -33,7 +31,6 @@ class ResolvedNav:
"""
...
def resolve_nav(
spec: NavSpec,
docs_root: Path,

View File

@@ -7,7 +7,6 @@ structure defined by the user in the doc-forge navigation specification
"""
from pathlib import Path
from typing import Dict, List, Optional
import yaml
@@ -28,8 +27,8 @@ class NavSpec:
def __init__(
self,
home: Optional[str],
groups: Dict[str, List[str]],
home: str | None,
groups: dict[str, list[str]],
) -> None:
"""
Initialize a NavSpec instance.
@@ -85,7 +84,7 @@ class NavSpec:
return cls(home=home, groups=groups)
def all_patterns(self) -> List[str]:
def all_patterns(self) -> list[str]:
"""
Return all path patterns referenced by the specification.
@@ -93,7 +92,7 @@ class NavSpec:
A list containing the home document (if defined) and all
group pattern entries.
"""
patterns: List[str] = []
patterns: list[str] = []
if self.home:
patterns.append(self.home)

View File

@@ -1,6 +1,4 @@
from pathlib import Path
from typing import Dict, List, Optional
class NavSpec:
"""
@@ -10,18 +8,16 @@ class NavSpec:
of filesystem structure or MkDocs specifics.
"""
home: Optional[str]
groups: Dict[str, List[str]]
home: str | None
groups: dict[str, list[str]]
def __init__(
self,
home: Optional[str],
groups: Dict[str, List[str]],
) -> None:
...
home: str | None,
groups: dict[str, list[str]],
) -> None: ...
@classmethod
def load(cls, path: Path) -> "NavSpec":
def load(cls, path: Path) -> NavSpec:
"""
Load and validate a nav specification from YAML.
@@ -31,12 +27,11 @@ class NavSpec:
"""
...
def all_patterns(self) -> List[str]:
def all_patterns(self) -> list[str]:
"""
Return all path patterns referenced by the spec
(including home and group entries).
"""
...
def load_nav_spec(path: Path) -> NavSpec: ...

0
docforge/py.typed Normal file
View File

View File

@@ -1,5 +1,5 @@
from .mkdocs_renderer import MkDocsRenderer
from .mcp_renderer import MCPRenderer
from .mkdocs_renderer import MkDocsRenderer
__all__ = [
"MkDocsRenderer",

View File

@@ -3,7 +3,6 @@ from typing import Protocol
from docforge.models import Project
class RendererConfig:
"""Renderer configuration container."""
@@ -12,7 +11,6 @@ class RendererConfig:
def __init__(self, out_dir: Path, project: Project) -> None: ...
class DocRenderer(Protocol):
"""Renderer interface."""

View File

@@ -9,9 +9,8 @@ resources compatible with the Model Context Protocol (MCP).
import json
from pathlib import Path
from typing import Dict, List
from docforge.models import Project, Module, DocObject
from docforge.models import DocObject, Module, Project
class MCPRenderer:
@@ -42,15 +41,17 @@ class MCPRenderer:
modules_dir = out_dir / "modules"
modules_dir.mkdir(parents=True, exist_ok=True)
nav: List[Dict[str, str]] = []
nav: list[dict[str, str]] = []
for module in project.get_all_modules():
self._write_module(module, modules_dir)
nav.append({
"module": module.path,
"resource": f"doc://modules/{module.path}",
})
nav.append(
{
"module": module.path,
"resource": f"doc://modules/{module.path}",
}
)
# Write nav.json
(out_dir / "nav.json").write_text(
@@ -91,7 +92,7 @@ class MCPRenderer:
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(self._json(payload), encoding="utf-8")
def _render_module(self, module: Module) -> Dict:
def _render_module(self, module: Module) -> dict:
"""
Convert a Module model into MCP-compatible structured data.
@@ -103,7 +104,7 @@ class MCPRenderer:
Dict:
Dictionary representing the module and its documented objects.
"""
data: Dict = {
data: dict = {
"path": module.path,
"docstring": module.docstring,
"objects": {},
@@ -114,7 +115,7 @@ class MCPRenderer:
return data
def _render_object(self, obj: DocObject) -> Dict:
def _render_object(self, obj: DocObject) -> dict:
"""
Recursively convert a DocObject into structured MCP data.
@@ -126,7 +127,7 @@ class MCPRenderer:
Dict:
Dictionary describing the object and any nested members.
"""
data: Dict = {
data: dict = {
"name": obj.name,
"kind": obj.kind,
"path": obj.path,
@@ -137,14 +138,13 @@ class MCPRenderer:
members = list(obj.get_all_members())
if members:
data["members"] = {
member.name: self._render_object(member)
for member in members
member.name: self._render_object(member) for member in members
}
return data
@staticmethod
def _json(data: Dict) -> str:
def _json(data: dict) -> str:
"""
Serialize data to formatted JSON.

View File

@@ -1,8 +1,7 @@
from pathlib import Path
from typing import Dict, List
from docforge.models import Project, Module, DocObject
from typing import Any
from docforge.models import DocObject, Module, Project
class MCPRenderer:
"""Renderer that emits MCP-native JSON resources from docforge models."""
@@ -15,12 +14,12 @@ class MCPRenderer:
def _write_module(self, module: Module, modules_dir: Path) -> None:
"""Serialize a module into an MCP JSON resource."""
def _render_module(self, module: Module) -> Dict:
def _render_module(self, module: Module) -> dict[str, Any]:
"""Render a Module into MCP-friendly structured data."""
def _render_object(self, obj: DocObject) -> Dict:
def _render_object(self, obj: DocObject) -> dict[str, Any]:
"""Recursively render a DocObject into structured MCP data."""
@staticmethod
def _json(data: Dict) -> str:
def _json(data: dict[str, Any]) -> str:
"""Serialize structured data to formatted JSON."""

View File

@@ -16,7 +16,8 @@ The renderer ensures a consistent documentation structure by:
"""
from pathlib import Path
from docforge.models import Project, Module
from docforge.models import Module, Project
class MkDocsRenderer:
@@ -65,8 +66,7 @@ class MkDocsRenderer:
# Detect packages (modules with children)
packages = {
p for p in paths
if any(other.startswith(p + ".") for other in paths)
p for p in paths if any(other.startswith(p + ".") for other in paths)
}
for module in modules:
@@ -127,12 +127,12 @@ class MkDocsRenderer:
str(root_module.docstring),
)
content = (
f"# {project.name}\n\n"
f"{doc.strip()}\n"
)
content = f"# {project.name}\n\n" f"{doc.strip()}\n"
if not readme_path.exists() or readme_path.read_text(encoding="utf-8") != content:
if (
not readme_path.exists()
or readme_path.read_text(encoding="utf-8") != content
):
readme_path.write_text(
content,
encoding="utf-8",
@@ -231,10 +231,7 @@ class MkDocsRenderer:
str:
Markdown source containing a mkdocstrings directive.
"""
return (
f"# {title}\n\n"
f"::: {module_path}\n"
)
return f"# {title}\n\n" f"::: {module_path}\n"
def _ensure_root_index(
self,
@@ -255,8 +252,7 @@ class MkDocsRenderer:
if not root_index.exists():
root_index.write_text(
f"# {project.name}\n\n"
"## Modules\n\n",
f"# {project.name}\n\n" "## Modules\n\n",
encoding="utf-8",
)

View File

@@ -1,6 +1,6 @@
from pathlib import Path
from docforge.models import Project, Module
from docforge.models import Module, Project
class MkDocsRenderer:
name: str
@@ -11,14 +11,12 @@ class MkDocsRenderer:
out_dir: Path,
module_is_source: bool | None = None,
) -> None: ...
def generate_readme(
self,
project: Project,
docs_dir: Path,
module_is_source: bool | None = None,
) -> None:
) -> None: ...
def _write_module(
self,
module: Module,
@@ -26,9 +24,8 @@ class MkDocsRenderer:
out_dir: Path,
module_is_source: bool | None = None,
) -> None: ...
def _render_markdown(self, title: str, module_path: str) -> str: ...
def _ensure_root_index(self, project, out_dir) -> None: ...
def _ensure_parent_index(self, parts, out_dir, link_target, title) -> None: ...
def _ensure_root_index(self, project: Project, out_dir: Path) -> None: ...
def _ensure_parent_index(
self, parts: list[str], out_dir: Path, link_target: str, title: str
) -> None: ...

View File

@@ -96,9 +96,7 @@ class MCPServer:
@self.app.resource("docs://modules/{module}")
def module(module: str):
return self._read_json(
self.mcp_root / "modules" / f"{module}.json"
)
return self._read_json(self.mcp_root / "modules" / f"{module}.json")
# ------------------------------------------------------------------
# MCP tools

View File

@@ -1,9 +1,8 @@
from pathlib import Path
from typing import Literal, Any
from typing import Any, Literal
from mcp.server.fastmcp import FastMCP
class MCPServer:
"""MCP server for serving documentation."""
@@ -11,12 +10,8 @@ class MCPServer:
app: FastMCP
def __init__(self, mcp_root: Path, name: str) -> None: ...
def _read_json(self, path: Path) -> Any: ...
def _register_resources(self) -> None: ...
def _register_tools(self) -> None: ...
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = ...) -> None:
"""Start the MCP server."""