370 lines
9.8 KiB
Markdown
370 lines
9.8 KiB
Markdown
---
|
|
name: python-package-writer
|
|
description: This skill should be used when writing Python packages following production-ready patterns and philosophy. It applies when creating new Python packages, refactoring existing packages, designing package APIs, or when clean, minimal, well-tested Python library code is needed. Triggers on requests like "create a package", "write a Python library", "design a package API", or mentions of PyPI publishing.
|
|
---
|
|
|
|
# Python Package Writer
|
|
|
|
Write Python packages following battle-tested patterns from production-ready libraries. Emphasis on simplicity, minimal dependencies, comprehensive testing, and modern packaging standards (pyproject.toml, type hints, pytest).
|
|
|
|
## Core Philosophy
|
|
|
|
**Simplicity over cleverness.** Zero or minimal dependencies. Explicit code over magic. Framework integration without framework coupling. Every pattern serves production use cases.
|
|
|
|
## Package Structure (src layout)
|
|
|
|
The modern recommended layout with proper namespace isolation:
|
|
|
|
```
|
|
package-name/
|
|
├── pyproject.toml # All metadata and configuration
|
|
├── README.md
|
|
├── LICENSE
|
|
├── py.typed # PEP 561 marker for type hints
|
|
├── src/
|
|
│ └── package_name/ # Actual package code
|
|
│ ├── __init__.py # Entry point, exports, version
|
|
│ ├── core.py # Core functionality
|
|
│ ├── models.py # Data models (Pydantic/dataclasses)
|
|
│ ├── exceptions.py # Custom exceptions
|
|
│ └── py.typed # Type hint marker (also here)
|
|
└── tests/
|
|
├── conftest.py # Pytest fixtures
|
|
├── test_core.py
|
|
└── test_models.py
|
|
```
|
|
|
|
## Entry Point Structure
|
|
|
|
Every package follows this pattern in `src/package_name/__init__.py`:
|
|
|
|
```python
|
|
"""Package description - one line."""
|
|
|
|
# Public API exports
|
|
from package_name.core import Client, process_data
|
|
from package_name.models import Config, Result
|
|
from package_name.exceptions import PackageError, ValidationError
|
|
|
|
__version__ = "1.0.0"
|
|
__all__ = [
|
|
"Client",
|
|
"process_data",
|
|
"Config",
|
|
"Result",
|
|
"PackageError",
|
|
"ValidationError",
|
|
]
|
|
```
|
|
|
|
## pyproject.toml Configuration
|
|
|
|
Modern packaging with all metadata in one file:
|
|
|
|
```toml
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[project]
|
|
name = "package-name"
|
|
version = "1.0.0"
|
|
description = "Brief description of what the package does"
|
|
readme = "README.md"
|
|
license = "MIT"
|
|
requires-python = ">=3.10"
|
|
authors = [
|
|
{ name = "Your Name", email = "you@example.com" }
|
|
]
|
|
classifiers = [
|
|
"Development Status :: 5 - Production/Stable",
|
|
"Intended Audience :: Developers",
|
|
"License :: OSI Approved :: MIT License",
|
|
"Programming Language :: Python :: 3",
|
|
"Programming Language :: Python :: 3.10",
|
|
"Programming Language :: Python :: 3.11",
|
|
"Programming Language :: Python :: 3.12",
|
|
"Typing :: Typed",
|
|
]
|
|
keywords = ["keyword1", "keyword2"]
|
|
|
|
# Zero or minimal runtime dependencies
|
|
dependencies = []
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=8.0",
|
|
"pytest-cov>=4.0",
|
|
"ruff>=0.4",
|
|
"mypy>=1.0",
|
|
]
|
|
# Optional integrations
|
|
fastapi = ["fastapi>=0.100", "pydantic>=2.0"]
|
|
|
|
[project.urls]
|
|
Homepage = "https://github.com/username/package-name"
|
|
Documentation = "https://package-name.readthedocs.io"
|
|
Repository = "https://github.com/username/package-name"
|
|
Changelog = "https://github.com/username/package-name/blob/main/CHANGELOG.md"
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["src/package_name"]
|
|
|
|
[tool.ruff]
|
|
target-version = "py310"
|
|
line-length = 88
|
|
|
|
[tool.ruff.lint]
|
|
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "SIM"]
|
|
|
|
[tool.mypy]
|
|
python_version = "3.10"
|
|
strict = true
|
|
warn_return_any = true
|
|
warn_unused_ignores = true
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
addopts = "-ra -q"
|
|
|
|
[tool.coverage.run]
|
|
source = ["src/package_name"]
|
|
branch = true
|
|
```
|
|
|
|
## Configuration Pattern
|
|
|
|
Use module-level configuration with dataclasses or simple attributes:
|
|
|
|
```python
|
|
# src/package_name/config.py
|
|
from dataclasses import dataclass, field
|
|
from os import environ
|
|
from typing import Any
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
"""Package configuration with sensible defaults."""
|
|
|
|
timeout: int = 30
|
|
retries: int = 3
|
|
api_key: str | None = field(default=None)
|
|
debug: bool = False
|
|
|
|
def __post_init__(self) -> None:
|
|
# Environment variable fallbacks
|
|
if self.api_key is None:
|
|
self.api_key = environ.get("PACKAGE_API_KEY")
|
|
|
|
|
|
# Module-level singleton (optional)
|
|
_config: Config | None = None
|
|
|
|
|
|
def get_config() -> Config:
|
|
"""Get or create the global config instance."""
|
|
global _config
|
|
if _config is None:
|
|
_config = Config()
|
|
return _config
|
|
|
|
|
|
def configure(**kwargs: Any) -> Config:
|
|
"""Configure the package with custom settings."""
|
|
global _config
|
|
_config = Config(**kwargs)
|
|
return _config
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
Simple hierarchy with informative messages:
|
|
|
|
```python
|
|
# src/package_name/exceptions.py
|
|
class PackageError(Exception):
|
|
"""Base exception for all package errors."""
|
|
pass
|
|
|
|
|
|
class ConfigError(PackageError):
|
|
"""Invalid configuration."""
|
|
pass
|
|
|
|
|
|
class ValidationError(PackageError):
|
|
"""Data validation failed."""
|
|
|
|
def __init__(self, message: str, field: str | None = None) -> None:
|
|
self.field = field
|
|
super().__init__(message)
|
|
|
|
|
|
class APIError(PackageError):
|
|
"""External API error."""
|
|
|
|
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
self.status_code = status_code
|
|
super().__init__(message)
|
|
|
|
|
|
# Validate early with ValueError
|
|
def process(data: bytes) -> str:
|
|
if not data:
|
|
raise ValueError("Data cannot be empty")
|
|
if len(data) > 1_000_000:
|
|
raise ValueError(f"Data too large: {len(data)} bytes (max 1MB)")
|
|
return data.decode("utf-8")
|
|
```
|
|
|
|
## Type Hints
|
|
|
|
Always use type hints with modern syntax (Python 3.10+):
|
|
|
|
```python
|
|
# Use built-in generics, not typing module
|
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
|
|
def process_items(
|
|
items: list[str],
|
|
transform: Callable[[str], str] | None = None,
|
|
*,
|
|
batch_size: int = 100,
|
|
) -> Iterator[str]:
|
|
"""Process items with optional transformation."""
|
|
for item in items:
|
|
if transform:
|
|
yield transform(item)
|
|
else:
|
|
yield item
|
|
|
|
|
|
# Use | for unions, not Union
|
|
def get_value(key: str) -> str | None:
|
|
return _cache.get(key)
|
|
|
|
|
|
# Use Self for return type annotations (Python 3.11+)
|
|
from typing import Self
|
|
|
|
class Client:
|
|
def configure(self, **kwargs: str) -> Self:
|
|
# Update configuration
|
|
return self
|
|
```
|
|
|
|
## Testing (pytest)
|
|
|
|
```python
|
|
# tests/conftest.py
|
|
import pytest
|
|
from package_name import Config, configure
|
|
|
|
|
|
@pytest.fixture
|
|
def config() -> Config:
|
|
"""Fresh config for each test."""
|
|
return configure(timeout=5, debug=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_data() -> bytes:
|
|
"""Sample input data."""
|
|
return b"test data content"
|
|
|
|
|
|
# tests/test_core.py
|
|
import pytest
|
|
from package_name import process_data, PackageError
|
|
|
|
|
|
class TestProcessData:
|
|
"""Tests for process_data function."""
|
|
|
|
def test_basic_functionality(self, sample_data: bytes) -> None:
|
|
result = process_data(sample_data)
|
|
assert result == "test data content"
|
|
|
|
def test_empty_input_raises_error(self) -> None:
|
|
with pytest.raises(ValueError, match="cannot be empty"):
|
|
process_data(b"")
|
|
|
|
def test_with_transform(self, sample_data: bytes) -> None:
|
|
result = process_data(sample_data, transform=str.upper)
|
|
assert result == "TEST DATA CONTENT"
|
|
|
|
|
|
class TestConfig:
|
|
"""Tests for configuration."""
|
|
|
|
def test_defaults(self) -> None:
|
|
config = Config()
|
|
assert config.timeout == 30
|
|
assert config.retries == 3
|
|
|
|
def test_env_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("PACKAGE_API_KEY", "test-key")
|
|
config = Config()
|
|
assert config.api_key == "test-key"
|
|
```
|
|
|
|
## FastAPI Integration
|
|
|
|
Optional FastAPI integration pattern:
|
|
|
|
```python
|
|
# src/package_name/fastapi.py
|
|
"""FastAPI integration - only import if FastAPI is installed."""
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from fastapi import FastAPI
|
|
|
|
from package_name.config import get_config
|
|
|
|
|
|
def init_app(app: "FastAPI") -> None:
|
|
"""Initialize package with FastAPI app."""
|
|
config = get_config()
|
|
|
|
@app.on_event("startup")
|
|
async def startup() -> None:
|
|
# Initialize connections, caches, etc.
|
|
pass
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown() -> None:
|
|
# Cleanup resources
|
|
pass
|
|
|
|
|
|
# Usage in FastAPI app:
|
|
# from package_name.fastapi import init_app
|
|
# init_app(app)
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
- `__getattr__` magic (use explicit imports)
|
|
- Global mutable state (use configuration objects)
|
|
- `*` imports in `__init__.py` (explicit `__all__`)
|
|
- Many runtime dependencies
|
|
- Committing `.venv/` or `__pycache__/`
|
|
- Not including `py.typed` marker
|
|
- Using `setup.py` (use `pyproject.toml`)
|
|
- Mixing src layout and flat layout
|
|
- `print()` for debugging (use logging)
|
|
- Bare `except:` clauses
|
|
|
|
## Reference Files
|
|
|
|
For deeper patterns, see:
|
|
- **[references/package-structure.md](./references/package-structure.md)** - Directory layouts, module organization
|
|
- **[references/pyproject-config.md](./references/pyproject-config.md)** - Complete pyproject.toml examples
|
|
- **[references/testing-patterns.md](./references/testing-patterns.md)** - pytest patterns, fixtures, CI setup
|
|
- **[references/type-hints.md](./references/type-hints.md)** - Modern typing patterns
|
|
- **[references/fastapi-integration.md](./references/fastapi-integration.md)** - FastAPI/Pydantic integration
|
|
- **[references/publishing.md](./references/publishing.md)** - PyPI publishing, CI/CD
|
|
- **[references/resources.md](./references/resources.md)** - Links to exemplary Python packages
|