mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-12 11:15:56 +00:00
Add new skill: Python PyPI Package Builder (#1302)
* Add python-pypi-package-builder skill for Python packaging - Created `SKILL.md` defining decision-driven workflow for building, testing, versioning, and publishing Python packages. - Added reference modules covering PyPA packaging, architecture patterns, CI/CD, testing, versioning strategy, and release governance. - Implemented scaffold script to generate complete project structure with pyproject.toml, CI workflows, tests, and configuration. - Included support for multiple build backends (setuptools_scm, hatchling, flit, poetry) with clear decision rules. - Added secure release practices including tag-based versioning, branch protection, and OIDC Trusted Publishing. * fix: correct spelling issues detected by codespell
This commit is contained in:
470
skills/python-pypi-package-builder/references/pyproject-toml.md
Normal file
470
skills/python-pypi-package-builder/references/pyproject-toml.md
Normal file
@@ -0,0 +1,470 @@
|
||||
# pyproject.toml, Backends, Versioning, and Typed Package
|
||||
|
||||
## Table of Contents
|
||||
1. [Complete pyproject.toml — setuptools + setuptools_scm](#1-complete-pyprojecttoml)
|
||||
2. [hatchling (modern, zero-config)](#2-hatchling-modern-zero-config)
|
||||
3. [flit (minimal, version from `__version__`)](#3-flit-minimal-version-from-__version__)
|
||||
4. [poetry (integrated dep manager)](#4-poetry-integrated-dep-manager)
|
||||
5. [Versioning Strategy — PEP 440, semver, dep specifiers](#5-versioning-strategy)
|
||||
6. [setuptools_scm — dynamic version from git tags](#6-dynamic-versioning-with-setuptools_scm)
|
||||
7. [setup.py shim for legacy editable installs](#7-setuppy-shim)
|
||||
8. [PEP 561 typed package (py.typed)](#8-typed-package-pep-561)
|
||||
|
||||
---
|
||||
|
||||
## 1. Complete pyproject.toml
|
||||
|
||||
### setuptools + setuptools_scm (recommended for git-tag versioning)
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel", "setuptools_scm"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "your-package"
|
||||
dynamic = ["version"] # Version comes from git tags via setuptools_scm
|
||||
description = "<your description> — <key feature 1>, <key feature 2>"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = "MIT" # PEP 639 SPDX expression (string, not {text = "MIT"})
|
||||
license-files = ["LICENSE"]
|
||||
authors = [
|
||||
{name = "Your Name", email = "you@example.com"},
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Your Name", email = "you@example.com"},
|
||||
]
|
||||
keywords = [
|
||||
"python",
|
||||
# Add 10-15 specific keywords that describe your library — they affect PyPI discoverability
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha", # Change to 5 at stable release
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Typing :: Typed", # Add this when shipping py.typed
|
||||
]
|
||||
dependencies = [
|
||||
# List your runtime dependencies here. Keep them minimal.
|
||||
# Example: "httpx>=0.24", "pydantic>=2.0"
|
||||
# Leave empty if your library has no required runtime deps.
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
redis = [
|
||||
"redis>=4.2", # Optional heavy backend
|
||||
]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"pytest-asyncio>=0.21",
|
||||
"httpx>=0.24",
|
||||
"pytest-cov>=4.0",
|
||||
"ruff>=0.4",
|
||||
"black>=24.0",
|
||||
"isort>=5.13",
|
||||
"mypy>=1.0",
|
||||
"pre-commit>=3.0",
|
||||
"build",
|
||||
"twine",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/your-package"
|
||||
Documentation = "https://github.com/yourusername/your-package#readme"
|
||||
Repository = "https://github.com/yourusername/your-package"
|
||||
"Bug Tracker" = "https://github.com/yourusername/your-package/issues"
|
||||
Changelog = "https://github.com/yourusername/your-package/blob/master/CHANGELOG.md"
|
||||
|
||||
# --- Setuptools configuration ---
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["your_package*"] # flat layout
|
||||
# For src/ layout, use:
|
||||
# where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
your_package = ["py.typed"] # Ship the py.typed marker in the wheel
|
||||
|
||||
# --- setuptools_scm: version from git tags ---
|
||||
[tool.setuptools_scm]
|
||||
version_scheme = "post-release"
|
||||
local_scheme = "no-local-version" # Prevents +local suffix breaking PyPI uploads
|
||||
|
||||
# --- Ruff (linting) ---
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "C4", "PTH", "RUF"]
|
||||
ignore = ["E501"] # Line length enforced by formatter
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["S101", "ANN"] # Allow assert and missing annotations in tests
|
||||
"scripts/*" = ["T201"] # Allow print in scripts
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
|
||||
# --- Black (formatting) ---
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py310", "py311", "py312", "py313"]
|
||||
|
||||
# --- isort (import sorting) ---
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
|
||||
# --- mypy (static type checking) ---
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
warn_unused_ignores = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
ignore_missing_imports = true
|
||||
strict = false # Set true for maximum strictness
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "tests.*"
|
||||
disallow_untyped_defs = false # Relaxed in tests
|
||||
|
||||
# --- pytest ---
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["."] # For flat layout; remove for src/
|
||||
python_files = "test_*.py"
|
||||
python_classes = "Test*"
|
||||
python_functions = "test_*"
|
||||
addopts = "-v --tb=short --cov=your_package --cov-report=term-missing"
|
||||
|
||||
# --- Coverage ---
|
||||
[tool.coverage.run]
|
||||
source = ["your_package"]
|
||||
omit = ["tests/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
fail_under = 80
|
||||
show_missing = true
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"raise NotImplementedError",
|
||||
"if TYPE_CHECKING:",
|
||||
"@abstractmethod",
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. hatchling (Modern, Zero-Config)
|
||||
|
||||
Best for new pure-Python projects that don't need C extensions. No `setup.py` needed. Use
|
||||
`hatch-vcs` for git-tag versioning, or omit it for manual version bumps.
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["hatchling", "hatch-vcs"] # hatch-vcs for git-tag versioning
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "your-package"
|
||||
dynamic = ["version"] # Remove and add version = "1.0.0" for manual versioning
|
||||
description = "One-line description"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = "MIT"
|
||||
license-files = ["LICENSE"]
|
||||
authors = [{name = "Your Name", email = "you@example.com"}]
|
||||
keywords = ["python"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.6", "mypy>=1.10"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/your-package"
|
||||
Changelog = "https://github.com/yourusername/your-package/blob/master/CHANGELOG.md"
|
||||
|
||||
# --- Hatchling build config ---
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/your_package"] # src/ layout
|
||||
# packages = ["your_package"] # ← flat layout
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs" # git-tag versioning via hatch-vcs
|
||||
|
||||
[tool.hatch.version.raw-options]
|
||||
local_scheme = "no-local-version"
|
||||
|
||||
# ruff, mypy, pytest, coverage sections — same as setuptools template above
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. flit (Minimal, Version from `__version__`)
|
||||
|
||||
Best for very simple, single-module packages. Zero config. Version is read directly from
|
||||
`your_package/__init__.py`. Always requires a **static string** for `__version__`.
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["flit_core>=3.9"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
[project]
|
||||
name = "your-package"
|
||||
dynamic = ["version", "description"] # Read from __init__.py __version__ and docstring
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = "MIT"
|
||||
authors = [{name = "Your Name", email = "you@example.com"}]
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/your-package"
|
||||
|
||||
# flit reads __version__ from your_package/__init__.py automatically.
|
||||
# Ensure __init__.py has: __version__ = "1.0.0" (static string — flit does NOT support
|
||||
# importlib.metadata for dynamic version discovery)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. poetry (Integrated Dependency + Build Manager)
|
||||
|
||||
Best for teams that want a single tool to manage deps, build, and publish. Poetry v2+
|
||||
supports the standard `[project]` table.
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[project]
|
||||
name = "your-package"
|
||||
version = "1.0.0"
|
||||
description = "One-line description"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = "MIT"
|
||||
authors = [{name = "Your Name", email = "you@example.com"}]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [] # poetry v2+ uses standard [project] table
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8.0", "ruff>=0.6", "mypy>=1.10"]
|
||||
|
||||
# Optional: use [tool.poetry] only for poetry-specific features
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
# Poetry-specific group syntax (alternative to [project.optional-dependencies])
|
||||
pytest = ">=8.0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Versioning Strategy
|
||||
|
||||
### PEP 440 — The Standard
|
||||
|
||||
```
|
||||
Canonical form: N[.N]+[{a|b|rc}N][.postN][.devN]
|
||||
|
||||
Examples:
|
||||
1.0.0 Stable release
|
||||
1.0.0a1 Alpha (pre-release)
|
||||
1.0.0b2 Beta
|
||||
1.0.0rc1 Release candidate
|
||||
1.0.0.post1 Post-release (e.g., packaging fix only — no code change)
|
||||
1.0.0.dev1 Development snapshot (NOT for PyPI)
|
||||
```
|
||||
|
||||
### Semantic Versioning (SemVer) — use this for every library
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
MAJOR: Breaking API change (remove/rename public function/class/arg)
|
||||
MINOR: New feature, fully backward-compatible
|
||||
PATCH: Bug fix, no API change
|
||||
```
|
||||
|
||||
| Change | What bumps | Example |
|
||||
|---|---|---|
|
||||
| Remove / rename a public function | MAJOR | `1.2.3 → 2.0.0` |
|
||||
| Add new public function | MINOR | `1.2.3 → 1.3.0` |
|
||||
| Bug fix, no API change | PATCH | `1.2.3 → 1.2.4` |
|
||||
| New pre-release | suffix | `2.0.0a1`, `2.0.0rc1` |
|
||||
|
||||
### Version in code — read from package metadata
|
||||
|
||||
```python
|
||||
# your_package/__init__.py
|
||||
from importlib.metadata import version, PackageNotFoundError
|
||||
|
||||
try:
|
||||
__version__ = version("your-package")
|
||||
except PackageNotFoundError:
|
||||
__version__ = "0.0.0-dev" # Fallback for uninstalled dev checkouts
|
||||
```
|
||||
|
||||
Never hardcode `__version__ = "1.0.0"` when using setuptools_scm — it goes stale after the
|
||||
first git tag. Use `importlib.metadata` always.
|
||||
|
||||
### Version specifier best practices for dependencies
|
||||
|
||||
```toml
|
||||
# In [project] dependencies — for a LIBRARY:
|
||||
"httpx>=0.24" # Minimum version — PREFERRED for libraries
|
||||
"httpx>=0.24,<1.0" # Upper bound only when a known breaking change exists
|
||||
|
||||
# ONLY for applications (never for libraries):
|
||||
"httpx==0.27.0" # Pin exactly — breaks dep resolution in libraries
|
||||
|
||||
# NEVER do this in a library:
|
||||
# "httpx~=0.24.0" # Compatible release operator — too tight
|
||||
# "httpx==0.27.*" # Wildcard pin — fragile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Dynamic Versioning with `setuptools_scm`
|
||||
|
||||
`setuptools_scm` reads your git tags and sets the package version automatically — no more manually
|
||||
editing version strings before each release.
|
||||
|
||||
### How it works
|
||||
|
||||
```
|
||||
git tag v1.0.0 → package version = 1.0.0
|
||||
git tag v1.1.0 → package version = 1.1.0
|
||||
(commits after tag) → version = 1.1.0.post1+g<hash> (stripped for PyPI)
|
||||
```
|
||||
|
||||
`local_scheme = "no-local-version"` strips the `+g<hash>` suffix so PyPI uploads never fail with
|
||||
a "local version label not allowed" error.
|
||||
|
||||
### Access version at runtime
|
||||
|
||||
```python
|
||||
# your_package/__init__.py
|
||||
from importlib.metadata import version, PackageNotFoundError
|
||||
|
||||
try:
|
||||
__version__ = version("your-package")
|
||||
except PackageNotFoundError:
|
||||
__version__ = "0.0.0-dev" # Fallback for uninstalled dev checkouts
|
||||
```
|
||||
|
||||
Never hardcode `__version__ = "1.0.0"` when using setuptools_scm — it will go stale after the
|
||||
first tag.
|
||||
|
||||
### Full release flow (this is it — nothing else needed)
|
||||
|
||||
```bash
|
||||
git tag v1.2.0
|
||||
git push origin master --tags
|
||||
# GitHub Actions publish.yml triggers automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. `setup.py` Shim
|
||||
|
||||
Some older tools and IDEs still expect a `setup.py`. Keep it as a three-line shim — all real
|
||||
configuration stays in `pyproject.toml`.
|
||||
|
||||
```python
|
||||
# setup.py — thin shim only. All config lives in pyproject.toml.
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
```
|
||||
|
||||
Never duplicate `name`, `version`, `dependencies`, or any other metadata from `pyproject.toml`
|
||||
into `setup.py`. If you copy anything there it will eventually drift and cause confusing conflicts.
|
||||
|
||||
---
|
||||
|
||||
## 8. Typed Package (PEP 561)
|
||||
|
||||
A properly declared typed package means mypy, pyright, and IDEs automatically pick up your type
|
||||
hints without any extra configuration from your users.
|
||||
|
||||
### Step 1: Create the marker file
|
||||
|
||||
```bash
|
||||
# The file must exist; its content doesn't matter — its presence is the signal.
|
||||
touch your_package/py.typed
|
||||
```
|
||||
|
||||
### Step 2: Include it in the wheel
|
||||
|
||||
Already in the template above:
|
||||
|
||||
```toml
|
||||
[tool.setuptools.package-data]
|
||||
your_package = ["py.typed"]
|
||||
```
|
||||
|
||||
### Step 3: Add the PyPI classifier
|
||||
|
||||
```toml
|
||||
classifiers = [
|
||||
...
|
||||
"Typing :: Typed",
|
||||
]
|
||||
```
|
||||
|
||||
### Step 4: Type-annotate all public functions
|
||||
|
||||
```python
|
||||
# Good — fully typed
|
||||
def process(
|
||||
self,
|
||||
data: dict[str, object],
|
||||
*,
|
||||
timeout: int = 30,
|
||||
) -> dict[str, object]:
|
||||
...
|
||||
|
||||
# Bad — mypy will flag this, and IDEs give no completions to users
|
||||
def process(self, data, timeout=30):
|
||||
...
|
||||
```
|
||||
|
||||
### Step 5: Verify py.typed ships in the wheel
|
||||
|
||||
```bash
|
||||
python -m build
|
||||
unzip -l dist/your_package-*.whl | grep py.typed
|
||||
# Must show: your_package/py.typed
|
||||
```
|
||||
|
||||
If it's missing, check your `[tool.setuptools.package-data]` config.
|
||||
Reference in New Issue
Block a user