Skip to content

Latest commit

 

History

History
92 lines (79 loc) · 4.73 KB

README.md

File metadata and controls

92 lines (79 loc) · 4.73 KB

template-python-package

Note

This repository is vibe coded with Claude.

A template for a typed, tested, benchmarked and publishable Python library: src/ layout with a py.typed marker, hatchling build, uv, strict ruff and mypy, pytest with a 100% coverage gate, import-linter, pip-audit, per-PR CodSpeed benchmarks, and a release that publishes to PyPI through trusted publishing. It is itself an instance of template-base (so it gets the devcontainer, CI, release workflow and template sync from there) and is itself a template: a package created from it receives fixes through template sync. See docs/TEMPLATE.md for the conventions shared by every instance, and docs/adrs/0001-python-package-template.md for why it exists.

Creating a package from this template

  1. On GitHub, use "Use this template" on this repository (or gh repo create <org>/<name> --template craftainer/template-python-package).

  2. Rename the distribution and the import package. Three names move together:

    • pyproject.toml: [project] name (template-python-package), the description, the import-linter root_packages/contract modules and any --cov=src/template_python_package setting;
    • src/template_python_package/ to src/<your_import_name>/, and every template_python_package import in tests/;
    • this file's title and description.

    Then run uv lock to refresh uv.lock for the new name.

  3. Point template sync at this template. .github/template-sync-state.json here records the template-base tag this template itself last synced from; the copy in a new package must instead name this repository and the tag it was created at (the latest release of craftainer/template-python-package):

    {"template_repo": "craftainer/template-python-package", "synced_tag": "vX.Y.Z"}

    Add a TEMPLATE_SYNC_TOKEN secret too (see .github/workflows/README.md, "Template sync"). Files the package owns beyond the manifest's ignore tier (extra scripts/post-create.d/ hooks, for instance) go in .github/template-sync-manifest.local.yml under ignore:.

  4. Open the repository in the devcontainer, then run prek run --all-files --hook-stage manual; everything should pass before you write any code.

  5. Keep LICENSE (GPL-3.0) or replace it with the license you choose.

Publishing to PyPI

Releases use PyPI trusted publishing (GitHub OIDC): no API token is stored anywhere. make publish-packages skips with a warning until you opt in, per repository:

  1. Register a "pending publisher" on the index before the first release (PyPI: Your account, Publishing, "Add a new pending publisher"; TestPyPI has its own, separate form on test.pypi.org). Enter the project name from pyproject.toml, the GitHub owner and repository, the workflow name release.yml, and the environment release. Don't leave the environment empty: without it, any branch workflow with id-token: write can publish by OIDC. The environment name must match what the workflow sends, so do this only once release.yml declares environment: release on the publishing jobs. That arrives with the sync from template-base; check release.yml first, otherwise PyPI rejects the OIDC token. Also create the release environment (deployment branches limited to the default branch) in Settings, Environments before the first release that carries it. template-base's docs/TEMPLATE.md documents the steps once that sync has landed.
  2. Set the repository variable PYPI_PUBLISH (Settings, Secrets and variables, Actions, Variables):
    • unset: make publish-packages warns that it skipped and the release only attaches the sdist, wheel and SBOM to the GitHub release;
    • testpypi: publish to TestPyPI;
    • pypi: publish to PyPI.
  3. Do the first release with testpypi as a dry run (a pre-release channel is enough), confirm that the release workflow's verify-published job (its last job; it runs make verify-published after the GitHub release exists, so a failure does not undo the release) installs the published version back from TestPyPI. The target needs the release's dist/ and RELEASE_VERSION, so run it only in that workflow, not locally. Then register the PyPI pending publisher and switch the variable to pypi.

The version lives only in the git tag release.yml computes; pyproject.toml stays at 0.0.0 and make build stamps RELEASE_VERSION into the build without committing it.

License

Licensed under the GNU General Public License v3.0.