317 lines
10 KiB
Python
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",
|
|
] |