diff --git a/CHANGELOG.md b/CHANGELOG.md index f37dec7..a4d3172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Changed - trivy: use 0.76.0.1 +- `check:deptry` and `check:trivy` ignore `.claude/`, so a git worktree created + there - a complete second checkout inside the repository - no longer fails + `task check` with findings from its own copy of the tests and its own lock + file (#109) +- `ARG002` on an argument whose presence the framework fixes - `inputs` in a + workflow task with no input ports - is a settled suppression in the shipped + rules, beside `PLR0913` and `PLR0917`, so it no longer needs a human decision + per project; `plugin-implementation` says why renaming it to `_inputs` is the + wrong answer, and the example plugin now carries the reason above its + `# noqa` (#108) +- the example files say to replace them with the first real task in the same + change, rather than to remove them after bootstrapping: an emptied package + fails `task check` three times over - deptry on the now unused + `cmem-plugin-base`, pytest on collecting no tests, and the coverage badge on + a package with no statements (#107) ## [9.9.0] 2026-10-13 diff --git a/CLAUDE.md b/CLAUDE.md index 6c97a99..61e3b5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -576,3 +576,23 @@ added. A project that wants the check can add a `check:links` task to its own `TaskfileCustom.yaml`, which is exactly what that file is for, and run it on a schedule rather than on every commit — which is the cadence link rot deserves. + +### The coverage badge is not made to tolerate a package with no code + +`check:pytest` ends by generating the coverage badge, and on a package with +zero statements that step fails: +`ValueError: Computed line rate (0) is different from the one in the file +(1.0)` — `coverage.xml` records `line-rate="1.0"` for nothing at all, genbadge +recomputes it as 0 and refuses the file. Report #107 asked for the badge step +to be skipped when the package has no lines, so that an emptied package fails +on deptry alone, which is the check that is actually right. + +Declined, because the state it would rescue is one the template now tells you +not to create. The example files say to replace them with the first real task +in the same change, precisely because a package emptied beforehand has no green +baseline to start from. And the badge is not the first thing to go: `pytest` +exits 5 on having collected no tests before genbadge runs at all, and deptry +reports `DEP002 'cmem-plugin-base' defined as a dependency but not used`. Three +correct signals say the same true thing, so silencing the third one changes +nothing a user would notice — while the shell conditional it needs would have +to hold on the Windows branch of that task too. diff --git a/src/Taskfile.yaml b/src/Taskfile.yaml index aade49a..1c75d8c 100644 --- a/src/Taskfile.yaml +++ b/src/Taskfile.yaml @@ -164,10 +164,14 @@ tasks: deps: - prepare cmds: + # .claude/ is skipped because a git worktree created there is a second + # checkout inside the repository, whose lock file is scanned as if it + # were this one - on an older branch, with older findings. - > poetry run trivy fs --include-dev-deps --scanners vuln + --skip-dirs .claude --skip-files .poetry/plugins/poetry.lock --exit-code 1 . @@ -177,7 +181,12 @@ tasks: deps: - prepare cmds: - - poetry run deptry . + # deptry's default excludes cover a tests directory at the root but not + # a nested copy of one, so the worktree described above would be read as + # source and its test imports reported. + - > + poetry run deptry . + --extend-exclude '\.claude' check:ruff: desc: Complain about everything else diff --git a/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja b/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja index 4e9be1d..e1c0f21 100644 --- a/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja +++ b/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Example tests. -Remove this and other example files after bootstrapping your project. +Replace these with tests for your first real module and delete the example +files in the same change - a package emptied beforehand fails `task check` on +deptry and on the coverage badge. """ from {{ package_dir }}.example import double diff --git a/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja b/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja index f8c3cc2..b9f9de1 100644 --- a/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja +++ b/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Plugin tests. -Remove this and other example files after bootstrapping your project. +Replace these with tests for your first real task and delete the example +files in the same change - a package emptied beforehand fails `task check` on +deptry and on the coverage badge. """ import os diff --git a/src/{{ '.claude' }}/rules/copier-template.md b/src/{{ '.claude' }}/rules/copier-template.md index 61277e6..05e41f2 100644 --- a/src/{{ '.claude' }}/rules/copier-template.md +++ b/src/{{ '.claude' }}/rules/copier-template.md @@ -60,6 +60,14 @@ reason above it. Neither escape ruff suggests is available, since keyword-only arguments answer `PLR0917` but not `PLR0913`, and folding the arguments into one object breaks the one-argument-per-option mapping the framework relies on. +`ARG002` is settled on the same grounds, one argument further in: a method the +framework calls has the signature the framework declares, so an argument the +implementation does not need cannot be dropped - a workflow task with no input +ports never reads `inputs`. Suppress it on the argument, again with the reason +above it. Renaming it to `_inputs` silences ruff as well, and is wrong here: +the name belongs to an API the caller may use by keyword, as this project's own +tests do. + ## Every user visible change gets a changelog entry `CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/). Add an diff --git a/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md b/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md index 7897ada..3903efa 100644 --- a/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md +++ b/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md @@ -350,6 +350,33 @@ start failing on it without anything in the task changing. Suppress it on the constructor only - it stays a real finding on an ordinary function, which is why it is not in the `ignore` list in `pyproject.toml`. +## An argument the task never reads + +`ARG002` complains about an unused method argument, and a task declaring +`FixedNumberOfInputs([])` generates its own entities - so `execute()` never +reads `inputs`, while `WorkflowPlugin` decides the signature all the same. A +`TransformPlugin` or a `PluginAction` ignoring an argument the framework +supplies is the same case. + +This is the standing exception the lint rules describe, so it needs no fresh +decision either. Suppress the code on the argument, with the reason above it: + +```python +def execute( + self, + # This task has no input ports, so it never reads inputs - but the + # signature is the one WorkflowPlugin declares. + inputs: Sequence[Entities], # noqa: ARG002 + context: ExecutionContext, +) -> Entities: +``` + +Do not rename it to `_inputs` instead. Ruff accepts the underscore and mypy +permits the rename, but the name is part of the API the caller uses: +`execute(inputs=(), context=TestExecutionContext())` is a keyword call, which is +how `tests/test_example.py` and the `plugin-testing` skill invoke a task, and it +raises `TypeError` against a renamed argument. + ## Parameters that carry secrets A password, token or API key is typed, never a plain string: diff --git a/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja b/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja index ce9466b..5c2f67b 100644 --- a/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja +++ b/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Example module -Remove this and other example files after bootstrapping your project. +Replace this with your first real module and delete the example files in the +same change - a package emptied beforehand fails `task check` on deptry and on +the coverage badge. """ diff --git a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja index f75ed80..f30efab 100644 --- a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja +++ b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja @@ -1,6 +1,8 @@ """lifetime(age) transform plugin module -Remove this and other example files after bootstrapping your project. +Replace this with your first real task and delete the example files in the +same change - a package emptied beforehand fails `task check` on deptry and on +the coverage badge. """ import datetime diff --git a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja index 4e5acbb..c0d668d 100644 --- a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja +++ b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Random values workflow plugin module -Remove this and other example files after bootstrapping your project. +Replace this with your first real task and delete the example files in the +same change - a package emptied beforehand fails `task check` on deptry and on +the coverage badge. """ import uuid @@ -79,6 +81,8 @@ class DollyPlugin(WorkflowPlugin): def execute( self, + # This task has no input ports, so it never reads inputs - but the + # signature is the one WorkflowPlugin declares. inputs: Sequence[Entities], # noqa: ARG002 context: ExecutionContext, ) -> Entities: