From 0129268cd3c7be39a46157588cbb59fb5b2d56f0 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Thu, 10 Sep 2026 18:49:04 +0530 Subject: [PATCH] standardize packaging, tooling, docs, CI, and licensing --- .drone.yml | 22 ++- CHANGELOG.md | 21 +++ LICENSE | 21 +++ docforge/__init__.pyi | 6 +- docforge/cli/__init__.py | 4 +- docforge/cli/__init__.pyi | 4 +- docforge/cli/commands.py | 78 ++++++++--- docforge/cli/commands.pyi | 20 ++- docforge/cli/main.py | 2 +- docforge/cli/mcp_utils.py | 2 + docforge/cli/mcp_utils.pyi | 4 +- docforge/cli/mkdocs_utils.py | 9 +- docforge/cli/mkdocs_utils.pyi | 4 +- docforge/loaders/griffe_loader.py | 29 ++-- docforge/loaders/griffe_loader.pyi | 11 +- docforge/models/__init__.pyi | 2 +- docforge/models/module.py | 6 +- docforge/models/module.pyi | 12 +- docforge/models/object.py | 8 +- docforge/models/object.pyi | 22 ++- docforge/models/project.py | 6 +- docforge/models/project.pyi | 9 +- docforge/nav/__init__.pyi | 4 +- docforge/nav/mkdocs.py | 10 +- docforge/nav/mkdocs.pyi | 5 +- docforge/nav/resolver.py | 20 ++- docforge/nav/resolver.pyi | 13 +- docforge/nav/spec.py | 9 +- docforge/nav/spec.pyi | 19 +-- docforge/py.typed | 0 docforge/renderers/__init__.pyi | 2 +- docforge/renderers/base.pyi | 2 - docforge/renderers/mcp_renderer.py | 28 ++-- docforge/renderers/mcp_renderer.pyi | 11 +- docforge/renderers/mkdocs_renderer.py | 24 ++-- docforge/renderers/mkdocs_renderer.pyi | 15 +-- docforge/servers/mcp_server.py | 4 +- docforge/servers/mcp_server.pyi | 7 +- pyproject.toml | 126 +++++++++++++++++- tests/cli/test_build_mcp.py | 2 + tests/cli/test_build_mkdocs.py | 10 +- tests/cli/test_serve_mcp.py | 1 + tests/cli/test_serve_mkdocs.py | 1 + tests/conftest.py | 6 +- tests/introspection/test_alias_safety.py | 6 +- .../introspection/test_classes_and_methods.py | 6 +- .../test_functions_and_signatures.py | 6 +- tests/introspection/test_import_failures.py | 8 +- .../introspection/test_missing_docstrings.py | 4 +- tests/introspection/test_private_members.py | 6 +- tests/introspection/test_single_module.py | 6 +- tests/nav/test_mkdocs.py | 3 +- tests/nav/test_resolver.py | 3 +- tests/renderers/mcp/test_mcp_content.py | 2 +- tests/renderers/mcp/test_mcp_idempotency.py | 2 +- .../renderers/mcp/test_mcp_module_coverage.py | 7 +- tests/renderers/mcp/test_mcp_structure.py | 2 +- tests/renderers/mkdocs/test_mkdocs_content.py | 4 +- .../mkdocs/test_mkdocs_idempotency.py | 4 +- .../mkdocs/test_mkdocs_module_coverage.py | 10 +- .../renderers/mkdocs/test_mkdocs_structure.py | 2 +- 61 files changed, 429 insertions(+), 273 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 docforge/py.typed diff --git a/.drone.yml b/.drone.yml index ddd0ac4..094db62 100644 --- a/.drone.yml +++ b/.drone.yml @@ -32,6 +32,26 @@ steps: echo "🆕 New version detected: $PACKAGE_NAME==$VERSION" 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 image: python:3.13-slim commands: @@ -126,4 +146,4 @@ steps: trigger: event: - - custom + - custom \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c9eca3c --- /dev/null +++ b/CHANGELOG.md @@ -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. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..87328b2 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/docforge/__init__.pyi b/docforge/__init__.pyi index 5823fb1..bd6340e 100644 --- a/docforge/__init__.pyi +++ b/docforge/__init__.pyi @@ -1,7 +1,7 @@ -from .loaders import GriffeLoader, discover_module_paths -from .renderers import MkDocsRenderer, MCPRenderer -from .cli import main from . import models +from .cli import main +from .loaders import GriffeLoader, discover_module_paths +from .renderers import MCPRenderer, MkDocsRenderer __all__ = [ "GriffeLoader", diff --git a/docforge/cli/__init__.py b/docforge/cli/__init__.py index feeca82..79ed9a3 100644 --- a/docforge/cli/__init__.py +++ b/docforge/cli/__init__.py @@ -37,6 +37,4 @@ Example: from .main import main -__all__ = [ - "main" -] +__all__ = ["main"] diff --git a/docforge/cli/__init__.pyi b/docforge/cli/__init__.pyi index aef0a0c..21db616 100644 --- a/docforge/cli/__init__.pyi +++ b/docforge/cli/__init__.pyi @@ -1,5 +1,3 @@ from .main import main -__all__ = [ - "main" -] +__all__ = ["main"] diff --git a/docforge/cli/commands.py b/docforge/cli/commands.py index f25f6f0..e940c66 100644 --- a/docforge/cli/commands.py +++ b/docforge/cli/commands.py @@ -6,12 +6,12 @@ Command definitions for the doc-forge CLI. Provides the CLI structure using Click, including build, serve, and tree commands. """ -import click 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.cli import mkdocs_utils -from docforge.cli import mcp_utils @click.group() @@ -28,26 +28,52 @@ def cli() -> None: @cli.command() @click.option("--mcp", is_flag=True, help="Build MCP resources") @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("--project-name", help="Project name override") @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("--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") +@click.option( + "--docs-dir", + type=click.Path(path_type=Path), + default=Path("docs"), + help="Directory for MD sources", +) +@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( mcp: bool, mkdocs: bool, module_is_source: bool, - module: Optional[str], - project_name: Optional[str], - site_name: Optional[str], + module: str | None, + project_name: str | None, + site_name: str | None, docs_dir: Path, nav_file: Path, - template: Optional[Path], + template: Path | None, mkdocs_yml: Path, out_dir: Path, ) -> None: @@ -121,7 +147,9 @@ def build( ) 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...") mkdocs_utils.build(mkdocs_yml) @@ -140,12 +168,22 @@ def build( @click.option("--mcp", is_flag=True, help="Serve MCP documentation") @click.option("--mkdocs", is_flag=True, help="Serve MkDocs site") @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("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP root directory") +@click.option( + "--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( mcp: bool, mkdocs: bool, - module: Optional[str], + module: str | None, mkdocs_yml: Path, out_dir: Path, ) -> None: @@ -202,7 +240,7 @@ def serve( ) def tree( module: str, - project_name: Optional[str], + project_name: str | None, ) -> None: """ Display the documentation object tree for a module. diff --git a/docforge/cli/commands.pyi b/docforge/cli/commands.pyi index a38d851..0c1f1ec 100644 --- a/docforge/cli/commands.pyi +++ b/docforge/cli/commands.pyi @@ -1,6 +1,7 @@ -from click.core import Group from pathlib import Path -from typing import Sequence, Optional, Any +from typing import Any + +from click.core import Group cli: Group @@ -8,27 +9,24 @@ def build( mcp: bool, mkdocs: bool, module_is_source: bool, - module: Optional[str], - project_name: Optional[str], - site_name: Optional[str], + module: str | None, + project_name: str | None, + site_name: str | None, docs_dir: Path, nav_file: Path, - template: Optional[Path], + template: Path | None, mkdocs_yml: Path, out_dir: Path, ) -> None: ... - def serve( mcp: bool, mkdocs: bool, - module: Optional[str], + module: str | None, mkdocs_yml: Path, out_dir: Path, ) -> None: ... - def tree( module: str, - project_name: Optional[str], + project_name: str | None, ) -> None: ... - def _print_object(obj: Any, indent: str) -> None: ... diff --git a/docforge/cli/main.py b/docforge/cli/main.py index d1ab265..3dea046 100644 --- a/docforge/cli/main.py +++ b/docforge/cli/main.py @@ -22,4 +22,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/docforge/cli/mcp_utils.py b/docforge/cli/mcp_utils.py index 4421a1f..09e3b2c 100644 --- a/docforge/cli/mcp_utils.py +++ b/docforge/cli/mcp_utils.py @@ -5,7 +5,9 @@ Utilities for working with MCP in the doc-forge CLI. """ from pathlib import Path + import click + from docforge.loaders import GriffeLoader, discover_module_paths from docforge.renderers import MCPRenderer from docforge.servers import MCPServer diff --git a/docforge/cli/mcp_utils.pyi b/docforge/cli/mcp_utils.pyi index 5e9ac4d..84d2fa1 100644 --- a/docforge/cli/mcp_utils.pyi +++ b/docforge/cli/mcp_utils.pyi @@ -1,4 +1,6 @@ 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: ... diff --git a/docforge/cli/mkdocs_utils.py b/docforge/cli/mkdocs_utils.py index 3244e38..289d524 100644 --- a/docforge/cli/mkdocs_utils.py +++ b/docforge/cli/mkdocs_utils.py @@ -4,13 +4,15 @@ Utilities for working with MkDocs in the doc-forge CLI. """ -from pathlib import Path from importlib import resources +from pathlib import Path + import click import yaml + 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.nav import load_nav_spec, resolve_nav, MkDocsNavEmitter def generate_sources( @@ -138,8 +140,8 @@ def build(mkdocs_yml: Path) -> None: if not mkdocs_yml.exists(): 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.config import load_config 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}") from mkdocs.commands.serve import serve as mkdocs_serve + mkdocs_serve(config_file=str(mkdocs_yml)) diff --git a/docforge/cli/mkdocs_utils.pyi b/docforge/cli/mkdocs_utils.pyi index 052bee9..c239ccb 100644 --- a/docforge/cli/mkdocs_utils.pyi +++ b/docforge/cli/mkdocs_utils.pyi @@ -6,6 +6,8 @@ def generate_sources( project_name: str | None = None, module_is_source: bool | 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 serve(mkdocs_yml: Path) -> None: ... diff --git a/docforge/loaders/griffe_loader.py b/docforge/loaders/griffe_loader.py index 2cac54a..c71c74f 100644 --- a/docforge/loaders/griffe_loader.py +++ b/docforge/loaders/griffe_loader.py @@ -10,17 +10,18 @@ into doc-forge documentation models. import logging from pathlib import Path -from typing import List, Optional from griffe import ( - GriffeLoader as _GriffeLoader, - ModulesCollection, - LinesCollection, - Object, 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__) @@ -28,7 +29,7 @@ logger = logging.getLogger(__name__) def discover_module_paths( module_name: str, project_root: Path | None = None, -) -> List[str]: +) -> list[str]: """ Discover Python modules within a package directory. @@ -65,7 +66,7 @@ def discover_module_paths( if not pkg_dir.exists(): raise FileNotFoundError(f"Package not found: {pkg_dir}") - module_paths: List[str] = [] + module_paths: list[str] = [] for path in pkg_dir.rglob("*.py"): if path.name == "__init__.py": @@ -102,10 +103,10 @@ class GriffeLoader: ) def load_project( - self, - module_paths: List[str], - project_name: Optional[str] = None, - skip_import_errors: bool = None, + self, + module_paths: list[str], + project_name: str | None = None, + skip_import_errors: bool = None, ) -> Project: """ Load multiple modules and assemble them into a Project model. @@ -250,7 +251,7 @@ class GriffeLoader: # 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. @@ -267,7 +268,7 @@ class GriffeLoader: except AliasResolutionError: 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. diff --git a/docforge/loaders/griffe_loader.pyi b/docforge/loaders/griffe_loader.pyi index 037f147..bbded1c 100644 --- a/docforge/loaders/griffe_loader.pyi +++ b/docforge/loaders/griffe_loader.pyi @@ -1,15 +1,11 @@ -from typing import List, Optional from pathlib import Path from docforge.models import Module, Project - def discover_module_paths( module_name: str, project_root: Path | None = None, -) -> List[str]: - ... - +) -> list[str]: ... class GriffeLoader: """Griffe-based introspection loaders. @@ -18,11 +14,10 @@ class GriffeLoader: """ def __init__(self) -> None: ... - def load_project( self, - module_paths: List[str], - project_name: Optional[str] = ..., + module_paths: list[str], + project_name: str | None = ..., skip_import_errors: bool = ..., ) -> Project: """Load a documentation project from Python modules.""" diff --git a/docforge/models/__init__.pyi b/docforge/models/__init__.pyi index 0fe95b1..0b0a510 100644 --- a/docforge/models/__init__.pyi +++ b/docforge/models/__init__.pyi @@ -1,6 +1,6 @@ -from .project import Project from .module import Module from .object import DocObject +from .project import Project __all__ = [ "Project", diff --git a/docforge/models/module.py b/docforge/models/module.py index ee1bffc..cc58412 100644 --- a/docforge/models/module.py +++ b/docforge/models/module.py @@ -9,7 +9,7 @@ model. A `Module` acts as a container for top-level documented objects introspection. """ -from typing import Dict, Iterable, Optional +from collections.abc import Iterable from docforge.models.object import DocObject @@ -36,7 +36,7 @@ class Module: def __init__( self, path: str, - docstring: Optional[str] = None, + docstring: str | None = None, ) -> None: """ Initialize a Module instance. @@ -50,7 +50,7 @@ class Module: """ self.path = path self.docstring = docstring - self.members: Dict[str, DocObject] = {} + self.members: dict[str, DocObject] = {} def add_object(self, obj: DocObject) -> None: """ diff --git a/docforge/models/module.pyi b/docforge/models/module.pyi index e5145be..e9abd8d 100644 --- a/docforge/models/module.pyi +++ b/docforge/models/module.pyi @@ -1,23 +1,19 @@ -from typing import Dict, Iterable, Optional +from collections.abc import Iterable from docforge.models.object import DocObject - class Module: """Represents a documented Python module.""" path: str - docstring: Optional[str] - members: Dict[str, DocObject] + docstring: str | None + members: dict[str, DocObject] def __init__( self, path: str, - docstring: Optional[str] = ..., + docstring: str | None = ..., ) -> None: ... - def add_object(self, obj: DocObject) -> None: ... - def get_object(self, name: str) -> DocObject: ... - def get_all_objects(self) -> Iterable[DocObject]: ... diff --git a/docforge/models/object.py b/docforge/models/object.py index b305c0d..200ec85 100644 --- a/docforge/models/object.py +++ b/docforge/models/object.py @@ -9,7 +9,7 @@ entity such as a class, function, method, or attribute, and may contain nested members that form a hierarchical documentation structure. """ -from typing import Dict, Iterable, Optional +from collections.abc import Iterable class DocObject: @@ -45,8 +45,8 @@ class DocObject: name: str, kind: str, path: str, - signature: Optional[str] = None, - docstring: Optional[str] = None, + signature: str | None = None, + docstring: str | None = None, ) -> None: """ Initialize a DocObject instance. @@ -72,7 +72,7 @@ class DocObject: self.path = path self.signature = signature self.docstring = docstring - self.members: Dict[str, "DocObject"] = {} + self.members: dict[str, DocObject] = {} def add_member(self, obj: "DocObject") -> None: """ diff --git a/docforge/models/object.pyi b/docforge/models/object.pyi index b7f589a..e92142b 100644 --- a/docforge/models/object.pyi +++ b/docforge/models/object.pyi @@ -1,5 +1,4 @@ -from typing import Dict, Iterable, Optional - +from collections.abc import Iterable class DocObject: """Represents a documented Python object.""" @@ -7,21 +6,18 @@ class DocObject: name: str kind: str path: str - signature: Optional[str] - docstring: Optional[str] - members: Dict[str, "DocObject"] + signature: str | None + docstring: str | None + members: dict[str, DocObject] def __init__( self, name: str, kind: str, path: str, - signature: Optional[str] = ..., - docstring: Optional[str] = ..., + signature: str | None = ..., + docstring: str | None = ..., ) -> None: ... - - def add_member(self, obj: "DocObject") -> None: ... - - def get_member(self, name: str) -> "DocObject": ... - - def get_all_members(self) -> Iterable["DocObject"]: ... + def add_member(self, obj: DocObject) -> None: ... + def get_member(self, name: str) -> DocObject: ... + def get_all_members(self) -> Iterable[DocObject]: ... diff --git a/docforge/models/project.py b/docforge/models/project.py index 431362b..0dde674 100644 --- a/docforge/models/project.py +++ b/docforge/models/project.py @@ -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. """ -from typing import Dict, Iterable +from collections.abc import Iterable from docforge.models.module import Module @@ -37,7 +37,7 @@ class Project: Name used to identify the documentation project. """ self.name = name - self.modules: Dict[str, Module] = {} + self.modules: dict[str, Module] = {} def add_module(self, module: Module) -> None: """ @@ -85,4 +85,4 @@ class Project: list[str]: A list containing the dotted paths of all modules in the project. """ - return list(self.modules.keys()) \ No newline at end of file + return list(self.modules.keys()) diff --git a/docforge/models/project.pyi b/docforge/models/project.pyi index 95f7844..3e209a9 100644 --- a/docforge/models/project.pyi +++ b/docforge/models/project.pyi @@ -1,20 +1,15 @@ -from typing import Dict, Iterable +from collections.abc import Iterable from docforge.models.module import Module - class Project: """Represents a documentation project.""" name: str - modules: Dict[str, Module] + modules: dict[str, Module] def __init__(self, name: str) -> None: ... - def add_module(self, module: Module) -> None: ... - def get_module(self, path: str) -> Module: ... - def get_all_modules(self) -> Iterable[Module]: ... - def get_module_list(self) -> list[str]: ... diff --git a/docforge/nav/__init__.pyi b/docforge/nav/__init__.pyi index 2db95eb..2a3580c 100644 --- a/docforge/nav/__init__.pyi +++ b/docforge/nav/__init__.pyi @@ -1,6 +1,6 @@ -from .spec import NavSpec, load_nav_spec -from .resolver import ResolvedNav, resolve_nav from .mkdocs import MkDocsNavEmitter +from .resolver import ResolvedNav, resolve_nav +from .spec import NavSpec, load_nav_spec __all__ = [ "NavSpec", diff --git a/docforge/nav/mkdocs.py b/docforge/nav/mkdocs.py index b053341..54418fd 100644 --- a/docforge/nav/mkdocs.py +++ b/docforge/nav/mkdocs.py @@ -7,7 +7,7 @@ MkDocs ``nav`` configuration. """ from pathlib import Path -from typing import List, Dict, Any +from typing import Any from docforge.nav.resolver import ResolvedNav @@ -20,7 +20,7 @@ class MkDocsNavEmitter: 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``. @@ -33,7 +33,7 @@ class MkDocsNavEmitter: Each dictionary maps a navigation label to a page or a list of pages. """ - result: List[Dict[str, Any]] = [] + result: list[dict[str, Any]] = [] # Home entry (semantic path) if nav.home: @@ -41,7 +41,7 @@ class MkDocsNavEmitter: # Group entries for group, paths in nav.groups.items(): - entries: List[str] = [] + entries: list[str] = [] for p in paths: # Convert filesystem path back to docs-relative path rel_path = self._to_relative(p, nav._docs_root) @@ -75,7 +75,7 @@ class MkDocsNavEmitter: 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:] + return path_str[len(docs_root_str) + 1 :] # Fallback for other cases return path.as_posix().split("/docs/", 1)[-1] diff --git a/docforge/nav/mkdocs.pyi b/docforge/nav/mkdocs.pyi index 9fb6aec..ec89e3e 100644 --- a/docforge/nav/mkdocs.pyi +++ b/docforge/nav/mkdocs.pyi @@ -1,15 +1,14 @@ -from typing import Dict, List, Any from pathlib import Path +from typing import Any from docforge.nav.resolver import ResolvedNav - class MkDocsNavEmitter: """ 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. diff --git a/docforge/nav/resolver.py b/docforge/nav/resolver.py index e1911a6..d32fa38 100644 --- a/docforge/nav/resolver.py +++ b/docforge/nav/resolver.py @@ -5,10 +5,9 @@ This module resolves a ``NavSpec`` against the filesystem by expanding glob patterns and validating that referenced documentation files exist. """ -from pathlib import Path -from typing import Dict, Iterable, List - import glob +from collections.abc import Iterable +from pathlib import Path from docforge.nav.spec import NavSpec @@ -29,7 +28,7 @@ class ResolvedNav: def __init__( self, home: str | None, - groups: Dict[str, List[Path]], + groups: dict[str, list[Path]], docs_root: Path | None = None, ) -> None: """ @@ -61,8 +60,7 @@ class ResolvedNav: yield self._docs_root / self.home for paths in self.groups.values(): - for p in paths: - yield p + yield from paths def resolve_nav( @@ -89,7 +87,7 @@ def resolve_nav( if not docs_root.exists(): 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. @@ -103,9 +101,7 @@ def resolve_nav( FileNotFoundError: If the pattern does not match any files. """ full = docs_root / pattern - matches = sorted( - Path(p) for p in glob.glob(str(full), recursive=True) - ) + matches = sorted(Path(p) for p in glob.glob(str(full), recursive=True)) if not matches: raise FileNotFoundError(pattern) @@ -121,10 +117,10 @@ def resolve_nav( home = spec.home # Resolve navigation groups - resolved_groups: Dict[str, List[Path]] = {} + resolved_groups: dict[str, list[Path]] = {} for group, patterns in spec.groups.items(): - files: List[Path] = [] + files: list[Path] = [] for pattern in patterns: files.extend(resolve_pattern(pattern)) resolved_groups[group] = files diff --git a/docforge/nav/resolver.pyi b/docforge/nav/resolver.pyi index ea7a239..0bc6d95 100644 --- a/docforge/nav/resolver.pyi +++ b/docforge/nav/resolver.pyi @@ -1,9 +1,8 @@ +from collections.abc import Iterable from pathlib import Path -from typing import Dict, List, Iterable, Optional from docforge.nav.spec import NavSpec - class ResolvedNav: """ Fully-resolved navigation tree. @@ -13,17 +12,16 @@ class ResolvedNav: - Order is preserved """ - home: Optional[str] - groups: Dict[str, List[Path]] - _docs_root: Optional[Path] + home: str | None + groups: dict[str, list[Path]] + _docs_root: Path | None def __init__( self, home: str | None, - groups: Dict[str, List[Path]], + groups: dict[str, list[Path]], docs_root: Path | None = ..., ) -> None: ... - def all_files(self) -> Iterable[Path]: """ Return all resolved documentation files in nav order. @@ -33,7 +31,6 @@ class ResolvedNav: """ ... - def resolve_nav( spec: NavSpec, docs_root: Path, diff --git a/docforge/nav/spec.py b/docforge/nav/spec.py index b3c8a51..dd87b37 100644 --- a/docforge/nav/spec.py +++ b/docforge/nav/spec.py @@ -7,7 +7,6 @@ structure defined by the user in the doc-forge navigation specification """ from pathlib import Path -from typing import Dict, List, Optional import yaml @@ -28,8 +27,8 @@ class NavSpec: def __init__( self, - home: Optional[str], - groups: Dict[str, List[str]], + home: str | None, + groups: dict[str, list[str]], ) -> None: """ Initialize a NavSpec instance. @@ -85,7 +84,7 @@ class NavSpec: 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. @@ -93,7 +92,7 @@ class NavSpec: A list containing the home document (if defined) and all group pattern entries. """ - patterns: List[str] = [] + patterns: list[str] = [] if self.home: patterns.append(self.home) diff --git a/docforge/nav/spec.pyi b/docforge/nav/spec.pyi index fd3bf4d..5f9022c 100644 --- a/docforge/nav/spec.pyi +++ b/docforge/nav/spec.pyi @@ -1,6 +1,4 @@ from pathlib import Path -from typing import Dict, List, Optional - class NavSpec: """ @@ -10,18 +8,16 @@ class NavSpec: of filesystem structure or MkDocs specifics. """ - home: Optional[str] - groups: Dict[str, List[str]] + home: str | None + groups: dict[str, list[str]] def __init__( self, - home: Optional[str], - groups: Dict[str, List[str]], - ) -> None: - ... - + home: str | None, + groups: dict[str, list[str]], + ) -> None: ... @classmethod - def load(cls, path: Path) -> "NavSpec": + def load(cls, path: Path) -> NavSpec: """ 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 (including home and group entries). """ ... - def load_nav_spec(path: Path) -> NavSpec: ... diff --git a/docforge/py.typed b/docforge/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/docforge/renderers/__init__.pyi b/docforge/renderers/__init__.pyi index f0135ff..ecc5bea 100644 --- a/docforge/renderers/__init__.pyi +++ b/docforge/renderers/__init__.pyi @@ -1,5 +1,5 @@ -from .mkdocs_renderer import MkDocsRenderer from .mcp_renderer import MCPRenderer +from .mkdocs_renderer import MkDocsRenderer __all__ = [ "MkDocsRenderer", diff --git a/docforge/renderers/base.pyi b/docforge/renderers/base.pyi index 5800b4f..600a931 100644 --- a/docforge/renderers/base.pyi +++ b/docforge/renderers/base.pyi @@ -3,7 +3,6 @@ from typing import Protocol from docforge.models import Project - class RendererConfig: """Renderer configuration container.""" @@ -12,7 +11,6 @@ class RendererConfig: def __init__(self, out_dir: Path, project: Project) -> None: ... - class DocRenderer(Protocol): """Renderer interface.""" diff --git a/docforge/renderers/mcp_renderer.py b/docforge/renderers/mcp_renderer.py index 8f01a01..425d18c 100644 --- a/docforge/renderers/mcp_renderer.py +++ b/docforge/renderers/mcp_renderer.py @@ -9,9 +9,8 @@ resources compatible with the Model Context Protocol (MCP). import json 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: @@ -42,15 +41,17 @@ class MCPRenderer: modules_dir = out_dir / "modules" 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(): self._write_module(module, modules_dir) - nav.append({ - "module": module.path, - "resource": f"doc://modules/{module.path}", - }) + nav.append( + { + "module": module.path, + "resource": f"doc://modules/{module.path}", + } + ) # Write nav.json (out_dir / "nav.json").write_text( @@ -91,7 +92,7 @@ class MCPRenderer: out.parent.mkdir(parents=True, exist_ok=True) 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. @@ -103,7 +104,7 @@ class MCPRenderer: Dict: Dictionary representing the module and its documented objects. """ - data: Dict = { + data: dict = { "path": module.path, "docstring": module.docstring, "objects": {}, @@ -114,7 +115,7 @@ class MCPRenderer: return data - def _render_object(self, obj: DocObject) -> Dict: + def _render_object(self, obj: DocObject) -> dict: """ Recursively convert a DocObject into structured MCP data. @@ -126,7 +127,7 @@ class MCPRenderer: Dict: Dictionary describing the object and any nested members. """ - data: Dict = { + data: dict = { "name": obj.name, "kind": obj.kind, "path": obj.path, @@ -137,14 +138,13 @@ class MCPRenderer: members = list(obj.get_all_members()) if members: data["members"] = { - member.name: self._render_object(member) - for member in members + member.name: self._render_object(member) for member in members } return data @staticmethod - def _json(data: Dict) -> str: + def _json(data: dict) -> str: """ Serialize data to formatted JSON. diff --git a/docforge/renderers/mcp_renderer.pyi b/docforge/renderers/mcp_renderer.pyi index 3025fee..b047d06 100644 --- a/docforge/renderers/mcp_renderer.pyi +++ b/docforge/renderers/mcp_renderer.pyi @@ -1,8 +1,7 @@ from pathlib import Path -from typing import Dict, List - -from docforge.models import Project, Module, DocObject +from typing import Any +from docforge.models import DocObject, Module, Project class MCPRenderer: """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: """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.""" - def _render_object(self, obj: DocObject) -> Dict: + def _render_object(self, obj: DocObject) -> dict[str, Any]: """Recursively render a DocObject into structured MCP data.""" @staticmethod - def _json(data: Dict) -> str: + def _json(data: dict[str, Any]) -> str: """Serialize structured data to formatted JSON.""" diff --git a/docforge/renderers/mkdocs_renderer.py b/docforge/renderers/mkdocs_renderer.py index c58e9ea..5b7df4f 100644 --- a/docforge/renderers/mkdocs_renderer.py +++ b/docforge/renderers/mkdocs_renderer.py @@ -16,7 +16,8 @@ The renderer ensures a consistent documentation structure by: """ from pathlib import Path -from docforge.models import Project, Module + +from docforge.models import Module, Project class MkDocsRenderer: @@ -65,8 +66,7 @@ class MkDocsRenderer: # Detect packages (modules with children) packages = { - p for p in paths - if any(other.startswith(p + ".") for other in paths) + p for p in paths if any(other.startswith(p + ".") for other in paths) } for module in modules: @@ -127,12 +127,12 @@ class MkDocsRenderer: str(root_module.docstring), ) - content = ( - f"# {project.name}\n\n" - f"{doc.strip()}\n" - ) + content = 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( content, encoding="utf-8", @@ -231,10 +231,7 @@ class MkDocsRenderer: str: Markdown source containing a mkdocstrings directive. """ - return ( - f"# {title}\n\n" - f"::: {module_path}\n" - ) + return f"# {title}\n\n" f"::: {module_path}\n" def _ensure_root_index( self, @@ -255,8 +252,7 @@ class MkDocsRenderer: if not root_index.exists(): root_index.write_text( - f"# {project.name}\n\n" - "## Modules\n\n", + f"# {project.name}\n\n" "## Modules\n\n", encoding="utf-8", ) diff --git a/docforge/renderers/mkdocs_renderer.pyi b/docforge/renderers/mkdocs_renderer.pyi index 584e425..a574149 100644 --- a/docforge/renderers/mkdocs_renderer.pyi +++ b/docforge/renderers/mkdocs_renderer.pyi @@ -1,6 +1,6 @@ from pathlib import Path -from docforge.models import Project, Module +from docforge.models import Module, Project class MkDocsRenderer: name: str @@ -11,14 +11,12 @@ class MkDocsRenderer: out_dir: Path, module_is_source: bool | None = None, ) -> None: ... - def generate_readme( self, project: Project, docs_dir: Path, module_is_source: bool | None = None, - ) -> None: - + ) -> None: ... def _write_module( self, module: Module, @@ -26,9 +24,8 @@ class MkDocsRenderer: out_dir: Path, module_is_source: bool | None = None, ) -> None: ... - def _render_markdown(self, title: str, module_path: str) -> str: ... - - def _ensure_root_index(self, project, out_dir) -> None: ... - - def _ensure_parent_index(self, parts, out_dir, link_target, title) -> None: ... + def _ensure_root_index(self, project: Project, out_dir: Path) -> None: ... + def _ensure_parent_index( + self, parts: list[str], out_dir: Path, link_target: str, title: str + ) -> None: ... diff --git a/docforge/servers/mcp_server.py b/docforge/servers/mcp_server.py index 1fde6ef..6b27541 100644 --- a/docforge/servers/mcp_server.py +++ b/docforge/servers/mcp_server.py @@ -96,9 +96,7 @@ class MCPServer: @self.app.resource("docs://modules/{module}") def module(module: str): - return self._read_json( - self.mcp_root / "modules" / f"{module}.json" - ) + return self._read_json(self.mcp_root / "modules" / f"{module}.json") # ------------------------------------------------------------------ # MCP tools diff --git a/docforge/servers/mcp_server.pyi b/docforge/servers/mcp_server.pyi index f4341d5..f469071 100644 --- a/docforge/servers/mcp_server.pyi +++ b/docforge/servers/mcp_server.pyi @@ -1,9 +1,8 @@ from pathlib import Path -from typing import Literal, Any +from typing import Any, Literal from mcp.server.fastmcp import FastMCP - class MCPServer: """MCP server for serving documentation.""" @@ -11,12 +10,8 @@ class MCPServer: app: FastMCP def __init__(self, mcp_root: Path, name: str) -> None: ... - def _read_json(self, path: Path) -> Any: ... - def _register_resources(self) -> None: ... - def _register_tools(self) -> None: ... - def run(self, transport: Literal["stdio", "sse", "streamable-http"] = ...) -> None: """Start the MCP server.""" diff --git a/pyproject.toml b/pyproject.toml index 4bd4257..3979bc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,19 @@ authors = [ maintainers = [ { name = "Aetos Skia", email = "dev@aetoskia.com" } ] + + +keywords = [ + "documentation", + "docs", + "compiler", + "mkdocs", + "mcp", + "griffe", + "docstrings", + "sphinx", +] + classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", @@ -25,9 +38,12 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Framework :: FastAPI", "Topic :: Software Development :: Libraries", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Documentation", + "Typing :: Typed", ] @@ -57,14 +73,25 @@ sphinx = [ "sphinx-autodoc-typehints>=1.19.0", ] mcp = [ - "mcp>=1.0.0", + "mcp>=1.0.0,<2.0.0", ] dev = [ - "pytest>=7.0.0", - "pytest-cov>=4.0.0", + "pytest>=8.0.0", + "pytest-asyncio>=0.21.0", + "pytest-cov>=4.1.0", "black>=23.0.0", - "ruff>=0.1.0", - "mypy>=1.0.0", + "ruff>=0.3.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*"] } } [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] -line-length = 100 +line-length = 88 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] python_version = "3.10" strict = true +exclude = [ + "tests/", +] +files = ["docforge"] + +[[tool.mypy.overrides]] +module = [ + "griffe.*", + "mcp.*", + "click", + "pydantic", +] 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", +] \ No newline at end of file diff --git a/tests/cli/test_build_mcp.py b/tests/cli/test_build_mcp.py index f783945..8cb1bb2 100644 --- a/tests/cli/test_build_mcp.py +++ b/tests/cli/test_build_mcp.py @@ -1,6 +1,8 @@ from pathlib import Path + from docforge.cli.main import cli + def test_mcp_build(cli_runner): with cli_runner.isolated_filesystem(): cwd = Path.cwd() diff --git a/tests/cli/test_build_mkdocs.py b/tests/cli/test_build_mkdocs.py index a84722f..7987ed4 100644 --- a/tests/cli/test_build_mkdocs.py +++ b/tests/cli/test_build_mkdocs.py @@ -1,6 +1,8 @@ from pathlib import Path + from docforge.cli.main import cli + def test_mkdocs_build_full_flow( cli_runner, mock_mkdocs_build, @@ -17,11 +19,11 @@ def test_mkdocs_build_full_flow( nav_file = cwd / "docforge.nav.yml" 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 - # But generate_sources will create it. + # But generate_sources will create it. # Wait, the current logic runs generate_sources first, THEN generate_config. - + result = cli_runner.invoke( cli, [ @@ -43,11 +45,13 @@ def test_mkdocs_build_full_flow( assert (cwd / "mkdocs.yml").exists() assert (cwd / "docs" / "testpkg" / "mod.md").exists() + def test_mkdocs_build_missing_module_fails(cli_runner): result = cli_runner.invoke(cli, ["build", "--mkdocs", "--site-name", "Test"]) assert result.exit_code != 0 assert "--module is required" in result.output + def test_mkdocs_build_without_site_name_uses_module_as_default_full_flow( cli_runner, mock_mkdocs_build, diff --git a/tests/cli/test_serve_mcp.py b/tests/cli/test_serve_mcp.py index 4e0db66..4e82ff0 100644 --- a/tests/cli/test_serve_mcp.py +++ b/tests/cli/test_serve_mcp.py @@ -1,5 +1,6 @@ from docforge.cli.main import cli + def test_mcp_serve( cli_runner, fake_mcp_docs, diff --git a/tests/cli/test_serve_mkdocs.py b/tests/cli/test_serve_mkdocs.py index 7ac9261..fa6415f 100644 --- a/tests/cli/test_serve_mkdocs.py +++ b/tests/cli/test_serve_mkdocs.py @@ -1,5 +1,6 @@ from docforge.cli.main import cli + def test_mkdocs_serve( cli_runner, fake_mkdocs_yml, diff --git a/tests/conftest.py b/tests/conftest.py index e3973b7..178441d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,8 @@ -import sys import json -import pytest +import sys from pathlib import Path +import pytest from click.testing import CliRunner @@ -45,6 +45,7 @@ plugins: @pytest.fixture def mock_mkdocs_load_config(monkeypatch): """Mock mkdocs.config.load_config.""" + def fake_load_config(path): return object() # dummy config object @@ -81,6 +82,7 @@ def mock_mkdocs_serve(monkeypatch): ) return lambda: called["value"] + @pytest.fixture def fake_mcp_docs(tmp_path: Path) -> Path: """ diff --git a/tests/introspection/test_alias_safety.py b/tests/introspection/test_alias_safety.py index 599a663..055af78 100644 --- a/tests/introspection/test_alias_safety.py +++ b/tests/introspection/test_alias_safety.py @@ -2,11 +2,9 @@ from docforge import GriffeLoader def test_alias_does_not_crash(temp_package): - (temp_package / "alias.py").write_text( - '''from typing import List + (temp_package / "alias.py").write_text("""from typing import List Alias = List[int] -''' - ) +""") loader = GriffeLoader() project = loader.load_project(["testpkg.alias"]) diff --git a/tests/introspection/test_classes_and_methods.py b/tests/introspection/test_classes_and_methods.py index 759652c..dbe2ea1 100644 --- a/tests/introspection/test_classes_and_methods.py +++ b/tests/introspection/test_classes_and_methods.py @@ -2,15 +2,13 @@ from docforge import GriffeLoader def test_class_and_methods(temp_package): - (temp_package / "cls.py").write_text( - '''class MyClass: + (temp_package / "cls.py").write_text('''class MyClass: """Class doc.""" def method(self, x: int) -> int: """Method doc.""" return x -''' - ) +''') loader = GriffeLoader() project = loader.load_project(["testpkg.cls"]) diff --git a/tests/introspection/test_functions_and_signatures.py b/tests/introspection/test_functions_and_signatures.py index 221ecb1..5164c5b 100644 --- a/tests/introspection/test_functions_and_signatures.py +++ b/tests/introspection/test_functions_and_signatures.py @@ -2,12 +2,10 @@ from docforge import GriffeLoader def test_function_signature(temp_package): - (temp_package / "fn.py").write_text( - '''def add(a: int, b: int = 1) -> int: + (temp_package / "fn.py").write_text('''def add(a: int, b: int = 1) -> int: """Adds numbers.""" return a + b -''' - ) +''') loader = GriffeLoader() project = loader.load_project(["testpkg.fn"]) diff --git a/tests/introspection/test_import_failures.py b/tests/introspection/test_import_failures.py index 8f80b71..acfc9a2 100644 --- a/tests/introspection/test_import_failures.py +++ b/tests/introspection/test_import_failures.py @@ -1,4 +1,5 @@ import pytest + from docforge import GriffeLoader @@ -6,9 +7,8 @@ def test_load_project_raises_on_missing_module_by_default(): loader = GriffeLoader() with pytest.raises(ImportError): - loader.load_project( - ["nonexistent.module", "sys"] - ) + loader.load_project(["nonexistent.module", "sys"]) + def test_load_project_skips_missing_modules_when_enabled(): loader = GriffeLoader() @@ -18,4 +18,4 @@ def test_load_project_skips_missing_modules_when_enabled(): skip_import_errors=True, ) - assert "sys" in project.modules \ No newline at end of file + assert "sys" in project.modules diff --git a/tests/introspection/test_missing_docstrings.py b/tests/introspection/test_missing_docstrings.py index 05e8499..97c18bf 100644 --- a/tests/introspection/test_missing_docstrings.py +++ b/tests/introspection/test_missing_docstrings.py @@ -2,9 +2,7 @@ from docforge import GriffeLoader def test_missing_docstrings(temp_package): - (temp_package / "nodoc.py").write_text( - '''def f(): pass''' - ) + (temp_package / "nodoc.py").write_text("""def f(): pass""") loader = GriffeLoader() project = loader.load_project(["testpkg.nodoc"]) diff --git a/tests/introspection/test_private_members.py b/tests/introspection/test_private_members.py index a7aae6d..a77b395 100644 --- a/tests/introspection/test_private_members.py +++ b/tests/introspection/test_private_members.py @@ -2,11 +2,9 @@ from docforge import GriffeLoader def test_private_members_excluded(temp_package): - (temp_package / "priv.py").write_text( - '''def _hidden(): pass + (temp_package / "priv.py").write_text("""def _hidden(): pass def visible(): pass -''' - ) +""") loader = GriffeLoader() project = loader.load_project(["testpkg.priv"]) diff --git a/tests/introspection/test_single_module.py b/tests/introspection/test_single_module.py index 3358f85..060f68c 100644 --- a/tests/introspection/test_single_module.py +++ b/tests/introspection/test_single_module.py @@ -2,13 +2,11 @@ from docforge import GriffeLoader def test_load_single_module(temp_package): - (temp_package / "mod.py").write_text( - '''"""Module docstring."""\n + (temp_package / "mod.py").write_text('''"""Module docstring."""\n def foo(): """Foo docstring.""" pass -''' - ) +''') loader = GriffeLoader() project = loader.load_project(["testpkg.mod"]) diff --git a/tests/nav/test_mkdocs.py b/tests/nav/test_mkdocs.py index 2022208..d275418 100644 --- a/tests/nav/test_mkdocs.py +++ b/tests/nav/test_mkdocs.py @@ -1,7 +1,6 @@ from pathlib import Path -from docforge.nav import ResolvedNav -from docforge.nav import MkDocsNavEmitter +from docforge.nav import MkDocsNavEmitter, ResolvedNav def test_emit_mkdocs_nav(): diff --git a/tests/nav/test_resolver.py b/tests/nav/test_resolver.py index 260d95a..77eb10a 100644 --- a/tests/nav/test_resolver.py +++ b/tests/nav/test_resolver.py @@ -2,8 +2,7 @@ from pathlib import Path import pytest -from docforge.nav import NavSpec -from docforge.nav import resolve_nav +from docforge.nav import NavSpec, resolve_nav def _write_docs(root: Path, paths: list[str]) -> None: diff --git a/tests/renderers/mcp/test_mcp_content.py b/tests/renderers/mcp/test_mcp_content.py index 1a6b5f6..f528e34 100644 --- a/tests/renderers/mcp/test_mcp_content.py +++ b/tests/renderers/mcp/test_mcp_content.py @@ -2,7 +2,7 @@ import json from pathlib import Path from docforge import MCPRenderer -from docforge.models import Project, Module +from docforge.models import Module, Project def test_mcp_file_content(tmp_path: Path): diff --git a/tests/renderers/mcp/test_mcp_idempotency.py b/tests/renderers/mcp/test_mcp_idempotency.py index 2b215bb..cd56ffb 100644 --- a/tests/renderers/mcp/test_mcp_idempotency.py +++ b/tests/renderers/mcp/test_mcp_idempotency.py @@ -1,7 +1,7 @@ from pathlib import Path from docforge import MCPRenderer -from docforge.models import Project, Module +from docforge.models import Module, Project def test_mcp_idempotent(tmp_path: Path): diff --git a/tests/renderers/mcp/test_mcp_module_coverage.py b/tests/renderers/mcp/test_mcp_module_coverage.py index b7623d4..47f373c 100644 --- a/tests/renderers/mcp/test_mcp_module_coverage.py +++ b/tests/renderers/mcp/test_mcp_module_coverage.py @@ -1,7 +1,7 @@ from pathlib import Path -from docforge.loaders import GriffeLoader, discover_module_paths from docforge import MCPRenderer +from docforge.loaders import GriffeLoader, discover_module_paths 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") } - expected = { - f"modules/{m.path}.json" - for m in project.get_all_modules() - } + expected = {f"modules/{m.path}.json" for m in project.get_all_modules()} missing = expected - emitted assert not missing, f"Missing MCP module JSON files: {missing}" diff --git a/tests/renderers/mcp/test_mcp_structure.py b/tests/renderers/mcp/test_mcp_structure.py index 341488f..ebf1f9b 100644 --- a/tests/renderers/mcp/test_mcp_structure.py +++ b/tests/renderers/mcp/test_mcp_structure.py @@ -1,7 +1,7 @@ from pathlib import Path from docforge import MCPRenderer -from docforge.models import Project, Module +from docforge.models import Module, Project def test_mcp_directory_structure(tmp_path: Path): diff --git a/tests/renderers/mkdocs/test_mkdocs_content.py b/tests/renderers/mkdocs/test_mkdocs_content.py index 80cee04..733a095 100644 --- a/tests/renderers/mkdocs/test_mkdocs_content.py +++ b/tests/renderers/mkdocs/test_mkdocs_content.py @@ -1,7 +1,7 @@ from pathlib import Path from docforge import MkDocsRenderer -from docforge.models import Project, Module +from docforge.models import Module, Project 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() assert "# testpkg" in content - assert "Test package documentation." in content \ No newline at end of file + assert "Test package documentation." in content diff --git a/tests/renderers/mkdocs/test_mkdocs_idempotency.py b/tests/renderers/mkdocs/test_mkdocs_idempotency.py index f894665..94d2f4d 100644 --- a/tests/renderers/mkdocs/test_mkdocs_idempotency.py +++ b/tests/renderers/mkdocs/test_mkdocs_idempotency.py @@ -1,7 +1,7 @@ from pathlib import Path from docforge import MkDocsRenderer -from docforge.models import Project, Module +from docforge.models import Module, Project def test_mkdocs_idempotent(tmp_path: Path): @@ -50,4 +50,4 @@ def test_generate_readme_idempotent(tmp_path: Path): second = readme.read_text() - assert first == second \ No newline at end of file + assert first == second diff --git a/tests/renderers/mkdocs/test_mkdocs_module_coverage.py b/tests/renderers/mkdocs/test_mkdocs_module_coverage.py index 835096e..bf2f16f 100644 --- a/tests/renderers/mkdocs/test_mkdocs_module_coverage.py +++ b/tests/renderers/mkdocs/test_mkdocs_module_coverage.py @@ -1,7 +1,7 @@ from pathlib import Path -from docforge.loaders import GriffeLoader, discover_module_paths from docforge import MkDocsRenderer +from docforge.loaders import GriffeLoader, discover_module_paths 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, ) - emitted = { - p.relative_to(tmp_path).as_posix() - for p in tmp_path.rglob("*.md") - } + emitted = {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()] @@ -34,8 +31,7 @@ def test_mkdocs_emits_all_modules(tmp_path: Path) -> None: for path in module_paths: parts = path.split(".") is_package = any( - other != path and other.startswith(path + ".") - for other in module_paths + other != path and other.startswith(path + ".") for other in module_paths ) if is_package: diff --git a/tests/renderers/mkdocs/test_mkdocs_structure.py b/tests/renderers/mkdocs/test_mkdocs_structure.py index 4c6788f..f204808 100644 --- a/tests/renderers/mkdocs/test_mkdocs_structure.py +++ b/tests/renderers/mkdocs/test_mkdocs_structure.py @@ -1,7 +1,7 @@ from pathlib import Path from docforge import MkDocsRenderer -from docforge.models import Project, Module +from docforge.models import Module, Project def test_mkdocs_directory_structure(tmp_path: Path):