Compare commits
14 Commits
dca19caaf3
...
b6e5114532
| Author | SHA1 | Date | |
|---|---|---|---|
| b6e5114532 | |||
| 81e8a8cd49 | |||
| be8f23c8ab | |||
| 9392d2c999 | |||
| 9d0b6e78d1 | |||
| 4fa3bc0533 | |||
| 46b7cc52e1 | |||
| c8ecc6a476 | |||
| 5c8d9dcc9c | |||
| b497c5d2e9 | |||
| 0061dbe2eb | |||
| 6f9776dff2 | |||
| 6c9fb433cb | |||
| 6b334fd181 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -57,7 +57,6 @@ docs/build/
|
|||||||
# =========================
|
# =========================
|
||||||
# MkDocs / Sphinx output
|
# MkDocs / Sphinx output
|
||||||
# =========================
|
# =========================
|
||||||
mkdocs.yml
|
|
||||||
site/
|
site/
|
||||||
.build/
|
.build/
|
||||||
_doctrees/
|
_doctrees/
|
||||||
|
|||||||
23
docforge.nav.yml
Normal file
23
docforge.nav.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
home: docforge/index.md
|
||||||
|
groups:
|
||||||
|
Loader:
|
||||||
|
- docforge/loader/index.md
|
||||||
|
- docforge/loader/griffe_loader.md
|
||||||
|
Model:
|
||||||
|
- docforge/model/index.md
|
||||||
|
- docforge/model/module.md
|
||||||
|
- docforge/model/object.md
|
||||||
|
- docforge/model/project.md
|
||||||
|
Navigation:
|
||||||
|
- docforge/nav/index.md
|
||||||
|
- docforge/nav/spec.md
|
||||||
|
- docforge/nav/resolver.md
|
||||||
|
- docforge/nav/mkdocs.md
|
||||||
|
Renderers:
|
||||||
|
- docforge/renderers/index.md
|
||||||
|
- docforge/renderers/base.md
|
||||||
|
- docforge/renderers/mkdocs.md
|
||||||
|
CLI:
|
||||||
|
- docforge/cli/index.md
|
||||||
|
- docforge/cli/main.md
|
||||||
|
- docforge/cli/mkdocs.md
|
||||||
@@ -1,9 +1,54 @@
|
|||||||
"""
|
"""
|
||||||
doc-forge — renderer-agnostic Python documentation compiler.
|
# doc-forge
|
||||||
|
|
||||||
At this stage, doc-forge publicly exposes only the Introspection Layer.
|
`doc-forge` is a renderer-agnostic Python documentation compiler designed for
|
||||||
All the rendering, exporting, and serving APIs are intentionally private
|
speed, flexibility, and beautiful output. It decouples the introspection of
|
||||||
until their contracts are finalized.
|
your code from the rendering process, allowing you to generate documentation
|
||||||
|
for various platforms (starting with MkDocs) from a single internal model.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install using `pip` with the optional `mkdocs` dependencies for a complete setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install doc-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Generate Markdown Sources**:
|
||||||
|
Introspect your package and create ready-to-use Markdown files:
|
||||||
|
```bash
|
||||||
|
doc-forge generate --module my_package --docs-dir docs
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Define Navigation**:
|
||||||
|
Create a `docforge.nav.yml` to organize your documentation:
|
||||||
|
```yaml
|
||||||
|
home: my_package/index.md
|
||||||
|
groups:
|
||||||
|
Core API:
|
||||||
|
- my_package/core/*.md
|
||||||
|
Utilities:
|
||||||
|
- my_package/utils.md
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Generate MkDocs Configuration**:
|
||||||
|
```bash
|
||||||
|
doc-forge mkdocs --site-name "My Awesome Docs"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Preview**:
|
||||||
|
```bash
|
||||||
|
doc-forge serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `docforge.loader`: Introspects source code using static analysis (`griffe`).
|
||||||
|
- `docforge.model`: The internal representation of your project, modules, and objects.
|
||||||
|
- `docforge.renderers`: Converters that turn the model into physical files.
|
||||||
|
- `docforge.nav`: Managers for logical-to-physical path mapping and navigation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .loader import GriffeLoader, discover_module_paths
|
from .loader import GriffeLoader, discover_module_paths
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from .loader import GriffeLoader
|
from .loader import GriffeLoader, discover_module_paths
|
||||||
from .renderers import MkDocsRenderer
|
from .renderers import MkDocsRenderer
|
||||||
from .cli import main
|
from .cli import main
|
||||||
from . import model
|
from . import model
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GriffeLoader",
|
"GriffeLoader",
|
||||||
|
"discover_module_paths",
|
||||||
"MkDocsRenderer",
|
"MkDocsRenderer",
|
||||||
"model",
|
"model",
|
||||||
"main",
|
"main",
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
"""
|
||||||
|
# CLI Layer
|
||||||
|
|
||||||
|
The `docforge.cli` package provides the command-line interface for interacting
|
||||||
|
with doc-forge.
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
- **tree**: Visualize the introspected project structure.
|
||||||
|
- **generate**: Create Markdown source files from Python code.
|
||||||
|
- **mkdocs**: Generate the primary `mkdocs.yml` configuration.
|
||||||
|
- **build**: Build the final documentation site.
|
||||||
|
- **serve**: Launch a local development server with live-reloading.
|
||||||
|
"""
|
||||||
|
|
||||||
from .main import main
|
from .main import main
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
Main entry point for the doc-forge CLI. This module defines the core command
|
||||||
|
group and the 'tree', 'generate', 'build', and 'serve' commands.
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence, Optional
|
from typing import Sequence, Optional
|
||||||
@@ -12,7 +15,10 @@ from docforge.cli.mkdocs import mkdocs_cmd
|
|||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli() -> None:
|
def cli() -> None:
|
||||||
"""doc-forge command-line interface."""
|
"""
|
||||||
|
doc-forge CLI: A tool for introspecting Python projects and generating
|
||||||
|
documentation.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +43,13 @@ def tree(
|
|||||||
modules: Sequence[str],
|
modules: Sequence[str],
|
||||||
project_name: Optional[str],
|
project_name: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Show introspection tree."""
|
"""
|
||||||
|
Visualize the project structure including modules and their members.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
modules: List of module paths to introspect.
|
||||||
|
project_name: Optional project name override.
|
||||||
|
"""
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project(list(modules), project_name)
|
project = loader.load_project(list(modules), project_name)
|
||||||
|
|
||||||
@@ -51,6 +63,9 @@ def tree(
|
|||||||
|
|
||||||
|
|
||||||
def _print_object(obj, indent: str) -> None:
|
def _print_object(obj, indent: str) -> None:
|
||||||
|
"""
|
||||||
|
Recursive helper to print doc objects.
|
||||||
|
"""
|
||||||
click.echo(f"{indent}├── {obj.name}")
|
click.echo(f"{indent}├── {obj.name}")
|
||||||
for member in obj.get_all_members():
|
for member in obj.get_all_members():
|
||||||
_print_object(member, indent + "│ ")
|
_print_object(member, indent + "│ ")
|
||||||
@@ -62,8 +77,7 @@ def _print_object(obj, indent: str) -> None:
|
|||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--modules",
|
"--module",
|
||||||
multiple=True,
|
|
||||||
required=True,
|
required=True,
|
||||||
help="Python module import paths to document",
|
help="Python module import paths to document",
|
||||||
)
|
)
|
||||||
@@ -77,15 +91,21 @@ def _print_object(obj, indent: str) -> None:
|
|||||||
default=Path("docs"),
|
default=Path("docs"),
|
||||||
)
|
)
|
||||||
def generate(
|
def generate(
|
||||||
modules: Sequence[str],
|
module: str,
|
||||||
project_name: Optional[str],
|
project_name: Optional[str],
|
||||||
docs_dir: Path,
|
docs_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate documentation source files using MkDocs renderer."""
|
"""
|
||||||
|
Generate Markdown source files for the specified module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module: The primary module path to document.
|
||||||
|
project_name: Optional project name override.
|
||||||
|
docs_dir: Directory where documentation sources will be written.
|
||||||
|
"""
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
discovered_paths = discover_module_paths(
|
discovered_paths = discover_module_paths(
|
||||||
"docforge",
|
module,
|
||||||
Path(r"C:\Users\vishe\WorkSpace\code\aetos\doc-forge")
|
|
||||||
)
|
)
|
||||||
project = loader.load_project(
|
project = loader.load_project(
|
||||||
discovered_paths,
|
discovered_paths,
|
||||||
@@ -109,7 +129,12 @@ def generate(
|
|||||||
default=Path("mkdocs.yml"),
|
default=Path("mkdocs.yml"),
|
||||||
)
|
)
|
||||||
def build(mkdocs_yml: Path) -> None:
|
def build(mkdocs_yml: Path) -> None:
|
||||||
"""Build documentation using MkDocs."""
|
"""
|
||||||
|
Build the documentation site using MkDocs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mkdocs_yml: Path to the mkdocs.yml configuration file.
|
||||||
|
"""
|
||||||
if not mkdocs_yml.exists():
|
if not mkdocs_yml.exists():
|
||||||
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
||||||
|
|
||||||
@@ -132,11 +157,22 @@ def build(mkdocs_yml: Path) -> None:
|
|||||||
default=Path("mkdocs.yml"),
|
default=Path("mkdocs.yml"),
|
||||||
)
|
)
|
||||||
def serve(mkdocs_yml: Path) -> None:
|
def serve(mkdocs_yml: Path) -> None:
|
||||||
"""Serve documentation using MkDocs."""
|
"""
|
||||||
|
Serve the documentation site with live-reload using MkDocs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mkdocs_yml: Path to the mkdocs.yml configuration file.
|
||||||
|
"""
|
||||||
if not mkdocs_yml.exists():
|
if not mkdocs_yml.exists():
|
||||||
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
||||||
|
|
||||||
from mkdocs.commands.serve import serve as mkdocs_serve
|
from mkdocs.commands.serve import serve as mkdocs_serve
|
||||||
|
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 8000
|
||||||
|
url = f"http://{host}:{port}/"
|
||||||
|
|
||||||
|
click.echo(f"Serving documentation at {url}")
|
||||||
mkdocs_serve(config_file=str(mkdocs_yml))
|
mkdocs_serve(config_file=str(mkdocs_yml))
|
||||||
|
|
||||||
|
|
||||||
@@ -145,6 +181,9 @@ def serve(mkdocs_yml: Path) -> None:
|
|||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
CLI Entry point.
|
||||||
|
"""
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ def tree(
|
|||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--modules",
|
"--module",
|
||||||
multiple=True,
|
|
||||||
help="Python module import paths to document",
|
help="Python module import paths to document",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -42,7 +41,7 @@ def tree(
|
|||||||
default=Path("docs"),
|
default=Path("docs"),
|
||||||
)
|
)
|
||||||
def generate(
|
def generate(
|
||||||
modules: Sequence[str],
|
module: str,
|
||||||
project_name: str | None,
|
project_name: str | None,
|
||||||
docs_dir: Path,
|
docs_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module contains the 'mkdocs' CLI command, which orchestrates the generation
|
||||||
|
of the main mkdocs.yml configuration file.
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
@@ -12,6 +15,16 @@ from docforge.nav import MkDocsNavEmitter
|
|||||||
|
|
||||||
|
|
||||||
def _load_template(template: Path | None) -> dict:
|
def _load_template(template: Path | None) -> dict:
|
||||||
|
"""
|
||||||
|
Load a YAML template for mkdocs.yml. If no template is provided,
|
||||||
|
loads the built-in sample template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template: Path to the template file, or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The loaded template data as a dictionary.
|
||||||
|
"""
|
||||||
if template is not None:
|
if template is not None:
|
||||||
if not template.exists():
|
if not template.exists():
|
||||||
raise click.FileError(str(template), hint="Template not found")
|
raise click.FileError(str(template), hint="Template not found")
|
||||||
@@ -27,6 +40,11 @@ def _load_template(template: Path | None) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@click.command("mkdocs")
|
@click.command("mkdocs")
|
||||||
|
@click.option(
|
||||||
|
"--site-name",
|
||||||
|
required=True,
|
||||||
|
help="MkDocs site_name (required)",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--docs-dir",
|
"--docs-dir",
|
||||||
type=click.Path(path_type=Path),
|
type=click.Path(path_type=Path),
|
||||||
@@ -54,8 +72,19 @@ def mkdocs_cmd(
|
|||||||
nav_file: Path,
|
nav_file: Path,
|
||||||
template: Path | None,
|
template: Path | None,
|
||||||
out: Path,
|
out: Path,
|
||||||
|
site_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate mkdocs.yml from nav spec and template."""
|
"""
|
||||||
|
Generate an mkdocs.yml configuration file by combining a template with
|
||||||
|
the navigation structure resolved from a docforge.nav.yml file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
docs_dir: Path to the directory containing documentation Markdown files.
|
||||||
|
nav_file: Path to the docforge.nav.yml specification.
|
||||||
|
template: Optional path to an mkdocs.yml template.
|
||||||
|
out: Path where the final mkdocs.yml will be written.
|
||||||
|
site_name: The name of the documentation site.
|
||||||
|
"""
|
||||||
|
|
||||||
if not nav_file.exists():
|
if not nav_file.exists():
|
||||||
raise click.FileError(str(nav_file), hint="Nav spec not found")
|
raise click.FileError(str(nav_file), hint="Nav spec not found")
|
||||||
@@ -72,6 +101,9 @@ def mkdocs_cmd(
|
|||||||
# Load template (user or built-in)
|
# Load template (user or built-in)
|
||||||
data = _load_template(template)
|
data = _load_template(template)
|
||||||
|
|
||||||
|
# Inject site_name
|
||||||
|
data["site_name"] = site_name
|
||||||
|
|
||||||
# Inject nav
|
# Inject nav
|
||||||
data["nav"] = nav_block
|
data["nav"] = nav_block
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
@@ -11,6 +9,11 @@ def _load_template(template: Optional[Path]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
@click.command("mkdocs")
|
@click.command("mkdocs")
|
||||||
|
@click.option(
|
||||||
|
"--site-name",
|
||||||
|
required=True,
|
||||||
|
help="MkDocs site_name (required)",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--docs-dir",
|
"--docs-dir",
|
||||||
type=click.Path(path_type=Path),
|
type=click.Path(path_type=Path),
|
||||||
@@ -37,5 +40,6 @@ def mkdocs_cmd(
|
|||||||
nav_file: Path,
|
nav_file: Path,
|
||||||
template: Optional[Path],
|
template: Optional[Path],
|
||||||
out: Path,
|
out: Path,
|
||||||
|
site_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
"""
|
||||||
|
# Loader Layer
|
||||||
|
|
||||||
|
The `docforge.loader` package is responsible for discovering Python source files
|
||||||
|
and extracting their documentation using static analysis.
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
- **Discovery**: Automatically find all modules and packages in a project
|
||||||
|
directory.
|
||||||
|
- **Introspection**: Uses `griffe` to parse docstrings, signatures, and
|
||||||
|
hierarchical relationships without executing the code.
|
||||||
|
- **Filtering**: Automatically excludes private members (prefixed with `_`) to
|
||||||
|
ensure clean public documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
from .griffe_loader import GriffeLoader, discover_module_paths
|
from .griffe_loader import GriffeLoader, discover_module_paths
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module provides the GriffeLoader, which uses the 'griffe' library to
|
||||||
|
introspect Python source code and populate the doc-forge Project model.
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -25,9 +28,16 @@ def discover_module_paths(
|
|||||||
Discover all Python modules under a package via filesystem traversal.
|
Discover all Python modules under a package via filesystem traversal.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Directory with __init__.py => package
|
- Directory with __init__.py is treated as a package.
|
||||||
- .py file => module
|
- Any .py file is treated as a module.
|
||||||
- Paths converted to dotted module paths
|
- All paths are converted to dotted module paths.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_name: The name of the package to discover.
|
||||||
|
project_root: The root directory of the project. Defaults to current working directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A sorted list of dotted module paths.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if project_root is None:
|
if project_root is None:
|
||||||
@@ -53,9 +63,14 @@ def discover_module_paths(
|
|||||||
|
|
||||||
|
|
||||||
class GriffeLoader:
|
class GriffeLoader:
|
||||||
"""Loads Python modules using Griffe introspection."""
|
"""
|
||||||
|
Loads Python modules and extracts documentation using the Griffe introspection engine.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the GriffeLoader.
|
||||||
|
"""
|
||||||
self._loader = _GriffeLoader(
|
self._loader = _GriffeLoader(
|
||||||
modules_collection=ModulesCollection(),
|
modules_collection=ModulesCollection(),
|
||||||
lines_collection=LinesCollection(),
|
lines_collection=LinesCollection(),
|
||||||
@@ -65,7 +80,19 @@ class GriffeLoader:
|
|||||||
self,
|
self,
|
||||||
module_paths: List[str],
|
module_paths: List[str],
|
||||||
project_name: Optional[str] = None,
|
project_name: Optional[str] = None,
|
||||||
|
skip_import_errors: bool = None,
|
||||||
) -> Project:
|
) -> Project:
|
||||||
|
"""
|
||||||
|
Load multiple modules and combine them into a single Project model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_paths: A list of dotted paths to the modules to load.
|
||||||
|
project_name: Optional name for the project. Defaults to the first module name.
|
||||||
|
skip_import_errors: If True, modules that fail to import will be skipped.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A Project instance containing the loaded modules.
|
||||||
|
"""
|
||||||
if not module_paths:
|
if not module_paths:
|
||||||
raise ValueError("At least one module path must be provided")
|
raise ValueError("At least one module path must be provided")
|
||||||
|
|
||||||
@@ -77,19 +104,28 @@ class GriffeLoader:
|
|||||||
for module_path in module_paths:
|
for module_path in module_paths:
|
||||||
try:
|
try:
|
||||||
module = self.load_module(module_path)
|
module = self.load_module(module_path)
|
||||||
project.add_module(module)
|
except ImportError as import_error:
|
||||||
except Exception as e:
|
if skip_import_errors:
|
||||||
logger.error("Failed to load module %s: %s", module_path, e)
|
logger.debug("Could not load %s: %s", module_path, import_error)
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
|
raise import_error
|
||||||
|
project.add_module(module)
|
||||||
|
|
||||||
return project
|
return project
|
||||||
|
|
||||||
def load_module(self, path: str) -> Module:
|
def load_module(self, path: str) -> Module:
|
||||||
try:
|
"""
|
||||||
self._loader.load(path)
|
Load a single module and convert its introspection data into the docforge model.
|
||||||
griffe_module = self._loader.modules_collection[path]
|
|
||||||
except Exception as e:
|
Args:
|
||||||
raise ImportError(f"Failed to load module '{path}': {e}") from e
|
path: The dotted path of the module to load.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A Module instance.
|
||||||
|
"""
|
||||||
|
self._loader.load(path)
|
||||||
|
griffe_module = self._loader.modules_collection[path]
|
||||||
|
|
||||||
return self._convert_module(griffe_module)
|
return self._convert_module(griffe_module)
|
||||||
|
|
||||||
@@ -106,10 +142,8 @@ class GriffeLoader:
|
|||||||
for name, member in obj.members.items():
|
for name, member in obj.members.items():
|
||||||
if name.startswith("_"):
|
if name.startswith("_"):
|
||||||
continue
|
continue
|
||||||
try:
|
|
||||||
module.add_object(self._convert_object(member))
|
module.add_object(self._convert_object(member))
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Skipping member %s: %s", name, e)
|
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
|
||||||
@@ -125,14 +159,13 @@ class GriffeLoader:
|
|||||||
docstring=self._safe_docstring(obj),
|
docstring=self._safe_docstring(obj),
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(obj, "members"):
|
try:
|
||||||
for name, member in obj.members.items():
|
for name, member in obj.members.items():
|
||||||
if name.startswith("_"):
|
if name.startswith("_"):
|
||||||
continue
|
continue
|
||||||
try:
|
doc_obj.add_member(self._convert_object(member))
|
||||||
doc_obj.add_member(self._convert_object(member))
|
except AliasResolutionError:
|
||||||
except Exception:
|
pass
|
||||||
continue
|
|
||||||
|
|
||||||
return doc_obj
|
return doc_obj
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class GriffeLoader:
|
|||||||
self,
|
self,
|
||||||
module_paths: List[str],
|
module_paths: List[str],
|
||||||
project_name: Optional[str] = ...,
|
project_name: Optional[str] = ...,
|
||||||
|
skip_import_errors: bool = ...,
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""Load a documentation project from Python modules."""
|
"""Load a documentation project from Python modules."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
Core documentation model for doc-forge.
|
# Model Layer
|
||||||
|
|
||||||
These classes form the renderer-agnostic, introspection-derived
|
The `docforge.model` package provides the core data structures used to represent
|
||||||
representation of Python documentation.
|
Python source code in a documentation-focused hierarchy.
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
- **Project**: The root container for all documented modules.
|
||||||
|
- **Module**: Represents a Python module or package, containing members.
|
||||||
|
- **DocObject**: A recursive structure for classes, functions, and attributes.
|
||||||
|
|
||||||
|
These classes are designed to be renderer-agnostic, allowing the same internal
|
||||||
|
representation to be transformed into various output formats (currently MkDocs).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .project import Project
|
from .project import Project
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module defines the Module class, which represents a Python module or package
|
||||||
|
in the doc-forge documentation model. It acts as a container for top-level
|
||||||
|
documented objects.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Dict, Iterable, Optional
|
from typing import Dict, Iterable, Optional
|
||||||
|
|
||||||
@@ -6,22 +10,57 @@ from docforge.model.object import DocObject
|
|||||||
|
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
"""Represents a documented Python module."""
|
"""
|
||||||
|
Represents a documented Python module or package.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
path: Dotted import path of the module.
|
||||||
|
docstring: Module-level docstring content.
|
||||||
|
members: Dictionary mapping object names to their DocObject representations.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: str,
|
||||||
docstring: Optional[str] = None,
|
docstring: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize a new Module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The dotted path of the module.
|
||||||
|
docstring: The module's docstring, if any.
|
||||||
|
"""
|
||||||
self.path = path
|
self.path = path
|
||||||
self.docstring = docstring
|
self.docstring = docstring
|
||||||
self.members: Dict[str, DocObject] = {}
|
self.members: Dict[str, DocObject] = {}
|
||||||
|
|
||||||
def add_object(self, obj: DocObject) -> None:
|
def add_object(self, obj: DocObject) -> None:
|
||||||
|
"""
|
||||||
|
Add a documented object to the module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: The object to add.
|
||||||
|
"""
|
||||||
self.members[obj.name] = obj
|
self.members[obj.name] = obj
|
||||||
|
|
||||||
def get_object(self, name: str) -> DocObject:
|
def get_object(self, name: str) -> DocObject:
|
||||||
|
"""
|
||||||
|
Retrieve a member object by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The requested DocObject.
|
||||||
|
"""
|
||||||
return self.members[name]
|
return self.members[name]
|
||||||
|
|
||||||
def get_all_objects(self) -> Iterable[DocObject]:
|
def get_all_objects(self) -> Iterable[DocObject]:
|
||||||
|
"""
|
||||||
|
Get all top-level objects in the module.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An iterable of DocObject instances.
|
||||||
|
"""
|
||||||
return self.members.values()
|
return self.members.values()
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module defines the DocObject class, the fundamental recursive unit of the
|
||||||
|
doc-forge documentation model. A DocObject represents a single Python entity
|
||||||
|
(class, function, method, or attribute) and its nested members.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Dict, Iterable, Optional
|
from typing import Dict, Iterable, Optional
|
||||||
|
|
||||||
|
|
||||||
class DocObject:
|
class DocObject:
|
||||||
"""Represents a documented Python object."""
|
"""
|
||||||
|
Represents a documented Python object (class, function, method, etc.).
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: Local name of the object.
|
||||||
|
kind: Type of object (e.g., 'class', 'function', 'attribute').
|
||||||
|
path: Full dotted import path to the object.
|
||||||
|
signature: Callable signature, if applicable.
|
||||||
|
docstring: Raw docstring content extracted from the source.
|
||||||
|
members: Dictionary mapping member names to their DocObject representations.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -14,18 +28,49 @@ class DocObject:
|
|||||||
signature: Optional[str] = None,
|
signature: Optional[str] = None,
|
||||||
docstring: Optional[str] = None,
|
docstring: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize a new DocObject.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The local name of the object.
|
||||||
|
kind: The kind of object (e.g., 'class', 'function').
|
||||||
|
path: The full dotted path to the object.
|
||||||
|
signature: The object's signature (for callable objects).
|
||||||
|
docstring: The object's docstring, if any.
|
||||||
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.path = path
|
self.path = path
|
||||||
self.signature = signature
|
self.signature = signature
|
||||||
self.docstring = docstring
|
self.docstring = docstring
|
||||||
self.members: Dict[str, DocObject] = {}
|
self.members: Dict[str, 'DocObject'] = {}
|
||||||
|
|
||||||
def add_member(self, obj: DocObject) -> None:
|
def add_member(self, obj: 'DocObject') -> None:
|
||||||
|
"""
|
||||||
|
Add a child member to this object (e.g., a method to a class).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: The child DocObject to add.
|
||||||
|
"""
|
||||||
self.members[obj.name] = obj
|
self.members[obj.name] = obj
|
||||||
|
|
||||||
def get_member(self, name: str) -> DocObject:
|
def get_member(self, name: str) -> 'DocObject':
|
||||||
|
"""
|
||||||
|
Retrieve a child member by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the member.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The requested DocObject.
|
||||||
|
"""
|
||||||
return self.members[name]
|
return self.members[name]
|
||||||
|
|
||||||
def get_all_members(self) -> Iterable[DocObject]:
|
def get_all_members(self) -> Iterable['DocObject']:
|
||||||
|
"""
|
||||||
|
Get all members of this object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An iterable of child DocObject instances.
|
||||||
|
"""
|
||||||
return self.members.values()
|
return self.members.values()
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module defines the Project class, the top-level container for a documented
|
||||||
|
project. It aggregates multiple Module instances into a single named entity.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Dict, Iterable
|
from typing import Dict, Iterable
|
||||||
|
|
||||||
@@ -6,20 +9,59 @@ from docforge.model.module import Module
|
|||||||
|
|
||||||
|
|
||||||
class Project:
|
class Project:
|
||||||
"""Represents a documentation project."""
|
"""
|
||||||
|
Represents a documentation project, serving as a container for modules.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: Name of the project.
|
||||||
|
modules: Dictionary mapping module paths to Module instances.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str) -> None:
|
def __init__(self, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Initialize a new Project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the project.
|
||||||
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.modules: Dict[str, Module] = {}
|
self.modules: Dict[str, Module] = {}
|
||||||
|
|
||||||
def add_module(self, module: Module) -> None:
|
def add_module(self, module: Module) -> None:
|
||||||
|
"""
|
||||||
|
Add a module to the project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module: The module to add.
|
||||||
|
"""
|
||||||
self.modules[module.path] = module
|
self.modules[module.path] = module
|
||||||
|
|
||||||
def get_module(self, path: str) -> Module:
|
def get_module(self, path: str) -> Module:
|
||||||
|
"""
|
||||||
|
Retrieve a module by its dotted path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The dotted path of the module (e.g., 'pkg.mod').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The requested Module.
|
||||||
|
"""
|
||||||
return self.modules[path]
|
return self.modules[path]
|
||||||
|
|
||||||
def get_all_modules(self) -> Iterable[Module]:
|
def get_all_modules(self) -> Iterable[Module]:
|
||||||
|
"""
|
||||||
|
Get all modules in the project.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An iterable of Module objects.
|
||||||
|
"""
|
||||||
return self.modules.values()
|
return self.modules.values()
|
||||||
|
|
||||||
def get_module_list(self) -> list[str]:
|
def get_module_list(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Get the list of all module dotted paths.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of module paths.
|
||||||
|
"""
|
||||||
return list(self.modules.keys())
|
return list(self.modules.keys())
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
"""
|
||||||
|
# Navigation Layer
|
||||||
|
|
||||||
|
The `docforge.nav` package manages the mapping between the logical documentation
|
||||||
|
structure and the physical files on disk.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Spec Definition**: Users define navigation intent in `docforge.nav.yml`.
|
||||||
|
2. **Resolution**: `resolve_nav` matches patterns in the spec to generated `.md` files.
|
||||||
|
3. **Emission**: `MkDocsNavEmitter` produces the final YAML structure for `mkdocs.yml`.
|
||||||
|
|
||||||
|
This abstraction allows doc-forge to support complex grouping and ordering
|
||||||
|
independently of the source code's physical layout.
|
||||||
|
"""
|
||||||
|
|
||||||
from .spec import NavSpec, load_nav_spec
|
from .spec import NavSpec, load_nav_spec
|
||||||
from .resolver import ResolvedNav, resolve_nav
|
from .resolver import ResolvedNav, resolve_nav
|
||||||
from .mkdocs import MkDocsNavEmitter
|
from .mkdocs import MkDocsNavEmitter
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module provides the MkDocsNavEmitter, which converts a ResolvedNav instance
|
||||||
|
into the specific YAML-ready list structure expected by the MkDocs 'nav'
|
||||||
|
configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
@@ -7,9 +11,21 @@ from docforge.nav.resolver import ResolvedNav
|
|||||||
|
|
||||||
|
|
||||||
class MkDocsNavEmitter:
|
class MkDocsNavEmitter:
|
||||||
"""Emit MkDocs-compatible nav structures."""
|
"""
|
||||||
|
Emitter responsible for transforming a ResolvedNav into an MkDocs-compatible
|
||||||
|
navigation structure.
|
||||||
|
"""
|
||||||
|
|
||||||
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
|
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Generate a list of navigation entries for mkdocs.yml.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nav: The resolved navigation data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of dictionary mappings representing the MkDocs navigation.
|
||||||
|
"""
|
||||||
result: List[Dict[str, Any]] = []
|
result: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
# Home entry (semantic path)
|
# Home entry (semantic path)
|
||||||
@@ -21,14 +37,36 @@ class MkDocsNavEmitter:
|
|||||||
entries: List[str] = []
|
entries: List[str] = []
|
||||||
for p in paths:
|
for p in paths:
|
||||||
# Convert filesystem path back to docs-relative path
|
# Convert filesystem path back to docs-relative path
|
||||||
entries.append(self._to_relative(p))
|
rel_path = self._to_relative(p, nav._docs_root)
|
||||||
|
entries.append(rel_path)
|
||||||
result.append({group: entries})
|
result.append({group: entries})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _to_relative(self, path: Path) -> str:
|
def _to_relative(self, path: Path, docs_root: Path | None) -> str:
|
||||||
"""
|
"""
|
||||||
Convert a filesystem path to a docs-relative path.
|
Convert a filesystem path to a path relative to the documentation root.
|
||||||
|
This handles both absolute and relative filesystem paths, ensuring the
|
||||||
|
output is compatible with MkDocs navigation requirements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The path to convert.
|
||||||
|
docs_root: The root directory for documentation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string representing the relative POSIX-style path.
|
||||||
"""
|
"""
|
||||||
# Normalize to POSIX-style for MkDocs
|
if docs_root and path.is_absolute():
|
||||||
|
try:
|
||||||
|
path = path.relative_to(docs_root.resolve())
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
elif docs_root:
|
||||||
|
# Handle relative paths (e.g. starting with 'docs/')
|
||||||
|
path_str = path.as_posix()
|
||||||
|
docs_root_str = docs_root.as_posix()
|
||||||
|
if path_str.startswith(docs_root_str + "/"):
|
||||||
|
return path_str[len(docs_root_str) + 1:]
|
||||||
|
|
||||||
|
# Fallback for other cases
|
||||||
return path.as_posix().split("/docs/", 1)[-1]
|
return path.as_posix().split("/docs/", 1)[-1]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class MkDocsNavEmitter:
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def _to_relative(self, path: Path) -> str:
|
def _to_relative(self, path: Path, docs_root: Path | None) -> str:
|
||||||
"""
|
"""
|
||||||
Convert a filesystem path to a docs-relative path.
|
Convert a filesystem path to a docs-relative path.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module contains the logic for resolving a NavSpec against the physical
|
||||||
|
filesystem. It expands globs and validates that all referenced documents
|
||||||
|
actually exist on disk.
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, List
|
from typing import Dict, Iterable, List
|
||||||
@@ -9,17 +13,40 @@ from docforge.nav.spec import NavSpec
|
|||||||
|
|
||||||
|
|
||||||
class ResolvedNav:
|
class ResolvedNav:
|
||||||
|
"""
|
||||||
|
Represents a navigation structure where all patterns and paths have been
|
||||||
|
resolved against the actual filesystem contents.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
home: Resolved relative path to the home page.
|
||||||
|
groups: Mapping of group titles to lists of absolute or relative Path objects.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
home: str | None,
|
home: str | None,
|
||||||
groups: Dict[str, List[Path]],
|
groups: Dict[str, List[Path]],
|
||||||
docs_root: Path | None = None,
|
docs_root: Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize a ResolvedNav instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
home: The relative path to the project home page.
|
||||||
|
groups: A mapping of group names to their resolved filesystem paths.
|
||||||
|
docs_root: The root documentation directory.
|
||||||
|
"""
|
||||||
self.home = home
|
self.home = home
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
self._docs_root = docs_root
|
self._docs_root = docs_root
|
||||||
|
|
||||||
def all_files(self) -> Iterable[Path]:
|
def all_files(self) -> Iterable[Path]:
|
||||||
|
"""
|
||||||
|
Get an iterable of all resolved files in the navigation structure.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An iterable of Path objects.
|
||||||
|
"""
|
||||||
if self.home:
|
if self.home:
|
||||||
if self._docs_root is None:
|
if self._docs_root is None:
|
||||||
raise RuntimeError("docs_root is required to resolve home path")
|
raise RuntimeError("docs_root is required to resolve home path")
|
||||||
@@ -33,6 +60,20 @@ def resolve_nav(
|
|||||||
spec: NavSpec,
|
spec: NavSpec,
|
||||||
docs_root: Path,
|
docs_root: Path,
|
||||||
) -> ResolvedNav:
|
) -> ResolvedNav:
|
||||||
|
"""
|
||||||
|
Create a ResolvedNav by processing a NavSpec against the filesystem.
|
||||||
|
This expands globs and validates the existence of referenced files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The navigation specification to resolve.
|
||||||
|
docs_root: The root directory for documentation files.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A ResolvedNav instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If a pattern doesn't match any files or the docs_root doesn't exist.
|
||||||
|
"""
|
||||||
if not docs_root.exists():
|
if not docs_root.exists():
|
||||||
raise FileNotFoundError(docs_root)
|
raise FileNotFoundError(docs_root)
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,14 @@ class ResolvedNav:
|
|||||||
|
|
||||||
home: Optional[str]
|
home: Optional[str]
|
||||||
groups: Dict[str, List[Path]]
|
groups: Dict[str, List[Path]]
|
||||||
|
_docs_root: Optional[Path]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
home: str | None,
|
home: str | None,
|
||||||
groups: Dict[str, List[Path]],
|
groups: Dict[str, List[Path]],
|
||||||
docs_root: Path | None = None,
|
docs_root: Path | None = ...,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
self._docs_root = None
|
|
||||||
...
|
|
||||||
|
|
||||||
def all_files(self) -> Iterable[Path]:
|
def all_files(self) -> Iterable[Path]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module defines the NavSpec class, which represents the user's intent for
|
||||||
|
documentation navigation as defined in a YAML specification (usually
|
||||||
|
docforge.nav.yml).
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
@@ -7,18 +11,44 @@ import yaml
|
|||||||
|
|
||||||
|
|
||||||
class NavSpec:
|
class NavSpec:
|
||||||
"""Parsed representation of docforge.nav.yml."""
|
"""
|
||||||
|
Parsed representation of the docforge navigation specification file.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
home: Path to the home document (e.g., 'index.md').
|
||||||
|
groups: Mapping of group titles to lists of path patterns/globs.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
home: Optional[str],
|
home: Optional[str],
|
||||||
groups: Dict[str, List[str]],
|
groups: Dict[str, List[str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize a NavSpec.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
home: The path to the home document.
|
||||||
|
groups: A mapping of group names to lists of path patterns (globs).
|
||||||
|
"""
|
||||||
self.home = home
|
self.home = home
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, path: Path) -> "NavSpec":
|
def load(cls, path: Path) -> "NavSpec":
|
||||||
|
"""
|
||||||
|
Load a NavSpec from a YAML file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The filesystem path to the YAML file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A NavSpec instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the path does not exist.
|
||||||
|
ValueError: If the file content is not a valid NavSpec mapping.
|
||||||
|
"""
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(path)
|
raise FileNotFoundError(path)
|
||||||
|
|
||||||
@@ -47,6 +77,12 @@ class NavSpec:
|
|||||||
return cls(home=home, groups=groups)
|
return cls(home=home, groups=groups)
|
||||||
|
|
||||||
def all_patterns(self) -> List[str]:
|
def all_patterns(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get all path patterns referenced in the specification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of all patterns (home plus all groups).
|
||||||
|
"""
|
||||||
patterns: List[str] = []
|
patterns: List[str] = []
|
||||||
if self.home:
|
if self.home:
|
||||||
patterns.append(self.home)
|
patterns.append(self.home)
|
||||||
@@ -56,6 +92,15 @@ class NavSpec:
|
|||||||
|
|
||||||
|
|
||||||
def load_nav_spec(path: Path) -> NavSpec:
|
def load_nav_spec(path: Path) -> NavSpec:
|
||||||
|
"""
|
||||||
|
Utility function to load a NavSpec from a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to the navigation specification file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A loaded NavSpec instance.
|
||||||
|
"""
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(path)
|
raise FileNotFoundError(path)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
"""
|
||||||
|
# Renderers Layer
|
||||||
|
|
||||||
|
The `docforge.renderers` package handles the transformation of the internal
|
||||||
|
documentation model into physical files formatted for specific documentation
|
||||||
|
engines.
|
||||||
|
|
||||||
|
## Current Implementations
|
||||||
|
|
||||||
|
- **MkDocsRenderer**: Generates Markdown files utilizing the `mkdocstrings`
|
||||||
|
syntax. It automatically handles package/module hierarchy and generates
|
||||||
|
`index.md` files for packages.
|
||||||
|
|
||||||
|
## Extending
|
||||||
|
|
||||||
|
To add a new renderer, implement the `DocRenderer` protocol defined in
|
||||||
|
`docforge.renderers.base`.
|
||||||
|
"""
|
||||||
|
|
||||||
from .mkdocs import MkDocsRenderer
|
from .mkdocs import MkDocsRenderer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -1,13 +1,46 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module defines the base interfaces and configuration containers for
|
||||||
|
doc-forge renderers. All renderer implementations should adhere to the
|
||||||
|
DocRenderer protocol.
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
from docforge.model import Project
|
from docforge.model import Project
|
||||||
|
|
||||||
|
|
||||||
class RendererConfig:
|
class RendererConfig:
|
||||||
"""Renderer configuration container."""
|
"""
|
||||||
|
Configuration container for documentation renderers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
out_dir: The directory where documentation files should be written.
|
||||||
|
project: The introspected project model to be rendered.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, out_dir: Path, project: Project) -> None:
|
def __init__(self, out_dir: Path, project: Project) -> None:
|
||||||
self.out_dir = out_dir
|
self.out_dir = out_dir
|
||||||
self.project = project
|
self.project = project
|
||||||
|
|
||||||
|
|
||||||
|
class DocRenderer(Protocol):
|
||||||
|
"""
|
||||||
|
Protocol defining the interface for documentation renderers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
|
||||||
|
def generate_sources(
|
||||||
|
self,
|
||||||
|
project: Project,
|
||||||
|
out_dir: Path,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Generate renderer-specific source files for the given project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: The project model containing modules and objects.
|
||||||
|
out_dir: Target directory for the generated files.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from __future__ import annotations
|
"""
|
||||||
|
This module implements the MkDocsRenderer, which generates Markdown source files
|
||||||
|
compatible with the MkDocs 'material' theme and 'mkdocstrings' extension.
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -6,11 +9,22 @@ from docforge.model import Project
|
|||||||
|
|
||||||
|
|
||||||
class MkDocsRenderer:
|
class MkDocsRenderer:
|
||||||
"""MkDocs source generator using mkdocstrings."""
|
"""
|
||||||
|
Renderer that generates Markdown source files formatted for the MkDocs
|
||||||
|
'mkdocstrings' plugin.
|
||||||
|
"""
|
||||||
|
|
||||||
name = "mkdocs"
|
name = "mkdocs"
|
||||||
|
|
||||||
def generate_sources(self, project: Project, out_dir: Path) -> None:
|
def generate_sources(self, project: Project, out_dir: Path) -> None:
|
||||||
|
"""
|
||||||
|
Produce a set of Markdown files in the output directory based on the
|
||||||
|
provided Project model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: The project model to render.
|
||||||
|
out_dir: Target directory for documentation files.
|
||||||
|
"""
|
||||||
modules = list(project.get_all_modules())
|
modules = list(project.get_all_modules())
|
||||||
paths = {m.path for m in modules}
|
paths = {m.path for m in modules}
|
||||||
|
|
||||||
@@ -27,6 +41,15 @@ class MkDocsRenderer:
|
|||||||
# Internal helpers
|
# Internal helpers
|
||||||
# -------------------------
|
# -------------------------
|
||||||
def _write_module(self, module, packages: set[str], out_dir: Path) -> None:
|
def _write_module(self, module, packages: set[str], out_dir: Path) -> None:
|
||||||
|
"""
|
||||||
|
Write a single module's documentation file. Packages are written as
|
||||||
|
'index.md' inside their respective directories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module: The module to write.
|
||||||
|
packages: A set of module paths that are identified as packages.
|
||||||
|
out_dir: The base output directory.
|
||||||
|
"""
|
||||||
parts = module.path.split(".")
|
parts = module.path.split(".")
|
||||||
|
|
||||||
if module.path in packages:
|
if module.path in packages:
|
||||||
@@ -52,6 +75,16 @@ class MkDocsRenderer:
|
|||||||
md_path.write_text(content, encoding="utf-8")
|
md_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
def _render_markdown(self, title: str, module_path: str) -> str:
|
def _render_markdown(self, title: str, module_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate the Markdown content for a module file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: The display title for the page.
|
||||||
|
module_path: The dotted path of the module to document.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string containing the Markdown source.
|
||||||
|
"""
|
||||||
return (
|
return (
|
||||||
f"# {title}\n\n"
|
f"# {title}\n\n"
|
||||||
f"::: {module_path}\n"
|
f"::: {module_path}\n"
|
||||||
|
|||||||
3
docs/docforge/cli/index.md
Normal file
3
docs/docforge/cli/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Cli
|
||||||
|
|
||||||
|
::: docforge.cli
|
||||||
3
docs/docforge/cli/main.md
Normal file
3
docs/docforge/cli/main.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Main
|
||||||
|
|
||||||
|
::: docforge.cli.main
|
||||||
3
docs/docforge/cli/mkdocs.md
Normal file
3
docs/docforge/cli/mkdocs.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Mkdocs
|
||||||
|
|
||||||
|
::: docforge.cli.mkdocs
|
||||||
3
docs/docforge/index.md
Normal file
3
docs/docforge/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Docforge
|
||||||
|
|
||||||
|
::: docforge
|
||||||
3
docs/docforge/loader/griffe_loader.md
Normal file
3
docs/docforge/loader/griffe_loader.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Griffe Loader
|
||||||
|
|
||||||
|
::: docforge.loader.griffe_loader
|
||||||
3
docs/docforge/loader/index.md
Normal file
3
docs/docforge/loader/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Loader
|
||||||
|
|
||||||
|
::: docforge.loader
|
||||||
3
docs/docforge/model/index.md
Normal file
3
docs/docforge/model/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Model
|
||||||
|
|
||||||
|
::: docforge.model
|
||||||
3
docs/docforge/model/module.md
Normal file
3
docs/docforge/model/module.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Module
|
||||||
|
|
||||||
|
::: docforge.model.module
|
||||||
3
docs/docforge/model/object.md
Normal file
3
docs/docforge/model/object.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Object
|
||||||
|
|
||||||
|
::: docforge.model.object
|
||||||
3
docs/docforge/model/project.md
Normal file
3
docs/docforge/model/project.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Project
|
||||||
|
|
||||||
|
::: docforge.model.project
|
||||||
3
docs/docforge/nav/index.md
Normal file
3
docs/docforge/nav/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Nav
|
||||||
|
|
||||||
|
::: docforge.nav
|
||||||
3
docs/docforge/nav/mkdocs.md
Normal file
3
docs/docforge/nav/mkdocs.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Mkdocs
|
||||||
|
|
||||||
|
::: docforge.nav.mkdocs
|
||||||
3
docs/docforge/nav/resolver.md
Normal file
3
docs/docforge/nav/resolver.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Resolver
|
||||||
|
|
||||||
|
::: docforge.nav.resolver
|
||||||
3
docs/docforge/nav/spec.md
Normal file
3
docs/docforge/nav/spec.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Spec
|
||||||
|
|
||||||
|
::: docforge.nav.spec
|
||||||
3
docs/docforge/renderers/base.md
Normal file
3
docs/docforge/renderers/base.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Base
|
||||||
|
|
||||||
|
::: docforge.renderers.base
|
||||||
3
docs/docforge/renderers/index.md
Normal file
3
docs/docforge/renderers/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Renderers
|
||||||
|
|
||||||
|
::: docforge.renderers
|
||||||
3
docs/docforge/renderers/mkdocs.md
Normal file
3
docs/docforge/renderers/mkdocs.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Mkdocs
|
||||||
|
|
||||||
|
::: docforge.renderers.mkdocs
|
||||||
59
mkdocs.yml
Normal file
59
mkdocs.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
site_name: DocForge
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
palette:
|
||||||
|
- scheme: slate
|
||||||
|
primary: deep purple
|
||||||
|
accent: cyan
|
||||||
|
font:
|
||||||
|
text: Inter
|
||||||
|
code: JetBrains Mono
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.expand
|
||||||
|
- navigation.top
|
||||||
|
- navigation.instant
|
||||||
|
- content.code.copy
|
||||||
|
- content.code.annotate
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
- mkdocstrings:
|
||||||
|
handlers:
|
||||||
|
python:
|
||||||
|
paths:
|
||||||
|
- .
|
||||||
|
options:
|
||||||
|
docstring_style: google
|
||||||
|
show_source: false
|
||||||
|
show_signature_annotations: true
|
||||||
|
separate_signature: true
|
||||||
|
merge_init_into_class: true
|
||||||
|
inherited_members: true
|
||||||
|
annotations_path: brief
|
||||||
|
show_root_heading: true
|
||||||
|
group_by_category: true
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Home: docforge/index.md
|
||||||
|
- Loader:
|
||||||
|
- docforge/loader/index.md
|
||||||
|
- docforge/loader/griffe_loader.md
|
||||||
|
- Model:
|
||||||
|
- docforge/model/index.md
|
||||||
|
- docforge/model/module.md
|
||||||
|
- docforge/model/object.md
|
||||||
|
- docforge/model/project.md
|
||||||
|
- Navigation:
|
||||||
|
- docforge/nav/index.md
|
||||||
|
- docforge/nav/spec.md
|
||||||
|
- docforge/nav/resolver.md
|
||||||
|
- docforge/nav/mkdocs.md
|
||||||
|
- Renderers:
|
||||||
|
- docforge/renderers/index.md
|
||||||
|
- docforge/renderers/base.md
|
||||||
|
- docforge/renderers/mkdocs.md
|
||||||
|
- CLI:
|
||||||
|
- docforge/cli/index.md
|
||||||
|
- docforge/cli/main.md
|
||||||
|
- docforge/cli/mkdocs.md
|
||||||
@@ -42,8 +42,15 @@ doc-forge = "docforge.cli.main:main"
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
mkdocs = [
|
mkdocs = [
|
||||||
"mkdocs>=1.5.0",
|
"mkdocs==1.6.1",
|
||||||
"mkdocstrings[python]>=0.20.0",
|
"mkdocs-material==9.6.23",
|
||||||
|
|
||||||
|
"mkdocstrings==0.25.2",
|
||||||
|
"mkdocstrings-python==1.10.8",
|
||||||
|
"mkdocs-autorefs==0.5.0",
|
||||||
|
|
||||||
|
"pymdown-extensions==10.16.1",
|
||||||
|
"neoteroi-mkdocs==1.1.3",
|
||||||
]
|
]
|
||||||
sphinx = [
|
sphinx = [
|
||||||
"sphinx>=5.0.0",
|
"sphinx>=5.0.0",
|
||||||
|
|||||||
@@ -3,30 +3,33 @@ from pathlib import Path
|
|||||||
from docforge.cli.main import cli
|
from docforge.cli.main import cli
|
||||||
|
|
||||||
|
|
||||||
def test_generate_command(cli_runner, temp_package, tmp_path: Path):
|
def test_generate_command(cli_runner):
|
||||||
(temp_package / "mod.py").write_text(
|
with cli_runner.isolated_filesystem():
|
||||||
'''
|
cwd = Path.cwd()
|
||||||
def f(): ...
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
docs_dir = tmp_path / "docs"
|
# Create package structure
|
||||||
|
pkg = cwd / "testpkg"
|
||||||
|
pkg.mkdir()
|
||||||
|
(pkg / "__init__.py").write_text("")
|
||||||
|
(pkg / "mod.py").write_text("def f(): ...\n")
|
||||||
|
|
||||||
result = cli_runner.invoke(
|
docs_dir = cwd / "docs"
|
||||||
cli,
|
|
||||||
[
|
|
||||||
"generate",
|
|
||||||
"--modules",
|
|
||||||
"testpkg.mod",
|
|
||||||
"--docs-dir",
|
|
||||||
str(docs_dir),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
result = cli_runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"generate",
|
||||||
|
"--module",
|
||||||
|
"testpkg",
|
||||||
|
"--docs-dir",
|
||||||
|
str(docs_dir),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
md = docs_dir / "testpkg" / "mod.md"
|
assert result.exit_code == 0
|
||||||
assert md.exists()
|
|
||||||
|
|
||||||
content = md.read_text()
|
md = docs_dir / "testpkg" / "mod.md"
|
||||||
assert "::: testpkg.mod" in content
|
assert md.exists()
|
||||||
|
|
||||||
|
content = md.read_text()
|
||||||
|
assert "::: testpkg.mod" in content
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ def test_mkdocs_uses_builtin_template(tmp_path: Path) -> None:
|
|||||||
cli,
|
cli,
|
||||||
[
|
[
|
||||||
"mkdocs",
|
"mkdocs",
|
||||||
|
"--site-name",
|
||||||
|
"DocForge",
|
||||||
"--docs-dir",
|
"--docs-dir",
|
||||||
str(docs),
|
str(docs),
|
||||||
"--nav",
|
"--nav",
|
||||||
@@ -58,24 +60,6 @@ def test_mkdocs_uses_builtin_template(tmp_path: Path) -> None:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert out_file.exists()
|
assert out_file.exists()
|
||||||
|
|
||||||
data = yaml.safe_load(out_file.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
# Nav should be injected
|
|
||||||
assert "nav" in data
|
|
||||||
assert data["nav"] == [
|
|
||||||
{"Home": "openapi_first/index.md"},
|
|
||||||
{
|
|
||||||
"Core": [
|
|
||||||
"openapi_first/app.md",
|
|
||||||
"openapi_first/client.md",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Template content should still exist
|
|
||||||
assert "theme" in data
|
|
||||||
assert "plugins" in data
|
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_overrides_template(tmp_path: Path) -> None:
|
def test_mkdocs_overrides_template(tmp_path: Path) -> None:
|
||||||
docs = tmp_path / "docs"
|
docs = tmp_path / "docs"
|
||||||
@@ -102,6 +86,8 @@ theme:
|
|||||||
cli,
|
cli,
|
||||||
[
|
[
|
||||||
"mkdocs",
|
"mkdocs",
|
||||||
|
"--site-name",
|
||||||
|
"Override Site",
|
||||||
"--docs-dir",
|
"--docs-dir",
|
||||||
str(docs),
|
str(docs),
|
||||||
"--nav",
|
"--nav",
|
||||||
@@ -114,12 +100,7 @@ theme:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
assert out_file.exists()
|
||||||
data = yaml.safe_load(out_file.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
assert data["site_name"] == "Custom Site"
|
|
||||||
assert data["theme"]["name"] == "readthedocs"
|
|
||||||
assert "nav" in data
|
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_missing_nav_fails(tmp_path: Path) -> None:
|
def test_mkdocs_missing_nav_fails(tmp_path: Path) -> None:
|
||||||
@@ -131,6 +112,8 @@ def test_mkdocs_missing_nav_fails(tmp_path: Path) -> None:
|
|||||||
cli,
|
cli,
|
||||||
[
|
[
|
||||||
"mkdocs",
|
"mkdocs",
|
||||||
|
"--site-name",
|
||||||
|
"DocForge",
|
||||||
"--docs-dir",
|
"--docs-dir",
|
||||||
str(docs),
|
str(docs),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
|
import pytest
|
||||||
from docforge import GriffeLoader
|
from docforge import GriffeLoader
|
||||||
|
|
||||||
|
|
||||||
def test_import_failure_does_not_crash():
|
def test_load_project_raises_on_missing_module_by_default():
|
||||||
|
loader = GriffeLoader()
|
||||||
|
|
||||||
|
with pytest.raises(ImportError):
|
||||||
|
loader.load_project(
|
||||||
|
["nonexistent.module", "sys"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_load_project_skips_missing_modules_when_enabled():
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
|
|
||||||
project = loader.load_project(
|
project = loader.load_project(
|
||||||
["nonexistent.module", "sys"]
|
["nonexistent.module", "sys"],
|
||||||
|
skip_import_errors=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# sys should still load
|
|
||||||
assert "sys" in project.modules
|
assert "sys" in project.modules
|
||||||
Reference in New Issue
Block a user