Improve documentation look & feel via MkDocs Material template enhancements #5
@@ -1,14 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
# CLI Layer
|
Command line interface entry point for doc-forge.
|
||||||
|
|
||||||
The `docforge.cli` package provides the command-line interface for interacting
|
This module exposes the primary CLI entry function used by the
|
||||||
with doc-forge.
|
``doc-forge`` command. The actual command implementation resides in
|
||||||
|
``docforge.cli.main``, while this module provides a stable import path
|
||||||
|
for external tools and the package entry point configuration.
|
||||||
|
|
||||||
## Available Commands
|
The CLI is responsible for orchestrating documentation workflows such as
|
||||||
|
generating renderer sources, building documentation sites, exporting
|
||||||
|
machine-readable documentation bundles, and starting development or MCP
|
||||||
|
servers.
|
||||||
|
|
||||||
- **build**: Build documentation (MkDocs site or MCP resources).
|
Typical usage
|
||||||
- **serve**: Serve documentation (MkDocs or MCP).
|
-------------
|
||||||
- **tree**: Visualize the introspected project structure.
|
|
||||||
|
The CLI is normally invoked through the installed command:
|
||||||
|
|
||||||
|
doc-forge <command> [options]
|
||||||
|
|
||||||
|
Programmatic invocation is also possible:
|
||||||
|
|
||||||
|
from docforge.cli import main
|
||||||
|
main()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .main import main
|
from .main import main
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ from docforge.cli import mcp_utils
|
|||||||
@click.group()
|
@click.group()
|
||||||
def cli() -> None:
|
def cli() -> None:
|
||||||
"""
|
"""
|
||||||
doc-forge CLI: A tool for introspecting Python projects and generating
|
Root command group for the doc-forge CLI.
|
||||||
documentation.
|
|
||||||
|
Provides commands for building, serving, and inspecting
|
||||||
|
documentation generated from Python source code.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -21,14 +23,12 @@ def cli() -> None:
|
|||||||
@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("--module", help="Python module to document")
|
||||||
@click.option("--project-name", help="Project name override")
|
@click.option("--project-name", help="Project name override")
|
||||||
# MkDocs specific
|
|
||||||
@click.option("--site-name", help="MkDocs site name")
|
@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("--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"),
|
@click.option("--nav", "nav_file", type=click.Path(path_type=Path), default=Path("docforge.nav.yml"),
|
||||||
help="Nav spec path")
|
help="Nav spec path")
|
||||||
@click.option("--template", type=click.Path(path_type=Path), help="MkDocs template 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("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="Output config path")
|
||||||
# MCP specific
|
|
||||||
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP output directory")
|
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP output directory")
|
||||||
def build(
|
def build(
|
||||||
mcp: bool,
|
mcp: bool,
|
||||||
@@ -44,25 +44,36 @@ def build(
|
|||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Build documentation (MkDocs site or MCP resources).
|
Build documentation artifacts.
|
||||||
|
|
||||||
This command orchestrates the full build process:
|
This command performs the full documentation build pipeline:
|
||||||
1. Introspects the code (Griffe)
|
|
||||||
2. Renders sources (MkDocs Markdown or MCP JSON)
|
1. Introspects the Python project using Griffe
|
||||||
3. (MkDocs only) Generates config and runs the final site build.
|
2. Generates renderer-specific documentation sources
|
||||||
|
3. Optionally builds the final documentation output
|
||||||
|
|
||||||
|
Depending on the selected options, the build can target:
|
||||||
|
|
||||||
|
- MkDocs static documentation sites
|
||||||
|
- MCP structured documentation resources
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mcp: Use the MCP documentation builder.
|
mcp: Enable MCP documentation generation.
|
||||||
mkdocs: Use the MkDocs documentation builder.
|
mkdocs: Enable MkDocs documentation generation.
|
||||||
module_is_source: Module is the source folder and to be treated as the root folder.
|
module_is_source: Treat the specified module directory as the
|
||||||
module: The dotted path of the module to the document.
|
project root.
|
||||||
|
module: Python module import path to 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: Display name for the MkDocs site.
|
||||||
docs_dir: (MkDocs) Target directory for Markdown sources.
|
docs_dir: Directory where Markdown documentation sources
|
||||||
nav_file: (MkDocs) Path to the docforge.nav.yml specification.
|
will be generated.
|
||||||
template: (MkDocs) Optional custom mkdocs.yml template.
|
nav_file: Path to the navigation specification file.
|
||||||
mkdocs_yml: (MkDocs) Target path for the generated mkdocs.yml.
|
template: Optional custom MkDocs configuration template.
|
||||||
out_dir: (MCP) Target directory for MCP JSON resources.
|
mkdocs_yml: Output path for the generated MkDocs configuration.
|
||||||
|
out_dir: Output directory for generated MCP resources.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
click.UsageError: If required options are missing or conflicting.
|
||||||
"""
|
"""
|
||||||
if not mcp and not mkdocs:
|
if not mcp and not mkdocs:
|
||||||
raise click.UsageError("Must specify either --mcp or --mkdocs")
|
raise click.UsageError("Must specify either --mcp or --mkdocs")
|
||||||
@@ -111,14 +122,22 @@ def serve(
|
|||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Serve documentation (MkDocs or MCP).
|
Serve generated documentation locally.
|
||||||
|
|
||||||
|
Depending on the selected mode, this command starts either:
|
||||||
|
|
||||||
|
- A MkDocs development server for browsing documentation
|
||||||
|
- An MCP server exposing structured documentation resources
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mcp: Serve MCP resources via an MCP server.
|
mcp: Serve documentation using the MCP server.
|
||||||
mkdocs: Serve the MkDocs site using the built-in development server.
|
mkdocs: Serve the MkDocs development site.
|
||||||
module: The dotted path of the module to serve.
|
module: Python module import path to serve via MCP.
|
||||||
mkdocs_yml: (MkDocs) Path to the mkdocs.yml configuration.
|
mkdocs_yml: Path to the MkDocs configuration file.
|
||||||
out_dir: (MCP) Path to the mcp_docs/ directory.
|
out_dir: Root directory containing MCP documentation resources.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
click.UsageError: If invalid or conflicting options are provided.
|
||||||
"""
|
"""
|
||||||
if mcp and mkdocs:
|
if mcp and mkdocs:
|
||||||
raise click.UsageError("Cannot specify both --mcp and --mkdocs")
|
raise click.UsageError("Cannot specify both --mcp and --mkdocs")
|
||||||
@@ -148,11 +167,15 @@ def tree(
|
|||||||
project_name: Optional[str],
|
project_name: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Visualize the project structure in the terminal.
|
Display the documentation object tree for a module.
|
||||||
|
|
||||||
|
This command introspects the specified module and prints a
|
||||||
|
hierarchical representation of the discovered documentation
|
||||||
|
objects, including modules, classes, functions, and members.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The module import path to recursively introspect.
|
module: Python module import path to introspect.
|
||||||
project_name: Optional override for the project name shown at the root.
|
project_name: Optional name to display as the project root.
|
||||||
"""
|
"""
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project([module], project_name)
|
project = loader.load_project([module], project_name)
|
||||||
@@ -167,11 +190,14 @@ def tree(
|
|||||||
|
|
||||||
def _print_object(obj, indent: str) -> None:
|
def _print_object(obj, indent: str) -> None:
|
||||||
"""
|
"""
|
||||||
Recursive helper to print doc objects and their members to the console.
|
Recursively print a documentation object and its members.
|
||||||
|
|
||||||
|
This helper function traverses the documentation object graph
|
||||||
|
and prints each object with indentation to represent hierarchy.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The DocObject instance to print.
|
obj: Documentation object to print.
|
||||||
indent: Current line indentation (e.g., '│ ').
|
indent: Current indentation prefix used for nested members.
|
||||||
"""
|
"""
|
||||||
click.echo(f"{indent}├── {obj.name}")
|
click.echo(f"{indent}├── {obj.name}")
|
||||||
for member in obj.get_all_members():
|
for member in obj.get_all_members():
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
"""
|
"""
|
||||||
Main entry point for the doc-forge CLI. This module delegates all command
|
Command-line entry point for the doc-forge CLI.
|
||||||
execution to docforge.cli.commands.
|
|
||||||
|
This module exposes the executable entry point that initializes the
|
||||||
|
Click command group defined in ``docforge.cli.commands``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from docforge.cli.commands import cli
|
from docforge.cli.commands import cli
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""
|
"""
|
||||||
CLI Entry point. Boots the click application.
|
Run the doc-forge command-line interface.
|
||||||
|
|
||||||
|
This function initializes and executes the Click CLI application.
|
||||||
|
It is used as the console entry point when invoking ``doc-forge``
|
||||||
|
from the command line.
|
||||||
"""
|
"""
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -4,14 +4,22 @@ from docforge.loaders import GriffeLoader, discover_module_paths
|
|||||||
from docforge.renderers import MCPRenderer
|
from docforge.renderers import MCPRenderer
|
||||||
from docforge.servers import MCPServer
|
from docforge.servers import MCPServer
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
Generate MCP-compatible documentation resources.
|
Generate MCP documentation resources from a Python module.
|
||||||
|
|
||||||
|
The function performs project introspection, builds the internal
|
||||||
|
documentation model, and renders MCP-compatible JSON resources
|
||||||
|
to the specified output directory.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The dotted path of the primary module to document.
|
module: Python module import path used as the entry point for
|
||||||
project_name: Optional override for the project name.
|
documentation generation.
|
||||||
out_dir: Directory where the MCP JSON resources and nav will be written.
|
project_name: Optional override for the project name used in
|
||||||
|
generated documentation metadata.
|
||||||
|
out_dir: Directory where MCP resources (index.json, nav.json,
|
||||||
|
and module data) will be written.
|
||||||
"""
|
"""
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
discovered_paths = discover_module_paths(module)
|
discovered_paths = discover_module_paths(module)
|
||||||
@@ -20,13 +28,23 @@ def generate_resources(module: str, project_name: str | None, out_dir: Path) ->
|
|||||||
renderer = MCPRenderer()
|
renderer = MCPRenderer()
|
||||||
renderer.generate_sources(project, out_dir)
|
renderer.generate_sources(project, out_dir)
|
||||||
|
|
||||||
|
|
||||||
def serve(module: str, mcp_root: Path) -> None:
|
def serve(module: str, mcp_root: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Serve MCP documentation from a pre-built bundle.
|
Start an MCP server for a pre-generated documentation bundle.
|
||||||
|
|
||||||
|
The server exposes documentation resources such as project metadata,
|
||||||
|
navigation structure, and module documentation through MCP endpoints.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The dotted path of the primary module to serve.
|
module: Python module import path used to identify the served
|
||||||
mcp_root: Path to the directory containing index.json, nav.json, and modules/.
|
documentation instance.
|
||||||
|
mcp_root: Path to the directory containing the MCP documentation
|
||||||
|
bundle (index.json, nav.json, and modules/).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
click.ClickException: If the MCP documentation bundle is missing
|
||||||
|
required files or directories.
|
||||||
"""
|
"""
|
||||||
if not mcp_root.exists():
|
if not mcp_root.exists():
|
||||||
raise click.ClickException(f"mcp_docs directory not found: {mcp_root}")
|
raise click.ClickException(f"mcp_docs directory not found: {mcp_root}")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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(
|
def generate_sources(
|
||||||
module: str,
|
module: str,
|
||||||
docs_dir: Path,
|
docs_dir: Path,
|
||||||
@@ -13,13 +14,20 @@ def generate_sources(
|
|||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Generate Markdown source files for the specified module.
|
Generate MkDocs Markdown sources for a Python module.
|
||||||
|
|
||||||
|
This function introspects the specified module, builds the internal
|
||||||
|
documentation model, and renders Markdown documentation files for
|
||||||
|
use with MkDocs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The dotted path of the primary module to document.
|
module: Python module import path used as the entry point for
|
||||||
project_name: Optional override for the project name.
|
documentation generation.
|
||||||
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.
|
project_name: Optional override for the project name used in
|
||||||
|
documentation metadata.
|
||||||
|
module_is_source: If True, treat the specified module directory as
|
||||||
|
the project root rather than a nested module.
|
||||||
"""
|
"""
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
discovered_paths = discover_module_paths(module)
|
discovered_paths = discover_module_paths(module)
|
||||||
@@ -38,16 +46,33 @@ def generate_sources(
|
|||||||
module_is_source,
|
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:
|
||||||
"""
|
"""
|
||||||
Generate an mkdocs.yml configuration file.
|
Generate an ``mkdocs.yml`` configuration file.
|
||||||
|
|
||||||
|
The configuration is created by combining a template configuration
|
||||||
|
with a navigation structure derived from the docforge navigation
|
||||||
|
specification.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
docs_dir: Path to the directory containing documentation Markdown files.
|
docs_dir: Directory containing generated documentation Markdown files.
|
||||||
nav_file: Path to the docforge.nav.yml specification.
|
nav_file: Path to the ``docforge.nav.yml`` navigation specification.
|
||||||
template: Optional path to an mkdocs.yml template (overrides built-in).
|
template: Optional path to a custom MkDocs configuration template.
|
||||||
out: Path where the final mkdocs.yml will be written.
|
If not provided, a built-in template will be used.
|
||||||
site_name: The display name for the documentation site.
|
out: Destination path where the generated ``mkdocs.yml`` file
|
||||||
|
will be written.
|
||||||
|
site_name: Display name for the generated documentation site.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
click.FileError: If the navigation specification or template
|
||||||
|
file cannot be found.
|
||||||
"""
|
"""
|
||||||
if not nav_file.exists():
|
if not nav_file.exists():
|
||||||
raise click.FileError(str(nav_file), hint="Nav spec not found")
|
raise click.FileError(str(nav_file), hint="Nav spec not found")
|
||||||
@@ -74,12 +99,19 @@ def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out:
|
|||||||
|
|
||||||
out.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
out.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def build(mkdocs_yml: Path) -> None:
|
def build(mkdocs_yml: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Build the documentation site using MkDocs.
|
Build the MkDocs documentation site.
|
||||||
|
|
||||||
|
This function loads the MkDocs configuration and runs the MkDocs
|
||||||
|
build command to generate the final static documentation site.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mkdocs_yml: Path to the mkdocs.yml configuration file.
|
mkdocs_yml: Path to the ``mkdocs.yml`` configuration file.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
click.ClickException: If the configuration file does not exist.
|
||||||
"""
|
"""
|
||||||
if not mkdocs_yml.exists():
|
if not mkdocs_yml.exists():
|
||||||
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
||||||
@@ -89,12 +121,19 @@ def build(mkdocs_yml: Path) -> None:
|
|||||||
|
|
||||||
mkdocs_build(load_config(str(mkdocs_yml)))
|
mkdocs_build(load_config(str(mkdocs_yml)))
|
||||||
|
|
||||||
|
|
||||||
def serve(mkdocs_yml: Path) -> None:
|
def serve(mkdocs_yml: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Serve the documentation site with live-reload using MkDocs.
|
Start an MkDocs development server with live reload.
|
||||||
|
|
||||||
|
The server watches documentation files and automatically reloads
|
||||||
|
the site when changes are detected.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mkdocs_yml: Path to the mkdocs.yml configuration file.
|
mkdocs_yml: Path to the ``mkdocs.yml`` configuration file.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
click.ClickException: If the configuration file does not exist.
|
||||||
"""
|
"""
|
||||||
if not mkdocs_yml.exists():
|
if not mkdocs_yml.exists():
|
||||||
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
"""
|
"""
|
||||||
# Loader Layer
|
Loader layer for doc-forge.
|
||||||
|
|
||||||
The `docforge.loaders` package is responsible for discovering Python source files
|
The ``docforge.loaders`` package is responsible for discovering Python modules
|
||||||
and extracting their documentation using static analysis.
|
and extracting documentation data using static analysis.
|
||||||
|
|
||||||
## Core Features
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
- **Discovery**: Automatically find all modules and packages in a project
|
This layer converts Python source code into an intermediate documentation
|
||||||
directory.
|
model used by doc-forge. It performs module discovery, introspection, and
|
||||||
- **Introspection**: Uses `griffe` to parse docstrings, signatures, and
|
initial filtering before the data is passed to the core documentation models.
|
||||||
hierarchical relationships without executing the code.
|
|
||||||
- **Filtering**: Automatically excludes private members (prefixed with `_`) to
|
Core capabilities include:
|
||||||
ensure clean public documentation.
|
|
||||||
|
- **Module discovery** – Locate Python modules and packages within a project.
|
||||||
|
- **Static introspection** – Parse docstrings, signatures, and object
|
||||||
|
hierarchies using the ``griffe`` library without executing the code.
|
||||||
|
- **Public API filtering** – Exclude private members (names prefixed with
|
||||||
|
``_``) to produce clean public documentation structures.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .griffe_loader import GriffeLoader, discover_module_paths
|
from .griffe_loader import GriffeLoader, discover_module_paths
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
This module provides the GriffeLoader, which uses the 'griffe' library to
|
Utilities for loading and introspecting Python modules using Griffe.
|
||||||
introspect Python source code and populate the doc-forge Project models.
|
|
||||||
|
This module provides the ``GriffeLoader`` class and helper utilities used to
|
||||||
|
discover Python modules, introspect their structure, and convert the results
|
||||||
|
into doc-forge documentation models.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -25,19 +28,27 @@ def discover_module_paths(
|
|||||||
project_root: Path | None = None,
|
project_root: Path | None = None,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Discover all Python modules under a package via filesystem traversal.
|
Discover Python modules within a package directory.
|
||||||
|
|
||||||
Rules:
|
The function scans the filesystem for ``.py`` files inside the specified
|
||||||
- Directory with __init__.py is treated as a package.
|
package and converts them into dotted module import paths.
|
||||||
- Any .py file is treated as a module.
|
|
||||||
- All paths are converted to dotted module paths.
|
Discovery rules:
|
||||||
|
|
||||||
|
- Directories containing ``__init__.py`` are treated as packages.
|
||||||
|
- Each ``.py`` file is treated as a module.
|
||||||
|
- Results are returned as dotted import paths.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module_name: The name of the package to discover.
|
module_name: Top-level package name to discover modules from.
|
||||||
project_root: The root directory of the project. Defaults to current working directory.
|
project_root: Root directory used to resolve module paths. If not
|
||||||
|
provided, the current working directory is used.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A sorted list of dotted module paths.
|
A sorted list of unique dotted module import paths.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the specified package directory does not exist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if project_root is None:
|
if project_root is None:
|
||||||
@@ -64,12 +75,19 @@ def discover_module_paths(
|
|||||||
|
|
||||||
class GriffeLoader:
|
class GriffeLoader:
|
||||||
"""
|
"""
|
||||||
Loads Python modules and extracts documentation using the Griffe introspection engine.
|
Load Python modules using Griffe and convert them into doc-forge models.
|
||||||
|
|
||||||
|
This loader uses the Griffe introspection engine to analyze Python source
|
||||||
|
code and transform the extracted information into ``Project``, ``Module``,
|
||||||
|
and ``DocObject`` instances used by doc-forge.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the GriffeLoader.
|
Initialize the Griffe-backed loader.
|
||||||
|
|
||||||
|
Creates an internal Griffe loader instance with dedicated collections
|
||||||
|
for modules and source lines.
|
||||||
"""
|
"""
|
||||||
self._loader = _GriffeLoader(
|
self._loader = _GriffeLoader(
|
||||||
modules_collection=ModulesCollection(),
|
modules_collection=ModulesCollection(),
|
||||||
@@ -77,21 +95,32 @@ class GriffeLoader:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def load_project(
|
def load_project(
|
||||||
self,
|
self,
|
||||||
module_paths: List[str],
|
module_paths: List[str],
|
||||||
project_name: Optional[str] = None,
|
project_name: Optional[str] = None,
|
||||||
skip_import_errors: bool = None,
|
skip_import_errors: bool = None,
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""
|
"""
|
||||||
Load multiple modules and combine them into a single Project models.
|
Load multiple modules and assemble them into a Project model.
|
||||||
|
|
||||||
|
Each module path is introspected and converted into a ``Module``
|
||||||
|
instance. All modules are then aggregated into a single ``Project``
|
||||||
|
object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module_paths: A list of dotted paths to the modules to load.
|
module_paths: List of dotted module import paths to load.
|
||||||
project_name: Optional name for the project. Defaults to the first module name.
|
project_name: Optional override for the project name. Defaults
|
||||||
skip_import_errors: If True, modules that fail to import will be skipped.
|
to the top-level name of the first module.
|
||||||
|
skip_import_errors: If True, modules that fail to load will be
|
||||||
|
skipped instead of raising an error.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Project instance containing the loaded modules.
|
A populated ``Project`` instance containing the loaded modules.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no module paths are provided.
|
||||||
|
ImportError: If a module fails to load and
|
||||||
|
``skip_import_errors`` is False.
|
||||||
"""
|
"""
|
||||||
if not module_paths:
|
if not module_paths:
|
||||||
raise ValueError("At least one module path must be provided")
|
raise ValueError("At least one module path must be provided")
|
||||||
@@ -116,13 +145,16 @@ class GriffeLoader:
|
|||||||
|
|
||||||
def load_module(self, path: str) -> Module:
|
def load_module(self, path: str) -> Module:
|
||||||
"""
|
"""
|
||||||
Load a single module and convert its introspection data into the docforge models.
|
Load and convert a single Python module.
|
||||||
|
|
||||||
|
The module is introspected using Griffe and then transformed into
|
||||||
|
a doc-forge ``Module`` model.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: The dotted path of the module to load.
|
path: Dotted import path of the module.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Module instance.
|
A populated ``Module`` instance.
|
||||||
"""
|
"""
|
||||||
self._loader.load(path)
|
self._loader.load(path)
|
||||||
griffe_module = self._loader.modules_collection[path]
|
griffe_module = self._loader.modules_collection[path]
|
||||||
@@ -135,13 +167,16 @@ class GriffeLoader:
|
|||||||
|
|
||||||
def _convert_module(self, obj: Object) -> Module:
|
def _convert_module(self, obj: Object) -> Module:
|
||||||
"""
|
"""
|
||||||
Convert a Griffe Object (module) into a docforge Module.
|
Convert a Griffe module object into a doc-forge Module.
|
||||||
|
|
||||||
|
All public members of the module are recursively converted into
|
||||||
|
``DocObject`` instances.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The Griffe Object representing the module.
|
obj: Griffe object representing the module.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A populated Module instance.
|
A populated ``Module`` model.
|
||||||
"""
|
"""
|
||||||
module = Module(
|
module = Module(
|
||||||
path=obj.path,
|
path=obj.path,
|
||||||
@@ -158,13 +193,17 @@ class GriffeLoader:
|
|||||||
|
|
||||||
def _convert_object(self, obj: Object) -> DocObject:
|
def _convert_object(self, obj: Object) -> DocObject:
|
||||||
"""
|
"""
|
||||||
Recursively convert a Griffe Object into a DocObject hierarchy.
|
Convert a Griffe object into a doc-forge DocObject.
|
||||||
|
|
||||||
|
The conversion preserves the object's metadata such as name,
|
||||||
|
kind, path, signature, and docstring. Child members are processed
|
||||||
|
recursively.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The Griffe Object to convert.
|
obj: Griffe object representing a documented Python object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A DocObject instance.
|
A ``DocObject`` instance representing the converted object.
|
||||||
"""
|
"""
|
||||||
kind = obj.kind.value
|
kind = obj.kind.value
|
||||||
signature = self._safe_signature(obj)
|
signature = self._safe_signature(obj)
|
||||||
@@ -193,13 +232,13 @@ class GriffeLoader:
|
|||||||
|
|
||||||
def _safe_docstring(self, obj: Object) -> Optional[str]:
|
def _safe_docstring(self, obj: Object) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Safely retrieve the docstring value from a Griffe object.
|
Safely extract a docstring from a Griffe object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The Griffe Object to inspect.
|
obj: Griffe object to inspect.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The raw docstring string, or None if missing or unresolvable.
|
The raw docstring text if available, otherwise ``None``.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return obj.docstring.value if obj.docstring else None
|
return obj.docstring.value if obj.docstring else None
|
||||||
@@ -208,13 +247,14 @@ class GriffeLoader:
|
|||||||
|
|
||||||
def _safe_signature(self, obj: Object) -> Optional[str]:
|
def _safe_signature(self, obj: Object) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Safely retrieve the signature string from a Griffe object.
|
Safely extract the signature of a Griffe object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The Griffe Object to inspect.
|
obj: Griffe object to inspect.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The string representation of the signature, or None.
|
String representation of the object's signature if available,
|
||||||
|
otherwise ``None``.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if hasattr(obj, "signature") and obj.signature:
|
if hasattr(obj, "signature") and obj.signature:
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
"""
|
"""
|
||||||
# Model Layer
|
Model layer for doc-forge.
|
||||||
|
|
||||||
The `docforge.models` package provides the core data structures used to represent
|
The ``docforge.models`` package defines the core data structures used to
|
||||||
Python source code in a documentation-focused hierarchy.
|
represent Python source code as a structured documentation model.
|
||||||
|
|
||||||
## Key Components
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
- **Project**: The root container for all documented modules.
|
The model layer forms the central intermediate representation used throughout
|
||||||
- **Module**: Represents a Python module or package, containing members.
|
doc-forge. Python modules and objects discovered during introspection are
|
||||||
- **DocObject**: A recursive structure for classes, functions, and attributes.
|
converted into a hierarchy of documentation models that can later be rendered
|
||||||
|
into different documentation formats.
|
||||||
|
|
||||||
These classes are designed to be renderer-agnostic, allowing the same internal
|
Key components:
|
||||||
representation to be transformed into various output formats (currently MkDocs).
|
|
||||||
|
- **Project** – Root container representing an entire documented codebase.
|
||||||
|
- **Module** – Representation of a Python module or package containing
|
||||||
|
documented members.
|
||||||
|
- **DocObject** – Recursive structure representing Python objects such as
|
||||||
|
classes, functions, methods, and attributes.
|
||||||
|
|
||||||
|
These models are intentionally **renderer-agnostic**, allowing the same
|
||||||
|
documentation structure to be transformed into multiple output formats
|
||||||
|
(e.g., MkDocs, MCP, or other renderers).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .project import Project
|
from .project import Project
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
This module defines the Module class, which represents a Python module or package
|
Documentation model representing a Python module or package.
|
||||||
in the doc-forge documentation models. It acts as a container for top-level
|
|
||||||
documented objects.
|
This module defines the ``Module`` class used in the doc-forge documentation
|
||||||
|
model. A ``Module`` acts as a container for top-level documented objects
|
||||||
|
(classes, functions, variables, and other members) discovered during
|
||||||
|
introspection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, Iterable, Optional
|
from typing import Dict, Iterable, Optional
|
||||||
@@ -11,12 +14,17 @@ from docforge.models.object import DocObject
|
|||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
"""
|
"""
|
||||||
Represents a documented Python module or package.
|
Representation of a documented Python module or package.
|
||||||
|
|
||||||
|
A ``Module`` stores metadata about the module itself and maintains a
|
||||||
|
collection of top-level documentation objects discovered during
|
||||||
|
introspection.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
path: Dotted import path of the module.
|
path: Dotted import path of the module.
|
||||||
docstring: Module-level docstring content.
|
docstring: Module-level documentation string, if present.
|
||||||
members: Dictionary mapping object names to their DocObject representations.
|
members: Mapping of object names to their corresponding
|
||||||
|
``DocObject`` representations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -25,11 +33,11 @@ class Module:
|
|||||||
docstring: Optional[str] = None,
|
docstring: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a new Module.
|
Initialize a Module instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: The dotted path of the module.
|
path: Dotted import path identifying the module.
|
||||||
docstring: The module's docstring, if any.
|
docstring: Module-level documentation text, if available.
|
||||||
"""
|
"""
|
||||||
self.path = path
|
self.path = path
|
||||||
self.docstring = docstring
|
self.docstring = docstring
|
||||||
@@ -40,27 +48,32 @@ class Module:
|
|||||||
Add a documented object to the module.
|
Add a documented object to the module.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The object to add.
|
obj: Documentation object to register as a top-level
|
||||||
|
member of the module.
|
||||||
"""
|
"""
|
||||||
self.members[obj.name] = obj
|
self.members[obj.name] = obj
|
||||||
|
|
||||||
def get_object(self, name: str) -> DocObject:
|
def get_object(self, name: str) -> DocObject:
|
||||||
"""
|
"""
|
||||||
Retrieve a member object by name.
|
Retrieve a documented object by name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: The name of the object.
|
name: Name of the object to retrieve.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The requested DocObject.
|
The corresponding ``DocObject`` instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If no object with the given name exists.
|
||||||
"""
|
"""
|
||||||
return self.members[name]
|
return self.members[name]
|
||||||
|
|
||||||
def get_all_objects(self) -> Iterable[DocObject]:
|
def get_all_objects(self) -> Iterable[DocObject]:
|
||||||
"""
|
"""
|
||||||
Get all top-level objects in the module.
|
Return all top-level documentation objects in the module.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An iterable of DocObject instances.
|
An iterable of ``DocObject`` instances representing the
|
||||||
|
module's public members.
|
||||||
"""
|
"""
|
||||||
return self.members.values()
|
return self.members.values()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
This module defines the DocObject class, the fundamental recursive unit of the
|
Documentation model representing individual Python objects.
|
||||||
doc-forge documentation models. A DocObject represents a single Python entity
|
|
||||||
(class, function, method, or attribute) and its nested members.
|
This module defines the ``DocObject`` class, the fundamental recursive unit of
|
||||||
|
the doc-forge documentation model. Each ``DocObject`` represents a Python
|
||||||
|
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 typing import Dict, Iterable, Optional
|
||||||
@@ -9,15 +12,20 @@ from typing import Dict, Iterable, Optional
|
|||||||
|
|
||||||
class DocObject:
|
class DocObject:
|
||||||
"""
|
"""
|
||||||
Represents a documented Python object (class, function, method, etc.).
|
Representation of a documented Python object.
|
||||||
|
|
||||||
|
A ``DocObject`` models a single Python entity discovered during
|
||||||
|
introspection. Objects may contain nested members, allowing the structure
|
||||||
|
of modules, classes, and other containers to be represented recursively.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
name: Local name of the object.
|
name: Local name of the object.
|
||||||
kind: Type of object (e.g., 'class', 'function', 'attribute').
|
kind: Type of object (for example ``class``, ``function``,
|
||||||
path: Full dotted import path to the object.
|
``method``, or ``attribute``).
|
||||||
signature: Callable signature, if applicable.
|
path: Fully qualified dotted path to the object.
|
||||||
docstring: Raw docstring content extracted from the source.
|
signature: Callable signature if the object represents a callable.
|
||||||
members: Dictionary mapping member names to their DocObject representations.
|
docstring: Raw docstring text extracted from the source code.
|
||||||
|
members: Mapping of member names to child ``DocObject`` instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -29,48 +37,54 @@ class DocObject:
|
|||||||
docstring: Optional[str] = None,
|
docstring: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a new DocObject.
|
Initialize a DocObject instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: The local name of the object.
|
name: Local name of the object.
|
||||||
kind: The kind of object (e.g., 'class', 'function').
|
kind: Object type identifier (for example ``class`` or ``function``).
|
||||||
path: The full dotted path to the object.
|
path: Fully qualified dotted path of the object.
|
||||||
signature: The object's signature (for callable objects).
|
signature: Callable signature if applicable.
|
||||||
docstring: The object's docstring, if any.
|
docstring: Documentation string associated with the object.
|
||||||
"""
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.path = path
|
self.path = path
|
||||||
self.signature = signature
|
self.signature = signature
|
||||||
self.docstring = docstring
|
self.docstring = docstring
|
||||||
self.members: Dict[str, 'DocObject'] = {}
|
self.members: Dict[str, "DocObject"] = {}
|
||||||
|
|
||||||
def add_member(self, obj: 'DocObject') -> None:
|
def add_member(self, obj: "DocObject") -> None:
|
||||||
"""
|
"""
|
||||||
Add a child member to this object (e.g., a method to a class).
|
Add a child documentation object.
|
||||||
|
|
||||||
|
This is typically used when attaching methods to classes or
|
||||||
|
nested objects to their parent containers.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The child DocObject to add.
|
obj: Documentation object to add as a member.
|
||||||
"""
|
"""
|
||||||
self.members[obj.name] = obj
|
self.members[obj.name] = obj
|
||||||
|
|
||||||
def get_member(self, name: str) -> 'DocObject':
|
def get_member(self, name: str) -> "DocObject":
|
||||||
"""
|
"""
|
||||||
Retrieve a child member by name.
|
Retrieve a member object by name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: The name of the member.
|
name: Name of the member to retrieve.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The requested DocObject.
|
The corresponding ``DocObject`` instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If the member does not exist.
|
||||||
"""
|
"""
|
||||||
return self.members[name]
|
return self.members[name]
|
||||||
|
|
||||||
def get_all_members(self) -> Iterable['DocObject']:
|
def get_all_members(self) -> Iterable["DocObject"]:
|
||||||
"""
|
"""
|
||||||
Get all members of this object.
|
Return all child members of the object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An iterable of child DocObject instances.
|
An iterable of ``DocObject`` instances representing nested members.
|
||||||
"""
|
"""
|
||||||
return self.members.values()
|
return self.members.values()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
This module defines the Project class, the top-level container for a documented
|
Documentation model representing a project.
|
||||||
project. It aggregates multiple Module instances into a single named entity.
|
|
||||||
|
This module defines the ``Project`` class, the top-level container used by
|
||||||
|
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 typing import Dict, Iterable
|
||||||
@@ -10,29 +13,32 @@ from docforge.models.module import Module
|
|||||||
|
|
||||||
class Project:
|
class Project:
|
||||||
"""
|
"""
|
||||||
Represents a documentation project, serving as a container for modules.
|
Representation of a documentation project.
|
||||||
|
|
||||||
|
A ``Project`` serves as the root container for all modules discovered during
|
||||||
|
introspection. Each module is stored by its dotted import path.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
name: Name of the project.
|
name: Name of the project.
|
||||||
modules: Dictionary mapping module paths to Module instances.
|
modules: Mapping of module paths to ``Module`` instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str) -> None:
|
def __init__(self, name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a new Project.
|
Initialize a Project instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: The name of the project.
|
name: Name used to identify the documentation project.
|
||||||
"""
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.modules: Dict[str, Module] = {}
|
self.modules: Dict[str, Module] = {}
|
||||||
|
|
||||||
def add_module(self, module: Module) -> None:
|
def add_module(self, module: Module) -> None:
|
||||||
"""
|
"""
|
||||||
Add a module to the project.
|
Register a module in the project.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The module to add.
|
module: Module instance to add to the project.
|
||||||
"""
|
"""
|
||||||
self.modules[module.path] = module
|
self.modules[module.path] = module
|
||||||
|
|
||||||
@@ -41,27 +47,30 @@ class Project:
|
|||||||
Retrieve a module by its dotted path.
|
Retrieve a module by its dotted path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: The dotted path of the module (e.g., 'pkg.mod').
|
path: Fully qualified dotted module path (for example ``pkg.module``).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The requested Module.
|
The corresponding ``Module`` instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If the module does not exist in the project.
|
||||||
"""
|
"""
|
||||||
return self.modules[path]
|
return self.modules[path]
|
||||||
|
|
||||||
def get_all_modules(self) -> Iterable[Module]:
|
def get_all_modules(self) -> Iterable[Module]:
|
||||||
"""
|
"""
|
||||||
Get all modules in the project.
|
Return all modules contained in the project.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An iterable of Module objects.
|
An iterable of ``Module`` instances.
|
||||||
"""
|
"""
|
||||||
return self.modules.values()
|
return self.modules.values()
|
||||||
|
|
||||||
def get_module_list(self) -> list[str]:
|
def get_module_list(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Get the list of all module dotted paths.
|
Return the list of module import paths.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of module paths.
|
A list containing the dotted paths of all modules in the project.
|
||||||
"""
|
"""
|
||||||
return list(self.modules.keys())
|
return list(self.modules.keys())
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
# Navigation Layer
|
Navigation layer for doc-forge.
|
||||||
|
|
||||||
The `docforge.nav` package manages the mapping between the logical documentation
|
The ``docforge.nav`` package manages the relationship between the logical
|
||||||
structure and the physical files on disk.
|
documentation structure defined by the user and the physical documentation
|
||||||
|
files generated on disk.
|
||||||
|
|
||||||
## Workflow
|
Workflow
|
||||||
|
--------
|
||||||
|
|
||||||
1. **Spec Definition**: Users define navigation intent in `docforge.nav.yml`.
|
1. **Specification** – Users define navigation intent in ``docforge.nav.yml``.
|
||||||
2. **Resolution**: `resolve_nav` matches patterns in the spec to generated `.md` files.
|
2. **Resolution** – ``resolve_nav`` expands patterns and matches them against
|
||||||
3. **Emission**: `MkDocsNavEmitter` produces the final YAML structure for `mkdocs.yml`.
|
generated Markdown files.
|
||||||
|
3. **Emission** – ``MkDocsNavEmitter`` converts the resolved structure into
|
||||||
|
the YAML navigation format required by ``mkdocs.yml``.
|
||||||
|
|
||||||
This abstraction allows doc-forge to support complex grouping and ordering
|
This layer separates documentation organization from the underlying source
|
||||||
independently of the source code's physical layout.
|
code layout, enabling flexible grouping, ordering, and navigation structures
|
||||||
|
independent of module hierarchy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .spec import NavSpec, load_nav_spec
|
from .spec import NavSpec, load_nav_spec
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
This module provides the MkDocsNavEmitter, which converts a ResolvedNav instance
|
MkDocs navigation emitter.
|
||||||
into the specific YAML-ready list structure expected by the MkDocs 'nav'
|
|
||||||
configuration.
|
This module provides the ``MkDocsNavEmitter`` class, which converts a
|
||||||
|
``ResolvedNav`` instance into the navigation structure required by the
|
||||||
|
MkDocs ``nav`` configuration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,19 +14,24 @@ from docforge.nav.resolver import ResolvedNav
|
|||||||
|
|
||||||
class MkDocsNavEmitter:
|
class MkDocsNavEmitter:
|
||||||
"""
|
"""
|
||||||
Emitter responsible for transforming a ResolvedNav into an MkDocs-compatible
|
Emit MkDocs navigation structures from resolved navigation data.
|
||||||
navigation structure.
|
|
||||||
|
The emitter transforms a ``ResolvedNav`` object into the YAML-compatible
|
||||||
|
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 list of navigation entries for mkdocs.yml.
|
Generate a navigation structure for ``mkdocs.yml``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nav: The resolved navigation data.
|
nav: Resolved navigation data describing documentation groups
|
||||||
|
and their associated Markdown files.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of dictionary mappings representing the MkDocs navigation.
|
A list of dictionaries representing the MkDocs navigation layout.
|
||||||
|
Each dictionary maps a navigation label to a page or a list of
|
||||||
|
pages.
|
||||||
"""
|
"""
|
||||||
result: List[Dict[str, Any]] = []
|
result: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
@@ -45,16 +52,18 @@ class MkDocsNavEmitter:
|
|||||||
|
|
||||||
def _to_relative(self, path: Path, docs_root: Path | None) -> str:
|
def _to_relative(self, path: Path, docs_root: Path | None) -> str:
|
||||||
"""
|
"""
|
||||||
Convert a filesystem path to a path relative to the documentation root.
|
Convert a filesystem path into a documentation-relative path.
|
||||||
This handles both absolute and relative filesystem paths, ensuring the
|
|
||||||
output is compatible with MkDocs navigation requirements.
|
This method normalizes paths so they can be used in MkDocs navigation.
|
||||||
|
It handles both absolute and relative filesystem paths and ensures the
|
||||||
|
resulting path is relative to the documentation root.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: The path to convert.
|
path: Filesystem path to convert.
|
||||||
docs_root: The root directory for documentation.
|
docs_root: Root directory of the documentation sources.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A string representing the relative POSIX-style path.
|
POSIX-style path relative to the documentation root.
|
||||||
"""
|
"""
|
||||||
if docs_root and path.is_absolute():
|
if docs_root and path.is_absolute():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the logic for resolving a NavSpec against the physical
|
Navigation resolution utilities.
|
||||||
filesystem. It expands globs and validates that all referenced documents
|
|
||||||
actually exist on disk.
|
This module resolves a ``NavSpec`` against the filesystem by expanding glob
|
||||||
|
patterns and validating that referenced documentation files exist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,12 +15,15 @@ from docforge.nav.spec import NavSpec
|
|||||||
|
|
||||||
class ResolvedNav:
|
class ResolvedNav:
|
||||||
"""
|
"""
|
||||||
Represents a navigation structure where all patterns and paths have been
|
Resolved navigation structure.
|
||||||
resolved against the actual filesystem contents.
|
|
||||||
|
A ``ResolvedNav`` represents navigation data after glob patterns have been
|
||||||
|
expanded and paths validated against the filesystem.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
home: Resolved relative path to the home page.
|
home: Relative path to the documentation home page.
|
||||||
groups: Mapping of group titles to lists of absolute or relative Path objects.
|
groups: Mapping of navigation group titles to lists of resolved
|
||||||
|
documentation file paths.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -32,9 +36,9 @@ class ResolvedNav:
|
|||||||
Initialize a ResolvedNav instance.
|
Initialize a ResolvedNav instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
home: The relative path to the project home page.
|
home: Relative path to the home page within the documentation root.
|
||||||
groups: A mapping of group names to their resolved filesystem paths.
|
groups: Mapping of group titles to resolved documentation file paths.
|
||||||
docs_root: The root documentation directory.
|
docs_root: Root directory of the documentation source files.
|
||||||
"""
|
"""
|
||||||
self.home = home
|
self.home = home
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
@@ -42,15 +46,20 @@ class ResolvedNav:
|
|||||||
|
|
||||||
def all_files(self) -> Iterable[Path]:
|
def all_files(self) -> Iterable[Path]:
|
||||||
"""
|
"""
|
||||||
Get an iterable of all resolved files in the navigation structure.
|
Iterate over all files referenced by the navigation structure.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An iterable of Path objects.
|
An iterable of ``Path`` objects representing documentation files.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the home page is defined but the documentation
|
||||||
|
root is not available for resolution.
|
||||||
"""
|
"""
|
||||||
if self.home:
|
if self.home:
|
||||||
if self._docs_root is None:
|
if self._docs_root is None:
|
||||||
raise RuntimeError("docs_root is required to resolve home path")
|
raise RuntimeError("docs_root is required to resolve home path")
|
||||||
yield self._docs_root / self.home
|
yield self._docs_root / self.home
|
||||||
|
|
||||||
for paths in self.groups.values():
|
for paths in self.groups.values():
|
||||||
for p in paths:
|
for p in paths:
|
||||||
yield p
|
yield p
|
||||||
@@ -61,34 +70,37 @@ def resolve_nav(
|
|||||||
docs_root: Path,
|
docs_root: Path,
|
||||||
) -> ResolvedNav:
|
) -> ResolvedNav:
|
||||||
"""
|
"""
|
||||||
Create a ResolvedNav by processing a NavSpec against the filesystem.
|
Resolve a navigation specification against the filesystem.
|
||||||
This expands globs and validates the existence of referenced files.
|
|
||||||
|
The function expands glob patterns defined in a ``NavSpec`` and verifies
|
||||||
|
that referenced documentation files exist within the documentation root.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
spec: The navigation specification to resolve.
|
spec: Navigation specification describing documentation layout.
|
||||||
docs_root: The root directory for documentation files.
|
docs_root: Root directory containing documentation Markdown files.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A ResolvedNav instance.
|
A ``ResolvedNav`` instance containing validated navigation paths.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FileNotFoundError: If a pattern doesn't match any files or the docs_root doesn't exist.
|
FileNotFoundError: If the documentation root does not exist or a
|
||||||
|
navigation pattern does not match any files.
|
||||||
"""
|
"""
|
||||||
if not docs_root.exists():
|
if not docs_root.exists():
|
||||||
raise FileNotFoundError(docs_root)
|
raise FileNotFoundError(docs_root)
|
||||||
|
|
||||||
def resolve_pattern(pattern: str) -> List[Path]:
|
def resolve_pattern(pattern: str) -> List[Path]:
|
||||||
"""
|
"""
|
||||||
Resolve a single glob pattern relative to the docs_root.
|
Resolve a glob pattern relative to the documentation root.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pattern: The glob pattern to resolve.
|
pattern: Glob pattern used to match documentation files.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A sorted list of matching Path objects.
|
A sorted list of matching ``Path`` objects.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FileNotFoundError: If the pattern doesn't match any files.
|
FileNotFoundError: If the pattern does not match any files.
|
||||||
"""
|
"""
|
||||||
full = docs_root / pattern
|
full = docs_root / pattern
|
||||||
matches = sorted(
|
matches = sorted(
|
||||||
@@ -100,7 +112,7 @@ def resolve_nav(
|
|||||||
|
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
# Resolve home
|
# Resolve home page
|
||||||
home: str | None = None
|
home: str | None = None
|
||||||
if spec.home:
|
if spec.home:
|
||||||
home_path = docs_root / spec.home
|
home_path = docs_root / spec.home
|
||||||
@@ -108,7 +120,7 @@ def resolve_nav(
|
|||||||
raise FileNotFoundError(spec.home)
|
raise FileNotFoundError(spec.home)
|
||||||
home = spec.home
|
home = spec.home
|
||||||
|
|
||||||
# Resolve groups
|
# Resolve navigation groups
|
||||||
resolved_groups: Dict[str, List[Path]] = {}
|
resolved_groups: Dict[str, List[Path]] = {}
|
||||||
|
|
||||||
for group, patterns in spec.groups.items():
|
for group, patterns in spec.groups.items():
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
This module defines the NavSpec class, which represents the user's intent for
|
Navigation specification model.
|
||||||
documentation navigation as defined in a YAML specification (usually
|
|
||||||
docforge.nav.yml).
|
This module defines the ``NavSpec`` class, which represents the navigation
|
||||||
|
structure defined by the user in the doc-forge navigation specification
|
||||||
|
(typically ``docforge.nav.yml``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,11 +14,16 @@ import yaml
|
|||||||
|
|
||||||
class NavSpec:
|
class NavSpec:
|
||||||
"""
|
"""
|
||||||
Parsed representation of the docforge navigation specification file.
|
Parsed representation of a navigation specification.
|
||||||
|
|
||||||
|
A ``NavSpec`` describes the intended documentation navigation layout before
|
||||||
|
it is resolved against the filesystem.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
home: Path to the home document (e.g., 'index.md').
|
home: Relative path to the documentation home page (for example
|
||||||
groups: Mapping of group titles to lists of path patterns/globs.
|
``index.md``).
|
||||||
|
groups: Mapping of navigation group titles to lists of file patterns
|
||||||
|
or glob expressions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -25,11 +32,12 @@ class NavSpec:
|
|||||||
groups: Dict[str, List[str]],
|
groups: Dict[str, List[str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a NavSpec.
|
Initialize a NavSpec instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
home: The path to the home document.
|
home: Relative path to the home document.
|
||||||
groups: A mapping of group names to lists of path patterns (globs).
|
groups: Mapping of group names to lists of path patterns
|
||||||
|
(glob expressions).
|
||||||
"""
|
"""
|
||||||
self.home = home
|
self.home = home
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
@@ -37,17 +45,18 @@ class NavSpec:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, path: Path) -> "NavSpec":
|
def load(cls, path: Path) -> "NavSpec":
|
||||||
"""
|
"""
|
||||||
Load a NavSpec from a YAML file.
|
Load a navigation specification from a YAML file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: The filesystem path to the YAML file.
|
path: Filesystem path to the navigation specification file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A NavSpec instance.
|
A ``NavSpec`` instance representing the parsed configuration.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FileNotFoundError: If the path does not exist.
|
FileNotFoundError: If the specified file does not exist.
|
||||||
ValueError: If the file content is not a valid NavSpec mapping.
|
ValueError: If the file contents are not a valid navigation
|
||||||
|
specification.
|
||||||
"""
|
"""
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(path)
|
raise FileNotFoundError(path)
|
||||||
@@ -78,33 +87,45 @@ class NavSpec:
|
|||||||
|
|
||||||
def all_patterns(self) -> List[str]:
|
def all_patterns(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get all path patterns referenced in the specification.
|
Return all path patterns referenced by the specification.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of all patterns (home plus all groups).
|
A list containing the home document (if defined) and all
|
||||||
|
group pattern entries.
|
||||||
"""
|
"""
|
||||||
patterns: List[str] = []
|
patterns: List[str] = []
|
||||||
|
|
||||||
if self.home:
|
if self.home:
|
||||||
patterns.append(self.home)
|
patterns.append(self.home)
|
||||||
|
|
||||||
for items in self.groups.values():
|
for items in self.groups.values():
|
||||||
patterns.extend(items)
|
patterns.extend(items)
|
||||||
|
|
||||||
return patterns
|
return patterns
|
||||||
|
|
||||||
|
|
||||||
def load_nav_spec(path: Path) -> NavSpec:
|
def load_nav_spec(path: Path) -> NavSpec:
|
||||||
"""
|
"""
|
||||||
Utility function to load a NavSpec from a file.
|
Load a navigation specification file.
|
||||||
|
|
||||||
|
This helper function reads a YAML navigation file and constructs a
|
||||||
|
corresponding ``NavSpec`` instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Path to the navigation specification file.
|
path: Path to the navigation specification file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A loaded NavSpec instance.
|
A ``NavSpec`` instance representing the parsed specification.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the specification file does not exist.
|
||||||
|
ValueError: If the YAML structure is invalid.
|
||||||
"""
|
"""
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(path)
|
raise FileNotFoundError(path)
|
||||||
|
|
||||||
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise ValueError("Nav spec must be a YAML mapping")
|
raise ValueError("Nav spec must be a YAML mapping")
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
"""
|
"""
|
||||||
# Renderers Layer
|
Renderers layer for doc-forge.
|
||||||
|
|
||||||
The `docforge.renderers` package handles the transformation of the internal
|
The ``docforge.renderers`` package transforms the internal documentation
|
||||||
documentation models into physical files formatted for specific documentation
|
models into files formatted for specific documentation systems.
|
||||||
engines.
|
|
||||||
|
|
||||||
## Current Implementations
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
- **MkDocsRenderer**: Generates Markdown files utilizing the `mkdocstrings`
|
Renderers consume the doc-forge project model and generate output suitable
|
||||||
syntax. It automatically handles package/module hierarchy and generates
|
for documentation tools or machine interfaces.
|
||||||
`index.md` files for packages.
|
|
||||||
|
|
||||||
## Extending
|
Current implementations:
|
||||||
|
|
||||||
To add a new renderer, implement the `DocRenderer` protocol defined in
|
- **MkDocsRenderer** – Produces Markdown files compatible with MkDocs and
|
||||||
`docforge.renderers.base`.
|
the ``mkdocstrings`` plugin. It automatically handles package hierarchy
|
||||||
|
and generates ``index.md`` files for packages.
|
||||||
|
- **MCPRenderer** – Emits structured JSON resources designed for consumption
|
||||||
|
by Model Context Protocol (MCP) clients.
|
||||||
|
|
||||||
|
Extending
|
||||||
|
---------
|
||||||
|
|
||||||
|
New renderers can be added by implementing the ``DocRenderer`` protocol
|
||||||
|
defined in ``docforge.renderers.base``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .mkdocs_renderer import MkDocsRenderer
|
from .mkdocs_renderer import MkDocsRenderer
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
This module defines the base interfaces and configuration containers for
|
Renderer base interfaces and configuration models.
|
||||||
doc-forge renderers. All renderer implementations should adhere to the
|
|
||||||
DocRenderer protocol.
|
This module defines the base protocol and configuration container used by
|
||||||
|
doc-forge renderers. Concrete renderer implementations should implement the
|
||||||
|
``DocRenderer`` protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,12 +16,22 @@ class RendererConfig:
|
|||||||
"""
|
"""
|
||||||
Configuration container for documentation renderers.
|
Configuration container for documentation renderers.
|
||||||
|
|
||||||
Args:
|
A ``RendererConfig`` instance groups together the project model and the
|
||||||
out_dir: The directory where documentation files should be written.
|
output directory used during rendering.
|
||||||
project: The introspected project models to be rendered.
|
|
||||||
|
Attributes:
|
||||||
|
out_dir: Directory where generated documentation files will be written.
|
||||||
|
project: Documentation project model to be rendered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, out_dir: Path, project: Project) -> None:
|
def __init__(self, out_dir: Path, project: Project) -> None:
|
||||||
|
"""
|
||||||
|
Initialize a RendererConfig instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
out_dir: Target directory where documentation files should be written.
|
||||||
|
project: Introspected project model to render.
|
||||||
|
"""
|
||||||
self.out_dir = out_dir
|
self.out_dir = out_dir
|
||||||
self.project = project
|
self.project = project
|
||||||
|
|
||||||
@@ -27,6 +39,9 @@ class RendererConfig:
|
|||||||
class DocRenderer(Protocol):
|
class DocRenderer(Protocol):
|
||||||
"""
|
"""
|
||||||
Protocol defining the interface for documentation renderers.
|
Protocol defining the interface for documentation renderers.
|
||||||
|
|
||||||
|
Implementations of this protocol are responsible for transforming a
|
||||||
|
``Project`` model into renderer-specific documentation sources.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
@@ -37,10 +52,11 @@ class DocRenderer(Protocol):
|
|||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Generate renderer-specific source files for the given project.
|
Generate renderer-specific documentation sources.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project: The project models containing modules and objects.
|
project: Project model containing modules and documentation objects.
|
||||||
out_dir: Target directory for the generated files.
|
out_dir: Directory where generated documentation sources
|
||||||
|
should be written.
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -7,18 +7,25 @@ from docforge.models import Project, Module, DocObject
|
|||||||
|
|
||||||
class MCPRenderer:
|
class MCPRenderer:
|
||||||
"""
|
"""
|
||||||
Renderer that emits MCP-native JSON resources from docforge models.
|
Renderer that generates MCP-compatible documentation resources.
|
||||||
|
|
||||||
|
This renderer converts doc-forge project models into structured JSON
|
||||||
|
resources suitable for consumption by systems implementing the Model
|
||||||
|
Context Protocol (MCP).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "mcp"
|
name = "mcp"
|
||||||
|
|
||||||
def generate_sources(self, project: Project, out_dir: Path) -> None:
|
def generate_sources(self, project: Project, out_dir: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Generate MCP-compatible JSON resources and navigation for the project.
|
Generate MCP documentation resources for a project.
|
||||||
|
|
||||||
|
The renderer serializes each module into a JSON resource and produces
|
||||||
|
supporting metadata files such as ``nav.json`` and ``index.json``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project: The project model to render.
|
project: Documentation project model to render.
|
||||||
out_dir: Target directory for the generated JSON files.
|
out_dir: Directory where MCP resources will be written.
|
||||||
"""
|
"""
|
||||||
modules_dir = out_dir / "modules"
|
modules_dir = out_dir / "modules"
|
||||||
modules_dir.mkdir(parents=True, exist_ok=True)
|
modules_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -54,11 +61,11 @@ class MCPRenderer:
|
|||||||
|
|
||||||
def _write_module(self, module: Module, modules_dir: Path) -> None:
|
def _write_module(self, module: Module, modules_dir: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Serialize a module into an MCP JSON resource on disk.
|
Serialize a module into an MCP resource file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The module instance to serialize.
|
module: Module instance to serialize.
|
||||||
modules_dir: The directory where the module JSON file should be written.
|
modules_dir: Directory where module JSON files are stored.
|
||||||
"""
|
"""
|
||||||
payload = {
|
payload = {
|
||||||
"module": module.path,
|
"module": module.path,
|
||||||
@@ -71,13 +78,13 @@ class MCPRenderer:
|
|||||||
|
|
||||||
def _render_module(self, module: Module) -> Dict:
|
def _render_module(self, module: Module) -> Dict:
|
||||||
"""
|
"""
|
||||||
Render a Module into MCP-friendly structured data.
|
Convert a Module model into MCP-compatible structured data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The module instance to render.
|
module: Module instance to convert.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary following the MCP documentation resource schema.
|
Dictionary representing the module and its documented objects.
|
||||||
"""
|
"""
|
||||||
data: Dict = {
|
data: Dict = {
|
||||||
"path": module.path,
|
"path": module.path,
|
||||||
@@ -92,13 +99,13 @@ class MCPRenderer:
|
|||||||
|
|
||||||
def _render_object(self, obj: DocObject) -> Dict:
|
def _render_object(self, obj: DocObject) -> Dict:
|
||||||
"""
|
"""
|
||||||
Recursively render a DocObject into structured MCP data.
|
Recursively convert a DocObject into structured MCP data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The documented object (class, func, etc.) to render.
|
obj: Documentation object to convert.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary representing the object and its members.
|
Dictionary describing the object and any nested members.
|
||||||
"""
|
"""
|
||||||
data: Dict = {
|
data: Dict = {
|
||||||
"name": obj.name,
|
"name": obj.name,
|
||||||
@@ -119,4 +126,13 @@ class MCPRenderer:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _json(data: Dict) -> str:
|
def _json(data: Dict) -> str:
|
||||||
|
"""
|
||||||
|
Serialize data to formatted JSON.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Dictionary to serialize.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string formatted with indentation and UTF-8 compatibility.
|
||||||
|
"""
|
||||||
return json.dumps(data, indent=2, ensure_ascii=False)
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
MkDocsRenderer
|
MkDocs renderer implementation.
|
||||||
|
|
||||||
Generates Markdown source files compatible with MkDocs Material
|
This module defines the ``MkDocsRenderer`` class, which generates Markdown
|
||||||
and mkdocstrings, ensuring:
|
documentation sources compatible with MkDocs Material and the mkdocstrings
|
||||||
|
plugin.
|
||||||
|
|
||||||
- Root index.md always exists
|
The renderer ensures a consistent documentation structure by:
|
||||||
- Parent package indexes are created automatically
|
|
||||||
- Child modules are linked in parent index files
|
- Creating a root ``index.md`` if one does not exist
|
||||||
- README.md can be generated from the root package docstring
|
- Generating package index pages automatically
|
||||||
|
- Linking child modules within parent package pages
|
||||||
|
- Optionally generating ``README.md`` from the root package docstring
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -16,8 +19,10 @@ from docforge.models import Project, Module
|
|||||||
|
|
||||||
class MkDocsRenderer:
|
class MkDocsRenderer:
|
||||||
"""
|
"""
|
||||||
Renderer that generates Markdown source files formatted for the MkDocs
|
Renderer that produces Markdown documentation for MkDocs.
|
||||||
'mkdocstrings' plugin.
|
|
||||||
|
Generated pages use mkdocstrings directives to reference Python modules,
|
||||||
|
allowing MkDocs to render API documentation dynamically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "mkdocs"
|
name = "mkdocs"
|
||||||
@@ -25,6 +30,7 @@ class MkDocsRenderer:
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
# Public API
|
# Public API
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
def generate_sources(
|
def generate_sources(
|
||||||
self,
|
self,
|
||||||
project: Project,
|
project: Project,
|
||||||
@@ -32,18 +38,17 @@ class MkDocsRenderer:
|
|||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Produce a set of Markdown files in the output directory based on the
|
Generate Markdown documentation files for a project.
|
||||||
provided Project models.
|
|
||||||
|
This method renders a documentation structure from the provided
|
||||||
|
project model and writes the resulting Markdown files to the
|
||||||
|
specified output directory.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project:
|
project: Project model containing modules to document.
|
||||||
The project model to render.
|
out_dir: Directory where generated Markdown files will be written.
|
||||||
|
module_is_source: If True, treat the specified module as the
|
||||||
out_dir:
|
documentation root rather than nesting it inside a folder.
|
||||||
Target directory for generated Markdown.
|
|
||||||
|
|
||||||
module_is_source:
|
|
||||||
If True, treat the module 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)
|
||||||
@@ -51,7 +56,7 @@ class MkDocsRenderer:
|
|||||||
modules = list(project.get_all_modules())
|
modules = list(project.get_all_modules())
|
||||||
paths = {m.path for m in modules}
|
paths = {m.path for m in modules}
|
||||||
|
|
||||||
# Package detection (level-agnostic)
|
# Detect packages (modules with children)
|
||||||
packages = {
|
packages = {
|
||||||
p for p in paths
|
p for p in paths
|
||||||
if any(other.startswith(p + ".") for other in paths)
|
if any(other.startswith(p + ".") for other in paths)
|
||||||
@@ -72,22 +77,23 @@ class MkDocsRenderer:
|
|||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Generate README.md from the root package docstring.
|
Generate a ``README.md`` file from the root module docstring.
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
- If module_is_source is True:
|
- If ``module_is_source`` is True, ``README.md`` is written to the
|
||||||
README.md is generated at project root (docs_dir.parent)
|
project root directory.
|
||||||
|
- If False, README generation is currently not implemented.
|
||||||
|
|
||||||
- If module_is_source is False:
|
Args:
|
||||||
TODO: generate README.md inside respective module folders
|
project: Project model containing documentation metadata.
|
||||||
|
docs_dir: Directory containing generated documentation sources.
|
||||||
|
module_is_source: Whether the module is treated as the project
|
||||||
|
source root.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Only implement source-root mode
|
|
||||||
# -------------------------
|
|
||||||
if not module_is_source:
|
if not module_is_source:
|
||||||
# TODO: support per-module README generation
|
# Future: support README generation per module
|
||||||
return
|
return
|
||||||
|
|
||||||
readme_path = docs_dir.parent / "README.md"
|
readme_path = docs_dir.parent / "README.md"
|
||||||
@@ -127,7 +133,13 @@ class MkDocsRenderer:
|
|||||||
|
|
||||||
def _find_root_module(self, project: Project) -> Module | None:
|
def _find_root_module(self, project: Project) -> Module | None:
|
||||||
"""
|
"""
|
||||||
Find the root module matching the project name.
|
Locate the root module corresponding to the project name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: Project model to inspect.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The root ``Module`` if found, otherwise ``None``.
|
||||||
"""
|
"""
|
||||||
for module in project.get_all_modules():
|
for module in project.get_all_modules():
|
||||||
if module.path == project.name:
|
if module.path == project.name:
|
||||||
@@ -142,14 +154,18 @@ class MkDocsRenderer:
|
|||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Write a single module's documentation file. Packages are written as
|
Write documentation for a single module.
|
||||||
'index.md' inside their respective directories.
|
|
||||||
|
Package modules are rendered as ``index.md`` files inside their
|
||||||
|
corresponding directories, while leaf modules are written as
|
||||||
|
standalone Markdown pages.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module: The module to write.
|
module: Module to render.
|
||||||
packages: A set of module paths that are identified as packages.
|
packages: Set of module paths identified as packages.
|
||||||
out_dir: The base output directory.
|
out_dir: Base directory for generated documentation files.
|
||||||
module_is_source: Module is the source folder and to be treated as the root folder.
|
module_is_source: Whether the module acts as the documentation
|
||||||
|
root directory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parts = module.path.split(".")
|
parts = module.path.split(".")
|
||||||
@@ -160,15 +176,12 @@ class MkDocsRenderer:
|
|||||||
module_name, parts = parts[0], parts
|
module_name, parts = parts[0], parts
|
||||||
|
|
||||||
if module.path in packages:
|
if module.path in packages:
|
||||||
# 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]}/" if parts else None
|
link_target = f"{parts[-1]}/" if parts else None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 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)
|
||||||
|
|
||||||
@@ -186,14 +199,14 @@ class MkDocsRenderer:
|
|||||||
|
|
||||||
def _render_markdown(self, title: str, module_path: str) -> str:
|
def _render_markdown(self, title: str, module_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
Generate the Markdown content for a module file.
|
Generate Markdown content for a module documentation page.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title: The display title for the page.
|
title: Page title displayed in the documentation.
|
||||||
module_path: The dotted path of the module to document.
|
module_path: Dotted import path of the module.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A string containing the Markdown source.
|
Markdown source containing a mkdocstrings directive.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
f"# {title}\n\n"
|
f"# {title}\n\n"
|
||||||
@@ -205,6 +218,13 @@ class MkDocsRenderer:
|
|||||||
project: Project,
|
project: Project,
|
||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Ensure that the root ``index.md`` page exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: Project model used for the page title.
|
||||||
|
out_dir: Documentation output directory.
|
||||||
|
"""
|
||||||
root_index = out_dir / "index.md"
|
root_index = out_dir / "index.md"
|
||||||
|
|
||||||
if not root_index.exists():
|
if not root_index.exists():
|
||||||
@@ -221,6 +241,16 @@ class MkDocsRenderer:
|
|||||||
link_target: str,
|
link_target: str,
|
||||||
title: str,
|
title: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Ensure that parent package index files exist and contain links to
|
||||||
|
child modules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parts: Module path components.
|
||||||
|
out_dir: Documentation output directory.
|
||||||
|
link_target: Link target used in the parent index.
|
||||||
|
title: Display title for the link.
|
||||||
|
"""
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
parent_index = out_dir / "index.md"
|
parent_index = out_dir / "index.md"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Server layer for doc-forge.
|
||||||
|
|
||||||
|
This module exposes server implementations used to provide live access
|
||||||
|
to generated documentation resources. Currently, it includes the MCP
|
||||||
|
documentation server.
|
||||||
|
"""
|
||||||
|
|
||||||
from .mcp_server import MCPServer
|
from .mcp_server import MCPServer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -9,16 +9,21 @@ from mcp.server.fastmcp import FastMCP
|
|||||||
|
|
||||||
class MCPServer:
|
class MCPServer:
|
||||||
"""
|
"""
|
||||||
MCP server for serving a pre-built MCP documentation bundle.
|
MCP server for serving a pre-generated documentation bundle.
|
||||||
|
|
||||||
|
The server exposes documentation resources and diagnostic tools through
|
||||||
|
MCP endpoints backed by JSON files generated by the MCP renderer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mcp_root: Path, name: str) -> None:
|
def __init__(self, mcp_root: Path, name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the MCPServer.
|
Initialize the MCP server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mcp_root: Path to the directory containing pre-built MCP JSON resources.
|
mcp_root: Directory containing the generated MCP documentation
|
||||||
name: Name of the MCP server.
|
bundle (for example ``index.json``, ``nav.json``, and
|
||||||
|
``modules/``).
|
||||||
|
name: Identifier used for the MCP server instance.
|
||||||
"""
|
"""
|
||||||
self.mcp_root = mcp_root
|
self.mcp_root = mcp_root
|
||||||
self.app = FastMCP(name)
|
self.app = FastMCP(name)
|
||||||
@@ -32,19 +37,24 @@ class MCPServer:
|
|||||||
|
|
||||||
def _read_json(self, path: Path) -> Any:
|
def _read_json(self, path: Path) -> Any:
|
||||||
"""
|
"""
|
||||||
Read and parse a JSON file, returning diagnostic errors if missing.
|
Load and parse a JSON file.
|
||||||
|
|
||||||
|
If the file does not exist, a structured error response is returned
|
||||||
|
instead of raising an exception.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Path to the JSON file.
|
path: Path to the JSON file to read.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The parsed JSON data or an error dictionary.
|
Parsed JSON data if the file exists, otherwise an error dictionary
|
||||||
|
describing the missing resource.
|
||||||
"""
|
"""
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
return {
|
return {
|
||||||
"error": "not_found",
|
"error": "not_found",
|
||||||
"path": str(path),
|
"path": str(path),
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.loads(path.read_text(encoding="utf-8"))
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -53,8 +63,16 @@ class MCPServer:
|
|||||||
|
|
||||||
def _register_resources(self) -> None:
|
def _register_resources(self) -> None:
|
||||||
"""
|
"""
|
||||||
Register MCP resources for index, nav, and individual modules.
|
Register MCP resource endpoints.
|
||||||
|
|
||||||
|
The server exposes documentation resources through the following
|
||||||
|
endpoints:
|
||||||
|
|
||||||
|
- ``docs://index`` – Project metadata
|
||||||
|
- ``docs://nav`` – Navigation structure
|
||||||
|
- ``docs://modules/{module}`` – Individual module documentation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@self.app.resource("docs://index")
|
@self.app.resource("docs://index")
|
||||||
def index():
|
def index():
|
||||||
return self._read_json(self.mcp_root / "index.json")
|
return self._read_json(self.mcp_root / "index.json")
|
||||||
@@ -70,26 +88,35 @@ class MCPServer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# MCP tools (optional / diagnostic)
|
# MCP tools
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _register_tools(self) -> None:
|
def _register_tools(self) -> None:
|
||||||
"""
|
"""
|
||||||
Register high-level MCP tools for diagnostics.
|
Register optional MCP diagnostic tools.
|
||||||
|
|
||||||
|
These tools provide lightweight endpoints useful for verifying that
|
||||||
|
the MCP server is operational.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@self.app.tool()
|
@self.app.tool()
|
||||||
def ping() -> str:
|
def ping() -> str:
|
||||||
|
"""Return a simple health check response."""
|
||||||
return "pong"
|
return "pong"
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Server lifecycle
|
# Server lifecycle
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http") -> None:
|
def run(
|
||||||
|
self,
|
||||||
|
transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http",
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Start the MCP server.
|
Start the MCP server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
transport: MCP transport (default: streamable-http)
|
transport: Transport mechanism used by the MCP server. Supported
|
||||||
|
options include ``stdio``, ``sse``, and ``streamable-http``.
|
||||||
"""
|
"""
|
||||||
self.app.run(transport=transport)
|
self.app.run(transport=transport)
|
||||||
|
|||||||
Reference in New Issue
Block a user