Compare commits
2 Commits
main
...
d4b79cf95a
| Author | SHA1 | Date | |
|---|---|---|---|
| d4b79cf95a | |||
| 0129268cd3 |
22
.drone.yml
22
.drone.yml
@@ -32,6 +32,26 @@ steps:
|
|||||||
echo "🆕 New version detected: $PACKAGE_NAME==$VERSION"
|
echo "🆕 New version detected: $PACKAGE_NAME==$VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: quality-gate
|
||||||
|
image: python:3.13-slim
|
||||||
|
environment:
|
||||||
|
PIP_REPO_URL:
|
||||||
|
from_secret: PIP_REPO_URL
|
||||||
|
PIP_USERNAME:
|
||||||
|
from_secret: PIP_USERNAME
|
||||||
|
PIP_PASSWORD:
|
||||||
|
from_secret: PIP_PASSWORD
|
||||||
|
commands:
|
||||||
|
- pip install --upgrade pip build
|
||||||
|
- |
|
||||||
|
AUTH_URL="https://${PIP_USERNAME}:${PIP_PASSWORD}@$(echo "${PIP_REPO_URL#*://}" | sed 's:/*$::')/simple"
|
||||||
|
pip install --index-url "$AUTH_URL" --extra-index-url https://pypi.org/simple/ -U ".[dev]"
|
||||||
|
- echo "🛡️ Running quality gate..."
|
||||||
|
- python -m black --check .
|
||||||
|
- python -m ruff check .
|
||||||
|
- python -m mypy
|
||||||
|
- python -m pytest
|
||||||
|
|
||||||
- name: build-package
|
- name: build-package
|
||||||
image: python:3.13-slim
|
image: python:3.13-slim
|
||||||
commands:
|
commands:
|
||||||
@@ -126,4 +146,4 @@ steps:
|
|||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
- custom
|
- custom
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -87,3 +87,8 @@ Thumbs.db
|
|||||||
# =========================
|
# =========================
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Testing artifacts
|
||||||
|
# =========================
|
||||||
|
coverage.xml
|
||||||
|
|||||||
21
CHANGELOG.md
Normal file
21
CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `py.typed` marker for PEP 561 type information.
|
||||||
|
- `.drone.yml` CI with a quality-gate step (black, ruff, mypy, pytest).
|
||||||
|
- MIT `LICENSE`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Standardized `pyproject.toml` (canonical packaging, lint tool config, extras).
|
||||||
|
- Pinned the `mcp` extra to `mcp>=1.0.0,<2.0.0` to restore compatibility with
|
||||||
|
the current MCP server API used by doc-forge.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Stub fixes for typed API surfaces.
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Aetoskia Platform
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from .loaders import GriffeLoader, discover_module_paths
|
|
||||||
from .renderers import MkDocsRenderer, MCPRenderer
|
|
||||||
from .cli import main
|
|
||||||
from . import models
|
from . import models
|
||||||
|
from .cli import main
|
||||||
|
from .loaders import GriffeLoader, discover_module_paths
|
||||||
|
from .renderers import MCPRenderer, MkDocsRenderer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GriffeLoader",
|
"GriffeLoader",
|
||||||
|
|||||||
@@ -37,6 +37,4 @@ Example:
|
|||||||
|
|
||||||
from .main import main
|
from .main import main
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ["main"]
|
||||||
"main"
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from .main import main
|
from .main import main
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ["main"]
|
||||||
"main"
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ Command definitions for the doc-forge CLI.
|
|||||||
Provides the CLI structure using Click, including build, serve, and tree commands.
|
Provides the CLI structure using Click, including build, serve, and tree commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import click
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence, Optional
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from docforge.cli import mcp_utils, mkdocs_utils
|
||||||
from docforge.loaders import GriffeLoader
|
from docforge.loaders import GriffeLoader
|
||||||
from docforge.cli import mkdocs_utils
|
|
||||||
from docforge.cli import mcp_utils
|
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@@ -28,26 +28,52 @@ def cli() -> None:
|
|||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option("--mcp", is_flag=True, help="Build MCP resources")
|
@click.option("--mcp", is_flag=True, help="Build MCP resources")
|
||||||
@click.option("--mkdocs", is_flag=True, help="Build MkDocs site")
|
@click.option("--mkdocs", is_flag=True, help="Build MkDocs site")
|
||||||
@click.option("--module-is-source", is_flag=True, help="Module is source folder and to be treated as root folder")
|
@click.option(
|
||||||
|
"--module-is-source",
|
||||||
|
is_flag=True,
|
||||||
|
help="Module is source folder and to be treated as root folder",
|
||||||
|
)
|
||||||
@click.option("--module", help="Python module to document")
|
@click.option("--module", help="Python module to document")
|
||||||
@click.option("--project-name", help="Project name override")
|
@click.option("--project-name", help="Project name override")
|
||||||
@click.option("--site-name", help="MkDocs site name")
|
@click.option("--site-name", help="MkDocs site name")
|
||||||
@click.option("--docs-dir", type=click.Path(path_type=Path), default=Path("docs"), help="Directory for MD sources")
|
@click.option(
|
||||||
@click.option("--nav", "nav_file", type=click.Path(path_type=Path), default=Path("docforge.nav.yml"),
|
"--docs-dir",
|
||||||
help="Nav spec path")
|
type=click.Path(path_type=Path),
|
||||||
@click.option("--template", type=click.Path(path_type=Path), help="MkDocs template path")
|
default=Path("docs"),
|
||||||
@click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="Output config path")
|
help="Directory for MD sources",
|
||||||
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP output directory")
|
)
|
||||||
|
@click.option(
|
||||||
|
"--nav",
|
||||||
|
"nav_file",
|
||||||
|
type=click.Path(path_type=Path),
|
||||||
|
default=Path("docforge.nav.yml"),
|
||||||
|
help="Nav spec path",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--template", type=click.Path(path_type=Path), help="MkDocs template path"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--mkdocs-yml",
|
||||||
|
type=click.Path(path_type=Path),
|
||||||
|
default=Path("mkdocs.yml"),
|
||||||
|
help="Output config path",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--out-dir",
|
||||||
|
type=click.Path(path_type=Path),
|
||||||
|
default=Path("mcp_docs"),
|
||||||
|
help="MCP output directory",
|
||||||
|
)
|
||||||
def build(
|
def build(
|
||||||
mcp: bool,
|
mcp: bool,
|
||||||
mkdocs: bool,
|
mkdocs: bool,
|
||||||
module_is_source: bool,
|
module_is_source: bool,
|
||||||
module: Optional[str],
|
module: str | None,
|
||||||
project_name: Optional[str],
|
project_name: str | None,
|
||||||
site_name: Optional[str],
|
site_name: str | None,
|
||||||
docs_dir: Path,
|
docs_dir: Path,
|
||||||
nav_file: Path,
|
nav_file: Path,
|
||||||
template: Optional[Path],
|
template: Path | None,
|
||||||
mkdocs_yml: Path,
|
mkdocs_yml: Path,
|
||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -121,7 +147,9 @@ def build(
|
|||||||
)
|
)
|
||||||
|
|
||||||
click.echo(f"Generating MkDocs config {mkdocs_yml}...")
|
click.echo(f"Generating MkDocs config {mkdocs_yml}...")
|
||||||
mkdocs_utils.generate_config(docs_dir, nav_file, template, mkdocs_yml, site_name)
|
mkdocs_utils.generate_config(
|
||||||
|
docs_dir, nav_file, template, mkdocs_yml, site_name
|
||||||
|
)
|
||||||
|
|
||||||
click.echo("Running MkDocs build...")
|
click.echo("Running MkDocs build...")
|
||||||
mkdocs_utils.build(mkdocs_yml)
|
mkdocs_utils.build(mkdocs_yml)
|
||||||
@@ -140,12 +168,22 @@ def build(
|
|||||||
@click.option("--mcp", is_flag=True, help="Serve MCP documentation")
|
@click.option("--mcp", is_flag=True, help="Serve MCP documentation")
|
||||||
@click.option("--mkdocs", is_flag=True, help="Serve MkDocs site")
|
@click.option("--mkdocs", is_flag=True, help="Serve MkDocs site")
|
||||||
@click.option("--module", help="Python module to serve")
|
@click.option("--module", help="Python module to serve")
|
||||||
@click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="MkDocs config path")
|
@click.option(
|
||||||
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP root directory")
|
"--mkdocs-yml",
|
||||||
|
type=click.Path(path_type=Path),
|
||||||
|
default=Path("mkdocs.yml"),
|
||||||
|
help="MkDocs config path",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--out-dir",
|
||||||
|
type=click.Path(path_type=Path),
|
||||||
|
default=Path("mcp_docs"),
|
||||||
|
help="MCP root directory",
|
||||||
|
)
|
||||||
def serve(
|
def serve(
|
||||||
mcp: bool,
|
mcp: bool,
|
||||||
mkdocs: bool,
|
mkdocs: bool,
|
||||||
module: Optional[str],
|
module: str | None,
|
||||||
mkdocs_yml: Path,
|
mkdocs_yml: Path,
|
||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -202,7 +240,7 @@ def serve(
|
|||||||
)
|
)
|
||||||
def tree(
|
def tree(
|
||||||
module: str,
|
module: str,
|
||||||
project_name: Optional[str],
|
project_name: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Display the documentation object tree for a module.
|
Display the documentation object tree for a module.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from click.core import Group
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence, Optional, Any
|
from typing import Any
|
||||||
|
|
||||||
|
from click.core import Group
|
||||||
|
|
||||||
cli: Group
|
cli: Group
|
||||||
|
|
||||||
@@ -8,27 +9,24 @@ def build(
|
|||||||
mcp: bool,
|
mcp: bool,
|
||||||
mkdocs: bool,
|
mkdocs: bool,
|
||||||
module_is_source: bool,
|
module_is_source: bool,
|
||||||
module: Optional[str],
|
module: str | None,
|
||||||
project_name: Optional[str],
|
project_name: str | None,
|
||||||
site_name: Optional[str],
|
site_name: str | None,
|
||||||
docs_dir: Path,
|
docs_dir: Path,
|
||||||
nav_file: Path,
|
nav_file: Path,
|
||||||
template: Optional[Path],
|
template: Path | None,
|
||||||
mkdocs_yml: Path,
|
mkdocs_yml: Path,
|
||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def serve(
|
def serve(
|
||||||
mcp: bool,
|
mcp: bool,
|
||||||
mkdocs: bool,
|
mkdocs: bool,
|
||||||
module: Optional[str],
|
module: str | None,
|
||||||
mkdocs_yml: Path,
|
mkdocs_yml: Path,
|
||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def tree(
|
def tree(
|
||||||
module: str,
|
module: str,
|
||||||
project_name: Optional[str],
|
project_name: str | None,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def _print_object(obj: Any, indent: str) -> None: ...
|
def _print_object(obj: Any, indent: str) -> None: ...
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ def main() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ Utilities for working with MCP in the doc-forge CLI.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from docforge.loaders import GriffeLoader, discover_module_paths
|
from docforge.loaders import GriffeLoader, discover_module_paths
|
||||||
from docforge.renderers import MCPRenderer
|
from docforge.renderers import MCPRenderer
|
||||||
from docforge.servers import MCPServer
|
from docforge.servers import MCPServer
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def generate_resources(module: str, project_name: str | None, out_dir: Path) -> None: ...
|
def generate_resources(
|
||||||
|
module: str, project_name: str | None, out_dir: Path
|
||||||
|
) -> None: ...
|
||||||
def serve(module: str, mcp_root: Path) -> None: ...
|
def serve(module: str, mcp_root: Path) -> None: ...
|
||||||
|
|||||||
@@ -4,13 +4,15 @@
|
|||||||
Utilities for working with MkDocs in the doc-forge CLI.
|
Utilities for working with MkDocs in the doc-forge CLI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from docforge.loaders import GriffeLoader, discover_module_paths
|
from docforge.loaders import GriffeLoader, discover_module_paths
|
||||||
|
from docforge.nav import MkDocsNavEmitter, load_nav_spec, resolve_nav
|
||||||
from docforge.renderers import MkDocsRenderer
|
from docforge.renderers import MkDocsRenderer
|
||||||
from docforge.nav import load_nav_spec, resolve_nav, MkDocsNavEmitter
|
|
||||||
|
|
||||||
|
|
||||||
def generate_sources(
|
def generate_sources(
|
||||||
@@ -138,8 +140,8 @@ def build(mkdocs_yml: Path) -> None:
|
|||||||
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.config import load_config
|
|
||||||
from mkdocs.commands.build import build as mkdocs_build
|
from mkdocs.commands.build import build as mkdocs_build
|
||||||
|
from mkdocs.config import load_config
|
||||||
|
|
||||||
mkdocs_build(load_config(str(mkdocs_yml)))
|
mkdocs_build(load_config(str(mkdocs_yml)))
|
||||||
|
|
||||||
@@ -163,4 +165,5 @@ def serve(mkdocs_yml: Path) -> None:
|
|||||||
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
|
||||||
|
|
||||||
mkdocs_serve(config_file=str(mkdocs_yml))
|
mkdocs_serve(config_file=str(mkdocs_yml))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ def generate_sources(
|
|||||||
project_name: str | None = None,
|
project_name: str | None = None,
|
||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str) -> None: ...
|
def generate_config(
|
||||||
|
docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str
|
||||||
|
) -> None: ...
|
||||||
def build(mkdocs_yml: Path) -> None: ...
|
def build(mkdocs_yml: Path) -> None: ...
|
||||||
def serve(mkdocs_yml: Path) -> None: ...
|
def serve(mkdocs_yml: Path) -> None: ...
|
||||||
|
|||||||
@@ -10,17 +10,18 @@ into doc-forge documentation models.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from griffe import (
|
from griffe import (
|
||||||
GriffeLoader as _GriffeLoader,
|
|
||||||
ModulesCollection,
|
|
||||||
LinesCollection,
|
|
||||||
Object,
|
|
||||||
AliasResolutionError,
|
AliasResolutionError,
|
||||||
|
LinesCollection,
|
||||||
|
ModulesCollection,
|
||||||
|
Object,
|
||||||
|
)
|
||||||
|
from griffe import (
|
||||||
|
GriffeLoader as _GriffeLoader,
|
||||||
)
|
)
|
||||||
|
|
||||||
from docforge.models import Module, Project, DocObject
|
from docforge.models import DocObject, Module, Project
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ logger = logging.getLogger(__name__)
|
|||||||
def discover_module_paths(
|
def discover_module_paths(
|
||||||
module_name: str,
|
module_name: str,
|
||||||
project_root: Path | None = None,
|
project_root: Path | None = None,
|
||||||
) -> List[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Discover Python modules within a package directory.
|
Discover Python modules within a package directory.
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ def discover_module_paths(
|
|||||||
if not pkg_dir.exists():
|
if not pkg_dir.exists():
|
||||||
raise FileNotFoundError(f"Package not found: {pkg_dir}")
|
raise FileNotFoundError(f"Package not found: {pkg_dir}")
|
||||||
|
|
||||||
module_paths: List[str] = []
|
module_paths: list[str] = []
|
||||||
|
|
||||||
for path in pkg_dir.rglob("*.py"):
|
for path in pkg_dir.rglob("*.py"):
|
||||||
if path.name == "__init__.py":
|
if path.name == "__init__.py":
|
||||||
@@ -102,10 +103,10 @@ class GriffeLoader:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def load_project(
|
def load_project(
|
||||||
self,
|
self,
|
||||||
module_paths: List[str],
|
module_paths: list[str],
|
||||||
project_name: Optional[str] = None,
|
project_name: str | None = None,
|
||||||
skip_import_errors: bool = None,
|
skip_import_errors: bool = None,
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""
|
"""
|
||||||
Load multiple modules and assemble them into a Project model.
|
Load multiple modules and assemble them into a Project model.
|
||||||
@@ -250,7 +251,7 @@ class GriffeLoader:
|
|||||||
# Safe extractors
|
# Safe extractors
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
def _safe_docstring(self, obj: Object) -> Optional[str]:
|
def _safe_docstring(self, obj: Object) -> str | None:
|
||||||
"""
|
"""
|
||||||
Safely extract a docstring from a Griffe object.
|
Safely extract a docstring from a Griffe object.
|
||||||
|
|
||||||
@@ -267,7 +268,7 @@ class GriffeLoader:
|
|||||||
except AliasResolutionError:
|
except AliasResolutionError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _safe_signature(self, obj: Object) -> Optional[str]:
|
def _safe_signature(self, obj: Object) -> str | None:
|
||||||
"""
|
"""
|
||||||
Safely extract the signature of a Griffe object.
|
Safely extract the signature of a Griffe object.
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
from typing import List, Optional
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge.models import Module, Project
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
def discover_module_paths(
|
def discover_module_paths(
|
||||||
module_name: str,
|
module_name: str,
|
||||||
project_root: Path | None = None,
|
project_root: Path | None = None,
|
||||||
) -> List[str]:
|
) -> list[str]: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class GriffeLoader:
|
class GriffeLoader:
|
||||||
"""Griffe-based introspection loaders.
|
"""Griffe-based introspection loaders.
|
||||||
@@ -18,11 +14,10 @@ class GriffeLoader:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None: ...
|
def __init__(self) -> None: ...
|
||||||
|
|
||||||
def load_project(
|
def load_project(
|
||||||
self,
|
self,
|
||||||
module_paths: List[str],
|
module_paths: list[str],
|
||||||
project_name: Optional[str] = ...,
|
project_name: str | None = ...,
|
||||||
skip_import_errors: bool = ...,
|
skip_import_errors: bool = ...,
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""Load a documentation project from Python modules."""
|
"""Load a documentation project from Python modules."""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from .project import Project
|
|
||||||
from .module import Module
|
from .module import Module
|
||||||
from .object import DocObject
|
from .object import DocObject
|
||||||
|
from .project import Project
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Project",
|
"Project",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ model. A `Module` acts as a container for top-level documented objects
|
|||||||
introspection.
|
introspection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, Iterable, Optional
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from docforge.models.object import DocObject
|
from docforge.models.object import DocObject
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class Module:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: str,
|
||||||
docstring: Optional[str] = None,
|
docstring: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a Module instance.
|
Initialize a Module instance.
|
||||||
@@ -50,7 +50,7 @@ class Module:
|
|||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
from typing import Dict, Iterable, Optional
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from docforge.models.object import DocObject
|
from docforge.models.object import DocObject
|
||||||
|
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
"""Represents a documented Python module."""
|
"""Represents a documented Python module."""
|
||||||
|
|
||||||
path: str
|
path: str
|
||||||
docstring: Optional[str]
|
docstring: str | None
|
||||||
members: Dict[str, DocObject]
|
members: dict[str, DocObject]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: str,
|
||||||
docstring: Optional[str] = ...,
|
docstring: str | None = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def add_object(self, obj: DocObject) -> None: ...
|
def add_object(self, obj: DocObject) -> None: ...
|
||||||
|
|
||||||
def get_object(self, name: str) -> DocObject: ...
|
def get_object(self, name: str) -> DocObject: ...
|
||||||
|
|
||||||
def get_all_objects(self) -> Iterable[DocObject]: ...
|
def get_all_objects(self) -> Iterable[DocObject]: ...
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ entity such as a class, function, method, or attribute, and may contain nested
|
|||||||
members that form a hierarchical documentation structure.
|
members that form a hierarchical documentation structure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, Iterable, Optional
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
|
||||||
class DocObject:
|
class DocObject:
|
||||||
@@ -45,8 +45,8 @@ class DocObject:
|
|||||||
name: str,
|
name: str,
|
||||||
kind: str,
|
kind: str,
|
||||||
path: str,
|
path: str,
|
||||||
signature: Optional[str] = None,
|
signature: str | None = None,
|
||||||
docstring: Optional[str] = None,
|
docstring: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a DocObject instance.
|
Initialize a DocObject instance.
|
||||||
@@ -72,7 +72,7 @@ class DocObject:
|
|||||||
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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from typing import Dict, Iterable, Optional
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
|
||||||
class DocObject:
|
class DocObject:
|
||||||
"""Represents a documented Python object."""
|
"""Represents a documented Python object."""
|
||||||
@@ -7,21 +6,18 @@ class DocObject:
|
|||||||
name: str
|
name: str
|
||||||
kind: str
|
kind: str
|
||||||
path: str
|
path: str
|
||||||
signature: Optional[str]
|
signature: str | None
|
||||||
docstring: Optional[str]
|
docstring: str | None
|
||||||
members: Dict[str, "DocObject"]
|
members: dict[str, DocObject]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
kind: str,
|
kind: str,
|
||||||
path: str,
|
path: str,
|
||||||
signature: Optional[str] = ...,
|
signature: str | None = ...,
|
||||||
docstring: Optional[str] = ...,
|
docstring: str | None = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
def add_member(self, obj: DocObject) -> None: ...
|
||||||
def add_member(self, obj: "DocObject") -> None: ...
|
def get_member(self, name: str) -> DocObject: ...
|
||||||
|
def get_all_members(self) -> Iterable[DocObject]: ...
|
||||||
def get_member(self, name: str) -> "DocObject": ...
|
|
||||||
|
|
||||||
def get_all_members(self) -> Iterable["DocObject"]: ...
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ doc-forge to represent a documented codebase. A `Project` aggregates multiple
|
|||||||
modules and provides access to them through a unified interface.
|
modules and provides access to them through a unified interface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from docforge.models.module import Module
|
from docforge.models.module import Module
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class Project:
|
|||||||
Name used to identify the documentation project.
|
Name used to identify the documentation 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:
|
||||||
"""
|
"""
|
||||||
@@ -85,4 +85,4 @@ class Project:
|
|||||||
list[str]:
|
list[str]:
|
||||||
A list containing the dotted paths of all modules in the project.
|
A list containing the dotted paths of all modules in the project.
|
||||||
"""
|
"""
|
||||||
return list(self.modules.keys())
|
return list(self.modules.keys())
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
from typing import Dict, Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from docforge.models.module import Module
|
from docforge.models.module import Module
|
||||||
|
|
||||||
|
|
||||||
class Project:
|
class Project:
|
||||||
"""Represents a documentation project."""
|
"""Represents a documentation project."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
modules: Dict[str, Module]
|
modules: dict[str, Module]
|
||||||
|
|
||||||
def __init__(self, name: str) -> None: ...
|
def __init__(self, name: str) -> None: ...
|
||||||
|
|
||||||
def add_module(self, module: Module) -> None: ...
|
def add_module(self, module: Module) -> None: ...
|
||||||
|
|
||||||
def get_module(self, path: str) -> Module: ...
|
def get_module(self, path: str) -> Module: ...
|
||||||
|
|
||||||
def get_all_modules(self) -> Iterable[Module]: ...
|
def get_all_modules(self) -> Iterable[Module]: ...
|
||||||
|
|
||||||
def get_module_list(self) -> list[str]: ...
|
def get_module_list(self) -> list[str]: ...
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from .spec import NavSpec, load_nav_spec
|
|
||||||
from .resolver import ResolvedNav, resolve_nav
|
|
||||||
from .mkdocs import MkDocsNavEmitter
|
from .mkdocs import MkDocsNavEmitter
|
||||||
|
from .resolver import ResolvedNav, resolve_nav
|
||||||
|
from .spec import NavSpec, load_nav_spec
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"NavSpec",
|
"NavSpec",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ MkDocs ``nav`` configuration.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Any
|
from typing import Any
|
||||||
|
|
||||||
from docforge.nav.resolver import ResolvedNav
|
from docforge.nav.resolver import ResolvedNav
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class MkDocsNavEmitter:
|
|||||||
list structure expected by the MkDocs ``nav`` configuration field.
|
list structure expected by the MkDocs ``nav`` configuration field.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
|
def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Generate a navigation structure for ``mkdocs.yml``.
|
Generate a navigation structure for ``mkdocs.yml``.
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class MkDocsNavEmitter:
|
|||||||
Each dictionary maps a navigation label to a page or a list of
|
Each dictionary maps a navigation label to a page or a list of
|
||||||
pages.
|
pages.
|
||||||
"""
|
"""
|
||||||
result: List[Dict[str, Any]] = []
|
result: list[dict[str, Any]] = []
|
||||||
|
|
||||||
# Home entry (semantic path)
|
# Home entry (semantic path)
|
||||||
if nav.home:
|
if nav.home:
|
||||||
@@ -41,7 +41,7 @@ class MkDocsNavEmitter:
|
|||||||
|
|
||||||
# Group entries
|
# Group entries
|
||||||
for group, paths in nav.groups.items():
|
for group, paths in nav.groups.items():
|
||||||
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
|
||||||
rel_path = self._to_relative(p, nav._docs_root)
|
rel_path = self._to_relative(p, nav._docs_root)
|
||||||
@@ -75,7 +75,7 @@ class MkDocsNavEmitter:
|
|||||||
path_str = path.as_posix()
|
path_str = path.as_posix()
|
||||||
docs_root_str = docs_root.as_posix()
|
docs_root_str = docs_root.as_posix()
|
||||||
if path_str.startswith(docs_root_str + "/"):
|
if path_str.startswith(docs_root_str + "/"):
|
||||||
return path_str[len(docs_root_str) + 1:]
|
return path_str[len(docs_root_str) + 1 :]
|
||||||
|
|
||||||
# Fallback for other cases
|
# Fallback for other cases
|
||||||
return path.as_posix().split("/docs/", 1)[-1]
|
return path.as_posix().split("/docs/", 1)[-1]
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from typing import Dict, List, Any
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from docforge.nav.resolver import ResolvedNav
|
from docforge.nav.resolver import ResolvedNav
|
||||||
|
|
||||||
|
|
||||||
class MkDocsNavEmitter:
|
class MkDocsNavEmitter:
|
||||||
"""
|
"""
|
||||||
Converts a ResolvedNav into MkDocs-compatible `nav` data.
|
Converts a ResolvedNav into MkDocs-compatible `nav` data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
|
def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Emit a structure suitable for insertion into mkdocs.yml.
|
Emit a structure suitable for insertion into mkdocs.yml.
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ This module resolves a ``NavSpec`` against the filesystem by expanding glob
|
|||||||
patterns and validating that referenced documentation files exist.
|
patterns and validating that referenced documentation files exist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Iterable, List
|
|
||||||
|
|
||||||
import glob
|
import glob
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge.nav.spec import NavSpec
|
from docforge.nav.spec import NavSpec
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ class ResolvedNav:
|
|||||||
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:
|
||||||
"""
|
"""
|
||||||
@@ -61,8 +60,7 @@ class ResolvedNav:
|
|||||||
yield self._docs_root / self.home
|
yield self._docs_root / self.home
|
||||||
|
|
||||||
for paths in self.groups.values():
|
for paths in self.groups.values():
|
||||||
for p in paths:
|
yield from paths
|
||||||
yield p
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_nav(
|
def resolve_nav(
|
||||||
@@ -89,7 +87,7 @@ def resolve_nav(
|
|||||||
if not docs_root.exists():
|
if not docs_root.exists():
|
||||||
raise FileNotFoundError(docs_root)
|
raise FileNotFoundError(docs_root)
|
||||||
|
|
||||||
def resolve_pattern(pattern: str) -> List[Path]:
|
def resolve_pattern(pattern: str) -> list[Path]:
|
||||||
"""
|
"""
|
||||||
Resolve a glob pattern relative to the documentation root.
|
Resolve a glob pattern relative to the documentation root.
|
||||||
|
|
||||||
@@ -103,9 +101,7 @@ def resolve_nav(
|
|||||||
FileNotFoundError: If the pattern does not match any files.
|
FileNotFoundError: If the pattern does not match any files.
|
||||||
"""
|
"""
|
||||||
full = docs_root / pattern
|
full = docs_root / pattern
|
||||||
matches = sorted(
|
matches = sorted(Path(p) for p in glob.glob(str(full), recursive=True))
|
||||||
Path(p) for p in glob.glob(str(full), recursive=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not matches:
|
if not matches:
|
||||||
raise FileNotFoundError(pattern)
|
raise FileNotFoundError(pattern)
|
||||||
@@ -121,10 +117,10 @@ def resolve_nav(
|
|||||||
home = spec.home
|
home = spec.home
|
||||||
|
|
||||||
# Resolve navigation groups
|
# Resolve navigation groups
|
||||||
resolved_groups: Dict[str, List[Path]] = {}
|
resolved_groups: dict[str, list[Path]] = {}
|
||||||
|
|
||||||
for group, patterns in spec.groups.items():
|
for group, patterns in spec.groups.items():
|
||||||
files: List[Path] = []
|
files: list[Path] = []
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
files.extend(resolve_pattern(pattern))
|
files.extend(resolve_pattern(pattern))
|
||||||
resolved_groups[group] = files
|
resolved_groups[group] = files
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Iterable, Optional
|
|
||||||
|
|
||||||
from docforge.nav.spec import NavSpec
|
from docforge.nav.spec import NavSpec
|
||||||
|
|
||||||
|
|
||||||
class ResolvedNav:
|
class ResolvedNav:
|
||||||
"""
|
"""
|
||||||
Fully-resolved navigation tree.
|
Fully-resolved navigation tree.
|
||||||
@@ -13,17 +12,16 @@ class ResolvedNav:
|
|||||||
- Order is preserved
|
- Order is preserved
|
||||||
"""
|
"""
|
||||||
|
|
||||||
home: Optional[str]
|
home: str | None
|
||||||
groups: Dict[str, List[Path]]
|
groups: dict[str, list[Path]]
|
||||||
_docs_root: Optional[Path]
|
_docs_root: Path | None
|
||||||
|
|
||||||
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 = ...,
|
docs_root: Path | None = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def all_files(self) -> Iterable[Path]:
|
def all_files(self) -> Iterable[Path]:
|
||||||
"""
|
"""
|
||||||
Return all resolved documentation files in nav order.
|
Return all resolved documentation files in nav order.
|
||||||
@@ -33,7 +31,6 @@ class ResolvedNav:
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
def resolve_nav(
|
def resolve_nav(
|
||||||
spec: NavSpec,
|
spec: NavSpec,
|
||||||
docs_root: Path,
|
docs_root: Path,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ structure defined by the user in the doc-forge navigation specification
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@@ -28,8 +27,8 @@ class NavSpec:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
home: Optional[str],
|
home: str | None,
|
||||||
groups: Dict[str, List[str]],
|
groups: dict[str, list[str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a NavSpec instance.
|
Initialize a NavSpec instance.
|
||||||
@@ -85,7 +84,7 @@ 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]:
|
||||||
"""
|
"""
|
||||||
Return all path patterns referenced by the specification.
|
Return all path patterns referenced by the specification.
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ class NavSpec:
|
|||||||
A list containing the home document (if defined) and all
|
A list containing the home document (if defined) and all
|
||||||
group pattern entries.
|
group pattern entries.
|
||||||
"""
|
"""
|
||||||
patterns: List[str] = []
|
patterns: list[str] = []
|
||||||
|
|
||||||
if self.home:
|
if self.home:
|
||||||
patterns.append(self.home)
|
patterns.append(self.home)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class NavSpec:
|
class NavSpec:
|
||||||
"""
|
"""
|
||||||
@@ -10,18 +8,16 @@ class NavSpec:
|
|||||||
of filesystem structure or MkDocs specifics.
|
of filesystem structure or MkDocs specifics.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
home: Optional[str]
|
home: str | None
|
||||||
groups: Dict[str, List[str]]
|
groups: dict[str, list[str]]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
home: Optional[str],
|
home: str | None,
|
||||||
groups: Dict[str, List[str]],
|
groups: dict[str, list[str]],
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, path: Path) -> "NavSpec":
|
def load(cls, path: Path) -> NavSpec:
|
||||||
"""
|
"""
|
||||||
Load and validate a nav specification from YAML.
|
Load and validate a nav specification from YAML.
|
||||||
|
|
||||||
@@ -31,12 +27,11 @@ class NavSpec:
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def all_patterns(self) -> List[str]:
|
def all_patterns(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Return all path patterns referenced by the spec
|
Return all path patterns referenced by the spec
|
||||||
(including home and group entries).
|
(including home and group entries).
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
def load_nav_spec(path: Path) -> NavSpec: ...
|
def load_nav_spec(path: Path) -> NavSpec: ...
|
||||||
|
|||||||
0
docforge/py.typed
Normal file
0
docforge/py.typed
Normal file
@@ -1,5 +1,5 @@
|
|||||||
from .mkdocs_renderer import MkDocsRenderer
|
|
||||||
from .mcp_renderer import MCPRenderer
|
from .mcp_renderer import MCPRenderer
|
||||||
|
from .mkdocs_renderer import MkDocsRenderer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"MkDocsRenderer",
|
"MkDocsRenderer",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from typing import Protocol
|
|||||||
|
|
||||||
from docforge.models import Project
|
from docforge.models import Project
|
||||||
|
|
||||||
|
|
||||||
class RendererConfig:
|
class RendererConfig:
|
||||||
"""Renderer configuration container."""
|
"""Renderer configuration container."""
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ class RendererConfig:
|
|||||||
|
|
||||||
def __init__(self, out_dir: Path, project: Project) -> None: ...
|
def __init__(self, out_dir: Path, project: Project) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
class DocRenderer(Protocol):
|
class DocRenderer(Protocol):
|
||||||
"""Renderer interface."""
|
"""Renderer interface."""
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ resources compatible with the Model Context Protocol (MCP).
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from docforge.models import Project, Module, DocObject
|
from docforge.models import DocObject, Module, Project
|
||||||
|
|
||||||
|
|
||||||
class MCPRenderer:
|
class MCPRenderer:
|
||||||
@@ -42,15 +41,17 @@ class MCPRenderer:
|
|||||||
modules_dir = out_dir / "modules"
|
modules_dir = out_dir / "modules"
|
||||||
modules_dir.mkdir(parents=True, exist_ok=True)
|
modules_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
nav: List[Dict[str, str]] = []
|
nav: list[dict[str, str]] = []
|
||||||
|
|
||||||
for module in project.get_all_modules():
|
for module in project.get_all_modules():
|
||||||
self._write_module(module, modules_dir)
|
self._write_module(module, modules_dir)
|
||||||
|
|
||||||
nav.append({
|
nav.append(
|
||||||
"module": module.path,
|
{
|
||||||
"resource": f"doc://modules/{module.path}",
|
"module": module.path,
|
||||||
})
|
"resource": f"doc://modules/{module.path}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Write nav.json
|
# Write nav.json
|
||||||
(out_dir / "nav.json").write_text(
|
(out_dir / "nav.json").write_text(
|
||||||
@@ -91,7 +92,7 @@ class MCPRenderer:
|
|||||||
out.parent.mkdir(parents=True, exist_ok=True)
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
out.write_text(self._json(payload), encoding="utf-8")
|
out.write_text(self._json(payload), encoding="utf-8")
|
||||||
|
|
||||||
def _render_module(self, module: Module) -> Dict:
|
def _render_module(self, module: Module) -> dict:
|
||||||
"""
|
"""
|
||||||
Convert a Module model into MCP-compatible structured data.
|
Convert a Module model into MCP-compatible structured data.
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ class MCPRenderer:
|
|||||||
Dict:
|
Dict:
|
||||||
Dictionary representing the module and its documented objects.
|
Dictionary representing the module and its documented objects.
|
||||||
"""
|
"""
|
||||||
data: Dict = {
|
data: dict = {
|
||||||
"path": module.path,
|
"path": module.path,
|
||||||
"docstring": module.docstring,
|
"docstring": module.docstring,
|
||||||
"objects": {},
|
"objects": {},
|
||||||
@@ -114,7 +115,7 @@ class MCPRenderer:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _render_object(self, obj: DocObject) -> Dict:
|
def _render_object(self, obj: DocObject) -> dict:
|
||||||
"""
|
"""
|
||||||
Recursively convert a DocObject into structured MCP data.
|
Recursively convert a DocObject into structured MCP data.
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ class MCPRenderer:
|
|||||||
Dict:
|
Dict:
|
||||||
Dictionary describing the object and any nested members.
|
Dictionary describing the object and any nested members.
|
||||||
"""
|
"""
|
||||||
data: Dict = {
|
data: dict = {
|
||||||
"name": obj.name,
|
"name": obj.name,
|
||||||
"kind": obj.kind,
|
"kind": obj.kind,
|
||||||
"path": obj.path,
|
"path": obj.path,
|
||||||
@@ -137,14 +138,13 @@ class MCPRenderer:
|
|||||||
members = list(obj.get_all_members())
|
members = list(obj.get_all_members())
|
||||||
if members:
|
if members:
|
||||||
data["members"] = {
|
data["members"] = {
|
||||||
member.name: self._render_object(member)
|
member.name: self._render_object(member) for member in members
|
||||||
for member in members
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _json(data: Dict) -> str:
|
def _json(data: dict) -> str:
|
||||||
"""
|
"""
|
||||||
Serialize data to formatted JSON.
|
Serialize data to formatted JSON.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
from typing import Any
|
||||||
|
|
||||||
from docforge.models import Project, Module, DocObject
|
|
||||||
|
|
||||||
|
from docforge.models import DocObject, Module, Project
|
||||||
|
|
||||||
class MCPRenderer:
|
class MCPRenderer:
|
||||||
"""Renderer that emits MCP-native JSON resources from docforge models."""
|
"""Renderer that emits MCP-native JSON resources from docforge models."""
|
||||||
@@ -15,12 +14,12 @@ class MCPRenderer:
|
|||||||
def _write_module(self, module: Module, modules_dir: Path) -> None:
|
def _write_module(self, module: Module, modules_dir: Path) -> None:
|
||||||
"""Serialize a module into an MCP JSON resource."""
|
"""Serialize a module into an MCP JSON resource."""
|
||||||
|
|
||||||
def _render_module(self, module: Module) -> Dict:
|
def _render_module(self, module: Module) -> dict[str, Any]:
|
||||||
"""Render a Module into MCP-friendly structured data."""
|
"""Render a Module into MCP-friendly structured data."""
|
||||||
|
|
||||||
def _render_object(self, obj: DocObject) -> Dict:
|
def _render_object(self, obj: DocObject) -> dict[str, Any]:
|
||||||
"""Recursively render a DocObject into structured MCP data."""
|
"""Recursively render a DocObject into structured MCP data."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _json(data: Dict) -> str:
|
def _json(data: dict[str, Any]) -> str:
|
||||||
"""Serialize structured data to formatted JSON."""
|
"""Serialize structured data to formatted JSON."""
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ The renderer ensures a consistent documentation structure by:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from docforge.models import Project, Module
|
|
||||||
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
class MkDocsRenderer:
|
class MkDocsRenderer:
|
||||||
@@ -65,8 +66,7 @@ class MkDocsRenderer:
|
|||||||
|
|
||||||
# Detect packages (modules with children)
|
# Detect packages (modules with children)
|
||||||
packages = {
|
packages = {
|
||||||
p for p in paths
|
p for p in paths if any(other.startswith(p + ".") for other in paths)
|
||||||
if any(other.startswith(p + ".") for other in paths)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for module in modules:
|
for module in modules:
|
||||||
@@ -127,12 +127,12 @@ class MkDocsRenderer:
|
|||||||
str(root_module.docstring),
|
str(root_module.docstring),
|
||||||
)
|
)
|
||||||
|
|
||||||
content = (
|
content = f"# {project.name}\n\n" f"{doc.strip()}\n"
|
||||||
f"# {project.name}\n\n"
|
|
||||||
f"{doc.strip()}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not readme_path.exists() or readme_path.read_text(encoding="utf-8") != content:
|
if (
|
||||||
|
not readme_path.exists()
|
||||||
|
or readme_path.read_text(encoding="utf-8") != content
|
||||||
|
):
|
||||||
readme_path.write_text(
|
readme_path.write_text(
|
||||||
content,
|
content,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -231,10 +231,7 @@ class MkDocsRenderer:
|
|||||||
str:
|
str:
|
||||||
Markdown source containing a mkdocstrings directive.
|
Markdown source containing a mkdocstrings directive.
|
||||||
"""
|
"""
|
||||||
return (
|
return f"# {title}\n\n" f"::: {module_path}\n"
|
||||||
f"# {title}\n\n"
|
|
||||||
f"::: {module_path}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _ensure_root_index(
|
def _ensure_root_index(
|
||||||
self,
|
self,
|
||||||
@@ -255,8 +252,7 @@ class MkDocsRenderer:
|
|||||||
|
|
||||||
if not root_index.exists():
|
if not root_index.exists():
|
||||||
root_index.write_text(
|
root_index.write_text(
|
||||||
f"# {project.name}\n\n"
|
f"# {project.name}\n\n" "## Modules\n\n",
|
||||||
"## Modules\n\n",
|
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from docforge.models import Project, Module
|
|
||||||
|
|
||||||
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
class MkDocsRenderer:
|
class MkDocsRenderer:
|
||||||
name: str
|
name: str
|
||||||
@@ -11,14 +11,12 @@ class MkDocsRenderer:
|
|||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def generate_readme(
|
def generate_readme(
|
||||||
self,
|
self,
|
||||||
project: Project,
|
project: Project,
|
||||||
docs_dir: Path,
|
docs_dir: Path,
|
||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
|
|
||||||
def _write_module(
|
def _write_module(
|
||||||
self,
|
self,
|
||||||
module: Module,
|
module: Module,
|
||||||
@@ -26,9 +24,8 @@ class MkDocsRenderer:
|
|||||||
out_dir: Path,
|
out_dir: Path,
|
||||||
module_is_source: bool | None = None,
|
module_is_source: bool | None = None,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def _render_markdown(self, title: str, module_path: str) -> str: ...
|
def _render_markdown(self, title: str, module_path: str) -> str: ...
|
||||||
|
def _ensure_root_index(self, project: Project, out_dir: Path) -> None: ...
|
||||||
def _ensure_root_index(self, project, out_dir) -> None: ...
|
def _ensure_parent_index(
|
||||||
|
self, parts: list[str], out_dir: Path, link_target: str, title: str
|
||||||
def _ensure_parent_index(self, parts, out_dir, link_target, title) -> None: ...
|
) -> None: ...
|
||||||
|
|||||||
@@ -96,9 +96,7 @@ class MCPServer:
|
|||||||
|
|
||||||
@self.app.resource("docs://modules/{module}")
|
@self.app.resource("docs://modules/{module}")
|
||||||
def module(module: str):
|
def module(module: str):
|
||||||
return self._read_json(
|
return self._read_json(self.mcp_root / "modules" / f"{module}.json")
|
||||||
self.mcp_root / "modules" / f"{module}.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# MCP tools
|
# MCP tools
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal, Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
|
||||||
class MCPServer:
|
class MCPServer:
|
||||||
"""MCP server for serving documentation."""
|
"""MCP server for serving documentation."""
|
||||||
|
|
||||||
@@ -11,12 +10,8 @@ class MCPServer:
|
|||||||
app: FastMCP
|
app: FastMCP
|
||||||
|
|
||||||
def __init__(self, mcp_root: Path, name: str) -> None: ...
|
def __init__(self, mcp_root: Path, name: str) -> None: ...
|
||||||
|
|
||||||
def _read_json(self, path: Path) -> Any: ...
|
def _read_json(self, path: Path) -> Any: ...
|
||||||
|
|
||||||
def _register_resources(self) -> None: ...
|
def _register_resources(self) -> None: ...
|
||||||
|
|
||||||
def _register_tools(self) -> None: ...
|
def _register_tools(self) -> None: ...
|
||||||
|
|
||||||
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = ...) -> None:
|
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = ...) -> None:
|
||||||
"""Start the MCP server."""
|
"""Start the MCP server."""
|
||||||
|
|||||||
126
pyproject.toml
126
pyproject.toml
@@ -17,6 +17,19 @@ authors = [
|
|||||||
maintainers = [
|
maintainers = [
|
||||||
{ name = "Aetos Skia", email = "dev@aetoskia.com" }
|
{ name = "Aetos Skia", email = "dev@aetoskia.com" }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
keywords = [
|
||||||
|
"documentation",
|
||||||
|
"docs",
|
||||||
|
"compiler",
|
||||||
|
"mkdocs",
|
||||||
|
"mcp",
|
||||||
|
"griffe",
|
||||||
|
"docstrings",
|
||||||
|
"sphinx",
|
||||||
|
]
|
||||||
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
@@ -25,9 +38,12 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
"Framework :: FastAPI",
|
"Framework :: FastAPI",
|
||||||
"Topic :: Software Development :: Libraries",
|
"Topic :: Software Development :: Libraries",
|
||||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||||
|
"Topic :: Documentation",
|
||||||
|
"Typing :: Typed",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -57,14 +73,25 @@ sphinx = [
|
|||||||
"sphinx-autodoc-typehints>=1.19.0",
|
"sphinx-autodoc-typehints>=1.19.0",
|
||||||
]
|
]
|
||||||
mcp = [
|
mcp = [
|
||||||
"mcp>=1.0.0",
|
"mcp>=1.0.0,<2.0.0",
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=7.0.0",
|
"pytest>=8.0.0",
|
||||||
"pytest-cov>=4.0.0",
|
"pytest-asyncio>=0.21.0",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
"black>=23.0.0",
|
"black>=23.0.0",
|
||||||
"ruff>=0.1.0",
|
"ruff>=0.3.0",
|
||||||
"mypy>=1.0.0",
|
"mypy>=1.8.0",
|
||||||
|
"build>=1.0.0",
|
||||||
|
"twine>=4.0.0",
|
||||||
|
"pre-commit>=3.4.0",
|
||||||
|
]
|
||||||
|
docs = [
|
||||||
|
"doc-forge[mkdocs]",
|
||||||
|
]
|
||||||
|
|
||||||
|
all = [
|
||||||
|
"doc-forge[dev,docs,mcp]",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -80,15 +107,100 @@ Versions = "https://git.aetoskia.com/aetos/doc-forge/tags"
|
|||||||
packages = { find = { include = ["docforge*"] } }
|
packages = { find = { include = ["docforge*"] } }
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
docforge = ["templates/*.yml"]
|
docforge = ["py.typed", "templates/*.yml"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"--strict-markers",
|
||||||
|
"--strict-config",
|
||||||
|
"--cov=docforge",
|
||||||
|
"--cov-report=term-missing",
|
||||||
|
"--cov-report=html",
|
||||||
|
"--cov-report=xml",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
target-version = ["py310", "py311", "py312", "py313"]
|
||||||
|
include = '\.pyi?$'
|
||||||
|
extend-exclude = '''
|
||||||
|
/(
|
||||||
|
\.eggs
|
||||||
|
| \.git
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 88
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"F",
|
||||||
|
"I",
|
||||||
|
"B",
|
||||||
|
"C4",
|
||||||
|
"UP",
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501",
|
||||||
|
"B008",
|
||||||
|
"C901",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"__init__.py" = ["F401", "I001"]
|
||||||
|
"tests/*" = ["B008"]
|
||||||
|
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
strict = true
|
strict = true
|
||||||
|
exclude = [
|
||||||
|
"tests/",
|
||||||
|
]
|
||||||
|
files = ["docforge"]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"griffe.*",
|
||||||
|
"mcp.*",
|
||||||
|
"click",
|
||||||
|
"pydantic",
|
||||||
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["docforge"]
|
||||||
|
omit = [
|
||||||
|
"*/tests/*",
|
||||||
|
"*/test_*.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"raise AssertionError",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
"@abstractmethod",
|
||||||
|
]
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge.cli.main import cli
|
from docforge.cli.main import cli
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_build(cli_runner):
|
def test_mcp_build(cli_runner):
|
||||||
with cli_runner.isolated_filesystem():
|
with cli_runner.isolated_filesystem():
|
||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge.cli.main import cli
|
from docforge.cli.main import cli
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_build_full_flow(
|
def test_mkdocs_build_full_flow(
|
||||||
cli_runner,
|
cli_runner,
|
||||||
mock_mkdocs_build,
|
mock_mkdocs_build,
|
||||||
@@ -17,11 +19,11 @@ def test_mkdocs_build_full_flow(
|
|||||||
|
|
||||||
nav_file = cwd / "docforge.nav.yml"
|
nav_file = cwd / "docforge.nav.yml"
|
||||||
nav_file.write_text("home: testpkg/index.md\ngroups: {}\n")
|
nav_file.write_text("home: testpkg/index.md\ngroups: {}\n")
|
||||||
|
|
||||||
# We need to create a dummy testpkg/index.md for nav resolution if it's there
|
# We need to create a dummy testpkg/index.md for nav resolution if it's there
|
||||||
# But generate_sources will create it.
|
# But generate_sources will create it.
|
||||||
# Wait, the current logic runs generate_sources first, THEN generate_config.
|
# Wait, the current logic runs generate_sources first, THEN generate_config.
|
||||||
|
|
||||||
result = cli_runner.invoke(
|
result = cli_runner.invoke(
|
||||||
cli,
|
cli,
|
||||||
[
|
[
|
||||||
@@ -43,11 +45,13 @@ def test_mkdocs_build_full_flow(
|
|||||||
assert (cwd / "mkdocs.yml").exists()
|
assert (cwd / "mkdocs.yml").exists()
|
||||||
assert (cwd / "docs" / "testpkg" / "mod.md").exists()
|
assert (cwd / "docs" / "testpkg" / "mod.md").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_build_missing_module_fails(cli_runner):
|
def test_mkdocs_build_missing_module_fails(cli_runner):
|
||||||
result = cli_runner.invoke(cli, ["build", "--mkdocs", "--site-name", "Test"])
|
result = cli_runner.invoke(cli, ["build", "--mkdocs", "--site-name", "Test"])
|
||||||
assert result.exit_code != 0
|
assert result.exit_code != 0
|
||||||
assert "--module is required" in result.output
|
assert "--module is required" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_build_without_site_name_uses_module_as_default_full_flow(
|
def test_mkdocs_build_without_site_name_uses_module_as_default_full_flow(
|
||||||
cli_runner,
|
cli_runner,
|
||||||
mock_mkdocs_build,
|
mock_mkdocs_build,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from docforge.cli.main import cli
|
from docforge.cli.main import cli
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_serve(
|
def test_mcp_serve(
|
||||||
cli_runner,
|
cli_runner,
|
||||||
fake_mcp_docs,
|
fake_mcp_docs,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from docforge.cli.main import cli
|
from docforge.cli.main import cli
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_serve(
|
def test_mkdocs_serve(
|
||||||
cli_runner,
|
cli_runner,
|
||||||
fake_mkdocs_yml,
|
fake_mkdocs_yml,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
import pytest
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ plugins:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_mkdocs_load_config(monkeypatch):
|
def mock_mkdocs_load_config(monkeypatch):
|
||||||
"""Mock mkdocs.config.load_config."""
|
"""Mock mkdocs.config.load_config."""
|
||||||
|
|
||||||
def fake_load_config(path):
|
def fake_load_config(path):
|
||||||
return object() # dummy config object
|
return object() # dummy config object
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ def mock_mkdocs_serve(monkeypatch):
|
|||||||
)
|
)
|
||||||
return lambda: called["value"]
|
return lambda: called["value"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fake_mcp_docs(tmp_path: Path) -> Path:
|
def fake_mcp_docs(tmp_path: Path) -> Path:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ from docforge import GriffeLoader
|
|||||||
|
|
||||||
|
|
||||||
def test_alias_does_not_crash(temp_package):
|
def test_alias_does_not_crash(temp_package):
|
||||||
(temp_package / "alias.py").write_text(
|
(temp_package / "alias.py").write_text("""from typing import List
|
||||||
'''from typing import List
|
|
||||||
Alias = List[int]
|
Alias = List[int]
|
||||||
'''
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project(["testpkg.alias"])
|
project = loader.load_project(["testpkg.alias"])
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ from docforge import GriffeLoader
|
|||||||
|
|
||||||
|
|
||||||
def test_class_and_methods(temp_package):
|
def test_class_and_methods(temp_package):
|
||||||
(temp_package / "cls.py").write_text(
|
(temp_package / "cls.py").write_text('''class MyClass:
|
||||||
'''class MyClass:
|
|
||||||
"""Class doc."""
|
"""Class doc."""
|
||||||
|
|
||||||
def method(self, x: int) -> int:
|
def method(self, x: int) -> int:
|
||||||
"""Method doc."""
|
"""Method doc."""
|
||||||
return x
|
return x
|
||||||
'''
|
''')
|
||||||
)
|
|
||||||
|
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project(["testpkg.cls"])
|
project = loader.load_project(["testpkg.cls"])
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ from docforge import GriffeLoader
|
|||||||
|
|
||||||
|
|
||||||
def test_function_signature(temp_package):
|
def test_function_signature(temp_package):
|
||||||
(temp_package / "fn.py").write_text(
|
(temp_package / "fn.py").write_text('''def add(a: int, b: int = 1) -> int:
|
||||||
'''def add(a: int, b: int = 1) -> int:
|
|
||||||
"""Adds numbers."""
|
"""Adds numbers."""
|
||||||
return a + b
|
return a + b
|
||||||
'''
|
''')
|
||||||
)
|
|
||||||
|
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project(["testpkg.fn"])
|
project = loader.load_project(["testpkg.fn"])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from docforge import GriffeLoader
|
from docforge import GriffeLoader
|
||||||
|
|
||||||
|
|
||||||
@@ -6,9 +7,8 @@ def test_load_project_raises_on_missing_module_by_default():
|
|||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
|
|
||||||
with pytest.raises(ImportError):
|
with pytest.raises(ImportError):
|
||||||
loader.load_project(
|
loader.load_project(["nonexistent.module", "sys"])
|
||||||
["nonexistent.module", "sys"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_load_project_skips_missing_modules_when_enabled():
|
def test_load_project_skips_missing_modules_when_enabled():
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
@@ -18,4 +18,4 @@ def test_load_project_skips_missing_modules_when_enabled():
|
|||||||
skip_import_errors=True,
|
skip_import_errors=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "sys" in project.modules
|
assert "sys" in project.modules
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ from docforge import GriffeLoader
|
|||||||
|
|
||||||
|
|
||||||
def test_missing_docstrings(temp_package):
|
def test_missing_docstrings(temp_package):
|
||||||
(temp_package / "nodoc.py").write_text(
|
(temp_package / "nodoc.py").write_text("""def f(): pass""")
|
||||||
'''def f(): pass'''
|
|
||||||
)
|
|
||||||
|
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project(["testpkg.nodoc"])
|
project = loader.load_project(["testpkg.nodoc"])
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ from docforge import GriffeLoader
|
|||||||
|
|
||||||
|
|
||||||
def test_private_members_excluded(temp_package):
|
def test_private_members_excluded(temp_package):
|
||||||
(temp_package / "priv.py").write_text(
|
(temp_package / "priv.py").write_text("""def _hidden(): pass
|
||||||
'''def _hidden(): pass
|
|
||||||
def visible(): pass
|
def visible(): pass
|
||||||
'''
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project(["testpkg.priv"])
|
project = loader.load_project(["testpkg.priv"])
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ from docforge import GriffeLoader
|
|||||||
|
|
||||||
|
|
||||||
def test_load_single_module(temp_package):
|
def test_load_single_module(temp_package):
|
||||||
(temp_package / "mod.py").write_text(
|
(temp_package / "mod.py").write_text('''"""Module docstring."""\n
|
||||||
'''"""Module docstring."""\n
|
|
||||||
def foo():
|
def foo():
|
||||||
"""Foo docstring."""
|
"""Foo docstring."""
|
||||||
pass
|
pass
|
||||||
'''
|
''')
|
||||||
)
|
|
||||||
|
|
||||||
loader = GriffeLoader()
|
loader = GriffeLoader()
|
||||||
project = loader.load_project(["testpkg.mod"])
|
project = loader.load_project(["testpkg.mod"])
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge.nav import ResolvedNav
|
from docforge.nav import MkDocsNavEmitter, ResolvedNav
|
||||||
from docforge.nav import MkDocsNavEmitter
|
|
||||||
|
|
||||||
|
|
||||||
def test_emit_mkdocs_nav():
|
def test_emit_mkdocs_nav():
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from docforge.nav import NavSpec
|
from docforge.nav import NavSpec, resolve_nav
|
||||||
from docforge.nav import resolve_nav
|
|
||||||
|
|
||||||
|
|
||||||
def _write_docs(root: Path, paths: list[str]) -> None:
|
def _write_docs(root: Path, paths: list[str]) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge import MCPRenderer
|
from docforge import MCPRenderer
|
||||||
from docforge.models import Project, Module
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_file_content(tmp_path: Path):
|
def test_mcp_file_content(tmp_path: Path):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge import MCPRenderer
|
from docforge import MCPRenderer
|
||||||
from docforge.models import Project, Module
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_idempotent(tmp_path: Path):
|
def test_mcp_idempotent(tmp_path: Path):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge.loaders import GriffeLoader, discover_module_paths
|
|
||||||
from docforge import MCPRenderer
|
from docforge import MCPRenderer
|
||||||
|
from docforge.loaders import GriffeLoader, discover_module_paths
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_emits_all_modules(tmp_path: Path) -> None:
|
def test_mcp_emits_all_modules(tmp_path: Path) -> None:
|
||||||
@@ -22,10 +22,7 @@ def test_mcp_emits_all_modules(tmp_path: Path) -> None:
|
|||||||
for p in (tmp_path / "modules").rglob("*.json")
|
for p in (tmp_path / "modules").rglob("*.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
expected = {
|
expected = {f"modules/{m.path}.json" for m in project.get_all_modules()}
|
||||||
f"modules/{m.path}.json"
|
|
||||||
for m in project.get_all_modules()
|
|
||||||
}
|
|
||||||
|
|
||||||
missing = expected - emitted
|
missing = expected - emitted
|
||||||
assert not missing, f"Missing MCP module JSON files: {missing}"
|
assert not missing, f"Missing MCP module JSON files: {missing}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge import MCPRenderer
|
from docforge import MCPRenderer
|
||||||
from docforge.models import Project, Module
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_directory_structure(tmp_path: Path):
|
def test_mcp_directory_structure(tmp_path: Path):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge import MkDocsRenderer
|
from docforge import MkDocsRenderer
|
||||||
from docforge.models import Project, Module
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_file_content(tmp_path: Path):
|
def test_mkdocs_file_content(tmp_path: Path):
|
||||||
@@ -47,4 +47,4 @@ def test_generate_readme_source_root(tmp_path: Path):
|
|||||||
content = readme.read_text()
|
content = readme.read_text()
|
||||||
|
|
||||||
assert "# testpkg" in content
|
assert "# testpkg" in content
|
||||||
assert "Test package documentation." in content
|
assert "Test package documentation." in content
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge import MkDocsRenderer
|
from docforge import MkDocsRenderer
|
||||||
from docforge.models import Project, Module
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_idempotent(tmp_path: Path):
|
def test_mkdocs_idempotent(tmp_path: Path):
|
||||||
@@ -50,4 +50,4 @@ def test_generate_readme_idempotent(tmp_path: Path):
|
|||||||
|
|
||||||
second = readme.read_text()
|
second = readme.read_text()
|
||||||
|
|
||||||
assert first == second
|
assert first == second
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge.loaders import GriffeLoader, discover_module_paths
|
|
||||||
from docforge import MkDocsRenderer
|
from docforge import MkDocsRenderer
|
||||||
|
from docforge.loaders import GriffeLoader, discover_module_paths
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_emits_all_modules(tmp_path: Path) -> None:
|
def test_mkdocs_emits_all_modules(tmp_path: Path) -> None:
|
||||||
@@ -23,10 +23,7 @@ def test_mkdocs_emits_all_modules(tmp_path: Path) -> None:
|
|||||||
module_is_source=True,
|
module_is_source=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
emitted = {
|
emitted = {p.relative_to(tmp_path).as_posix() for p in tmp_path.rglob("*.md")}
|
||||||
p.relative_to(tmp_path).as_posix()
|
|
||||||
for p in tmp_path.rglob("*.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
module_paths = [m.path for m in project.get_all_modules()]
|
module_paths = [m.path for m in project.get_all_modules()]
|
||||||
|
|
||||||
@@ -34,8 +31,7 @@ def test_mkdocs_emits_all_modules(tmp_path: Path) -> None:
|
|||||||
for path in module_paths:
|
for path in module_paths:
|
||||||
parts = path.split(".")
|
parts = path.split(".")
|
||||||
is_package = any(
|
is_package = any(
|
||||||
other != path and other.startswith(path + ".")
|
other != path and other.startswith(path + ".") for other in module_paths
|
||||||
for other in module_paths
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_package:
|
if is_package:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docforge import MkDocsRenderer
|
from docforge import MkDocsRenderer
|
||||||
from docforge.models import Project, Module
|
from docforge.models import Module, Project
|
||||||
|
|
||||||
|
|
||||||
def test_mkdocs_directory_structure(tmp_path: Path):
|
def test_mkdocs_directory_structure(tmp_path: Path):
|
||||||
|
|||||||
Reference in New Issue
Block a user