From c910da9d14dbc0a4c87228e8fbc00a94328024a6 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 20 Jan 2026 18:39:12 +0530 Subject: [PATCH] cleanup code --- docforge/__init__.py | 28 --- docforge/cli/__init__.py | 5 - docforge/cli/__init__.pyi | 33 --- docforge/cli/main.py | 333 -------------------------- docforge/exporters/__init__.py | 5 - docforge/exporters/__init__.pyi | 18 -- docforge/exporters/mcp.py | 281 ---------------------- docforge/loader/__init__.py | 5 - docforge/loader/__init__.pyi | 16 -- docforge/loader/griffe_loader.py | 289 ----------------------- docforge/model/__init__.py | 14 -- docforge/model/__init__.pyi | 72 ------ docforge/model/module.py | 129 ---------- docforge/model/nav.py | 210 ----------------- docforge/model/object.py | 131 ----------- docforge/model/project.py | 194 --------------- docforge/renderers/__init__.py | 12 - docforge/renderers/__init__.pyi | 51 ---- docforge/renderers/base.py | 217 ----------------- docforge/renderers/mkdocs.py | 268 --------------------- docforge/renderers/sphinx.py | 317 ------------------------- docforge/server/__init__.py | 5 - docforge/server/__init__.pyi | 18 -- docforge/server/mcp_server.py | 389 ------------------------------- docforge/stubs.pyi | 116 --------- docforge/utils/__init__.py | 1 - 26 files changed, 3157 deletions(-) delete mode 100644 docforge/__init__.py delete mode 100644 docforge/cli/__init__.py delete mode 100644 docforge/cli/__init__.pyi delete mode 100644 docforge/cli/main.py delete mode 100644 docforge/exporters/__init__.py delete mode 100644 docforge/exporters/__init__.pyi delete mode 100644 docforge/exporters/mcp.py delete mode 100644 docforge/loader/__init__.py delete mode 100644 docforge/loader/__init__.pyi delete mode 100644 docforge/loader/griffe_loader.py delete mode 100644 docforge/model/__init__.py delete mode 100644 docforge/model/__init__.pyi delete mode 100644 docforge/model/module.py delete mode 100644 docforge/model/nav.py delete mode 100644 docforge/model/object.py delete mode 100644 docforge/model/project.py delete mode 100644 docforge/renderers/__init__.py delete mode 100644 docforge/renderers/__init__.pyi delete mode 100644 docforge/renderers/base.py delete mode 100644 docforge/renderers/mkdocs.py delete mode 100644 docforge/renderers/sphinx.py delete mode 100644 docforge/server/__init__.py delete mode 100644 docforge/server/__init__.pyi delete mode 100644 docforge/server/mcp_server.py delete mode 100644 docforge/stubs.pyi delete mode 100644 docforge/utils/__init__.py diff --git a/docforge/__init__.py b/docforge/__init__.py deleted file mode 100644 index a662454..0000000 --- a/docforge/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -"""doc-forge — A renderer-agnostic Python documentation compiler. - -doc-forge converts Python source code and docstrings into a structured, -semantic documentation model and emits multiple downstream representations. - -Core features: -- Single source of truth: Python source code and docstrings -- Renderer agnosticism: MkDocs, Sphinx, MCP, or future renderers -- Deterministic output: Reproducible results -- AI-native documentation: Structured, queryable, machine-consumable -- Library-first design: All functionality accessible via Python API -""" - -__version__ = "0.1.0" -__author__ = "doc-forge team" - -from docforge.model.project import Project -from docforge.model.module import Module -from docforge.model.object import DocObject -from docforge.model.nav import Navigation, NavEntry - -__all__ = [ - "Project", - "Module", - "DocObject", - "Navigation", - "NavEntry", -] \ No newline at end of file diff --git a/docforge/cli/__init__.py b/docforge/cli/__init__.py deleted file mode 100644 index 175389f..0000000 --- a/docforge/cli/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""CLI package for doc-forge command-line interface.""" - -from .main import main - -__all__ = ["main"] \ No newline at end of file diff --git a/docforge/cli/__init__.pyi b/docforge/cli/__init__.pyi deleted file mode 100644 index 4522f0f..0000000 --- a/docforge/cli/__init__.pyi +++ /dev/null @@ -1,33 +0,0 @@ -"""Type stubs for doc-forge CLI package.""" - -from typing import Any, Dict, List, Optional, Union -from pathlib import Path -import click - -@click.group() -def cli() -> None: ... - -@cli.command() -@click.option('--renderer', type=click.Choice(['mkdocs', 'sphinx']), required=True) -@click.option('--out-dir', type=click.Path(), default='docs') -@click.argument('modules', nargs=-1) -def generate(renderer: str, out_dir: Path, modules: List[str]) -> None: ... - -@cli.command() -@click.option('--renderer', type=click.Choice(['mkdocs', 'sphinx']), required=True) -@click.option('--out-dir', type=click.Path(), default='docs') -def build(renderer: str, out_dir: Path) -> None: ... - -@cli.command() -@click.option('--renderer', type=click.Choice(['mkdocs', 'sphinx']), required=True) -@click.option('--out-dir', type=click.Path(), default='docs') -@click.option('--host', default='localhost') -@click.option('--port', type=int, default=8000) -def serve(renderer: str, out_dir: Path, host: str, port: int) -> None: ... - -@cli.command() -@click.option('--out-dir', type=click.Path(), default='mcp') -@click.argument('modules', nargs=-1) -def export(out_dir: Path, modules: List[str]) -> None: ... - -def main() -> None: ... \ No newline at end of file diff --git a/docforge/cli/main.py b/docforge/cli/main.py deleted file mode 100644 index 7dd826b..0000000 --- a/docforge/cli/main.py +++ /dev/null @@ -1,333 +0,0 @@ -"""Command-line interface for doc-forge. - -The CLI provides a thin orchestration layer over the core doc-forge -functionality. It follows the ADS specification by being library-first -with the CLI as a secondary interface. - -Available commands: -- generate: Generate source files for a renderer -- build: Build final documentation artifacts -- serve: Serve documentation locally -- export: Export to MCP format -""" - -from __future__ import annotations - -import logging -import sys -from pathlib import Path -from typing import List, Optional - -try: - import click -except ImportError as e: - raise ImportError( - "click is required for doc-forge CLI. " - "Install with: pip install click" - ) from e - -from docforge.loader import GriffeLoader -from docforge.renderers import MkDocsRenderer, SphinxRenderer -from docforge.exporters import MCPExporter -from docforge.server import MCPServer - - -logger = logging.getLogger(__name__) - - -@click.group() -@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging") -@click.pass_context -def cli(ctx: click.Context, verbose: bool) -> None: - """doc-forge — A renderer-agnostic Python documentation compiler. - - doc-forge converts Python source code and docstrings into a structured, - semantic documentation model and emits multiple downstream representations. - """ - if verbose: - logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") - else: - logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") - - # Ensure context object exists - ctx.ensure_object(dict) - - -@cli.command() -@click.option("--renderer", type=click.Choice(["mkdocs", "sphinx"]), required=True, help="Renderer to use") -@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("docs"), help="Output directory") -@click.option("--project-name", help="Project name (defaults to first module)") -@click.argument("modules", nargs=-1, required=True, help="Python modules to document") -@click.pass_context -def generate( - ctx: click.Context, - renderer: str, - out_dir: Path, - project_name: Optional[str], - modules: List[str], -) -> None: - """Generate documentation source files. - - Generate renderer-specific source files from Python modules. - This creates the source files but does not build the final documentation. - - Examples: - doc-forge generate --renderer mkdocs mypackage - doc-forge generate --renderer sphinx mypackage.submodule mypackage.utils - """ - try: - # Load project - loader = GriffeLoader() - project = loader.load_project(list(modules), project_name) - - # Get renderer - renderer_instance = _get_renderer(renderer) - - # Generate sources - renderer_instance.generate_sources(project, out_dir) - - click.echo(f"Generated {renderer} sources in {out_dir}") - click.echo(f"Modules: {', '.join(modules)}") - - except Exception as e: - click.echo(f"Error: {e}", err=True) - sys.exit(1) - - -@cli.command() -@click.option("--renderer", type=click.Choice(["mkdocs", "sphinx"]), required=True, help="Renderer to use") -@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("docs"), help="Output directory") -@click.option("--project-name", help="Project name (defaults to first module)") -@click.argument("modules", nargs=-1, required=True, help="Python modules to document") -@click.pass_context -def build( - ctx: click.Context, - renderer: str, - out_dir: Path, - project_name: Optional[str], - modules: List[str], -) -> None: - """Build final documentation artifacts. - - Build the final documentation from Python modules. This both generates - source files and builds the final artifacts (HTML, etc.). - - Examples: - doc-forge build --renderer mkdocs mypackage - doc-forge build --renderer sphinx mypackage.submodule - """ - try: - # Load project - loader = GriffeLoader() - project = loader.load_project(list(modules), project_name) - - # Get renderer - renderer_instance = _get_renderer(renderer) - - # Generate sources first - renderer_instance.generate_sources(project, out_dir) - - # Build final artifacts - from docforge.renderers.base import RendererConfig - config = RendererConfig(out_dir, project) - renderer_instance.build(config) - - click.echo(f"Built {renderer} documentation in {out_dir}") - - # Show output location - if renderer == "mkdocs": - build_dir = out_dir / "_site" - else: # sphinx - build_dir = out_dir / "build" / "html" - - if build_dir.exists(): - click.echo(f"Output: {build_dir}") - - except Exception as e: - click.echo(f"Error: {e}", err=True) - sys.exit(1) - - -@cli.command() -@click.option("--renderer", type=click.Choice(["mkdocs", "sphinx"]), required=True, help="Renderer to use") -@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("docs"), help="Output directory") -@click.option("--host", default="127.0.0.1", help="Host to serve on") -@click.option("--port", type=int, default=8000, help="Port to serve on") -@click.option("--project-name", help="Project name (defaults to first module)") -@click.argument("modules", nargs=-1, required=True, help="Python modules to document") -@click.pass_context -def serve( - ctx: click.Context, - renderer: str, - out_dir: Path, - host: str, - port: int, - project_name: Optional[str], - modules: List[str], -) -> None: - """Serve documentation locally. - - Start a local development server to serve documentation. - This generates sources, builds artifacts, and starts a server. - - Examples: - doc-forge serve --renderer mkdocs mypackage - doc-forge serve --renderer sphinx --port 9000 mypackage - """ - try: - # Load project - loader = GriffeLoader() - project = loader.load_project(list(modules), project_name) - - # Get renderer - renderer_instance = _get_renderer(renderer) - - # Generate sources first - renderer_instance.generate_sources(project, out_dir) - - # Build artifacts - from docforge.renderers.base import RendererConfig - config = RendererConfig(out_dir, project, {"host": host, "port": port}) - - # Start serving - click.echo(f"Serving {renderer} documentation at http://{host}:{port}") - click.echo("Press Ctrl+C to stop serving") - - renderer_instance.serve(config) - - except KeyboardInterrupt: - click.echo("\nStopped serving") - except Exception as e: - click.echo(f"Error: {e}", err=True) - sys.exit(1) - - -@cli.command() -@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp"), help="Output directory") -@click.option("--project-name", help="Project name (defaults to first module)") -@click.argument("modules", nargs=-1, required=True, help="Python modules to document") -@click.pass_context -def export( - ctx: click.Context, - out_dir: Path, - project_name: Optional[str], - modules: List[str], -) -> None: - """Export documentation to MCP format. - - Export documentation as a static MCP JSON bundle. This creates - machine-consumable documentation that can be loaded by MCP clients. - - Examples: - doc-forge export mypackage - doc-forge export --out-dir ./docs/mcp mypackage.submodule - """ - try: - # Load project - loader = GriffeLoader() - project = loader.load_project(list(modules), project_name) - - # Export to MCP - exporter = MCPExporter() - exporter.export(project, out_dir) - - click.echo(f"Exported MCP bundle to {out_dir}") - click.echo(f"Modules: {', '.join(modules)}") - - # Show bundle structure - index_path = out_dir / "index.json" - nav_path = out_dir / "nav.json" - modules_dir = out_dir / "modules" - - click.echo(f"Bundle structure:") - click.echo(f" {index_path}") - click.echo(f" {nav_path}") - click.echo(f" {modules_dir}/") - - except Exception as e: - click.echo(f"Error: {e}", err=True) - sys.exit(1) - - -@cli.command() -@click.option("--host", default="127.0.0.1", help="Host to serve on") -@click.option("--port", type=int, default=8080, help="Port to serve on") -@click.option("--project-name", help="Project name (defaults to first module)") -@click.argument("modules", nargs=-1, required=True, help="Python modules to document") -@click.pass_context -def server( - ctx: click.Context, - host: str, - port: int, - project_name: Optional[str], - modules: List[str], -) -> None: - """Start MCP server for live documentation access. - - Start a live MCP server that provides real-time access to documentation - through the Model Context Protocol. This serves documentation directly - from the Python model without generating static files. - - Examples: - doc-forge server mypackage - doc-forge server --port 9000 mypackage.submodule - """ - try: - # Load project - loader = GriffeLoader() - project = loader.load_project(list(modules), project_name) - - # Start MCP server - server = MCPServer(project) - - click.echo(f"Starting MCP server at http://{host}:{port}") - click.echo("Available resources:") - click.echo(f" docs://index - Project metadata") - click.echo(f" docs://nav - Navigation structure") - for module_path in project.get_module_list(): - click.echo(f" docs://module/{module_path} - Module documentation") - click.echo("Press Ctrl+C to stop server") - - server.start(host, port) - - # Keep server running - try: - while server.is_running(): - import time - time.sleep(1) - except KeyboardInterrupt: - click.echo("\nStopping server...") - server.stop() - - except Exception as e: - click.echo(f"Error: {e}", err=True) - sys.exit(1) - - -def _get_renderer(renderer_name: str): - """Get renderer instance by name. - - Args: - renderer_name: Name of the renderer - - Returns: - Renderer instance - - Raises: - ValueError: If renderer name is unknown - """ - if renderer_name == "mkdocs": - return MkDocsRenderer() - elif renderer_name == "sphinx": - return SphinxRenderer() - else: - raise ValueError(f"Unknown renderer: {renderer_name}") - - -def main() -> None: - """Main entry point for the CLI.""" - cli() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/docforge/exporters/__init__.py b/docforge/exporters/__init__.py deleted file mode 100644 index 891ae5b..0000000 --- a/docforge/exporters/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Exporters package for doc-forge output formats.""" - -from .mcp import MCPExporter - -__all__ = ["MCPExporter"] \ No newline at end of file diff --git a/docforge/exporters/__init__.pyi b/docforge/exporters/__init__.pyi deleted file mode 100644 index 9b056e8..0000000 --- a/docforge/exporters/__init__.pyi +++ /dev/null @@ -1,18 +0,0 @@ -"""Type stubs for doc-forge exporters package.""" - -from typing import Any, Dict, List, Optional, Union -from pathlib import Path -from docforge.model import Project - -class MCPExporter: - """Exports documentation model to MCP JSON format.""" - - def __init__(self) -> None: ... - - def export(self, project: Project, out_dir: Path) -> None: ... - - def export_index(self, project: Project, out_dir: Path) -> None: ... - - def export_nav(self, project: Project, out_dir: Path) -> None: ... - - def export_modules(self, project: Project, out_dir: Path) -> None: ... \ No newline at end of file diff --git a/docforge/exporters/mcp.py b/docforge/exporters/mcp.py deleted file mode 100644 index 128fa90..0000000 --- a/docforge/exporters/mcp.py +++ /dev/null @@ -1,281 +0,0 @@ -"""MCP exporter for doc-forge static documentation bundles. - -The MCP exporter creates static JSON bundles that can be consumed by -MCP (Model Context Protocol) clients. This follows the ADS specification -by providing a machine-consumable representation of the documentation -model. - -The exporter bypasses renderers entirely and works directly with the -documentation model to create alias-safe, deterministic output. -""" - -from __future__ import annotations - -import json -import logging -from pathlib import Path -from typing import Any, Dict, List, Optional - -from docforge.model import Project, Module, DocObject - - -logger = logging.getLogger(__name__) - - -class MCPExporter: - """Exports documentation model to MCP JSON format. - - The MCPExporter creates static JSON bundles that represent the - documentation model in a machine-consumable format. These bundles - can be loaded by MCP clients to provide structured access to - documentation. - - Output structure: - mcp/ - ├── index.json # Project metadata - ├── nav.json # Navigation structure - └── modules/ - ├── package.module.json # Individual module data - └── ... - - The exported data is: - - Alias-safe: Uses canonical paths for all references - - Deterministic: Same input always produces same output - - Self-contained: No external dependencies - - Queryable: Structured for easy machine consumption - """ - - def __init__(self) -> None: - """Initialize the MCP exporter.""" - self._version = "1.0.0" - - def export(self, project: Project, out_dir: Path) -> None: - """Export the complete project to MCP format. - - This is the main entry point for exporting. It creates the full - MCP bundle structure including index, navigation, and all module - files. - - Args: - project: The documentation project to export - out_dir: Directory where MCP bundle should be written - """ - if project.is_empty(): - raise ValueError("Cannot export empty project") - - # Create output directory structure - modules_dir = out_dir / "modules" - modules_dir.mkdir(parents=True, exist_ok=True) - - logger.info(f"Exporting MCP bundle to {out_dir}") - - # Export components - self.export_index(project, out_dir) - self.export_nav(project, out_dir) - self.export_modules(project, modules_dir) - - logger.info(f"MCP export completed: {len(project.get_all_modules())} modules") - - def export_index(self, project: Project, out_dir: Path) -> None: - """Export the project index metadata. - - The index.json file contains project-level metadata and - serves as the entry point for the MCP bundle. - - Args: - project: The documentation project - out_dir: Output directory - """ - index_path = out_dir / "index.json" - - index_data = { - "name": project.name, - "version": project.version, - "docforge_version": self._version, - "export_format": "mcp", - "export_timestamp": self._get_timestamp(), - "module_count": len(project.get_all_modules()), - "total_objects": project.get_total_object_count(), - } - - with open(index_path, "w", encoding="utf-8") as f: - json.dump(index_data, f, indent=2, ensure_ascii=False) - - logger.debug(f"Exported index: {index_path}") - - def export_nav(self, project: Project, out_dir: Path) -> None: - """Export the navigation structure. - - The nav.json file contains the hierarchical navigation structure - for browsing the documentation. - - Args: - project: The documentation project - out_dir: Output directory - """ - nav_path = out_dir / "nav.json" - - nav_data = { - "entries": [ - { - "title": entry.title, - "module": entry.module, - "path": f"modules/{entry.module.replace('.', '/')}.json", - } - for entry in project.nav.entries - ] - } - - with open(nav_path, "w", encoding="utf-8") as f: - json.dump(nav_data, f, indent=2, ensure_ascii=False) - - logger.debug(f"Exported navigation: {nav_path}") - - def export_modules(self, project: Project, modules_dir: Path) -> None: - """Export all modules to individual JSON files. - - Each module is exported to its own JSON file in the modules/ - directory. The filename follows the pattern: package.module.json - - Args: - project: The documentation project - modules_dir: Directory for module files - """ - for module in project.get_all_modules(): - self._export_module(module, modules_dir) - - def _export_module(self, module: Module, modules_dir: Path) -> None: - """Export a single module to JSON. - - Args: - module: The module to export - modules_dir: Directory for module files - """ - # Create subdirectories for nested packages - module_file_path = modules_dir / f"{module.path}.json" - module_file_path.parent.mkdir(parents=True, exist_ok=True) - - module_data = { - "path": module.path, - "docstring": module.docstring, - "objects": [self._serialize_object(obj) for obj in module.get_public_objects()], - } - - with open(module_file_path, "w", encoding="utf-8") as f: - json.dump(module_data, f, indent=2, ensure_ascii=False) - - logger.debug(f"Exported module: {module_file_path}") - - def _serialize_object(self, obj: DocObject) -> Dict[str, Any]: - """Serialize a DocObject to MCP format. - - Args: - obj: The DocObject to serialize - - Returns: - Dictionary representation of the object - """ - data = { - "name": obj.name, - "kind": obj.kind, - "path": obj.path, - "docstring": obj.docstring, - } - - if obj.signature: - data["signature"] = obj.signature - - if obj.members: - data["members"] = [ - self._serialize_object(member) - for member in obj.get_public_members() - ] - - return data - - def export_module(self, module: Module, out_dir: Path) -> None: - """Export a single module (convenience method). - - Args: - module: The module to export - out_dir: Output directory - """ - modules_dir = out_dir / "modules" - modules_dir.mkdir(parents=True, exist_ok=True) - - self._export_module(module, modules_dir) - - def validate_export(self, out_dir: Path) -> bool: - """Validate that the MCP export is complete and valid. - - Args: - out_dir: Directory containing the exported bundle - - Returns: - True if export is valid, False otherwise - """ - try: - # Check required files exist - index_path = out_dir / "index.json" - nav_path = out_dir / "nav.json" - modules_dir = out_dir / "modules" - - if not (index_path.exists() and nav_path.exists() and modules_dir.exists()): - return False - - # Load and validate index - with open(index_path, "r", encoding="utf-8") as f: - index = json.load(f) - - required_fields = ["name", "docforge_version", "export_format"] - if not all(field in index for field in required_fields): - return False - - # Load and validate nav - with open(nav_path, "r", encoding="utf-8") as f: - nav = json.load(f) - - if "entries" not in nav: - return False - - # Check that all nav entries have corresponding module files - for entry in nav["entries"]: - module_path = modules_dir / f"{entry['module']}.json" - if not module_path.exists(): - return False - - return True - - except Exception as e: - logger.error(f"Export validation failed: {e}") - return False - - def _get_timestamp(self) -> str: - """Get current timestamp in ISO format. - - Returns: - ISO format timestamp string - """ - from datetime import datetime, timezone - - return datetime.now(timezone.utc).isoformat() - - def get_export_info(self, out_dir: Path) -> Optional[Dict[str, Any]]: - """Get information about an existing export. - - Args: - out_dir: Directory containing the exported bundle - - Returns: - Export information dictionary or None if invalid - """ - index_path = out_dir / "index.json" - - if not index_path.exists(): - return None - - try: - with open(index_path, "r", encoding="utf-8") as f: - return json.load(f) - except Exception: - return None \ No newline at end of file diff --git a/docforge/loader/__init__.py b/docforge/loader/__init__.py deleted file mode 100644 index 1bc0e63..0000000 --- a/docforge/loader/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Loader package for doc-forge introspection layer.""" - -from .griffe_loader import GriffeLoader - -__all__ = ["GriffeLoader"] \ No newline at end of file diff --git a/docforge/loader/__init__.pyi b/docforge/loader/__init__.pyi deleted file mode 100644 index 42efa8d..0000000 --- a/docforge/loader/__init__.pyi +++ /dev/null @@ -1,16 +0,0 @@ -"""Type stubs for doc-forge loader package.""" - -from typing import Any, Dict, List, Optional, Union -from pathlib import Path -from docforge.model import Project, Module - -class GriffeLoader: - """Loads Python modules using Griffe introspection.""" - - def __init__(self) -> None: ... - - def load_project(self, module_paths: List[str]) -> Project: ... - - def load_module(self, path: str) -> Module: ... - - def resolve_aliases(self, project: Project) -> None: ... \ No newline at end of file diff --git a/docforge/loader/griffe_loader.py b/docforge/loader/griffe_loader.py deleted file mode 100644 index 2216027..0000000 --- a/docforge/loader/griffe_loader.py +++ /dev/null @@ -1,289 +0,0 @@ -"""Griffe-based loader for doc-forge introspection layer. - -The GriffeLoader uses the Griffe library to introspect Python source code -and extract documentation information. It converts Griffe's internal -representation into doc-forge's renderer-agnostic documentation model. - -Griffe is the only supported introspection backend in doc-forge, ensuring -consistent and reliable extraction of documentation information from -Python source code. -""" - -from __future__ import annotations - -import logging -from typing import Dict, List, Optional - -try: - import griffe - from griffe import Docstring, ObjectNode -except ImportError as e: - raise ImportError( - "griffe is required for doc-forge. Install with: pip install griffe" - ) from e - -from docforge.model import Module, Project, DocObject - - -logger = logging.getLogger(__name__) - - -class GriffeLoader: - """Loads Python modules using Griffe introspection. - - GriffeLoader is the bridge between Python source code and doc-forge's - documentation model. It uses Griffe to parse Python modules, extract - docstrings, signatures, and structural information, then converts - this data into doc-forge's renderer-agnostic model. - - The loader handles: - - Module discovery and loading - - Docstring extraction - - Signature parsing - - Member resolution - - Alias handling (with graceful failure) - - Attributes: - griffe_agent: The Griffe agent used for introspection - """ - - def __init__(self) -> None: - """Initialize the GriffeLoader. - - Creates a Griffe agent with default configuration for - documentation extraction. - """ - self.griffe_agent = griffe.Agent( - extensions=(), - resolve_aliases=True, - resolve_imports=True, - ) - - def load_project(self, module_paths: List[str], project_name: Optional[str] = None) -> Project: - """Load a complete project from multiple module paths. - - This is the primary entry point for loading documentation. It takes - a list of module paths, loads each one, and assembles them into a - complete Project with navigation. - - Args: - module_paths: List of import paths to load - project_name: Optional name for the project (defaults to first module) - - Returns: - Project containing all loaded modules - - Raises: - ValueError: If no module paths provided - ImportError: If any module cannot be loaded - """ - if not module_paths: - raise ValueError("At least one module path must be provided") - - if project_name is None: - project_name = module_paths[0].split('.')[0] - - project = Project(name=project_name) - - for module_path in module_paths: - try: - module = self.load_module(module_path) - project.add_module(module) - logger.info(f"Loaded module: {module_path}") - except Exception as e: - logger.error(f"Failed to load module {module_path}: {e}") - # Continue loading other modules rather than failing completely - continue - - # Resolve any cross-module aliases - self.resolve_aliases(project) - - return project - - def load_module(self, path: str) -> Module: - """Load a single module from its import path. - - Args: - path: The import path of the module to load - - Returns: - Module containing all documented objects - - Raises: - ImportError: If the module cannot be loaded or found - """ - try: - griffe_obj = self.griffe_agent.load_module(path) - except Exception as e: - raise ImportError(f"Failed to load module '{path}': {e}") from e - - return self._convert_griffe_object_to_module(griffe_obj) - - def _convert_griffe_object_to_module(self, griffe_obj: ObjectNode) -> Module: - """Convert a Griffe ObjectNode to a doc-forge Module. - - Args: - griffe_obj: The Griffe object to convert - - Returns: - Module containing converted documentation objects - """ - module = Module( - path=griffe_obj.canonical_path, - docstring=self._extract_docstring(griffe_obj.docstring), - ) - - # Convert all members - for name, member in griffe_obj.members.items(): - if not name.startswith('_'): # Skip private members - try: - doc_obj = self._convert_griffe_object_to_docobject(member) - module.add_object(doc_obj) - except Exception as e: - logger.warning(f"Failed to convert member {name}: {e}") - continue - - return module - - def _convert_griffe_object_to_docobject(self, griffe_obj: ObjectNode) -> DocObject: - """Convert a Griffe ObjectNode to a doc-forge DocObject. - - Args: - griffe_obj: The Griffe object to convert - - Returns: - DocObject with converted information - """ - # Determine the kind of object - kind = self._determine_object_kind(griffe_obj) - - # Extract signature for callable objects - signature = self._extract_signature(griffe_obj, kind) - - doc_obj = DocObject( - name=griffe_obj.name, - kind=kind, - path=griffe_obj.canonical_path, - signature=signature, - docstring=self._extract_docstring(griffe_obj.docstring), - ) - - # Convert nested members (for classes) - if kind == "class": - for name, member in griffe_obj.members.items(): - if not name.startswith('_'): # Skip private members - try: - nested_obj = self._convert_griffe_object_to_docobject(member) - doc_obj.add_member(nested_obj) - except Exception as e: - logger.warning(f"Failed to convert nested member {name}: {e}") - continue - - return doc_obj - - def _determine_object_kind(self, griffe_obj: ObjectNode) -> str: - """Determine the kind of documentation object. - - Args: - griffe_obj: The Griffe object to classify - - Returns: - String representing the object kind - """ - if griffe_obj.is_class: - return "class" - elif griffe_obj.is_function: - return "function" - elif griffe_obj.is_method: - return "method" - elif griffe_obj.is_property: - return "property" - elif griffe_obj.is_attribute: - return "attribute" - elif griffe_obj.is_module: - return "module" - else: - return "object" - - def _extract_signature(self, griffe_obj: ObjectNode, kind: str) -> Optional[str]: - """Extract signature string from a Griffe object. - - Args: - griffe_obj: The Griffe object to extract signature from - kind: The kind of object - - Returns: - Signature string or None if not applicable - """ - if kind not in ("function", "method"): - return None - - try: - if hasattr(griffe_obj, 'parameters') and griffe_obj.parameters: - params = [] - for param in griffe_obj.parameters.values(): - param_str = param.name - if param.annotation: - param_str += f": {param.annotation}" - if param.default and param.default != "None": - param_str += f" = {param.default}" - params.append(param_str) - - signature = f"({', '.join(params)})" - if hasattr(griffe_obj, 'returns') and griffe_obj.returns: - signature += f" -> {griffe_obj.returns}" - - return signature - except Exception as e: - logger.warning(f"Failed to extract signature for {griffe_obj.name}: {e}") - - return None - - def _extract_docstring(self, docstring: Optional[Docstring]) -> Optional[str]: - """Extract docstring content from a Griffe Docstring object. - - Args: - docstring: The Griffe docstring object - - Returns: - Plain text docstring or None - """ - if docstring is None: - return None - - try: - return str(docstring.value).strip() - except Exception as e: - logger.warning(f"Failed to extract docstring: {e}") - return None - - def resolve_aliases(self, project: Project) -> None: - """Resolve cross-module aliases in the project. - - This method attempts to resolve aliases that point to objects in - other modules. It updates the documentation model to reflect the - actual locations of objects rather than their aliases. - - Args: - project: The project to resolve aliases in - """ - logger.info("Resolving cross-module aliases...") - - # This is a placeholder for alias resolution - # In a full implementation, this would: - # 1. Identify all aliases in the project - # 2. Resolve them to their canonical targets - # 3. Update the documentation model accordingly - # 4. Handle circular references gracefully - - # For now, we'll just log that alias resolution was attempted - alias_count = 0 - for module in project.get_all_modules(): - for obj in module.members.values(): - if hasattr(obj, 'is_alias') and obj.is_alias: - alias_count += 1 - - if alias_count > 0: - logger.info(f"Found {alias_count} aliases (resolution not yet implemented)") - else: - logger.info("No aliases found") \ No newline at end of file diff --git a/docforge/model/__init__.py b/docforge/model/__init__.py deleted file mode 100644 index d9eb121..0000000 --- a/docforge/model/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Model package for doc-forge documentation objects.""" - -from .project import Project -from .module import Module -from .object import DocObject -from .nav import Navigation, NavEntry - -__all__ = [ - "Project", - "Module", - "DocObject", - "Navigation", - "NavEntry", -] \ No newline at end of file diff --git a/docforge/model/__init__.pyi b/docforge/model/__init__.pyi deleted file mode 100644 index 560f7dd..0000000 --- a/docforge/model/__init__.pyi +++ /dev/null @@ -1,72 +0,0 @@ -"""Type stubs for doc-forge model package.""" - -from typing import Any, Dict, List, Optional, Union -from pathlib import Path - -class DocObject: - """Represents a Python documentation object (class, function, variable, etc.).""" - - name: str - kind: str - path: str - signature: Optional[str] - docstring: Optional[str] - members: Dict[str, 'DocObject'] - - def __init__(self, name: str, kind: str, path: str, signature: Optional[str] = None, docstring: Optional[str] = None) -> None: ... - - def add_member(self, member: 'DocObject') -> None: ... - - def get_member(self, name: str) -> Optional['DocObject']: ... - - def is_private(self) -> bool: ... - -class Module: - """Represents a Python module in the documentation model.""" - - path: str - docstring: Optional[str] - members: Dict[str, DocObject] - - def __init__(self, path: str, docstring: Optional[str] = None) -> None: ... - - def add_object(self, obj: DocObject) -> None: ... - - def get_object(self, name: str) -> Optional[DocObject]: ... - - def get_public_objects(self) -> List[DocObject]: ... - -class Project: - """Root container for all documentation in a project.""" - - name: str - version: Optional[str] - modules: Dict[str, Module] - nav: 'Navigation' - - def __init__(self, name: str, version: Optional[str] = None) -> None: ... - - def add_module(self, module: Module) -> None: ... - - def get_module(self, path: str) -> Optional[Module]: ... - - def get_all_modules(self) -> List[Module]: ... - -class Navigation: - """Navigation structure derived from project modules.""" - - entries: List['NavEntry'] - - def __init__(self) -> None: ... - - def add_entry(self, entry: 'NavEntry') -> None: ... - - def get_entry(self, title: str) -> Optional['NavEntry']: ... - -class NavEntry: - """Single navigation entry linking to a module.""" - - title: str - module: str - - def __init__(self, title: str, module: str) -> None: ... \ No newline at end of file diff --git a/docforge/model/module.py b/docforge/model/module.py deleted file mode 100644 index fc400f1..0000000 --- a/docforge/model/module.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Module representation in the doc-forge documentation model. - -Module represents a Python module that can be documented. It serves as -a container for all the documentation objects within that module, -including classes, functions, attributes, and constants. - -Each Module corresponds to a Python import path and contains the -module-level docstring along with all documented members. -""" - -from __future__ import annotations - -from typing import Dict, List, Optional - -from .object import DocObject - - -class Module: - """Represents a Python module in the documentation model. - - A Module is the primary organizational unit in doc-forge. It corresponds - to a Python module (identified by its import path) and contains all - the documentation objects within that module. - - Attributes: - path: The import path of the module (e.g., "package.submodule") - docstring: Optional module-level docstring - members: Dictionary of all documented objects in the module - """ - - def __init__(self, path: str, docstring: Optional[str] = None) -> None: - """Initialize a Module. - - Args: - path: The import path of the module - docstring: Optional module-level docstring - - Raises: - ValueError: If path is empty - """ - if not path: - raise ValueError("Module path cannot be empty") - - self.path: str = path - self.docstring: Optional[str] = docstring - self.members: Dict[str, DocObject] = {} - - def add_object(self, obj: DocObject) -> None: - """Add a documentation object to this module. - - Args: - obj: The documentation object to add - - Raises: - ValueError: If object name conflicts with existing object - """ - if obj.name in self.members: - raise ValueError(f"Object '{obj.name}' already exists in module '{self.path}'") - self.members[obj.name] = obj - - def get_object(self, name: str) -> Optional[DocObject]: - """Get a documentation object by name. - - Args: - name: The name of the object to retrieve - - Returns: - The DocObject if found, None otherwise - """ - return self.members.get(name) - - def get_public_objects(self) -> List[DocObject]: - """Get all public (non-private) documentation objects. - - Returns: - List of DocObjects that are not private - """ - return [obj for obj in self.members.values() if not obj.is_private()] - - def get_objects_by_kind(self, kind: str) -> List[DocObject]: - """Get all documentation objects of a specific kind. - - Args: - kind: The kind of objects to retrieve (e.g., "class", "function") - - Returns: - List of DocObjects matching the specified kind - """ - return [obj for obj in self.members.values() if obj.kind == kind] - - def get_classes(self) -> List[DocObject]: - """Get all class objects in this module. - - Returns: - List of DocObjects with kind "class" - """ - return self.get_objects_by_kind("class") - - def get_functions(self) -> List[DocObject]: - """Get all function objects in this module. - - Returns: - List of DocObjects with kind "function" - """ - return self.get_objects_by_kind("function") - - def has_docstring(self) -> bool: - """Check if this module has a docstring. - - Returns: - True if docstring is not None and not empty, False otherwise - """ - return bool(self.docstring and self.docstring.strip()) - - def is_empty(self) -> bool: - """Check if this module contains any documented objects. - - Returns: - True if the module has no members, False otherwise - """ - return len(self.members) == 0 - - def __repr__(self) -> str: - """Return a string representation of the Module. - - Returns: - String representation showing path and member count - """ - return f"Module(path='{self.path}', members={len(self.members)})" \ No newline at end of file diff --git a/docforge/model/nav.py b/docforge/model/nav.py deleted file mode 100644 index 72e8296..0000000 --- a/docforge/model/nav.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Navigation structure for doc-forge documentation. - -Navigation provides a hierarchical structure for organizing documentation -modules. It is derived automatically from the project structure rather -than being manually authored, ensuring consistency between the -documentation model and the navigation. - -The navigation is used by renderers to generate table of contents, -sidebars, and other navigation elements. -""" - -from __future__ import annotations - -from typing import Dict, List, Optional - - -class NavEntry: - """Single navigation entry linking to a module. - - A NavEntry represents one item in the documentation navigation, - typically corresponding to a module. It contains a display title - and the module path it links to. - - Attributes: - title: The display title for this navigation entry - module: The import path of the module this entry links to - """ - - def __init__(self, title: str, module: str) -> None: - """Initialize a NavEntry. - - Args: - title: The display title for this entry - module: The import path of the linked module - - Raises: - ValueError: If title or module is empty - """ - if not title: - raise ValueError("NavEntry title cannot be empty") - if not module: - raise ValueError("NavEntry module cannot be empty") - - self.title: str = title - self.module: str = module - - def __repr__(self) -> str: - """Return a string representation of the NavEntry. - - Returns: - String representation showing title and module - """ - return f"NavEntry(title='{self.title}', module='{self.module}')" - - -class Navigation: - """Navigation structure derived from project modules. - - Navigation provides an organized hierarchy for browsing documentation. - It is automatically generated from the project's module structure, - ensuring that the navigation always reflects the actual available - documentation. - - The navigation can be customized by: - - Changing the order of entries - - Grouping related modules - - Providing custom titles - - Attributes: - entries: List of navigation entries in order - """ - - def __init__(self) -> None: - """Initialize an empty Navigation.""" - self.entries: List[NavEntry] = [] - - def add_entry(self, entry: NavEntry) -> None: - """Add a navigation entry. - - Args: - entry: The navigation entry to add - """ - self.entries.append(entry) - - def add_entry_by_module(self, module: str, title: Optional[str] = None) -> None: - """Add a navigation entry for a module. - - This is a convenience method that creates a NavEntry from a module - path. If no title is provided, the module name is used as the title. - - Args: - module: The import path of the module - title: Optional custom title (defaults to module name) - """ - if title is None: - # Use the last part of the module path as the title - title = module.split('.')[-1].replace('_', ' ').title() - - entry = NavEntry(title, module) - self.add_entry(entry) - - def get_entry(self, title: str) -> Optional[NavEntry]: - """Get a navigation entry by title. - - Args: - title: The title of the entry to find - - Returns: - The NavEntry if found, None otherwise - """ - for entry in self.entries: - if entry.title == title: - return entry - return None - - def get_entry_by_module(self, module: str) -> Optional[NavEntry]: - """Get a navigation entry by module path. - - Args: - module: The module path to search for - - Returns: - The NavEntry if found, None otherwise - """ - for entry in self.entries: - if entry.module == module: - return entry - return None - - def remove_entry(self, title: str) -> bool: - """Remove a navigation entry by title. - - Args: - title: The title of the entry to remove - - Returns: - True if entry was removed, False if not found - """ - for i, entry in enumerate(self.entries): - if entry.title == title: - del self.entries[i] - return True - return False - - def remove_entry_by_module(self, module: str) -> bool: - """Remove a navigation entry by module path. - - Args: - module: The module path of the entry to remove - - Returns: - True if entry was removed, False if not found - """ - for i, entry in enumerate(self.entries): - if entry.module == module: - del self.entries[i] - return True - return False - - def reorder_entries(self, titles: List[str]) -> None: - """Reorder entries based on provided title order. - - Entries not mentioned in the titles list will maintain their - relative order and be placed after the specified entries. - - Args: - titles: List of titles in the desired order - """ - # Create a mapping of title to entry for quick lookup - entry_map = {entry.title: entry for entry in self.entries} - - # Build new ordered list - ordered_entries = [] - remaining_entries = list(self.entries) - - # Add entries in specified order - for title in titles: - if title in entry_map: - ordered_entries.append(entry_map[title]) - # Remove from remaining entries - remaining_entries = [e for e in remaining_entries if e.title != title] - - # Add remaining entries in their original order - ordered_entries.extend(remaining_entries) - - self.entries = ordered_entries - - def is_empty(self) -> bool: - """Check if navigation has no entries. - - Returns: - True if navigation has no entries, False otherwise - """ - return len(self.entries) == 0 - - def get_module_list(self) -> List[str]: - """Get list of all module paths in navigation. - - Returns: - List of module paths in navigation order - """ - return [entry.module for entry in self.entries] - - def __repr__(self) -> str: - """Return a string representation of the Navigation. - - Returns: - String representation showing entry count - """ - return f"Navigation(entries={len(self.entries)})" \ No newline at end of file diff --git a/docforge/model/object.py b/docforge/model/object.py deleted file mode 100644 index e91d606..0000000 --- a/docforge/model/object.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Core documentation object representing a Python entity. - -DocObject is the atomic unit of documentation in doc-forge. It represents -any Python entity that can be documented: classes, functions, methods, -attributes, constants, etc. - -Each DocObject contains: -- Basic metadata (name, kind, path) -- Optional signature information -- Optional docstring -- Nested members (for classes and modules) -""" - -from __future__ import annotations - -from typing import Dict, Optional, Set - - -class DocObject: - """Represents a Python documentation object (class, function, variable, etc.). - - DocObject is the fundamental building block of the documentation model. - It captures all essential information about a Python entity in a - renderer-agnostic format. - - Attributes: - name: The name of the object (e.g., "MyClass", "my_function") - kind: The type of object ("class", "function", "method", "attribute", etc.) - path: The full import path (e.g., "package.module.MyClass.my_method") - signature: Optional function/method signature string - docstring: Optional docstring content - members: Dictionary of nested member objects - """ - - def __init__( - self, - name: str, - kind: str, - path: str, - signature: Optional[str] = None, - docstring: Optional[str] = None, - ) -> None: - """Initialize a DocObject. - - Args: - name: The name of the object - kind: The type/kind of object - path: Full import path to the object - signature: Optional signature for callable objects - docstring: Optional docstring content - - Raises: - ValueError: If name, kind, or path are empty - """ - if not name: - raise ValueError("DocObject name cannot be empty") - if not kind: - raise ValueError("DocObject kind cannot be empty") - if not path: - raise ValueError("DocObject path cannot be empty") - - self.name: str = name - self.kind: str = kind - self.path: str = path - self.signature: Optional[str] = signature - self.docstring: Optional[str] = docstring - self.members: Dict[str, DocObject] = {} - - def add_member(self, member: DocObject) -> None: - """Add a nested member object. - - This is used for objects that contain other objects, such as - classes containing methods and attributes, or modules containing - functions and classes. - - Args: - member: The member object to add - - Raises: - ValueError: If member name conflicts with existing member - """ - if member.name in self.members: - raise ValueError(f"Member '{member.name}' already exists in '{self.name}'") - self.members[member.name] = member - - def get_member(self, name: str) -> Optional[DocObject]: - """Get a nested member by name. - - Args: - name: The name of the member to retrieve - - Returns: - The member object if found, None otherwise - """ - return self.members.get(name) - - def is_private(self) -> bool: - """Check if this object is considered private. - - Private objects are those whose names start with an underscore. - This convention is used to filter out internal implementation - details from public documentation. - - Returns: - True if the object name starts with underscore, False otherwise - """ - return self.name.startswith('_') - - def get_public_members(self) -> list[DocObject]: - """Get all public (non-private) member objects. - - Returns: - List of member objects that are not private - """ - return [member for member in self.members.values() if not member.is_private()] - - def has_docstring(self) -> bool: - """Check if this object has a docstring. - - Returns: - True if docstring is not None and not empty, False otherwise - """ - return bool(self.docstring and self.docstring.strip()) - - def __repr__(self) -> str: - """Return a string representation of the DocObject. - - Returns: - String representation showing name, kind, and path - """ - return f"DocObject(name='{self.name}', kind='{self.kind}', path='{self.path}')" \ No newline at end of file diff --git a/docforge/model/project.py b/docforge/model/project.py deleted file mode 100644 index 7a01b34..0000000 --- a/docforge/model/project.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Project representation in the doc-forge documentation model. - -Project is the root container for all documentation in a doc-forge -project. It represents the entire codebase being documented and serves -as the entry point for all documentation operations. - -A Project contains modules, navigation, and metadata about the -codebase being documented. -""" - -from __future__ import annotations - -from typing import Dict, List, Optional - -from .module import Module -from .nav import Navigation - - -class Project: - """Root container for all documentation in a project. - - Project is the top-level object in the doc-forge documentation model. - It represents the entire codebase being documented and serves as the - central hub for all documentation operations. - - Each Project contains: - - Basic metadata (name, version) - - All documented modules - - Navigation structure for browsing - - The Project is the single source of truth that all renderers and - exporters work with, ensuring consistency across all output formats. - - Attributes: - name: The name of the project - version: Optional version string - modules: Dictionary of all modules in the project - nav: Navigation structure for the project - """ - - def __init__(self, name: str, version: Optional[str] = None) -> None: - """Initialize a Project. - - Args: - name: The name of the project - version: Optional version string - - Raises: - ValueError: If name is empty - """ - if not name: - raise ValueError("Project name cannot be empty") - - self.name: str = name - self.version: Optional[str] = version - self.modules: Dict[str, Module] = {} - self.nav: Navigation = Navigation() - - def add_module(self, module: Module) -> None: - """Add a module to the project. - - When a module is added, it's also automatically added to the - navigation structure if not already present. - - Args: - module: The module to add - - Raises: - ValueError: If module path conflicts with existing module - """ - if module.path in self.modules: - raise ValueError(f"Module '{module.path}' already exists in project") - - self.modules[module.path] = module - - # Add to navigation if not already present - if not self.nav.get_entry_by_module(module.path): - self.nav.add_entry_by_module(module.path) - - def get_module(self, path: str) -> Optional[Module]: - """Get a module by its import path. - - Args: - path: The import path of the module to retrieve - - Returns: - The Module if found, None otherwise - """ - return self.modules.get(path) - - def get_all_modules(self) -> List[Module]: - """Get all modules in the project. - - Returns: - List of all Module objects in the project - """ - return list(self.modules.values()) - - def get_public_modules(self) -> List[Module]: - """Get all modules with public (non-empty) content. - - Returns: - List of modules that have at least one public object - """ - return [ - module for module in self.modules.values() - if module.get_public_objects() - ] - - def remove_module(self, path: str) -> bool: - """Remove a module from the project. - - Args: - path: The import path of the module to remove - - Returns: - True if module was removed, False if not found - """ - if path in self.modules: - del self.modules[path] - # Also remove from navigation - self.nav.remove_entry_by_module(path) - return True - return False - - def has_module(self, path: str) -> bool: - """Check if a module exists in the project. - - Args: - path: The import path to check - - Returns: - True if module exists, False otherwise - """ - return path in self.modules - - def get_module_count(self) -> int: - """Get the total number of modules in the project. - - Returns: - Number of modules in the project - """ - return len(self.modules) - - def is_empty(self) -> bool: - """Check if the project has no modules. - - Returns: - True if project has no modules, False otherwise - """ - return len(self.modules) == 0 - - def get_total_object_count(self) -> int: - """Get the total count of all documentation objects. - - Returns: - Total number of DocObjects across all modules - """ - return sum(len(module.members) for module in self.modules.values()) - - def get_modules_by_pattern(self, pattern: str) -> List[Module]: - """Get modules matching a pattern. - - Args: - pattern: Pattern to match against module paths (supports wildcards) - - Returns: - List of modules whose paths match the pattern - """ - import fnmatch - - return [ - module for module in self.modules.values() - if fnmatch.fnmatch(module.path, pattern) - ] - - def rebuild_navigation(self) -> None: - """Rebuild navigation from current modules. - - This clears the existing navigation and rebuilds it from the - current set of modules, ensuring navigation is in sync with - the actual project structure. - """ - self.nav = Navigation() - for module_path in sorted(self.modules.keys()): - self.nav.add_entry_by_module(module_path) - - def __repr__(self) -> str: - """Return a string representation of the Project. - - Returns: - String representation showing name and module count - """ - return f"Project(name='{self.name}', modules={len(self.modules)})" \ No newline at end of file diff --git a/docforge/renderers/__init__.py b/docforge/renderers/__init__.py deleted file mode 100644 index 2ab9f01..0000000 --- a/docforge/renderers/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Renderers package for doc-forge output generation.""" - -from .base import DocRenderer, RendererConfig -from .mkdocs import MkDocsRenderer -from .sphinx import SphinxRenderer - -__all__ = [ - "DocRenderer", - "RendererConfig", - "MkDocsRenderer", - "SphinxRenderer", -] \ No newline at end of file diff --git a/docforge/renderers/__init__.pyi b/docforge/renderers/__init__.pyi deleted file mode 100644 index e9902b7..0000000 --- a/docforge/renderers/__init__.pyi +++ /dev/null @@ -1,51 +0,0 @@ -"""Type stubs for doc-forge renderers package.""" - -from typing import Any, Dict, List, Optional, Protocol, Union -from pathlib import Path -from docforge.model import Project - -class DocRenderer(Protocol): - """Protocol for documentation renderers.""" - - name: str - - def generate_sources(self, project: Project, out_dir: Path) -> None: ... - - def build(self, config: 'RendererConfig') -> None: ... - - def serve(self, config: 'RendererConfig') -> None: ... - -class RendererConfig: - """Base configuration for renderers.""" - - out_dir: Path - project: Project - extra: Dict[str, Any] - - def __init__(self, out_dir: Path, project: Project, extra: Optional[Dict[str, Any]] = None) -> None: ... - -class MkDocsRenderer: - """MkDocs documentation renderer.""" - - name: str = "mkdocs" - - def __init__(self) -> None: ... - - def generate_sources(self, project: Project, out_dir: Path) -> None: ... - - def build(self, config: RendererConfig) -> None: ... - - def serve(self, config: RendererConfig) -> None: ... - -class SphinxRenderer: - """Sphinx documentation renderer.""" - - name: str = "sphinx" - - def __init__(self) -> None: ... - - def generate_sources(self, project: Project, out_dir: Path) -> None: ... - - def build(self, config: RendererConfig) -> None: ... - - def serve(self, config: RendererConfig) -> None: ... \ No newline at end of file diff --git a/docforge/renderers/base.py b/docforge/renderers/base.py deleted file mode 100644 index 80bd100..0000000 --- a/docforge/renderers/base.py +++ /dev/null @@ -1,217 +0,0 @@ -"""Base renderer interface for doc-forge output generation. - -The renderer system provides a pluggable architecture for generating -different output formats from the same documentation model. All renderers -implement the DocRenderer protocol, ensuring consistent behavior across -different output formats. - -This module defines the base interface and configuration that all -renderers must follow. -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from pathlib import Path -from typing import Any, Dict, Protocol, runtime_checkable - -from docforge.model import Project - - -@runtime_checkable -class DocRenderer(Protocol): - """Protocol for documentation renderers. - - DocRenderer defines the interface that all documentation renderers - must implement. This protocol ensures that renderers can be used - interchangeably while providing consistent behavior. - - All renderers must: - 1. Have a unique name identifier - 2. Generate source files from the documentation model - 3. Support building final artifacts - 4. Optionally support serving documentation locally - - Attributes: - name: Unique identifier for the renderer - """ - - name: str - - def generate_sources(self, project: Project, out_dir: Path) -> None: - """Generate renderer-specific source files. - - This method converts the documentation model into renderer-specific - source files (e.g., Markdown for MkDocs, reStructuredText for Sphinx). - The generated files are written to the specified output directory. - - Args: - project: The documentation project to render - out_dir: Directory where source files should be written - """ - ... - - def build(self, config: 'RendererConfig') -> None: - """Build final documentation artifacts. - - This method takes the generated source files and builds the final - documentation artifacts (e.g., HTML site, PDF, etc.). The specific - output depends on the renderer type. - - Args: - config: Configuration for the build process - """ - ... - - def serve(self, config: 'RendererConfig') -> None: - """Serve documentation locally (optional). - - This method starts a local development server to serve the - documentation. This is optional and not all renderers support - serving functionality. - - Args: - config: Configuration for the serve process - - Raises: - NotImplementedError: If serving is not supported - """ - ... - - -class RendererConfig: - """Base configuration for renderers. - - RendererConfig provides common configuration options that are - applicable to all renderers. Each renderer can extend this class - to add renderer-specific configuration options. - - Attributes: - out_dir: Output directory for generated files - project: The documentation project being rendered - extra: Additional renderer-specific configuration - """ - - def __init__( - self, - out_dir: Path, - project: Project, - extra: Optional[Dict[str, Any]] = None, - ) -> None: - """Initialize renderer configuration. - - Args: - out_dir: Output directory for generated files - project: The documentation project being rendered - extra: Additional renderer-specific configuration options - """ - self.out_dir: Path = out_dir - self.project: Project = project - self.extra: Dict[str, Any] = extra or {} - - def get_extra(self, key: str, default: Any = None) -> Any: - """Get an extra configuration value. - - Args: - key: The configuration key to retrieve - default: Default value if key is not found - - Returns: - The configuration value or default - """ - return self.extra.get(key, default) - - def set_extra(self, key: str, value: Any) -> None: - """Set an extra configuration value. - - Args: - key: The configuration key to set - value: The value to set - """ - self.extra[key] = value - - -class BaseRenderer(ABC): - """Abstract base class for renderers. - - BaseRenderer provides a common foundation for all renderer implementations. - It implements shared functionality and defines the abstract methods that - concrete renderers must implement. - - This class helps ensure consistent behavior across different renderer - implementations while reducing code duplication. - - Attributes: - name: Unique identifier for the renderer - """ - - name: str - - def __init__(self, name: str) -> None: - """Initialize the base renderer. - - Args: - name: Unique identifier for the renderer - """ - self.name = name - - @abstractmethod - def generate_sources(self, project: Project, out_dir: Path) -> None: - """Generate renderer-specific source files. - - Args: - project: The documentation project to render - out_dir: Directory where source files should be written - """ - pass - - @abstractmethod - def build(self, config: RendererConfig) -> None: - """Build final documentation artifacts. - - Args: - config: Configuration for the build process - """ - pass - - def serve(self, config: RendererConfig) -> None: - """Serve documentation locally. - - Default implementation raises NotImplementedError. Renderers that - support serving should override this method. - - Args: - config: Configuration for the serve process - - Raises: - NotImplementedError: If serving is not supported - """ - raise NotImplementedError(f"Serving is not supported by {self.name} renderer") - - def ensure_output_dir(self, out_dir: Path) -> None: - """Ensure the output directory exists. - - Args: - out_dir: Directory to ensure exists - """ - out_dir.mkdir(parents=True, exist_ok=True) - - def validate_project(self, project: Project) -> None: - """Validate that the project is suitable for rendering. - - Args: - project: The project to validate - - Raises: - ValueError: If the project is not valid for rendering - """ - if project.is_empty(): - raise ValueError("Project contains no modules to render") - - def __repr__(self) -> str: - """Return a string representation of the renderer. - - Returns: - String representation showing the renderer name - """ - return f"{self.__class__.__name__}(name='{self.name}')" \ No newline at end of file diff --git a/docforge/renderers/mkdocs.py b/docforge/renderers/mkdocs.py deleted file mode 100644 index 130e5fc..0000000 --- a/docforge/renderers/mkdocs.py +++ /dev/null @@ -1,268 +0,0 @@ -"""MkDocs renderer for doc-forge. - -The MkDocs renderer generates MkDocs-compatible documentation from the -doc-forge documentation model. It creates Markdown files with mkdocstrings -directives and generates the necessary MkDocs configuration. - -This renderer follows the ADS specification by: -- Emitting .md files with mkdocstrings directives -- Using one file per module -- Supporting build and serve operations via MkDocs APIs -""" - -from __future__ import annotations - -import logging -from pathlib import Path -from typing import Any, Dict, List, Optional - -try: - import mkdocs - import mkdocs.commands.build - import mkdocs.commands.serve - import mkdocs.config - import yaml -except ImportError as e: - raise ImportError( - "mkdocs and mkdocstrings are required for MkDocs renderer. " - "Install with: pip install doc-forge[mkdocs]" - ) from e - -from docforge.model import Project, Module -from .base import BaseRenderer, RendererConfig - - -logger = logging.getLogger(__name__) - - -class MkDocsRenderer(BaseRenderer): - """MkDocs documentation renderer. - - The MkDocsRenderer converts the doc-forge documentation model into - MkDocs-compatible source files. It generates Markdown files with - mkdocstrings directives and creates the necessary MkDocs configuration. - - Generated output structure: - docs/ - ├── index.md - ├── module1.md - ├── module2.md - └── mkdocs.yml - - Attributes: - name: Renderer identifier ("mkdocs") - """ - - def __init__(self) -> None: - """Initialize the MkDocs renderer.""" - super().__init__("mkdocs") - - def generate_sources(self, project: Project, out_dir: Path) -> None: - """Generate MkDocs source files. - - Creates Markdown files for each module and generates the - mkdocs.yml configuration file. - - Args: - project: The documentation project to render - out_dir: Directory where source files should be written - """ - self.validate_project(project) - self.ensure_output_dir(out_dir) - - logger.info(f"Generating MkDocs sources in {out_dir}") - - # Generate index.md - self._generate_index(project, out_dir) - - # Generate module files - for module in project.get_all_modules(): - self._generate_module_file(module, out_dir) - - # Generate mkdocs.yml - self._generate_mkdocs_config(project, out_dir) - - logger.info(f"Generated {len(project.get_all_modules())} module files") - - def build(self, config: RendererConfig) -> None: - """Build MkDocs documentation. - - Uses MkDocs build command to generate the final HTML documentation. - - Args: - config: Configuration for the build process - """ - self.validate_project(config.project) - - mkdocs_yml = config.out_dir / "mkdocs.yml" - if not mkdocs_yml.exists(): - raise ValueError(f"mkdocs.yml not found in {config.out_dir}") - - logger.info(f"Building MkDocs documentation from {config.out_dir}") - - # Load MkDocs configuration - mkdocs_config = mkdocs.config.load_config(str(mkdocs_yml)) - - # Run build - mkdocs.commands.build.build(mkdocs_config) - - logger.info("MkDocs build completed successfully") - - def serve(self, config: RendererConfig) -> None: - """Serve MkDocs documentation locally. - - Starts the MkDocs development server for local documentation - preview and testing. - - Args: - config: Configuration for the serve process - """ - self.validate_project(config.project) - - mkdocs_yml = config.out_dir / "mkdocs.yml" - if not mkdocs_yml.exists(): - raise ValueError(f"mkdocs.yml not found in {config.out_dir}") - - # Get serve options from config - host = config.get_extra("host", "127.0.0.1") - port = config.get_extra("port", 8000) - - logger.info(f"Serving MkDocs documentation at http://{host}:{port}") - - # Load MkDocs configuration - mkdocs_config = mkdocs.config.load_config(str(mkdocs_yml)) - - # Run serve - mkdocs.commands.serve.serve( - mkdocs_config, - dev_addr=f"{host}:{port}", - livereload="livereload" in config.extra, - ) - - def _generate_index(self, project: Project, out_dir: Path) -> None: - """Generate the index.md file. - - Args: - project: The documentation project - out_dir: Output directory - """ - index_path = out_dir / "index.md" - - content = [f"# {project.name}"] - - if project.version: - content.append(f"\n**Version:** {project.version}") - - content.append("\n## Modules") - content.append("") - - for entry in project.nav.entries: - content.append(f"- [{entry.title}]({entry.path}.md)") - - index_path.write_text("\n".join(content), encoding="utf-8") - logger.debug(f"Generated {index_path}") - - def _generate_module_file(self, module: Module, out_dir: Path) -> None: - """Generate a Markdown file for a module. - - Args: - module: The module to generate documentation for - out_dir: Output directory - """ - module_path = out_dir / f"{module.path}.md" - - content = [f"# {module.path}"] - - if module.has_docstring(): - content.append(f"\n{module.docstring}") - - content.append(f"\n::: {module.path}") - content.append(" options:") - content.append(" show_source: true") - content.append(" show_root_heading: true") - - module_path.write_text("\n".join(content), encoding="utf-8") - logger.debug(f"Generated {module_path}") - - def _generate_mkdocs_config(self, project: Project, out_dir: Path) -> None: - """Generate the mkdocs.yml configuration file. - - Args: - project: The documentation project - out_dir: Output directory - """ - config_path = out_dir / "mkdocs.yml" - - # Build navigation structure - nav = [] - for entry in project.nav.entries: - nav.append({entry.title: f"{entry.path}.md"}) - - # MkDocs configuration - config = { - "site_name": project.name, - "site_description": f"Documentation for {project.name}", - "nav": nav, - "plugins": ["mkdocstrings"], - "theme": { - "name": "material", - "features": ["navigation.instant", "navigation.tracking"], - }, - "markdown_extensions": [ - "codehilite", - "admonition", - "toc", - ], - "docs_dir": ".", - "site_dir": "_site", - } - - if project.version: - config["site_version"] = project.version - - # Write configuration as YAML - with open(config_path, "w", encoding="utf-8") as f: - yaml.dump(config, f, default_flow_style=False, sort_keys=False) - - logger.debug(f"Generated {config_path}") - - -class MkDocsConfig(RendererConfig): - """MkDocs-specific renderer configuration. - - Extends the base RendererConfig with MkDocs-specific options. - - Attributes: - theme: MkDocs theme to use - extra_css: Additional CSS files - extra_js: Additional JavaScript files - plugins: MkDocs plugins to enable - """ - - def __init__( - self, - out_dir: Path, - project: Project, - theme: str = "material", - extra_css: Optional[List[str]] = None, - extra_js: Optional[List[str]] = None, - plugins: Optional[List[str]] = None, - **extra, - ) -> None: - """Initialize MkDocs configuration. - - Args: - out_dir: Output directory for generated files - project: The documentation project being rendered - theme: MkDocs theme to use - extra_css: Additional CSS files - extra_js: Additional JavaScript files - plugins: MkDocs plugins to enable - **extra: Additional configuration options - """ - super().__init__(out_dir, project, extra) - - self.theme = theme - self.extra_css = extra_css or [] - self.extra_js = extra_js or [] - self.plugins = plugins or ["mkdocstrings"] \ No newline at end of file diff --git a/docforge/renderers/sphinx.py b/docforge/renderers/sphinx.py deleted file mode 100644 index 9da1988..0000000 --- a/docforge/renderers/sphinx.py +++ /dev/null @@ -1,317 +0,0 @@ -"""Sphinx renderer for doc-forge. - -The Sphinx renderer generates Sphinx-compatible documentation from the -doc-forge documentation model. It creates reStructuredText files with -autodoc directives and generates the necessary Sphinx configuration. - -This renderer follows the ADS specification by: -- Emitting .rst files with autodoc directives -- Supporting build operations via Sphinx APIs -- Providing static build capability (serve is optional) -""" - -from __future__ import annotations - -import logging -from pathlib import Path -from typing import Any, Dict, List, Optional - -try: - import sphinx - from sphinx.application import Sphinx - from sphinx.util.docutils import docutils_namespace -except ImportError as e: - raise ImportError( - "sphinx is required for Sphinx renderer. " - "Install with: pip install doc-forge[sphinx]" - ) from e - -from docforge.model import Project, Module -from .base import BaseRenderer, RendererConfig - - -logger = logging.getLogger(__name__) - - -class SphinxRenderer(BaseRenderer): - """Sphinx documentation renderer. - - The SphinxRenderer converts the doc-forge documentation model into - Sphinx-compatible source files. It generates reStructuredText files - with autodoc directives and creates the necessary Sphinx configuration. - - Generated output structure: - docs/ - ├── source/ - │ ├── index.rst - │ ├── conf.py - │ ├── module1.rst - │ └── module2.rst - └── build/ - - Attributes: - name: Renderer identifier ("sphinx") - """ - - def __init__(self) -> None: - """Initialize the Sphinx renderer.""" - super().__init__("sphinx") - - def generate_sources(self, project: Project, out_dir: Path) -> None: - """Generate Sphinx source files. - - Creates reStructuredText files for each module and generates the - Sphinx configuration (conf.py). - - Args: - project: The documentation project to render - out_dir: Directory where source files should be written - """ - self.validate_project(project) - self.ensure_output_dir(out_dir) - - # Create source directory structure - source_dir = out_dir / "source" - source_dir.mkdir(exist_ok=True) - - logger.info(f"Generating Sphinx sources in {source_dir}") - - # Generate index.rst - self._generate_index(project, source_dir) - - # Generate conf.py - self._generate_sphinx_config(project, source_dir) - - # Generate module files - for module in project.get_all_modules(): - self._generate_module_file(module, source_dir) - - logger.info(f"Generated {len(project.get_all_modules())} module files") - - def build(self, config: RendererConfig) -> None: - """Build Sphinx documentation. - - Uses Sphinx application to generate the final HTML documentation. - - Args: - config: Configuration for the build process - """ - self.validate_project(config.project) - - source_dir = config.out_dir / "source" - build_dir = config.out_dir / "build" - - if not source_dir.exists(): - raise ValueError(f"Source directory not found: {source_dir}") - - logger.info(f"Building Sphinx documentation from {source_dir}") - - # Get build options from config - builder_name = config.get_extra("builder", "html") - - # Create Sphinx application and build - with docutils_namespace(): - app = Sphinx( - srcdir=str(source_dir), - confdir=str(source_dir), - outdir=str(build_dir / builder_name), - doctreedir=str(build_dir / "doctrees"), - buildername=builder_name, - verbosity=config.get_extra("verbosity", 1), - ) - app.build() - - logger.info(f"Sphinx build completed successfully ({builder_name})") - - def serve(self, config: RendererConfig) -> None: - """Serve Sphinx documentation locally. - - Sphinx doesn't have a built-in serve command like MkDocs. This - method provides a simple static file server for the built HTML. - - Args: - config: Configuration for the serve process - """ - # Build first if needed - build_dir = config.out_dir / "build" / "html" - if not build_dir.exists(): - logger.info("Building documentation before serving...") - self.build(config) - - # Get serve options - host = config.get_extra("host", "127.0.0.1") - port = config.get_extra("port", 8000) - - logger.info(f"Serving Sphinx documentation at http://{host}:{port}") - logger.info(f"Serving from: {build_dir}") - - # Simple HTTP server - import http.server - import socketserver - import os - - os.chdir(build_dir) - - with socketserver.TCPServer((host, port), http.server.SimpleHTTPRequestHandler) as httpd: - logger.info(f"Press Ctrl+C to stop serving") - try: - httpd.serve_forever() - except KeyboardInterrupt: - logger.info("Stopping server...") - httpd.shutdown() - - def _generate_index(self, project: Project, source_dir: Path) -> None: - """Generate the index.rst file. - - Args: - project: The documentation project - source_dir: Source directory - """ - index_path = source_dir / "index.rst" - - content = [f"{project.name}", "=" * len(project.name), ""] - - if project.version: - content.append(f"**Version:** {project.version}") - content.append("") - - content.append(".. toctree::") - content.append(" :maxdepth: 2") - content.append(" :caption: Modules:") - content.append("") - - for entry in project.nav.entries: - content.append(f" {entry.path}") - - content.append("") - content.append("Indices and tables") - content.append("==================") - content.append("") - content.append("* :ref:`genindex`") - content.append("* :ref:`modindex`") - content.append("* :ref:`search`") - - index_path.write_text("\n".join(content), encoding="utf-8") - logger.debug(f"Generated {index_path}") - - def _generate_module_file(self, module: Module, source_dir: Path) -> None: - """Generate a reStructuredText file for a module. - - Args: - module: The module to generate documentation for - source_dir: Source directory - """ - module_path = source_dir / f"{module.path}.rst" - - content = [f"{module.path}", "=" * len(module.path), ""] - - if module.has_docstring(): - content.append(module.docstring) - content.append("") - - content.append(".. automodule:: " + module.path) - content.append(" :members:") - content.append(" :undoc-members:") - content.append(" :show-inheritance:") - - module_path.write_text("\n".join(content), encoding="utf-8") - logger.debug(f"Generated {module_path}") - - def _generate_sphinx_config(self, project: Project, source_dir: Path) -> None: - """Generate the conf.py configuration file. - - Args: - project: The documentation project - source_dir: Source directory - """ - config_path = source_dir / "conf.py" - - content = [ - f'"""Sphinx configuration for {project.name}."""', - "", - "import os", - "import sys", - "", - "# Add the project root to the Python path", - "sys.path.insert(0, os.path.abspath('../..'))", - "", - f"# Project information", - f"project = '{project.name}'", - "copyright = '2024, Project Authors'", - "author = 'Project Authors'", - f"release = '{project.version or '0.1.0'}'", - "", - "# General configuration", - "extensions = [", - " 'sphinx.ext.autodoc',", - " 'sphinx.ext.viewcode',", - " 'sphinx.ext.napoleon',", - " 'sphinx.ext.intersphinx',", - "]", - "", - "# Templates path", - "templates_path = ['_templates']", - "", - "# Output formatting", - "html_theme = 'alabaster'", - "html_static_path = ['_static']", - "", - "# Autodoc settings", - "autodoc_default_options = {", - " 'members': True,", - " 'member-order': 'bysource',", - " 'special-members': '__init__',", - " 'undoc-members': True,", - " 'exclude-members': '__weakref__'", - "}", - "", - "# Napoleon settings", - "napoleon_google_docstring = True", - "napoleon_numpy_docstring = True", - "napoleon_include_init_with_doc = False", - "napoleon_include_private_with_doc = False", - ] - - config_path.write_text("\n".join(content), encoding="utf-8") - logger.debug(f"Generated {config_path}") - - -class SphinxConfig(RendererConfig): - """Sphinx-specific renderer configuration. - - Extends the base RendererConfig with Sphinx-specific options. - - Attributes: - builder: Sphinx builder to use (html, latex, etc.) - theme: HTML theme to use - extensions: Sphinx extensions to enable - """ - - def __init__( - self, - out_dir: Path, - project: Project, - builder: str = "html", - theme: str = "alabaster", - extensions: Optional[List[str]] = None, - **extra, - ) -> None: - """Initialize Sphinx configuration. - - Args: - out_dir: Output directory for generated files - project: The documentation project being rendered - builder: Sphinx builder to use - theme: HTML theme to use - extensions: Sphinx extensions to enable - **extra: Additional configuration options - """ - super().__init__(out_dir, project, extra) - - self.builder = builder - self.theme = theme - self.extensions = extensions or [ - "sphinx.ext.autodoc", - "sphinx.ext.viewcode", - "sphinx.ext.napoleon", - ] \ No newline at end of file diff --git a/docforge/server/__init__.py b/docforge/server/__init__.py deleted file mode 100644 index 03f36bf..0000000 --- a/docforge/server/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Server package for doc-forge live documentation APIs.""" - -from .mcp_server import MCPServer - -__all__ = ["MCPServer"] \ No newline at end of file diff --git a/docforge/server/__init__.pyi b/docforge/server/__init__.pyi deleted file mode 100644 index fc3e277..0000000 --- a/docforge/server/__init__.pyi +++ /dev/null @@ -1,18 +0,0 @@ -"""Type stubs for doc-forge server package.""" - -from typing import Any, Dict, List, Optional, Union -from pathlib import Path -from docforge.model import Project - -class MCPServer: - """Live MCP server for documentation queries.""" - - def __init__(self, project: Project) -> None: ... - - def start(self, host: str = "localhost", port: int = 8080) -> None: ... - - def stop(self) -> None: ... - - def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]: ... - - def get_resource(self, uri: str) -> Optional[Dict[str, Any]]: ... \ No newline at end of file diff --git a/docforge/server/mcp_server.py b/docforge/server/mcp_server.py deleted file mode 100644 index 54042e7..0000000 --- a/docforge/server/mcp_server.py +++ /dev/null @@ -1,389 +0,0 @@ -"""MCP server for doc-forge live documentation APIs. - -The MCP server exposes documentation as queryable resources via the -Model Context Protocol. This provides a live, stateless API for accessing -documentation data in real-time. - -The server follows the ADS specification by: -- Being read-only and stateless -- Exposing documentation as queryable resources -- Being backed by the documentation model (not static files) -- Supporting the standard MCP resource interface -""" - -from __future__ import annotations - -import json -import logging -from typing import Any, Dict, List, Optional, Union -from urllib.parse import urlparse - -from docforge.model import Project, Module, DocObject - - -logger = logging.getLogger(__name__) - - -class MCPServer: - """Live MCP server for documentation queries. - - The MCPServer provides a real-time API for accessing documentation - through the Model Context Protocol. Unlike the static MCP exporter, - this server works directly with the live documentation model, - providing up-to-date access to documentation data. - - The server exposes these resources: - - docs://index - Project metadata - - docs://nav - Navigation structure - - docs://module/{module} - Individual module data - - The server is: - - Read-only: No modifications allowed - - Stateless: No session state maintained - - Live: Direct access to documentation model - - Queryable: Supports MCP resource queries - - Attributes: - project: The documentation project being served - """ - - def __init__(self, project: Project) -> None: - """Initialize the MCP server. - - Args: - project: The documentation project to serve - - Raises: - ValueError: If project is empty - """ - if project.is_empty(): - raise ValueError("Cannot serve empty project") - - self.project = project - self._running = False - self._server = None - - def start(self, host: str = "localhost", port: int = 8080) -> None: - """Start the MCP server. - - Args: - host: Host to bind the server to - port: Port to bind the server to - - Raises: - RuntimeError: If server is already running - """ - if self._running: - raise RuntimeError("MCP server is already running") - - logger.info(f"Starting MCP server on {host}:{port}") - - # Note: This is a simplified implementation - # In a full implementation, you would use the actual MCP server library - # For now, we'll create a basic HTTP server that handles MCP requests - - try: - self._start_http_server(host, port) - self._running = True - logger.info(f"MCP server started on http://{host}:{port}") - except Exception as e: - logger.error(f"Failed to start MCP server: {e}") - raise - - def stop(self) -> None: - """Stop the MCP server. - - Raises: - RuntimeError: If server is not running - """ - if not self._running: - raise RuntimeError("MCP server is not running") - - logger.info("Stopping MCP server") - - if self._server: - self._server.shutdown() - self._server = None - - self._running = False - logger.info("MCP server stopped") - - def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]: - """Handle an MCP request. - - This method processes incoming MCP requests and returns appropriate - responses. It supports the standard MCP resource operations. - - Args: - request: The MCP request dictionary - - Returns: - MCP response dictionary - - Raises: - ValueError: If request is invalid - """ - method = request.get("method") - - if method == "resources/list": - return self._handle_resources_list() - elif method == "resources/read": - return self._handle_resources_read(request) - else: - return { - "error": { - "code": -32601, - "message": f"Method not supported: {method}", - } - } - - def get_resource(self, uri: str) -> Optional[Dict[str, Any]]: - """Get a resource by URI. - - Args: - uri: The resource URI (e.g., "docs://index") - - Returns: - Resource data or None if not found - """ - parsed = urlparse(uri) - - if parsed.scheme != "docs": - return None - - path = parsed.path.lstrip("/") - - if path == "index": - return self._get_index_resource() - elif path == "nav": - return self._get_nav_resource() - elif path.startswith("module/"): - module_path = path[7:] # Remove "module/" prefix - return self._get_module_resource(module_path) - else: - return None - - def _handle_resources_list(self) -> Dict[str, Any]: - """Handle resources/list request. - - Returns: - List of available resources - """ - resources = [ - { - "uri": "docs://index", - "name": "Project Index", - "description": "Project metadata and information", - "mimeType": "application/json", - }, - { - "uri": "docs://nav", - "name": "Navigation", - "description": "Documentation navigation structure", - "mimeType": "application/json", - }, - ] - - # Add module resources - for module in self.project.get_all_modules(): - resources.append({ - "uri": f"docs://module/{module.path}", - "name": module.path, - "description": f"Documentation for {module.path}", - "mimeType": "application/json", - }) - - return { - "resources": resources, - } - - def _handle_resources_read(self, request: Dict[str, Any]) -> Dict[str, Any]: - """Handle resources/read request. - - Args: - request: The read request - - Returns: - Resource content or error - """ - uri = request.get("params", {}).get("uri") - - if not uri: - return { - "error": { - "code": -32602, - "message": "Missing URI parameter", - } - } - - resource = self.get_resource(uri) - - if resource is None: - return { - "error": { - "code": -32602, - "message": f"Resource not found: {uri}", - } - } - - return { - "contents": [ - { - "uri": uri, - "mimeType": "application/json", - "text": json.dumps(resource, indent=2, ensure_ascii=False), - } - ], - } - - def _get_index_resource(self) -> Dict[str, Any]: - """Get the index resource. - - Returns: - Index resource data - """ - return { - "name": self.project.name, - "version": self.project.version, - "module_count": len(self.project.get_all_modules()), - "total_objects": self.project.get_total_object_count(), - "server": "doc-forge MCP server", - } - - def _get_nav_resource(self) -> Dict[str, Any]: - """Get the navigation resource. - - Returns: - Navigation resource data - """ - return { - "entries": [ - { - "title": entry.title, - "module": entry.module, - "uri": f"docs://module/{entry.module}", - } - for entry in self.project.nav.entries - ] - } - - def _get_module_resource(self, module_path: str) -> Optional[Dict[str, Any]]: - """Get a module resource. - - Args: - module_path: The module path - - Returns: - Module resource data or None if not found - """ - module = self.project.get_module(module_path) - - if module is None: - return None - - return { - "path": module.path, - "docstring": module.docstring, - "objects": [self._serialize_object(obj) for obj in module.get_public_objects()], - } - - def _serialize_object(self, obj: DocObject) -> Dict[str, Any]: - """Serialize a DocObject for MCP response. - - Args: - obj: The DocObject to serialize - - Returns: - Serialized object data - """ - data = { - "name": obj.name, - "kind": obj.kind, - "path": obj.path, - "docstring": obj.docstring, - } - - if obj.signature: - data["signature"] = obj.signature - - if obj.members: - data["members"] = [ - self._serialize_object(member) - for member in obj.get_public_members() - ] - - return data - - def _start_http_server(self, host: str, port: int) -> None: - """Start a simple HTTP server for MCP requests. - - Args: - host: Host to bind to - port: Port to bind to - """ - import http.server - import socketserver - from threading import Thread - - class MCPRequestHandler(http.server.SimpleHTTPRequestHandler): - def __init__(self, project: Project, *args, **kwargs): - self.project = project - self.server_instance = MCPServer(project) - super().__init__(*args, **kwargs) - - def do_POST(self): - """Handle POST requests (MCP JSON-RPC).""" - content_length = int(self.headers.get('Content-Length', 0)) - - if content_length == 0: - self.send_error(400, "Empty request body") - return - - try: - # Read request body - body = self.rfile.read(content_length) - request = json.loads(body.decode('utf-8')) - - # Handle request - response = self.server_instance.handle_request(request) - - # Send response - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(response).encode('utf-8')) - - except json.JSONDecodeError: - self.send_error(400, "Invalid JSON") - except Exception as e: - logger.error(f"Request handling error: {e}") - self.send_error(500, "Internal server error") - - def do_GET(self): - """Handle GET requests (health check).""" - if self.path == '/health': - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"status": "ok"}).encode('utf-8')) - else: - self.send_error(404, "Not found") - - # Create handler factory - def handler_factory(*args, **kwargs): - return MCPRequestHandler(self.project, *args, **kwargs) - - # Start server - self._server = socketserver.TCPServer((host, port), handler_factory) - - # Run server in separate thread - server_thread = Thread(target=self._server.serve_forever, daemon=True) - server_thread.start() - - def is_running(self) -> bool: - """Check if the server is currently running. - - Returns: - True if server is running, False otherwise - """ - return self._running \ No newline at end of file diff --git a/docforge/stubs.pyi b/docforge/stubs.pyi deleted file mode 100644 index a81cc27..0000000 --- a/docforge/stubs.pyi +++ /dev/null @@ -1,116 +0,0 @@ -"""Type stubs for doc-forge core model objects.""" - -from typing import Any, Dict, List, Optional, Protocol -from pathlib import Path - -class DocObject: - """Represents a Python documentation object (class, function, variable, etc.).""" - - name: str - kind: str - path: str - signature: Optional[str] - docstring: Optional[str] - members: Dict[str, 'DocObject'] - - def __init__(self, name: str, kind: str, path: str, signature: Optional[str] = None, docstring: Optional[str] = None) -> None: ... - - def add_member(self, member: 'DocObject') -> None: ... - - def get_member(self, name: str) -> Optional['DocObject']: ... - - def is_private(self) -> bool: ... - -class Module: - """Represents a Python module in the documentation model.""" - - path: str - docstring: Optional[str] - members: Dict[str, DocObject] - - def __init__(self, path: str, docstring: Optional[str] = None) -> None: ... - - def add_object(self, obj: DocObject) -> None: ... - - def get_object(self, name: str) -> Optional[DocObject]: ... - - def get_public_objects(self) -> List[DocObject]: ... - -class Project: - """Root container for all documentation in a project.""" - - name: str - version: Optional[str] - modules: Dict[str, Module] - nav: 'Navigation' - - def __init__(self, name: str, version: Optional[str] = None) -> None: ... - - def add_module(self, module: Module) -> None: ... - - def get_module(self, path: str) -> Optional[Module]: ... - - def get_all_modules(self) -> List[Module]: ... - -class Navigation: - """Navigation structure derived from project modules.""" - - entries: List['NavEntry'] - - def __init__(self) -> None: ... - - def add_entry(self, entry: 'NavEntry') -> None: ... - - def get_entry(self, title: str) -> Optional['NavEntry']: ... - -class NavEntry: - """Single navigation entry linking to a module.""" - - title: str - module: str - - def __init__(self, title: str, module: str) -> None: ... - -class DocRenderer(Protocol): - """Protocol for documentation renderers.""" - - name: str - - def generate_sources(self, project: Project, out_dir: Path) -> None: ... - - def build(self, config: 'RendererConfig') -> None: ... - - def serve(self, config: 'RendererConfig') -> None: ... - -class RendererConfig: - """Base configuration for renderers.""" - - out_dir: Path - project: Project - - def __init__(self, out_dir: Path, project: Project) -> None: ... - -class GriffeLoader: - """Loads Python modules using Griffe introspection.""" - - def __init__(self) -> None: ... - - def load_project(self, module_paths: List[str]) -> Project: ... - - def load_module(self, path: str) -> Module: ... - -class MCPExporter: - """Exports documentation model to MCP JSON format.""" - - def __init__(self) -> None: ... - - def export(self, project: Project, out_dir: Path) -> None: ... - -class MCPServer: - """Live MCP server for documentation queries.""" - - def __init__(self, project: Project) -> None: ... - - def start(self, host: str = "localhost", port: int = 8080) -> None: ... - - def stop(self) -> None: ... \ No newline at end of file diff --git a/docforge/utils/__init__.py b/docforge/utils/__init__.py deleted file mode 100644 index 485fb59..0000000 --- a/docforge/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Utility functions and helpers for doc-forge.""" \ No newline at end of file