From 73c76bab25ed6115d27b01e68c5efdb9c21daca5 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 8 Jun 2026 19:10:17 +0200 Subject: [PATCH 01/10] Enforce docstring coverage in lint gates Add `interrogate` to rendered project dev dependencies and wire it into both rendered and parent `make lint` targets so docstring coverage is gated. --- Makefile | 1 + template/AGENTS.md.jinja | 5 +++-- template/Makefile.jinja | 1 + template/pyproject.toml.jinja | 1 + tests/helpers/agents_contracts.py | 2 ++ tests/helpers/makefile_contracts.py | 4 ++++ tests/helpers/pyproject_contracts.py | 10 +++++++++- tests/test_github_actions_integration.py | 1 + tests/test_helpers.py | 3 +++ tests/utilities.py | 2 ++ 10 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3355b8c..e0ce73d 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ check-fmt: ## Verify template test formatting lint: ## Run template test lint checks $(UV) ruff check tests/ + $(UV) --with interrogate interrogate --fail-under 100 tests/ typecheck: ## Run template test type checks $(UV) --with pytest --with pytest-copier --with pyyaml --with syrupy --with make-parser ty check tests/ diff --git a/template/AGENTS.md.jinja b/template/AGENTS.md.jinja index bb6c5e4..bfdd31c 100644 --- a/template/AGENTS.md.jinja +++ b/template/AGENTS.md.jinja @@ -81,8 +81,9 @@ - `make check-fmt` runs Ruff formatting checks with `ruff format --check $(PYTHON_TARGETS)`. - `make lint` runs `make lint-python`; `make lint-python` runs - `ruff check $(PYTHON_TARGETS)` and the PyPy-backed Pylint runner against - `$(PYLINT_TARGETS)`. + `ruff check $(PYTHON_TARGETS)`, enforces 100% docstring coverage with + `interrogate --fail-under 100 {{ package_name }}`, and runs the + PyPy-backed Pylint runner against `$(PYLINT_TARGETS)`. - `make typecheck` runs `ty check $(PYTHON_TARGETS)`. - `make test` runs `pytest -v -n $(PYTEST_XDIST_WORKERS)` and honours `WITH_ACT=1` through `RUN_ACT_VALIDATION=1`. diff --git a/template/Makefile.jinja b/template/Makefile.jinja index 3882c4e..19fa0d3 100644 --- a/template/Makefile.jinja +++ b/template/Makefile.jinja @@ -136,6 +136,7 @@ lint: lint-python{% if use_rust %} lint-rust{% endif %} ## Run linters lint-python: build ## Run Python linters $(UV_ENV) $(UV) run ruff check $(PYTHON_TARGETS) + $(UV_ENV) $(UV) run interrogate --fail-under 100 {{ package_name }} $(PYLINT) $(PYLINT_TARGETS) {% if use_rust %} diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 6689f87..9f2fddf 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -10,6 +10,7 @@ dependencies = [] [dependency-groups] dev = [ "pytest", + "interrogate", "pip-audit", "ruff", "pyright", diff --git a/tests/helpers/agents_contracts.py b/tests/helpers/agents_contracts.py index 947108b..5bd3be3 100644 --- a/tests/helpers/agents_contracts.py +++ b/tests/helpers/agents_contracts.py @@ -98,7 +98,9 @@ def _assert_documented_command_flags( ], "lint-python": [ ("AGENTS.md", "ruff check $(PYTHON_TARGETS)"), + ("AGENTS.md", f"interrogate --fail-under 100 {package_name}"), ("Makefile", f"ruff check {python_targets}"), + ("Makefile", f"interrogate --fail-under 100 {package_name}"), ], "typecheck": [ ("AGENTS.md", "ty check $(PYTHON_TARGETS)"), diff --git a/tests/helpers/makefile_contracts.py b/tests/helpers/makefile_contracts.py index 9e579c9..8157f0f 100644 --- a/tests/helpers/makefile_contracts.py +++ b/tests/helpers/makefile_contracts.py @@ -56,6 +56,7 @@ def _parse_makefile_rules(makefile: str) -> dict[str, list[str]]: } def normalise_target(match: re.Match[str]) -> str: + """Replace hyphenated target names with parser-compatible aliases.""" target = match.group(1) return normalised_targets.get(target, target) + ":" @@ -104,6 +105,9 @@ def _assert_makefile_contracts(*, makefile: str, use_rust: bool) -> None: assert "$(UV_ENV) $(UV) run pip-audit" in makefile, ( "expected generated audit target to run pip-audit" ) + assert "$(UV_ENV) $(UV) run interrogate --fail-under 100" in makefile, ( + "expected generated lint target to enforce docstring coverage" + ) if use_rust: assert "TEST_CMD :=" in makefile, ( "expected Rust variant to select nextest or cargo test" diff --git a/tests/helpers/pyproject_contracts.py b/tests/helpers/pyproject_contracts.py index 7e32024..ea2033b 100644 --- a/tests/helpers/pyproject_contracts.py +++ b/tests/helpers/pyproject_contracts.py @@ -32,7 +32,15 @@ def _assert_pyproject_contracts( assert isinstance(dev_dependencies, list), ( "expected generated pyproject.toml to include a dev dependency group" ) - for dependency in ["pytest", "pip-audit", "ruff", "pyright", "ty", "pytest-xdist"]: + for dependency in [ + "pytest", + "interrogate", + "pip-audit", + "ruff", + "pyright", + "ty", + "pytest-xdist", + ]: assert dependency in dev_dependencies, ( f"expected generated dev dependencies to include {dependency}" ) diff --git a/tests/test_github_actions_integration.py b/tests/test_github_actions_integration.py index 0a840ad..3bb15a1 100644 --- a/tests/test_github_actions_integration.py +++ b/tests/test_github_actions_integration.py @@ -173,6 +173,7 @@ def test_run_act_forwards_only_explicit_act_github_token( captured_command: list[str] = [] def fake_run(command: list[str], **_: Any) -> subprocess.CompletedProcess[str]: + """Capture the act subprocess command without executing it.""" captured_command.extend(command) return subprocess.CompletedProcess(command, 0, "stdout", "stderr") diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 976a7fe..b549b42 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -456,6 +456,9 @@ def test_parent_makefile_test_target_uses_requisite_pytest_command() -> None: assert "$(UV) ruff check tests/" in makefile, ( "expected parent Makefile lint target to run Ruff checks" ) + assert "$(UV) --with interrogate interrogate --fail-under 100 tests/" in makefile, ( + "expected parent Makefile lint target to enforce docstring coverage" + ) assert "typecheck: ## Run template test type checks" in makefile, ( "expected parent Makefile to expose a documented typecheck target" ) diff --git a/tests/utilities.py b/tests/utilities.py index 29a7fc7..bc5b9ea 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -39,6 +39,7 @@ def _resolved_socket_from_docker_host( docker_host: str, allowed_dirs: tuple[Path, ...] ) -> Path | None: + """Return a trusted Unix socket path parsed from ``DOCKER_HOST``.""" parsed = urlparse(docker_host) if parsed.scheme != "unix" or parsed.netloc or not parsed.path: return None @@ -54,6 +55,7 @@ def _resolved_socket_from_docker_host( def _user_podman_socket() -> Path | None: + """Return the current user's Podman socket when it exists under runtime dir.""" socket_path = Path(f"/run/user/{os.getuid()}/podman/podman.sock") if not socket_path.exists(): return None From edb85d59933025d585290482a4c238633d1b5109 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 12 Jun 2026 19:31:26 +0200 Subject: [PATCH 02/10] Bump template pyo3 to 0.29.0 to clear RUSTSEC-2026-0176 `cargo audit` in the generated CI workflow fails on pyo3 0.28.3, which is affected by RUSTSEC-2026-0176 (out-of-bounds read in `nth` / `nth_back` for `PyList` and `PyTuple` iterators). This caused the `Audit dependencies` step to fail in the act validation test before the coverage step could run. The advisory's fix is pyo3 >= 0.29.0. The rendered extension crate compiles unchanged against 0.29.0 (the `Bound` module API is stable across the bump), and `cargo audit` on the rendered lockfile is clean. --- .../{% if use_rust %}rust_extension{% endif %}/Cargo.toml.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/{% if use_rust %}rust_extension{% endif %}/Cargo.toml.jinja b/template/{% if use_rust %}rust_extension{% endif %}/Cargo.toml.jinja index a3d8e7d..588c21e 100644 --- a/template/{% if use_rust %}rust_extension{% endif %}/Cargo.toml.jinja +++ b/template/{% if use_rust %}rust_extension{% endif %}/Cargo.toml.jinja @@ -8,7 +8,7 @@ name = "_{{ package_name }}_rs" crate-type = ["cdylib", "rlib"] [dependencies] -pyo3 = { version = "0.28.3", features = ["extension-module"] } +pyo3 = { version = "0.29.0", features = ["extension-module"] } [lints.clippy] pedantic = { level = "warn", priority = -1 } From a44bfeda3c3b1dff22560663f6aedd27ff26173d Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 16:51:46 +0200 Subject: [PATCH 03/10] Assert pytest-timeout in generated dependencies Require `pytest-timeout` in the rendered pyproject contract so the existing template dependency cannot be dropped without a failing test. --- tests/helpers/pyproject_contracts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers/pyproject_contracts.py b/tests/helpers/pyproject_contracts.py index ea2033b..4425daf 100644 --- a/tests/helpers/pyproject_contracts.py +++ b/tests/helpers/pyproject_contracts.py @@ -39,6 +39,7 @@ def _assert_pyproject_contracts( "ruff", "pyright", "ty", + "pytest-timeout", "pytest-xdist", ]: assert dependency in dev_dependencies, ( From bb97f873d514d211cc6369cca2c01801e0febd93 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 16:59:49 +0200 Subject: [PATCH 04/10] Document and test docstring lint gate Update parent and generated project documentation for the Interrogate coverage check, and add a rendered-project behavioural test proving `make lint` passes with complete docstrings and fails when Interrogate finds missing private docstring coverage. --- docs/developers-guide.md | 3 +- template/docs/developers-guide.md | 13 +++-- template/docs/users-guide.md.jinja | 6 ++- tests/test_interrogate_lint.py | 81 ++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 tests/test_interrogate_lint.py diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 1c91f98..52d8a50 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -12,7 +12,8 @@ template itself. - `make help` — lists all `##`-annotated targets. - `make check-fmt` — runs Ruff formatting checks against the parent template test suite. -- `make lint` — runs Ruff lint checks against the parent template test suite. +- `make lint` — runs Ruff lint checks and `interrogate --fail-under 100` against + the `tests/` directory in the parent template test suite. - `make typecheck` — runs `ty check` against the parent template test suite, supplying the test dependencies through `uvx`. - `make test` — runs the template test suite via `uvx`, supplying diff --git a/template/docs/developers-guide.md b/template/docs/developers-guide.md index 5723a94..9c90889 100644 --- a/template/docs/developers-guide.md +++ b/template/docs/developers-guide.md @@ -9,6 +9,9 @@ The public entrypoint for formatting, linting, typechecking, and tests is failure, and changes should be reconciled with the aggregate gate before being considered complete. +`make lint` runs Ruff, `interrogate --fail-under 100` for 100% generated +package docstring coverage, and Pylint. + Run `make audit` as the dependency vulnerability gate. It runs `pip-audit` for Python dependencies, and Rust-enabled projects also run `cargo audit` from the `rust_extension` crate directory. @@ -32,10 +35,12 @@ actions under `.github/`. - `.github/workflows/ci.yml` runs on pushes to `main` and on pull requests. It sets up Python 3.13, installs `uv`, validates the generated `Makefile` with - `mbake`, runs `make build`, `make check-fmt`, `make lint`, `make typecheck`, - and `make audit`, then delegates coverage generation to the shared coverage - action. When the Rust extension is enabled, it also sets up Rust, installs - Rust lint and test tools, and passes `rust_extension/Cargo.toml` to coverage. + `mbake`, runs `make build`, `make check-fmt`, + `make lint` (Ruff + `interrogate --fail-under 100` + Pylint), `make + typecheck`, and `make audit`, then delegates coverage generation to the shared + coverage action. When the Rust extension is enabled, it also sets up Rust, + installs Rust lint and test tools, and passes `rust_extension/Cargo.toml` to + coverage. - `.github/workflows/act-validation.yml` runs rendered workflow validation in a separate workflow. It installs `act`, checks Docker availability, and runs `make test WITH_ACT=1` outside the coverage path. diff --git a/template/docs/users-guide.md.jinja b/template/docs/users-guide.md.jinja index 2b9e639..39b4217 100644 --- a/template/docs/users-guide.md.jinja +++ b/template/docs/users-guide.md.jinja @@ -14,8 +14,10 @@ these targets in order: - `test`: run pytest and, when Rust is enabled, Rust tests. - `audit`: run `pip-audit` and, when Rust is enabled, `cargo audit`. -The `lint-python` target runs Ruff followed by Pylint via a PyPy-backed runner. -The Pylint runner is installed through `uv tool run` from the pinned +The `lint-python` target runs Ruff, then Interrogate with +`interrogate --fail-under 100 {{ package_name }}` to enforce 100% docstring +coverage for the package, then Pylint via a PyPy-backed runner. The Pylint +runner is installed through `uv tool run` from the pinned `pylint-pypy-shim` repository. Pytest discovery is limited to the top-level `tests/` tree. Keep generated diff --git a/tests/test_interrogate_lint.py b/tests/test_interrogate_lint.py new file mode 100644 index 0000000..aae781d --- /dev/null +++ b/tests/test_interrogate_lint.py @@ -0,0 +1,81 @@ +"""Validate rendered docstring coverage lint behaviour.""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +from pytest_copier.plugin import CopierFixture + +from tests.helpers.rendering import render_project + + +def test_generated_make_lint_enforces_interrogate_docstrings( + copier: CopierFixture, tmp_path: Path +) -> None: + """Validate rendered ``make lint`` fails on missing private docstrings. + + Parameters + ---------- + copier : CopierFixture + Fixture used to render the template into a temporary project. + tmp_path : Path + Temporary directory used as the generated project root. + + Returns + ------- + None + The test passes when a complete generated project passes ``make lint`` + and a private helper without a docstring fails at the Interrogate gate. + """ + package_name = "lint_docstrings_pkg" + project = render_project( + tmp_path / "lint-docstrings", + copier, + project_name="LintDocstrings", + package_name=package_name, + use_rust=False, + ) + make = shutil.which("make") + assert make is not None, "expected make to be available for generated tests" + + passing_result = subprocess.run( + [make, "lint"], + cwd=project.path, + check=False, + capture_output=True, + text=True, + ) + assert passing_result.returncode == 0, ( + f"expected generated make lint to pass with complete docstrings:\n" + f"{passing_result.stdout}\n{passing_result.stderr}" + ) + + pure_module = project / package_name / "pure.py" + undocumented_helper = ( + '\n\ndef _missing_interrogate_docstring() -> str:\n return "undocumented"\n' + ) + pure_module.write_text( + pure_module.read_text(encoding="utf-8") + undocumented_helper, + encoding="utf-8", + ) + + failing_result = subprocess.run( + [make, "lint"], + cwd=project.path, + check=False, + capture_output=True, + text=True, + ) + output = f"{failing_result.stdout}\n{failing_result.stderr}" + assert failing_result.returncode != 0, ( + "expected generated make lint to fail when Interrogate finds a missing " + f"private docstring:\n{output}" + ) + assert "RESULT: FAILED" in output, ( + "expected generated make lint to run the Interrogate docstring gate" + ) + assert "actual: 75.0%" in output, ( + "expected Interrogate output to report reduced docstring coverage" + ) From 09daa095ccac636f3d21946438f93bfbf64363fa Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 22:04:27 +0200 Subject: [PATCH 05/10] Align Interrogate with Python targets Run generated Interrogate linting against `$(PYTHON_TARGETS)` so its coverage scope matches the Python lint target set. Update the generated AGENTS guidance, user and developer docs, command contracts, and the behavioural coverage expectation for the wider target set. --- template/AGENTS.md.jinja | 2 +- template/Makefile.jinja | 2 +- template/docs/developers-guide.md | 6 +++--- template/docs/users-guide.md.jinja | 4 ++-- tests/helpers/agents_contracts.py | 4 ++-- tests/test_interrogate_lint.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/template/AGENTS.md.jinja b/template/AGENTS.md.jinja index bfdd31c..220f1e9 100644 --- a/template/AGENTS.md.jinja +++ b/template/AGENTS.md.jinja @@ -82,7 +82,7 @@ `ruff format --check $(PYTHON_TARGETS)`. - `make lint` runs `make lint-python`; `make lint-python` runs `ruff check $(PYTHON_TARGETS)`, enforces 100% docstring coverage with - `interrogate --fail-under 100 {{ package_name }}`, and runs the + `interrogate --fail-under 100 $(PYTHON_TARGETS)`, and runs the PyPy-backed Pylint runner against `$(PYLINT_TARGETS)`. - `make typecheck` runs `ty check $(PYTHON_TARGETS)`. - `make test` runs `pytest -v -n $(PYTEST_XDIST_WORKERS)` and honours diff --git a/template/Makefile.jinja b/template/Makefile.jinja index 19fa0d3..e73647f 100644 --- a/template/Makefile.jinja +++ b/template/Makefile.jinja @@ -136,7 +136,7 @@ lint: lint-python{% if use_rust %} lint-rust{% endif %} ## Run linters lint-python: build ## Run Python linters $(UV_ENV) $(UV) run ruff check $(PYTHON_TARGETS) - $(UV_ENV) $(UV) run interrogate --fail-under 100 {{ package_name }} + $(UV_ENV) $(UV) run interrogate --fail-under 100 $(PYTHON_TARGETS) $(PYLINT) $(PYLINT_TARGETS) {% if use_rust %} diff --git a/template/docs/developers-guide.md b/template/docs/developers-guide.md index 9c90889..2f36d3b 100644 --- a/template/docs/developers-guide.md +++ b/template/docs/developers-guide.md @@ -9,8 +9,8 @@ The public entrypoint for formatting, linting, typechecking, and tests is failure, and changes should be reconciled with the aggregate gate before being considered complete. -`make lint` runs Ruff, `interrogate --fail-under 100` for 100% generated -package docstring coverage, and Pylint. +`make lint` runs Ruff, `interrogate --fail-under 100 $(PYTHON_TARGETS)` for +100% Python target docstring coverage, and Pylint. Run `make audit` as the dependency vulnerability gate. It runs `pip-audit` for Python dependencies, and Rust-enabled projects also run `cargo audit` from the @@ -36,7 +36,7 @@ actions under `.github/`. - `.github/workflows/ci.yml` runs on pushes to `main` and on pull requests. It sets up Python 3.13, installs `uv`, validates the generated `Makefile` with `mbake`, runs `make build`, `make check-fmt`, - `make lint` (Ruff + `interrogate --fail-under 100` + Pylint), `make + `make lint` (Ruff + `interrogate --fail-under 100 $(PYTHON_TARGETS)` + Pylint), `make typecheck`, and `make audit`, then delegates coverage generation to the shared coverage action. When the Rust extension is enabled, it also sets up Rust, installs Rust lint and test tools, and passes `rust_extension/Cargo.toml` to diff --git a/template/docs/users-guide.md.jinja b/template/docs/users-guide.md.jinja index 39b4217..ab295e9 100644 --- a/template/docs/users-guide.md.jinja +++ b/template/docs/users-guide.md.jinja @@ -15,8 +15,8 @@ these targets in order: - `audit`: run `pip-audit` and, when Rust is enabled, `cargo audit`. The `lint-python` target runs Ruff, then Interrogate with -`interrogate --fail-under 100 {{ package_name }}` to enforce 100% docstring -coverage for the package, then Pylint via a PyPy-backed runner. The Pylint +`interrogate --fail-under 100 $(PYTHON_TARGETS)` to enforce 100% docstring +coverage for the Python targets, then Pylint via a PyPy-backed runner. The Pylint runner is installed through `uv tool run` from the pinned `pylint-pypy-shim` repository. diff --git a/tests/helpers/agents_contracts.py b/tests/helpers/agents_contracts.py index 5bd3be3..b1e321a 100644 --- a/tests/helpers/agents_contracts.py +++ b/tests/helpers/agents_contracts.py @@ -98,9 +98,9 @@ def _assert_documented_command_flags( ], "lint-python": [ ("AGENTS.md", "ruff check $(PYTHON_TARGETS)"), - ("AGENTS.md", f"interrogate --fail-under 100 {package_name}"), + ("AGENTS.md", "interrogate --fail-under 100 $(PYTHON_TARGETS)"), ("Makefile", f"ruff check {python_targets}"), - ("Makefile", f"interrogate --fail-under 100 {package_name}"), + ("Makefile", f"interrogate --fail-under 100 {python_targets}"), ], "typecheck": [ ("AGENTS.md", "ty check $(PYTHON_TARGETS)"), diff --git a/tests/test_interrogate_lint.py b/tests/test_interrogate_lint.py index aae781d..507a8d5 100644 --- a/tests/test_interrogate_lint.py +++ b/tests/test_interrogate_lint.py @@ -76,6 +76,6 @@ def test_generated_make_lint_enforces_interrogate_docstrings( assert "RESULT: FAILED" in output, ( "expected generated make lint to run the Interrogate docstring gate" ) - assert "actual: 75.0%" in output, ( + assert "actual: 83.3%" in output, ( "expected Interrogate output to report reduced docstring coverage" ) From fad9858b71be537229b4947429f5ddf451a3f6b4 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 23:14:31 +0200 Subject: [PATCH 06/10] Complete Interrogate target documentation Align the generated AGENTS contract with the rendered Python target list and document the generated `lint-python` Interrogate scope in the parent and template developer guides. --- docs/developers-guide.md | 4 +++- template/AGENTS.md.jinja | 2 +- template/docs/developers-guide.md | 2 +- tests/helpers/agents_contracts.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 52d8a50..0f9869b 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -51,7 +51,9 @@ runs `cargo audit` in the Rust extension crate. The generated lint targets are split by language: -- `lint-python` runs Ruff and Pylint via the pinned PyPy-backed runner. +- `lint-python` runs Ruff, `interrogate --fail-under 100` for 100% docstring + coverage across `$(PYTHON_TARGETS)`, and Pylint via the pinned PyPy-backed + runner. - `lint-rust` exists only when `use_rust` is enabled and runs rustdoc, Clippy, and Whitaker. - `lint` delegates to the applicable language-specific targets. diff --git a/template/AGENTS.md.jinja b/template/AGENTS.md.jinja index 220f1e9..c998662 100644 --- a/template/AGENTS.md.jinja +++ b/template/AGENTS.md.jinja @@ -82,7 +82,7 @@ `ruff format --check $(PYTHON_TARGETS)`. - `make lint` runs `make lint-python`; `make lint-python` runs `ruff check $(PYTHON_TARGETS)`, enforces 100% docstring coverage with - `interrogate --fail-under 100 $(PYTHON_TARGETS)`, and runs the + `interrogate --fail-under 100 {{ package_name }} tests`, and runs the PyPy-backed Pylint runner against `$(PYLINT_TARGETS)`. - `make typecheck` runs `ty check $(PYTHON_TARGETS)`. - `make test` runs `pytest -v -n $(PYTEST_XDIST_WORKERS)` and honours diff --git a/template/docs/developers-guide.md b/template/docs/developers-guide.md index 2f36d3b..98ff017 100644 --- a/template/docs/developers-guide.md +++ b/template/docs/developers-guide.md @@ -10,7 +10,7 @@ failure, and changes should be reconciled with the aggregate gate before being considered complete. `make lint` runs Ruff, `interrogate --fail-under 100 $(PYTHON_TARGETS)` for -100% Python target docstring coverage, and Pylint. +100% docstring coverage across `$(PYTHON_TARGETS)`, and Pylint. Run `make audit` as the dependency vulnerability gate. It runs `pip-audit` for Python dependencies, and Rust-enabled projects also run `cargo audit` from the diff --git a/tests/helpers/agents_contracts.py b/tests/helpers/agents_contracts.py index b1e321a..9e9c5e5 100644 --- a/tests/helpers/agents_contracts.py +++ b/tests/helpers/agents_contracts.py @@ -98,7 +98,7 @@ def _assert_documented_command_flags( ], "lint-python": [ ("AGENTS.md", "ruff check $(PYTHON_TARGETS)"), - ("AGENTS.md", "interrogate --fail-under 100 $(PYTHON_TARGETS)"), + ("AGENTS.md", f"interrogate --fail-under 100 {python_targets}"), ("Makefile", f"ruff check {python_targets}"), ("Makefile", f"interrogate --fail-under 100 {python_targets}"), ], From b766307ba7d8d911c5a34ebf85250a4e0dc83ef4 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 23:48:57 +0200 Subject: [PATCH 07/10] Avoid brittle Interrogate coverage assertion Parse the Interrogate actual coverage percentage and assert it drops below 100% instead of pinning the behavioural test to a specific rendered-project baseline count. --- tests/test_interrogate_lint.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_interrogate_lint.py b/tests/test_interrogate_lint.py index 507a8d5..b42e6ad 100644 --- a/tests/test_interrogate_lint.py +++ b/tests/test_interrogate_lint.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import shutil import subprocess from pathlib import Path @@ -76,6 +77,11 @@ def test_generated_make_lint_enforces_interrogate_docstrings( assert "RESULT: FAILED" in output, ( "expected generated make lint to run the Interrogate docstring gate" ) - assert "actual: 83.3%" in output, ( + actual_match = re.search(r"actual:\s+([0-9]+(?:\.[0-9]+)?)%", output) + assert actual_match is not None, ( + f"expected Interrogate output to report actual docstring coverage:\n{output}" + ) + actual_coverage = float(actual_match.group(1)) + assert actual_coverage < 100.0, ( "expected Interrogate output to report reduced docstring coverage" ) From e73e8533b27d02ee137b8f2ab749242bd7662188 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 23:54:30 +0200 Subject: [PATCH 08/10] Record Interrogate in quality gate ADR Update the template quality gate decision record so the Python lint tier mentions Interrogate with `--fail-under 100` alongside Ruff and Pylint. --- docs/adr-001-template-quality-gates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr-001-template-quality-gates.md b/docs/adr-001-template-quality-gates.md index d82ddbf..2905fd7 100644 --- a/docs/adr-001-template-quality-gates.md +++ b/docs/adr-001-template-quality-gates.md @@ -16,7 +16,7 @@ failures are predictable. Generated projects use `make all` as the public aggregate gate. The `lint` target delegates to language-specific targets: -- `lint-python` runs Ruff and Pylint through the PyPy-backed runner. +- `lint-python` runs Ruff, Interrogate (`--fail-under 100`), and Pylint through the PyPy-backed runner. - `lint-rust` is rendered only when `use_rust` is enabled and runs rustdoc, Clippy, and Whitaker. From bfaf3f69eb5af518d094886f711349dd5a7d8bec Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 15 Jun 2026 09:16:48 +0200 Subject: [PATCH 09/10] Use Python target variable in AGENTS lint docs Keep generated AGENTS guidance consistent with the surrounding Makefile command examples by documenting the Interrogate target as `$(PYTHON_TARGETS)`, and update the AGENTS contract to assert that form. --- template/AGENTS.md.jinja | 2 +- tests/helpers/agents_contracts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/template/AGENTS.md.jinja b/template/AGENTS.md.jinja index c998662..220f1e9 100644 --- a/template/AGENTS.md.jinja +++ b/template/AGENTS.md.jinja @@ -82,7 +82,7 @@ `ruff format --check $(PYTHON_TARGETS)`. - `make lint` runs `make lint-python`; `make lint-python` runs `ruff check $(PYTHON_TARGETS)`, enforces 100% docstring coverage with - `interrogate --fail-under 100 {{ package_name }} tests`, and runs the + `interrogate --fail-under 100 $(PYTHON_TARGETS)`, and runs the PyPy-backed Pylint runner against `$(PYLINT_TARGETS)`. - `make typecheck` runs `ty check $(PYTHON_TARGETS)`. - `make test` runs `pytest -v -n $(PYTEST_XDIST_WORKERS)` and honours diff --git a/tests/helpers/agents_contracts.py b/tests/helpers/agents_contracts.py index 9e9c5e5..b1e321a 100644 --- a/tests/helpers/agents_contracts.py +++ b/tests/helpers/agents_contracts.py @@ -98,7 +98,7 @@ def _assert_documented_command_flags( ], "lint-python": [ ("AGENTS.md", "ruff check $(PYTHON_TARGETS)"), - ("AGENTS.md", f"interrogate --fail-under 100 {python_targets}"), + ("AGENTS.md", "interrogate --fail-under 100 $(PYTHON_TARGETS)"), ("Makefile", f"ruff check {python_targets}"), ("Makefile", f"interrogate --fail-under 100 {python_targets}"), ], From 13d511ac08e2b10b44009ae77b39889888813ebc Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 15 Jun 2026 09:33:15 +0200 Subject: [PATCH 10/10] Document Interrogate lint coverage Expand the Interrogate behavioural test module docstring and include the Interrogate docstring gate in the README tooling list and generated quality gate diagram. --- README.md | 8 ++++++-- tests/test_interrogate_lint.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f4db656..6818522 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Run `copier copy` and answer the prompts to generate a project. The test suite relies on the `pytest-copier` plugin and renders generated projects that run Ruff, Pylint via a PyPy-backed runner, `ty`, pytest, and, when the Rust extension is enabled, Clippy, Whitaker, and nextest-aware Rust tests. +Generated Python linting also runs Interrogate as a 100% docstring-coverage +gate. Run the parent template tests through the repository `Makefile`. Run `make help` to list the available parent Makefile targets. Parent gates include @@ -30,8 +32,9 @@ when `act` and Docker are available. Parent repository CI runs this mode in a separate act-validation workflow so rendered in-template GitHub workflows do not hold up the normal template test workflow. -Generated projects install and run their own tooling, including Ruff, Pylint via -PyPy, `ty`, pytest, and, when Rust is enabled, Clippy, Whitaker, and nextest. +Generated projects install and run their own tooling, including Ruff, +Interrogate, Pylint via PyPy, `ty`, pytest, and, when Rust is enabled, Clippy, +Whitaker, and nextest. You can also run `scripts/setup_test_deps.sh` to install parent test dependencies into the current Python environment manually. @@ -54,6 +57,7 @@ flowchart LR All --> Test[test] Lint --> RuffCheck[ruff check] + Lint --> Interrogate[interrogate --fail-under 100] Lint --> PylintPyPy[pylint-pypy via PyPy] Typecheck --> TyCheck[ty check] Audit --> PipAudit[pip-audit] diff --git a/tests/test_interrogate_lint.py b/tests/test_interrogate_lint.py index b42e6ad..a6aa930 100644 --- a/tests/test_interrogate_lint.py +++ b/tests/test_interrogate_lint.py @@ -1,4 +1,14 @@ -"""Validate rendered docstring coverage lint behaviour.""" +"""Validate rendered docstring coverage lint behaviour. + +These integration tests render a project from the Copier template and exercise +its public ``make lint`` target instead of inspecting only generated text. They +prove that a clean render passes the full Python lint tier, then mutate the +rendered package to introduce a Ruff-clean missing private docstring and verify +that Interrogate fails the target. + +The module protects the template contract that generated projects enforce 100% +docstring coverage through their normal lint workflow. +""" from __future__ import annotations