mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-11 18:55:55 +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:
257
skills/python-pypi-package-builder/references/testing-quality.md
Normal file
257
skills/python-pypi-package-builder/references/testing-quality.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Testing and Code Quality
|
||||
|
||||
## Table of Contents
|
||||
1. [conftest.py](#1-conftestpy)
|
||||
2. [Unit tests](#2-unit-tests)
|
||||
3. [Backend unit tests](#3-backend-unit-tests)
|
||||
4. [Running tests](#4-running-tests)
|
||||
5. [Code quality tools](#5-code-quality-tools)
|
||||
6. [Pre-commit hooks](#6-pre-commit-hooks)
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
|
||||
```python
|
||||
# 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).
|
||||
|
||||
```python
|
||||
# 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.
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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)
|
||||
|
||||
```bash
|
||||
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)
|
||||
|
||||
```bash
|
||||
pip install black
|
||||
black . # Format all files
|
||||
black . --check # CI mode — reports issues without modifying files
|
||||
```
|
||||
|
||||
### isort (import sorting)
|
||||
|
||||
```bash
|
||||
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)
|
||||
|
||||
```bash
|
||||
pip install mypy
|
||||
mypy your_package/ # Type-check your package source only
|
||||
```
|
||||
|
||||
Common fixes:
|
||||
|
||||
- `ignore_missing_imports = true` — ignore untyped third-party deps
|
||||
- `from __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
|
||||
|
||||
```bash
|
||||
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`.
|
||||
|
||||
```yaml
|
||||
# .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]
|
||||
```
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user