init docforge lib

This commit is contained in:
2026-01-20 18:22:16 +05:30
parent 86a4f8f41a
commit a45725160d
28 changed files with 3486 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
"""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",
]

View File

@@ -0,0 +1,51 @@
"""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: ...

217
docforge/renderers/base.py Normal file
View File

@@ -0,0 +1,217 @@
"""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}')"

View File

@@ -0,0 +1,268 @@
"""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"]

View File

@@ -0,0 +1,317 @@
"""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",
]