Preen is the conformance-and-adoption CLI for the py-canon fleet standard. py-canon defines the standard — a copier template, reusable GitHub workflows, and shared Sphinx configuration. Preen is how repos enter the fleet and stay in it: it scaffolds new packages, retrofits existing ones, pulls template updates, checks conformance, and cuts tag-driven releases.
preen requires Python >=3.12, matching the floor the fleet standard sets for every repo it checks.
uv tool install preen # or: pipx install preen/plugin marketplace add gojiplus/preen
/plugin install preen@gojiplus
The bundled skill teaches Claude Code to reach for the preen CLI — scaffolding, adoption, checks, and releases — instead of reimplementing its logic.
| Command | What it does |
|---|---|
preen new NAME |
Scaffold a new package from the py-canon copier template |
preen adopt [PATH] |
Retrofit an existing repo: mine answers from the repo, copy in the managed files, rewrite [tool.*] in pyproject.toml |
preen update [PATH] |
Pull the latest template changes into an adopted repo (copier update) |
preen check [PATH] |
Run conformance checks (detection only); --strict for CI |
preen fix [CHECK] |
Apply fixes for issues the checks found |
preen release [X.Y.Z] |
Guided release: run checks, confirm, git tag vX.Y.Z, push — the tag triggers the release workflow |
cd my-package
preen adopt
# review the ADOPTION REPORT, then:
uv lock && uv sync --all-groups
preen checkpreen adopt mines the copier answers from the repo itself (name,
description, and authors from pyproject.toml; org from the git remote),
renders the template into a temp directory, and copies in only the managed
files: the CI/docs/release workflow shims, .pre-commit-config.yaml and
dependabot config (if absent), docs/conf.py (old one backed up),
.copier-answers.yml, py.typed, plus LICENSE and CITATION.cff if
missing. It rewrites the [tool.ruff] (preserving any repo-specific lint
ignores already present, and setting target-version from the repo's own
requires-python floor, falling back to py311), [tool.pyright], and
[tool.pydoclint] sections to the standard with tomlkit (comments elsewhere
survive) and deletes legacy [tool.black], [tool.isort], [tool.flake8],
and [tool.mypy] sections.
Pass --release-migration to also convert the build backend to the fleet's
current uv_build series. The minimum is the latest tested release and the upper
bound prevents an unreviewed backend-series upgrade. An existing project.version
is preserved; a legacy dynamic-version project takes its current version from its
latest v* tag.
preen check runs: template (copier adoption + drift against the latest
py-canon tag), workflows (the four canon workflows are callers of the
reusable workflows, not stale copies), ruff, tests, citation,
changelog (Keep a Changelog structure), deps (deptry), deptree
(circular imports), depgroups (PEP 735 dependency-groups usage),
dropped-args (a parameter a caller accepts but never forwards), audit
(pip-audit over the locked dependencies), ci-matrix (canon shim, or a
matrix covering the requires-python floor), structure, runtime-assets
(schema-bearing data formats, pinned Hugging Face revisions), files
(README and .gitignore exist), precommit (.pre-commit-config.yaml
exists and parses), pytest-config (pytest settings the standard requires), examples (documented examples still name symbols the package defines), python-floor (requires-python meets the floor the standard declares; off until the fleet migration lands), version (hardcoded version strings), license (PEP
639 license metadata), links, metadata (build backend, requires-python
upper bound, PEP 561 py.typed), pydoclint, pyright, and codespell.
Issues carry an impact level: critical blocks release, important can
be overridden with informed consent, info is advisory. preen release
walks that ladder interactively before tagging. Most checks are
detection-only; preen fix license migrates the deprecated
{ text = ... } license table form to an SPDX string where the mapping is
unambiguous, drops redundant License :: classifiers, and adds a missing
license-files entry.
The fleet standard keeps one explicit version in pyproject.toml, set with
uv version X.Y.Z; the matching vX.Y.Z tag identifies the release. preen release
runs the checks, then refuses to proceed unless the version is committed in
pyproject.toml, is PEP 440-valid, the tag doesn't already exist, and
CHANGELOG.md has an entry for it (offering to rename [Unreleased] to the new
version and commit that rename if the Unreleased section has content).
It then asks for confirmation, tags vX.Y.Z, and pushes the tag; the repo's
release workflow does the rest (build, PEP 740 attestations, PyPI trusted
publishing, GitHub Release). Use --dry-run to see the plan without
acting.
Preen reads an optional [tool.preen] section in pyproject.toml:
[tool.preen]
src_layout = true # expect src/ layout (default: true)
tests_at_root = true # expect tests/ at the repo root (default: true)
examples_at_root = true # expect examples/ at the repo root (default: true)
skip_checks = ["links"] # checks to skip by default
run_doctests = false # execute `>>>` examples too (default: false)
enforce_python_floor = false # gate on the standard's Python floor
# URLs the `links` check should not fetch. Some endpoints are real but do not
# answer a bare GET, such as an API base that needs a path. Skipping the whole
# `links` check to silence one of those would stop the README being checked,
# which is where a broken link actually costs a reader something.
link_ignore = ["https://api.example.com/*"]pydoclintcovers docstring-signature consistency for now; ruff ships equivalentDOCrules ([tool.ruff.lint] external = ["DOC"]reserves the codes), but they're still preview-only, sopydoclintstays until ruff stabilizes them.codespellstays: ruff has no spelling-check rules.
MIT