Files
doc-forge/docforge/renderers/sphinx.py

317 lines
10 KiB
Python

"""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",
]