Available Checks

Every issue a check reports carries an impact level:

  • critical — blocks release (preen release refuses to proceed)

  • important — should be fixed, but can be overridden with informed consent

  • info — advisory

Fleet conformance

template

Copier adoption and drift. Critical if the repo has no .copier-answers.yml. Important if _commit is not a release tag at all — a mangled value like v1.0.1.0.1, a git describe string like v1.2.0-3-gabc1234, or a bare SHA — since copier update cannot resolve any of them. Important if it records a moving major tag like v1, which makes copier update compare the tag against itself and no-op — re-run preen adopt to pin the concrete release tag. Drift between the recorded concrete tag and the latest py-canon v* tag (queried via git ls-remote, skipped gracefully offline) is informational: the repo did nothing wrong when the template moves, and gating on it would turn the whole fleet red on every py-canon release.

workflows

The four canon workflow files (ci.yml, docs.yml, release.yml, dependabot-auto-merge.yml) must be thin callers of py-canon’s reusable workflows, not materialized copies. Important for each file that exists but does not call the matching gojiplus/py-canon/.github/workflows/reusable-* workflow — a copy stops receiving fleet fixes the moment it is written.

ci-matrix

The Python versions CI actually runs must cover the requires-python floor. For a canon shim (one calling gojiplus/py-canon/.github/workflows/reusable-ci.yml) that means the python-versions input if the shim passes one, and otherwise the reusable workflow’s own default, fetched from the ref on the uses: line. Important when a version below the floor is in the matrix — that leg cannot resolve and uv sync exits 2. A floor that is merely never run is advisory on a shim, since the matrix came from py-canon rather than from the repo; on a hand-written matrix it stays important. When the reusable workflow cannot be fetched, the comparison is skipped with an info note rather than reported as verified.

citation

CITATION.cff exists, parses as YAML, and has the core CFF keys. Its version must match project.version: important when the two disagree, info when the key is absent. preen fix citation rewrites the version line in place, leaving the rest of a hand-written file alone.

files

README (any of the common spellings) and .gitignore exist (sp-repo-review PY002/PY008). preen fix files writes a standard Python .gitignore when it is missing.

precommit

.pre-commit-config.yaml exists and parses as YAML. Deliberately does not police which hooks it configures — CI is the gate; pre-commit is the fast local echo of it.

structure

Project layout: tests/ and examples/ at the repo root, src/ layout, no committed __pycache__ or .pyc files.

runtime-assets

Packaged runtime data must carry a schema. Tabular data uses Parquet; structured records may use Protobuf. CSV, TSV, compressed variants, and opaque archives inside import packages are critical failures. Serialized model files are also critical: publish them on Hugging Face and resolve them at runtime. Modules that access Hugging Face must declare a revision pinned to the full 40-character commit SHA.

A file that [tool.uv.build-backend] keeps out of the wheel — via source-exclude or wheel-exclude — is reported at info instead: nothing installs it, so it is source-tree hygiene rather than a packaging defect. Unlike the scanning checks, this one ignores [tool.ruff] excludes: whether a file ships in the wheel is not a question a lint setting gets to answer.

version

project.version is authoritative and the matching Git tag identifies a release. Literal __version__ = "..." assignments and other copies are flagged; runtime code should read installed package metadata.

changelog

CHANGELOG.md follows Keep a Changelog structure. Important if the file is missing, or has neither a ## [Unreleased] heading nor a version heading. Info if version headings exist but there’s no [Unreleased] section. preen release refuses to tag without a changelog entry for the release.

license

[project.license] follows PEP 639. Important: no license at all; the deprecated { text = ... } / { file = ... } table form; a string value that isn’t a structurally valid SPDX expression; redundant License :: trove classifiers alongside license. Info: an SPDX identifier outside preen’s allowlist (advisory — verify at spdx.org), or a missing license-files when a LICENSE/LICENCE/COPYING file exists at the repo root. preen fix license migrates unambiguous table-form values to an SPDX string, drops the redundant classifiers, and adds license-files.

pytest-config

pytest is configured to fail on what it should fail on: minversion, testpaths, log_level, xfail_strict, filterwarnings, and -ra, --strict-config, --strict-markers in addopts (sp-repo-review PP301–PP309). Without filterwarnings, a DeprecationWarning from a dependency is invisible until the release that removes the API; without --strict-markers, a typo in a marker name silently selects nothing.

Important for each missing setting. These were informational until py-canon 1.3.0 put the whole set in the template — gating before that would have failed every repo in the fleet for following a standard that did not ask for this yet. copier update delivers them now, and preen fix pytest-config writes them into a repo directly. Never critical: a missing setting is not a broken build.

PP301 — no pytest table at all — stays informational, since a repo without one may have no tests to configure.

python-floor

requires-python admits nothing older than the floor the fleet standard declares, currently 3.12. The whole specifier decides: >3.10 still admits 3.10.1 and ~=3.11 admits 3.11, so both are flagged, while >=3.10,>=3.12 has an effective floor of 3.12 and passes.

Off by default, deliberately. STANDARD.md had declared >=3.12 for some time while 30 of 51 adopted repos shipped >=3.11 and every one passed, because no check compared the two. Turning it on for everyone at once is how a check gets switched off rather than obeyed. Enable it per repo as each one migrates:

[tool.preen]
enforce_python_floor = true

metadata

Three independent pyproject.toml checks. build-system: important if an existing table does not use the uv_build backend with one requirement in the approved series. Newer minimums are accepted within >=0.12.5,<0.13; the exclusive upper bound must stay at <0.13. Package-name normalization and equivalent version spellings are accepted. Extras, markers, and direct URLs are not. preen adopt --release-migration applies the template default. requires-python: important if it has an upper bound (<, <=, ==, ===, ~=), which caps installs on future Pythons for no benefit (sp-repo-review PP004); info if it’s absent entirely. py.typed: important if [tool.pyright] or [tool.mypy] is configured but the package directory has no PEP 561 py.typed marker.

The file is also validated against PyPA’s own schemas with validate-pyproject before any of that: critical if it fails, since every other check reads this file by key lookup and cannot otherwise tell an absent key from a misspelled one. Reported alongside the semantic findings rather than instead of them.

Code quality

ruff

Lint and format with ruff — the standard’s only linter/formatter.

pyright

Type checking in standard mode.

pydoclint

Docstring–signature consistency (google style). Important for a docstring that contradicts its code, informational for the --arg-type-hints-* option codes (DOC106–DOC111), which report a configuration preference rather than a docstring that misleads a reader. Never critical: a docstring is not a broken build, and canon’s CI runs bare pydoclint as its own gate, so a second veto here would add no information.

A repo with no [tool.pydoclint] of its own is judged against canon’s option set, not pydoclint’s stricter defaults. Otherwise the check reports “you have not adopted” once per docstring — one fleet repo drew 218 findings that way, of which all but 20 were the type-hints-in-docstring family canon turns off. The template check reports non-adoption once, which is the right number.

codespell

Common misspellings in code and docs.

Tests and dependencies

tests

Runs the pytest suite.

deps

Dependency hygiene via deptry (unused/missing/transitive dependencies).

deptree

Circular imports within the package.

depgroups

PEP 735 [dependency-groups] usage. Important: no [dependency-groups] section; one with no dev group; a dev-type extra (test, docs, lint, etc.) left in [project.optional-dependencies] instead of [dependency-groups]. Info: a name defined in both sections. No auto-fix — move entries manually or with uv add --group.

dropped-args

A parameter the caller accepts and then fails to forward. Function f takes p and calls g, which also takes p and gives it a default; the call omits p, so g uses its default and f’s p reaches nothing. Important, because the failure is silent: the code runs, the tests pass, and a documented knob does nothing.

ruff’s ARG001 does not cover this. ARG001 fires when a parameter is never read in the body, but here f may read p elsewhere and the call to g has a valid signature. Only comparing the two signatures shows it.

The check resolves callees by name within the package and skips a name defined more than once, since a bare call cannot then be attributed with confidence. A **kwargs forward counts as passing everything. No auto-fix: forwarding the parameter is usually right, but sometimes the callee is meant to use its own default, and only the author knows which. Mark a deliberate one with # preen: allow-dropped-arg on the call or in the comment block directly above it; the marker may open a rationale that runs to several lines.

audit

Known vulnerabilities in locked dependencies, via pip-audit over a uv export --all-groups of the project. Important: a locked package has a known vulnerability (reports the CVE/GHSA ids and a fix version when pip-audit has one). Info: a dependency pinned via a direct git/file/URL reference, which pip-audit --disable-pip can’t hash-verify and so is skipped rather than scanned. Skips entirely (info, non-blocking) if there’s no uv.lock, uv export fails, or pip-audit isn’t installed. No auto-fix — bumping a vulnerable dependency needs manual review.

When no fixed release exists yet, a repo lists the advisory ids in [tool.preen] audit_ignore. Those are reported as info rather than failing the check, naming the package, version and id, so the exception stays visible and the entry is removed once upstream ships a fix. Skipping the whole check to silence one advisory would stop every other dependency being scanned.

Documentation

examples

The documentation still names symbols the package has. Every fenced Python block in README.md and under docs/ is parsed with ast, the attributes and imports reached for on the package are collected, and each is looked up in what the package exposes: the names its __init__ defines, relative star imports followed, plus every child module and subpackage. Nothing is imported or executed, so this works against a repo whose dependencies are not installed. Informational in 0.6.0: an example naming something that no longer exists fails for the first person who copies it, and nothing else in the suite reads documentation, but a static reading of examples has corners, and a false positive here fails someone else’s CI. It gates once a release has gone through a fleet sweep without one.

Permissive where it cannot be sure. A fragment that does not parse is skipped, a name the enclosing scope binds itself (a fixture parameter or loop variable that happens to share the package’s name) is not the package there, dunders are language protocol rather than API, and a star import that cannot be resolved statically or a module-level __getattr__ makes the exports unknown rather than empty.

Opt in to also executing >>> examples under the repo’s own .venv:

[tool.preen]
run_doctests = true

Off by default because, measured across the fleet, the only doctest failures were illustrative blocks that depend on state from an earlier block or on a live API. A document that takes longer than two minutes is reported as a failure rather than aborting the run.

Running subsets

preen check --only template --only ci-matrix
preen check --skip links