* 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
6.4 KiB
Testing and Code Quality
Table of Contents
1. conftest.py
Use conftest.py to define shared fixtures. Keep fixtures focused — one fixture per concern.
For async tests, use pytest-asyncio with asyncio_mode = "auto" in pyproject.toml.
# tests/conftest.py
import pytest
from your_package.core import YourClient
from your_package.backends.memory import MemoryBackend
@pytest.fixture
def memory_backend() -> MemoryBackend:
return MemoryBackend()
@pytest.fixture
def client(memory_backend: MemoryBackend) -> YourClient:
return YourClient(
api_key="test-key",
backend=memory_backend,
)
2. Unit Tests
Test both the happy path and the edge cases (e.g. invalid inputs, error conditions).
# tests/test_core.py
import pytest
from your_package import YourClient
from your_package.exceptions import YourPackageError
def test_client_creates_with_valid_key():
client = YourClient(api_key="sk-test")
assert client is not None
def test_client_raises_on_empty_key():
with pytest.raises(ValueError, match="api_key"):
YourClient(api_key="")
def test_client_raises_on_invalid_timeout():
with pytest.raises(ValueError, match="timeout"):
YourClient(api_key="sk-test", timeout=-1)
@pytest.mark.asyncio
async def test_process_returns_expected_result(client: YourClient):
result = await client.process({"input": "value"})
assert "output" in result
@pytest.mark.asyncio
async def test_process_raises_on_invalid_input(client: YourClient):
with pytest.raises(YourPackageError):
await client.process({}) # empty input should fail
3. Backend Unit Tests
Test each backend independently, in isolation from the rest of the library. This makes failures easier to diagnose and ensures your abstract interface is actually implemented correctly.
# tests/test_backends.py
import pytest
from your_package.backends.memory import MemoryBackend
@pytest.mark.asyncio
async def test_set_and_get():
backend = MemoryBackend()
await backend.set("key1", "value1")
result = await backend.get("key1")
assert result == "value1"
@pytest.mark.asyncio
async def test_get_missing_key_returns_none():
backend = MemoryBackend()
result = await backend.get("nonexistent")
assert result is None
@pytest.mark.asyncio
async def test_delete_removes_key():
backend = MemoryBackend()
await backend.set("key1", "value1")
await backend.delete("key1")
result = await backend.get("key1")
assert result is None
@pytest.mark.asyncio
async def test_ttl_expires_entry():
import asyncio
backend = MemoryBackend()
await backend.set("key1", "value1", ttl=1)
await asyncio.sleep(1.1)
result = await backend.get("key1")
assert result is None
@pytest.mark.asyncio
async def test_different_keys_are_independent():
backend = MemoryBackend()
await backend.set("key1", "a")
await backend.set("key2", "b")
assert await backend.get("key1") == "a"
assert await backend.get("key2") == "b"
await backend.delete("key1")
assert await backend.get("key2") == "b"
4. Running Tests
pip install -e ".[dev]"
pytest # All tests
pytest --cov --cov-report=html # With HTML coverage report (opens in browser)
pytest -k "test_middleware" # Filter by name
pytest -x # Stop on first failure
pytest -v # Verbose output
Coverage config in pyproject.toml enforces a minimum threshold (fail_under = 80). CI will
fail if you drop below it, which catches coverage regressions automatically.
5. Code Quality Tools
Ruff (linting — replaces flake8, pylint, many others)
pip install ruff
ruff check . # Check for issues
ruff check . --fix # Auto-fix safe issues
Ruff is extremely fast and replaces most of the Python linting ecosystem. Configure it in
pyproject.toml — see references/pyproject-toml.md for the full config.
Black (formatting)
pip install black
black . # Format all files
black . --check # CI mode — reports issues without modifying files
isort (import sorting)
pip install isort
isort . # Sort imports
isort . --check-only # CI mode
Always set profile = "black" in [tool.isort] — otherwise black and isort conflict.
mypy (static type checking)
pip install mypy
mypy your_package/ # Type-check your package source only
Common fixes:
ignore_missing_imports = true— ignore untyped third-party depsfrom __future__ import annotations— enables PEP 563 deferred evaluation (Python 3.9 compat)pip install types-redis— type stubs for the redis library
Run all at once
ruff check . && black . --check && isort . --check-only && mypy your_package/
6. Pre-commit Hooks
Pre-commit runs all quality tools automatically before each commit, so issues never reach CI.
Install once per clone with pre-commit install.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [types-redis] # Add stubs for typed dependencies
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-merge-conflict
- id: debug-statements
- id: no-commit-to-branch
args: [--branch, master, --branch, main]
pip install pre-commit
pre-commit install # Install once per clone
pre-commit run --all-files # Run all hooks manually (useful before the first install)
The no-commit-to-branch hook prevents accidentally committing directly to main/master,
which would bypass CI checks. Always work on a feature branch.