From 17d39a3e88c82287fde2c4bf4a586b1a28631384 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sat, 7 Mar 2026 15:44:02 +0530 Subject: [PATCH] updated doc strings --- docforge/cli/__init__.py | 27 ++++-- docforge/cli/commands.py | 88 ++++++++++++------- docforge/cli/main.py | 17 +++- docforge/cli/mcp_utils.py | 32 +++++-- docforge/cli/mkdocs_utils.py | 69 +++++++++++---- docforge/loaders/__init__.py | 26 +++--- docforge/loaders/griffe_loader.py | 112 +++++++++++++++++-------- docforge/models/__init__.py | 29 +++++-- docforge/models/module.py | 43 ++++++---- docforge/models/object.py | 66 +++++++++------ docforge/models/project.py | 39 +++++---- docforge/nav/__init__.py | 23 +++-- docforge/nav/mkdocs.py | 39 +++++---- docforge/nav/resolver.py | 60 +++++++------ docforge/nav/spec.py | 57 +++++++++---- docforge/renderers/__init__.py | 30 ++++--- docforge/renderers/base.py | 34 ++++++-- docforge/renderers/mcp_renderer.py | 42 +++++++--- docforge/renderers/mkdocs_renderer.py | 116 ++++++++++++++++---------- docforge/servers/__init__.py | 10 ++- docforge/servers/mcp_server.py | 51 ++++++++--- 21 files changed, 680 insertions(+), 330 deletions(-) diff --git a/docforge/cli/__init__.py b/docforge/cli/__init__.py index 094887b..abce5d3 100644 --- a/docforge/cli/__init__.py +++ b/docforge/cli/__init__.py @@ -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 -with doc-forge. +This module exposes the primary CLI entry function used by the +``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). -- **serve**: Serve documentation (MkDocs or MCP). -- **tree**: Visualize the introspected project structure. +Typical usage +------------- + +The CLI is normally invoked through the installed command: + + doc-forge [options] + +Programmatic invocation is also possible: + + from docforge.cli import main + main() """ from .main import main diff --git a/docforge/cli/commands.py b/docforge/cli/commands.py index 12305a4..2bc057b 100644 --- a/docforge/cli/commands.py +++ b/docforge/cli/commands.py @@ -9,8 +9,10 @@ from docforge.cli import mcp_utils @click.group() def cli() -> None: """ - doc-forge CLI: A tool for introspecting Python projects and generating - documentation. + Root command group for the doc-forge CLI. + + Provides commands for building, serving, and inspecting + documentation generated from Python source code. """ 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", help="Python module to document") @click.option("--project-name", help="Project name override") -# MkDocs specific @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") -# MCP specific @click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP output directory") def build( mcp: bool, @@ -44,25 +44,36 @@ def build( out_dir: Path, ) -> None: """ - Build documentation (MkDocs site or MCP resources). + Build documentation artifacts. - This command orchestrates the full build process: - 1. Introspects the code (Griffe) - 2. Renders sources (MkDocs Markdown or MCP JSON) - 3. (MkDocs only) Generates config and runs the final site build. + This command performs the full documentation build pipeline: + + 1. Introspects the Python project using Griffe + 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: - mcp: Use the MCP documentation builder. - mkdocs: Use the MkDocs documentation builder. - module_is_source: Module is the source folder and to be treated as the root folder. - module: The dotted path of the module to the document. + mcp: Enable MCP documentation generation. + mkdocs: Enable MkDocs documentation generation. + module_is_source: Treat the specified module directory as the + project root. + module: Python module import path to document. project_name: Optional override for the project name. - site_name: (MkDocs) The site display name. Defaults to module name. - docs_dir: (MkDocs) Target directory for Markdown sources. - nav_file: (MkDocs) Path to the docforge.nav.yml specification. - template: (MkDocs) Optional custom mkdocs.yml template. - mkdocs_yml: (MkDocs) Target path for the generated mkdocs.yml. - out_dir: (MCP) Target directory for MCP JSON resources. + site_name: Display name for the MkDocs site. + docs_dir: Directory where Markdown documentation sources + will be generated. + nav_file: Path to the navigation specification file. + template: Optional custom MkDocs configuration template. + 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: raise click.UsageError("Must specify either --mcp or --mkdocs") @@ -111,14 +122,22 @@ def serve( out_dir: Path, ) -> 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: - mcp: Serve MCP resources via an MCP server. - mkdocs: Serve the MkDocs site using the built-in development server. - module: The dotted path of the module to serve. - mkdocs_yml: (MkDocs) Path to the mkdocs.yml configuration. - out_dir: (MCP) Path to the mcp_docs/ directory. + mcp: Serve documentation using the MCP server. + mkdocs: Serve the MkDocs development site. + module: Python module import path to serve via MCP. + mkdocs_yml: Path to the MkDocs configuration file. + out_dir: Root directory containing MCP documentation resources. + + Raises: + click.UsageError: If invalid or conflicting options are provided. """ if mcp and mkdocs: raise click.UsageError("Cannot specify both --mcp and --mkdocs") @@ -148,11 +167,15 @@ def tree( project_name: Optional[str], ) -> 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: - module: The module import path to recursively introspect. - project_name: Optional override for the project name shown at the root. + module: Python module import path to introspect. + project_name: Optional name to display as the project root. """ loader = GriffeLoader() project = loader.load_project([module], project_name) @@ -167,11 +190,14 @@ def tree( 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: - obj: The DocObject instance to print. - indent: Current line indentation (e.g., '│ '). + obj: Documentation object to print. + indent: Current indentation prefix used for nested members. """ click.echo(f"{indent}├── {obj.name}") for member in obj.get_all_members(): diff --git a/docforge/cli/main.py b/docforge/cli/main.py index 70c7051..c3b5a05 100644 --- a/docforge/cli/main.py +++ b/docforge/cli/main.py @@ -1,14 +1,23 @@ """ -Main entry point for the doc-forge CLI. This module delegates all command -execution to docforge.cli.commands. +Command-line entry point for the doc-forge CLI. + +This module exposes the executable entry point that initializes the +Click command group defined in ``docforge.cli.commands``. """ + from docforge.cli.commands import cli + 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() + if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/docforge/cli/mcp_utils.py b/docforge/cli/mcp_utils.py index add7dcb..f2ab54d 100644 --- a/docforge/cli/mcp_utils.py +++ b/docforge/cli/mcp_utils.py @@ -4,14 +4,22 @@ from docforge.loaders import GriffeLoader, discover_module_paths from docforge.renderers import MCPRenderer from docforge.servers import MCPServer + 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: - module: The dotted path of the primary module to document. - project_name: Optional override for the project name. - out_dir: Directory where the MCP JSON resources and nav will be written. + module: Python module import path used as the entry point for + documentation generation. + 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() 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.generate_sources(project, out_dir) + 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: - module: The dotted path of the primary module to serve. - mcp_root: Path to the directory containing index.json, nav.json, and modules/. + module: Python module import path used to identify the served + 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(): raise click.ClickException(f"mcp_docs directory not found: {mcp_root}") diff --git a/docforge/cli/mkdocs_utils.py b/docforge/cli/mkdocs_utils.py index dbcad85..01d2b11 100644 --- a/docforge/cli/mkdocs_utils.py +++ b/docforge/cli/mkdocs_utils.py @@ -6,6 +6,7 @@ from docforge.loaders import GriffeLoader, discover_module_paths from docforge.renderers import MkDocsRenderer from docforge.nav import load_nav_spec, resolve_nav, MkDocsNavEmitter + def generate_sources( module: str, docs_dir: Path, @@ -13,13 +14,20 @@ def generate_sources( module_is_source: bool | 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: - module: The dotted path of the primary module to document. - project_name: Optional override for the project name. + module: Python module import path used as the entry point for + documentation generation. 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() discovered_paths = discover_module_paths(module) @@ -38,16 +46,33 @@ def generate_sources( 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: - docs_dir: Path to the directory containing documentation Markdown files. - nav_file: Path to the docforge.nav.yml specification. - template: Optional path to an mkdocs.yml template (overrides built-in). - out: Path where the final mkdocs.yml will be written. - site_name: The display name for the documentation site. + docs_dir: Directory containing generated documentation Markdown files. + nav_file: Path to the ``docforge.nav.yml`` navigation specification. + template: Optional path to a custom MkDocs configuration template. + If not provided, a built-in template will be used. + 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(): 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") + 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: - 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(): 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))) + 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: - 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(): raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}") diff --git a/docforge/loaders/__init__.py b/docforge/loaders/__init__.py index 2845f8c..511e51c 100644 --- a/docforge/loaders/__init__.py +++ b/docforge/loaders/__init__.py @@ -1,17 +1,23 @@ """ -# Loader Layer +Loader layer for doc-forge. -The `docforge.loaders` package is responsible for discovering Python source files -and extracting their documentation using static analysis. +The ``docforge.loaders`` package is responsible for discovering Python modules +and extracting documentation data using static analysis. -## Core Features +Overview +-------- -- **Discovery**: Automatically find all modules and packages in a project - directory. -- **Introspection**: Uses `griffe` to parse docstrings, signatures, and - hierarchical relationships without executing the code. -- **Filtering**: Automatically excludes private members (prefixed with `_`) to - ensure clean public documentation. +This layer converts Python source code into an intermediate documentation +model used by doc-forge. It performs module discovery, introspection, and +initial filtering before the data is passed to the core documentation models. + +Core capabilities include: + +- **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 diff --git a/docforge/loaders/griffe_loader.py b/docforge/loaders/griffe_loader.py index c4ff540..4505e96 100644 --- a/docforge/loaders/griffe_loader.py +++ b/docforge/loaders/griffe_loader.py @@ -1,6 +1,9 @@ """ -This module provides the GriffeLoader, which uses the 'griffe' library to -introspect Python source code and populate the doc-forge Project models. +Utilities for loading and introspecting Python modules using Griffe. + +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 @@ -25,19 +28,27 @@ def discover_module_paths( project_root: Path | None = None, ) -> List[str]: """ - Discover all Python modules under a package via filesystem traversal. + Discover Python modules within a package directory. - Rules: - - Directory with __init__.py is treated as a package. - - Any .py file is treated as a module. - - All paths are converted to dotted module paths. + The function scans the filesystem for ``.py`` files inside the specified + package and converts them into dotted module import 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: - module_name: The name of the package to discover. - project_root: The root directory of the project. Defaults to current working directory. + module_name: Top-level package name to discover modules from. + project_root: Root directory used to resolve module paths. If not + provided, the current working directory is used. 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: @@ -64,12 +75,19 @@ def discover_module_paths( 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: """ - 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( modules_collection=ModulesCollection(), @@ -77,21 +95,32 @@ 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: Optional[str] = None, + skip_import_errors: bool = None, ) -> 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: - module_paths: A list of dotted paths to the modules to load. - project_name: Optional name for the project. Defaults to the first module name. - skip_import_errors: If True, modules that fail to import will be skipped. + module_paths: List of dotted module import paths to load. + project_name: Optional override for the project name. Defaults + 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: - 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: raise ValueError("At least one module path must be provided") @@ -116,13 +145,16 @@ class GriffeLoader: 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: - path: The dotted path of the module to load. + path: Dotted import path of the module. Returns: - A Module instance. + A populated ``Module`` instance. """ self._loader.load(path) griffe_module = self._loader.modules_collection[path] @@ -135,13 +167,16 @@ class GriffeLoader: 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: - obj: The Griffe Object representing the module. + obj: Griffe object representing the module. Returns: - A populated Module instance. + A populated ``Module`` model. """ module = Module( path=obj.path, @@ -158,13 +193,17 @@ class GriffeLoader: 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: - obj: The Griffe Object to convert. + obj: Griffe object representing a documented Python object. Returns: - A DocObject instance. + A ``DocObject`` instance representing the converted object. """ kind = obj.kind.value signature = self._safe_signature(obj) @@ -193,13 +232,13 @@ class GriffeLoader: 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: - obj: The Griffe Object to inspect. + obj: Griffe object to inspect. Returns: - The raw docstring string, or None if missing or unresolvable. + The raw docstring text if available, otherwise ``None``. """ try: return obj.docstring.value if obj.docstring else None @@ -208,13 +247,14 @@ class GriffeLoader: 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: - obj: The Griffe Object to inspect. + obj: Griffe object to inspect. Returns: - The string representation of the signature, or None. + String representation of the object's signature if available, + otherwise ``None``. """ try: if hasattr(obj, "signature") and obj.signature: diff --git a/docforge/models/__init__.py b/docforge/models/__init__.py index 58e0d58..8f6f1e6 100644 --- a/docforge/models/__init__.py +++ b/docforge/models/__init__.py @@ -1,17 +1,28 @@ """ -# Model Layer +Model layer for doc-forge. -The `docforge.models` package provides the core data structures used to represent -Python source code in a documentation-focused hierarchy. +The ``docforge.models`` package defines the core data structures used to +represent Python source code as a structured documentation model. -## Key Components +Overview +-------- -- **Project**: The root container for all documented modules. -- **Module**: Represents a Python module or package, containing members. -- **DocObject**: A recursive structure for classes, functions, and attributes. +The model layer forms the central intermediate representation used throughout +doc-forge. Python modules and objects discovered during introspection are +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 -representation to be transformed into various output formats (currently MkDocs). +Key components: + +- **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 diff --git a/docforge/models/module.py b/docforge/models/module.py index 498bfc9..ecd762b 100644 --- a/docforge/models/module.py +++ b/docforge/models/module.py @@ -1,7 +1,10 @@ """ -This module defines the Module class, which represents a Python module or package -in the doc-forge documentation models. It acts as a container for top-level -documented objects. +Documentation model representing a Python module or package. + +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 @@ -11,12 +14,17 @@ from docforge.models.object import DocObject 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: path: Dotted import path of the module. - docstring: Module-level docstring content. - members: Dictionary mapping object names to their DocObject representations. + docstring: Module-level documentation string, if present. + members: Mapping of object names to their corresponding + ``DocObject`` representations. """ def __init__( @@ -25,11 +33,11 @@ class Module: docstring: Optional[str] = None, ) -> None: """ - Initialize a new Module. + Initialize a Module instance. Args: - path: The dotted path of the module. - docstring: The module's docstring, if any. + path: Dotted import path identifying the module. + docstring: Module-level documentation text, if available. """ self.path = path self.docstring = docstring @@ -40,27 +48,32 @@ class Module: Add a documented object to the module. Args: - obj: The object to add. + obj: Documentation object to register as a top-level + member of the module. """ self.members[obj.name] = obj def get_object(self, name: str) -> DocObject: """ - Retrieve a member object by name. + Retrieve a documented object by name. Args: - name: The name of the object. + name: Name of the object to retrieve. Returns: - The requested DocObject. + The corresponding ``DocObject`` instance. + + Raises: + KeyError: If no object with the given name exists. """ return self.members[name] 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: - An iterable of DocObject instances. + An iterable of ``DocObject`` instances representing the + module's public members. """ return self.members.values() diff --git a/docforge/models/object.py b/docforge/models/object.py index 666ca2b..6661387 100644 --- a/docforge/models/object.py +++ b/docforge/models/object.py @@ -1,7 +1,10 @@ """ -This module defines the DocObject class, the fundamental recursive unit of the -doc-forge documentation models. A DocObject represents a single Python entity -(class, function, method, or attribute) and its nested members. +Documentation model representing individual Python objects. + +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 @@ -9,15 +12,20 @@ from typing import Dict, Iterable, Optional 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: name: Local name of the object. - kind: Type of object (e.g., 'class', 'function', 'attribute'). - path: Full dotted import path to the object. - signature: Callable signature, if applicable. - docstring: Raw docstring content extracted from the source. - members: Dictionary mapping member names to their DocObject representations. + kind: Type of object (for example ``class``, ``function``, + ``method``, or ``attribute``). + path: Fully qualified dotted path to the object. + signature: Callable signature if the object represents a callable. + docstring: Raw docstring text extracted from the source code. + members: Mapping of member names to child ``DocObject`` instances. """ def __init__( @@ -29,48 +37,54 @@ class DocObject: docstring: Optional[str] = None, ) -> None: """ - Initialize a new DocObject. + Initialize a DocObject instance. Args: - name: The local name of the object. - kind: The kind of object (e.g., 'class', 'function'). - path: The full dotted path to the object. - signature: The object's signature (for callable objects). - docstring: The object's docstring, if any. + name: Local name of the object. + kind: Object type identifier (for example ``class`` or ``function``). + path: Fully qualified dotted path of the object. + signature: Callable signature if applicable. + docstring: Documentation string associated with the object. """ self.name = name self.kind = kind 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: + 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: - obj: The child DocObject to add. + obj: Documentation object to add as a member. """ 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: - name: The name of the member. + name: Name of the member to retrieve. Returns: - The requested DocObject. + The corresponding ``DocObject`` instance. + + Raises: + KeyError: If the member does not exist. """ 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: - An iterable of child DocObject instances. + An iterable of ``DocObject`` instances representing nested members. """ return self.members.values() diff --git a/docforge/models/project.py b/docforge/models/project.py index 81c036f..3249cf6 100644 --- a/docforge/models/project.py +++ b/docforge/models/project.py @@ -1,6 +1,9 @@ """ -This module defines the Project class, the top-level container for a documented -project. It aggregates multiple Module instances into a single named entity. +Documentation model representing a project. + +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 @@ -10,29 +13,32 @@ from docforge.models.module import Module 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: 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: """ - Initialize a new Project. + Initialize a Project instance. Args: - name: The name of the project. + name: Name used to identify the documentation project. """ self.name = name self.modules: Dict[str, Module] = {} def add_module(self, module: Module) -> None: """ - Add a module to the project. + Register a module in the project. Args: - module: The module to add. + module: Module instance to add to the project. """ self.modules[module.path] = module @@ -41,27 +47,30 @@ class Project: Retrieve a module by its dotted path. Args: - path: The dotted path of the module (e.g., 'pkg.mod'). + path: Fully qualified dotted module path (for example ``pkg.module``). Returns: - The requested Module. + The corresponding ``Module`` instance. + + Raises: + KeyError: If the module does not exist in the project. """ return self.modules[path] def get_all_modules(self) -> Iterable[Module]: """ - Get all modules in the project. + Return all modules contained in the project. Returns: - An iterable of Module objects. + An iterable of ``Module`` instances. """ return self.modules.values() def get_module_list(self) -> list[str]: """ - Get the list of all module dotted paths. + Return the list of module import paths. 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()) \ No newline at end of file diff --git a/docforge/nav/__init__.py b/docforge/nav/__init__.py index 7a2aa94..9a9b51c 100644 --- a/docforge/nav/__init__.py +++ b/docforge/nav/__init__.py @@ -1,17 +1,22 @@ """ -# Navigation Layer +Navigation layer for doc-forge. -The `docforge.nav` package manages the mapping between the logical documentation -structure and the physical files on disk. +The ``docforge.nav`` package manages the relationship between the logical +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`. -2. **Resolution**: `resolve_nav` matches patterns in the spec to generated `.md` files. -3. **Emission**: `MkDocsNavEmitter` produces the final YAML structure for `mkdocs.yml`. +1. **Specification** – Users define navigation intent in ``docforge.nav.yml``. +2. **Resolution** – ``resolve_nav`` expands patterns and matches them against + 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 -independently of the source code's physical layout. +This layer separates documentation organization from the underlying source +code layout, enabling flexible grouping, ordering, and navigation structures +independent of module hierarchy. """ from .spec import NavSpec, load_nav_spec diff --git a/docforge/nav/mkdocs.py b/docforge/nav/mkdocs.py index d304cf8..b053341 100644 --- a/docforge/nav/mkdocs.py +++ b/docforge/nav/mkdocs.py @@ -1,7 +1,9 @@ """ -This module provides the MkDocsNavEmitter, which converts a ResolvedNav instance -into the specific YAML-ready list structure expected by the MkDocs 'nav' -configuration. +MkDocs navigation emitter. + +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 @@ -12,19 +14,24 @@ from docforge.nav.resolver import ResolvedNav class MkDocsNavEmitter: """ - Emitter responsible for transforming a ResolvedNav into an MkDocs-compatible - navigation structure. + Emit MkDocs navigation structures from resolved navigation data. + + 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]]: """ - Generate a list of navigation entries for mkdocs.yml. + Generate a navigation structure for ``mkdocs.yml``. Args: - nav: The resolved navigation data. + nav: Resolved navigation data describing documentation groups + and their associated Markdown files. 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]] = [] @@ -45,16 +52,18 @@ class MkDocsNavEmitter: def _to_relative(self, path: Path, docs_root: Path | None) -> str: """ - Convert a filesystem path to a path relative to the documentation root. - This handles both absolute and relative filesystem paths, ensuring the - output is compatible with MkDocs navigation requirements. + Convert a filesystem path into a documentation-relative path. + + 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: - path: The path to convert. - docs_root: The root directory for documentation. + path: Filesystem path to convert. + docs_root: Root directory of the documentation sources. Returns: - A string representing the relative POSIX-style path. + POSIX-style path relative to the documentation root. """ if docs_root and path.is_absolute(): try: @@ -67,6 +76,6 @@ class MkDocsNavEmitter: docs_root_str = docs_root.as_posix() if path_str.startswith(docs_root_str + "/"): return path_str[len(docs_root_str) + 1:] - + # Fallback for other cases return path.as_posix().split("/docs/", 1)[-1] diff --git a/docforge/nav/resolver.py b/docforge/nav/resolver.py index ee97617..e1911a6 100644 --- a/docforge/nav/resolver.py +++ b/docforge/nav/resolver.py @@ -1,7 +1,8 @@ """ -This module contains the logic for resolving a NavSpec against the physical -filesystem. It expands globs and validates that all referenced documents -actually exist on disk. +Navigation resolution utilities. + +This module resolves a ``NavSpec`` against the filesystem by expanding glob +patterns and validating that referenced documentation files exist. """ from pathlib import Path @@ -14,12 +15,15 @@ from docforge.nav.spec import NavSpec class ResolvedNav: """ - Represents a navigation structure where all patterns and paths have been - resolved against the actual filesystem contents. + Resolved navigation structure. + + A ``ResolvedNav`` represents navigation data after glob patterns have been + expanded and paths validated against the filesystem. Attributes: - home: Resolved relative path to the home page. - groups: Mapping of group titles to lists of absolute or relative Path objects. + home: Relative path to the documentation home page. + groups: Mapping of navigation group titles to lists of resolved + documentation file paths. """ def __init__( @@ -32,9 +36,9 @@ class ResolvedNav: Initialize a ResolvedNav instance. Args: - home: The relative path to the project home page. - groups: A mapping of group names to their resolved filesystem paths. - docs_root: The root documentation directory. + home: Relative path to the home page within the documentation root. + groups: Mapping of group titles to resolved documentation file paths. + docs_root: Root directory of the documentation source files. """ self.home = home self.groups = groups @@ -42,15 +46,20 @@ class ResolvedNav: 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: - 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._docs_root is None: raise RuntimeError("docs_root is required to resolve home path") yield self._docs_root / self.home + for paths in self.groups.values(): for p in paths: yield p @@ -61,34 +70,37 @@ def resolve_nav( docs_root: Path, ) -> ResolvedNav: """ - Create a ResolvedNav by processing a NavSpec against the filesystem. - This expands globs and validates the existence of referenced files. + Resolve a navigation specification against the filesystem. + + The function expands glob patterns defined in a ``NavSpec`` and verifies + that referenced documentation files exist within the documentation root. Args: - spec: The navigation specification to resolve. - docs_root: The root directory for documentation files. + spec: Navigation specification describing documentation layout. + docs_root: Root directory containing documentation Markdown files. Returns: - A ResolvedNav instance. + A ``ResolvedNav`` instance containing validated navigation paths. 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(): raise FileNotFoundError(docs_root) 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: - pattern: The glob pattern to resolve. + pattern: Glob pattern used to match documentation files. Returns: - A sorted list of matching Path objects. + A sorted list of matching ``Path`` objects. Raises: - FileNotFoundError: If the pattern doesn't match any files. + FileNotFoundError: If the pattern does not match any files. """ full = docs_root / pattern matches = sorted( @@ -100,7 +112,7 @@ def resolve_nav( return matches - # Resolve home + # Resolve home page home: str | None = None if spec.home: home_path = docs_root / spec.home @@ -108,7 +120,7 @@ def resolve_nav( raise FileNotFoundError(spec.home) home = spec.home - # Resolve groups + # Resolve navigation groups resolved_groups: Dict[str, List[Path]] = {} for group, patterns in spec.groups.items(): diff --git a/docforge/nav/spec.py b/docforge/nav/spec.py index c7cc784..b3c8a51 100644 --- a/docforge/nav/spec.py +++ b/docforge/nav/spec.py @@ -1,7 +1,9 @@ """ -This module defines the NavSpec class, which represents the user's intent for -documentation navigation as defined in a YAML specification (usually -docforge.nav.yml). +Navigation specification model. + +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 @@ -12,11 +14,16 @@ import yaml 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: - home: Path to the home document (e.g., 'index.md'). - groups: Mapping of group titles to lists of path patterns/globs. + home: Relative path to the documentation home page (for example + ``index.md``). + groups: Mapping of navigation group titles to lists of file patterns + or glob expressions. """ def __init__( @@ -25,11 +32,12 @@ class NavSpec: groups: Dict[str, List[str]], ) -> None: """ - Initialize a NavSpec. + Initialize a NavSpec instance. Args: - home: The path to the home document. - groups: A mapping of group names to lists of path patterns (globs). + home: Relative path to the home document. + groups: Mapping of group names to lists of path patterns + (glob expressions). """ self.home = home self.groups = groups @@ -37,17 +45,18 @@ class NavSpec: @classmethod def load(cls, path: Path) -> "NavSpec": """ - Load a NavSpec from a YAML file. + Load a navigation specification from a YAML file. Args: - path: The filesystem path to the YAML file. + path: Filesystem path to the navigation specification file. Returns: - A NavSpec instance. + A ``NavSpec`` instance representing the parsed configuration. Raises: - FileNotFoundError: If the path does not exist. - ValueError: If the file content is not a valid NavSpec mapping. + FileNotFoundError: If the specified file does not exist. + ValueError: If the file contents are not a valid navigation + specification. """ if not path.exists(): raise FileNotFoundError(path) @@ -78,33 +87,45 @@ class NavSpec: def all_patterns(self) -> List[str]: """ - Get all path patterns referenced in the specification. + Return all path patterns referenced by the specification. 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] = [] + if self.home: patterns.append(self.home) + for items in self.groups.values(): patterns.extend(items) + return patterns 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: path: Path to the navigation specification file. 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(): raise FileNotFoundError(path) data = yaml.safe_load(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): raise ValueError("Nav spec must be a YAML mapping") diff --git a/docforge/renderers/__init__.py b/docforge/renderers/__init__.py index e2dc17a..0da89e5 100644 --- a/docforge/renderers/__init__.py +++ b/docforge/renderers/__init__.py @@ -1,20 +1,28 @@ """ -# Renderers Layer +Renderers layer for doc-forge. -The `docforge.renderers` package handles the transformation of the internal -documentation models into physical files formatted for specific documentation -engines. +The ``docforge.renderers`` package transforms the internal documentation +models into files formatted for specific documentation systems. -## Current Implementations +Overview +-------- -- **MkDocsRenderer**: Generates Markdown files utilizing the `mkdocstrings` - syntax. It automatically handles package/module hierarchy and generates - `index.md` files for packages. +Renderers consume the doc-forge project model and generate output suitable +for documentation tools or machine interfaces. -## Extending +Current implementations: -To add a new renderer, implement the `DocRenderer` protocol defined in -`docforge.renderers.base`. +- **MkDocsRenderer** – Produces Markdown files compatible with MkDocs and + 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 diff --git a/docforge/renderers/base.py b/docforge/renderers/base.py index e688803..43835fe 100644 --- a/docforge/renderers/base.py +++ b/docforge/renderers/base.py @@ -1,7 +1,9 @@ """ -This module defines the base interfaces and configuration containers for -doc-forge renderers. All renderer implementations should adhere to the -DocRenderer protocol. +Renderer base interfaces and configuration models. + +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 @@ -14,12 +16,22 @@ class RendererConfig: """ Configuration container for documentation renderers. - Args: - out_dir: The directory where documentation files should be written. - project: The introspected project models to be rendered. + A ``RendererConfig`` instance groups together the project model and the + output directory used during rendering. + + 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: + """ + 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.project = project @@ -27,6 +39,9 @@ class RendererConfig: class DocRenderer(Protocol): """ 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 @@ -37,10 +52,11 @@ class DocRenderer(Protocol): out_dir: Path, ) -> None: """ - Generate renderer-specific source files for the given project. + Generate renderer-specific documentation sources. Args: - project: The project models containing modules and objects. - out_dir: Target directory for the generated files. + project: Project model containing modules and documentation objects. + out_dir: Directory where generated documentation sources + should be written. """ ... diff --git a/docforge/renderers/mcp_renderer.py b/docforge/renderers/mcp_renderer.py index 58ca317..d23d913 100644 --- a/docforge/renderers/mcp_renderer.py +++ b/docforge/renderers/mcp_renderer.py @@ -7,18 +7,25 @@ from docforge.models import Project, Module, DocObject 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" 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: - project: The project model to render. - out_dir: Target directory for the generated JSON files. + project: Documentation project model to render. + out_dir: Directory where MCP resources will be written. """ modules_dir = out_dir / "modules" modules_dir.mkdir(parents=True, exist_ok=True) @@ -54,11 +61,11 @@ class MCPRenderer: 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: - module: The module instance to serialize. - modules_dir: The directory where the module JSON file should be written. + module: Module instance to serialize. + modules_dir: Directory where module JSON files are stored. """ payload = { "module": module.path, @@ -71,13 +78,13 @@ class MCPRenderer: 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: - module: The module instance to render. + module: Module instance to convert. Returns: - A dictionary following the MCP documentation resource schema. + Dictionary representing the module and its documented objects. """ data: Dict = { "path": module.path, @@ -92,13 +99,13 @@ class MCPRenderer: def _render_object(self, obj: DocObject) -> Dict: """ - Recursively render a DocObject into structured MCP data. + Recursively convert a DocObject into structured MCP data. Args: - obj: The documented object (class, func, etc.) to render. + obj: Documentation object to convert. Returns: - A dictionary representing the object and its members. + Dictionary describing the object and any nested members. """ data: Dict = { "name": obj.name, @@ -119,4 +126,13 @@ class MCPRenderer: @staticmethod 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) diff --git a/docforge/renderers/mkdocs_renderer.py b/docforge/renderers/mkdocs_renderer.py index d70c8ce..5e85143 100644 --- a/docforge/renderers/mkdocs_renderer.py +++ b/docforge/renderers/mkdocs_renderer.py @@ -1,13 +1,16 @@ """ -MkDocsRenderer +MkDocs renderer implementation. -Generates Markdown source files compatible with MkDocs Material -and mkdocstrings, ensuring: +This module defines the ``MkDocsRenderer`` class, which generates Markdown +documentation sources compatible with MkDocs Material and the mkdocstrings +plugin. -- Root index.md always exists -- Parent package indexes are created automatically -- Child modules are linked in parent index files -- README.md can be generated from the root package docstring +The renderer ensures a consistent documentation structure by: + +- Creating a root ``index.md`` if one does not exist +- 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 @@ -16,8 +19,10 @@ from docforge.models import Project, Module class MkDocsRenderer: """ - Renderer that generates Markdown source files formatted for the MkDocs - 'mkdocstrings' plugin. + Renderer that produces Markdown documentation for MkDocs. + + Generated pages use mkdocstrings directives to reference Python modules, + allowing MkDocs to render API documentation dynamically. """ name = "mkdocs" @@ -25,6 +30,7 @@ class MkDocsRenderer: # ------------------------- # Public API # ------------------------- + def generate_sources( self, project: Project, @@ -32,18 +38,17 @@ class MkDocsRenderer: module_is_source: bool | None = None, ) -> None: """ - Produce a set of Markdown files in the output directory based on the - provided Project models. + Generate Markdown documentation files for a project. + + This method renders a documentation structure from the provided + project model and writes the resulting Markdown files to the + specified output directory. Args: - project: - The project model to render. - - out_dir: - Target directory for generated Markdown. - - module_is_source: - If True, treat the module as the root folder. + project: Project model containing modules to document. + out_dir: Directory where generated Markdown files will be written. + module_is_source: If True, treat the specified module as the + documentation root rather than nesting it inside a folder. """ out_dir.mkdir(parents=True, exist_ok=True) self._ensure_root_index(project, out_dir) @@ -51,7 +56,7 @@ class MkDocsRenderer: modules = list(project.get_all_modules()) paths = {m.path for m in modules} - # Package detection (level-agnostic) + # Detect packages (modules with children) packages = { p for p in paths if any(other.startswith(p + ".") for other in paths) @@ -72,22 +77,23 @@ class MkDocsRenderer: module_is_source: bool | None = None, ) -> None: """ - Generate README.md from the root package docstring. + Generate a ``README.md`` file from the root module docstring. Behavior: - - If module_is_source is True: - README.md is generated at project root (docs_dir.parent) + - If ``module_is_source`` is True, ``README.md`` is written to the + project root directory. + - If False, README generation is currently not implemented. - - If module_is_source is False: - TODO: generate README.md inside respective module folders + Args: + 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: - # TODO: support per-module README generation + # Future: support README generation per module return readme_path = docs_dir.parent / "README.md" @@ -127,7 +133,13 @@ class MkDocsRenderer: 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(): if module.path == project.name: @@ -142,14 +154,18 @@ class MkDocsRenderer: module_is_source: bool | None = None, ) -> None: """ - Write a single module's documentation file. Packages are written as - 'index.md' inside their respective directories. + Write documentation for a single module. + + Package modules are rendered as ``index.md`` files inside their + corresponding directories, while leaf modules are written as + standalone Markdown pages. Args: - module: The module to write. - packages: A set of module paths that are identified as packages. - out_dir: The base output directory. - module_is_source: Module is the source folder and to be treated as the root folder. + module: Module to render. + packages: Set of module paths identified as packages. + out_dir: Base directory for generated documentation files. + module_is_source: Whether the module acts as the documentation + root directory. """ parts = module.path.split(".") @@ -160,15 +176,12 @@ class MkDocsRenderer: module_name, parts = parts[0], parts if module.path in packages: - # Package → directory/index.md dir_path = out_dir.joinpath(*parts) dir_path.mkdir(parents=True, exist_ok=True) md_path = dir_path / "index.md" link_target = f"{parts[-1]}/" if parts else None - else: - # Leaf module → parent_dir/.md dir_path = out_dir.joinpath(*parts[:-1]) dir_path.mkdir(parents=True, exist_ok=True) @@ -186,14 +199,14 @@ class MkDocsRenderer: 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: - title: The display title for the page. - module_path: The dotted path of the module to document. + title: Page title displayed in the documentation. + module_path: Dotted import path of the module. Returns: - A string containing the Markdown source. + Markdown source containing a mkdocstrings directive. """ return ( f"# {title}\n\n" @@ -205,6 +218,13 @@ class MkDocsRenderer: project: Project, out_dir: Path, ) -> 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" if not root_index.exists(): @@ -221,6 +241,16 @@ class MkDocsRenderer: link_target: str, title: str, ) -> 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: parent_index = out_dir / "index.md" else: diff --git a/docforge/servers/__init__.py b/docforge/servers/__init__.py index ec31273..c4b4e5f 100644 --- a/docforge/servers/__init__.py +++ b/docforge/servers/__init__.py @@ -1,5 +1,13 @@ +""" +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 __all__ = [ "MCPServer", -] \ No newline at end of file +] diff --git a/docforge/servers/mcp_server.py b/docforge/servers/mcp_server.py index 9ba34d9..a512edd 100644 --- a/docforge/servers/mcp_server.py +++ b/docforge/servers/mcp_server.py @@ -9,16 +9,21 @@ from mcp.server.fastmcp import FastMCP 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: """ - Initialize the MCPServer. + Initialize the MCP server. Args: - mcp_root: Path to the directory containing pre-built MCP JSON resources. - name: Name of the MCP server. + mcp_root: Directory containing the generated MCP documentation + bundle (for example ``index.json``, ``nav.json``, and + ``modules/``). + name: Identifier used for the MCP server instance. """ self.mcp_root = mcp_root self.app = FastMCP(name) @@ -32,19 +37,24 @@ class MCPServer: 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: - path: Path to the JSON file. + path: Path to the JSON file to read. 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(): return { "error": "not_found", "path": str(path), } + return json.loads(path.read_text(encoding="utf-8")) # ------------------------------------------------------------------ @@ -53,8 +63,16 @@ class MCPServer: 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") def index(): 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: """ - 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() def ping() -> str: + """Return a simple health check response.""" return "pong" # ------------------------------------------------------------------ # 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. 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)