This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user