Files
2025-11-30 08:51:46 +08:00

502 lines
11 KiB
Markdown

---
name: python-packaging
description: Automatically applies when configuring Python project packaging. Ensures proper pyproject.toml setup, project layout, build configuration, metadata, and distribution best practices.
category: python
---
# Python Packaging Patterns
When packaging Python projects, follow these patterns for modern, maintainable package configuration.
**Trigger Keywords**: pyproject.toml, packaging, setup.py, build, distribution, package, publish, PyPI, wheel, sdist, project structure
**Agent Integration**: Used by `backend-architect`, `devops-engineer`, `python-engineer`
## ✅ Correct Pattern: pyproject.toml Setup
```toml
# pyproject.toml - Modern Python packaging
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
version = "0.1.0"
description = "A short description of your package"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
maintainers = [
{name = "Maintainer Name", email = "maintainer@example.com"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries",
]
keywords = ["example", "package", "keywords"]
requires-python = ">=3.11"
# Core dependencies
dependencies = [
"fastapi>=0.109.0",
"pydantic>=2.5.0",
"sqlalchemy>=2.0.0",
"httpx>=0.26.0",
]
# Optional dependencies (extras)
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.23.0",
"black>=24.0.0",
"ruff>=0.1.0",
"mypy>=1.8.0",
]
docs = [
"sphinx>=7.2.0",
"sphinx-rtd-theme>=2.0.0",
]
all = [
"mypackage[dev,docs]",
]
[project.urls]
Homepage = "https://github.com/username/mypackage"
Documentation = "https://mypackage.readthedocs.io"
Repository = "https://github.com/username/mypackage.git"
Issues = "https://github.com/username/mypackage/issues"
Changelog = "https://github.com/username/mypackage/blob/main/CHANGELOG.md"
[project.scripts]
# Entry points for CLI commands
mypackage-cli = "mypackage.cli:main"
# Tool configurations
[tool.setuptools]
# Package discovery
packages = ["mypackage", "mypackage.submodule"]
[tool.setuptools.package-data]
mypackage = ["py.typed", "*.json", "templates/*.html"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--cov=mypackage",
"--cov-report=term-missing",
"--cov-report=html",
]
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
]
[tool.black]
line-length = 100
target-version = ["py311", "py312"]
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.venv
| build
| dist
)/
'''
[tool.ruff]
line-length = 100
target-version = "py311"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = []
exclude = [
".git",
".venv",
"build",
"dist",
]
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_generics = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
check_untyped_defs = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
```
## Project Structure
```
mypackage/
├── pyproject.toml # Project configuration
├── README.md # Project documentation
├── LICENSE # License file
├── CHANGELOG.md # Version history
├── .gitignore # Git ignore patterns
├── src/ # Source code (src layout)
│ └── mypackage/
│ ├── __init__.py # Package init
│ ├── __main__.py # Entry point for -m
│ ├── py.typed # PEP 561 marker
│ ├── core.py
│ ├── models.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
├── tests/ # Test suite
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures
│ ├── test_core.py
│ └── test_models.py
├── docs/ # Documentation
│ ├── conf.py
│ ├── index.rst
│ └── api.rst
└── scripts/ # Utility scripts
└── setup_dev.sh
```
## Package Init
```python
# src/mypackage/__init__.py
"""
MyPackage - A Python package for doing things.
This package provides tools for X, Y, and Z.
"""
from mypackage.core import MainClass, main_function
from mypackage.models import Model1, Model2
# Version
__version__ = "0.1.0"
# Public API
__all__ = [
"MainClass",
"main_function",
"Model1",
"Model2",
]
# Convenience imports for common use cases
def quick_start():
"""Quick start helper for new users."""
return MainClass()
```
## CLI Entry Point
```python
# src/mypackage/__main__.py
"""
Entry point for python -m mypackage.
"""
from mypackage.cli import main
if __name__ == "__main__":
main()
# src/mypackage/cli.py
"""Command-line interface."""
import argparse
import sys
from typing import List, Optional
def main(argv: Optional[List[str]] = None) -> int:
"""
Main CLI entry point.
Args:
argv: Command-line arguments (defaults to sys.argv)
Returns:
Exit code (0 for success, non-zero for error)
"""
parser = argparse.ArgumentParser(
prog="mypackage",
description="MyPackage CLI tool",
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {__version__}"
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Enable verbose output"
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Add subcommands
init_parser = subparsers.add_parser("init", help="Initialize project")
init_parser.add_argument("path", help="Project path")
run_parser = subparsers.add_parser("run", help="Run the application")
run_parser.add_argument("--config", help="Config file path")
# Parse arguments
args = parser.parse_args(argv)
# Execute command
if args.command == "init":
return init_command(args)
elif args.command == "run":
return run_command(args)
else:
parser.print_help()
return 1
def init_command(args) -> int:
"""Handle init command."""
print(f"Initializing project at {args.path}")
return 0
def run_command(args) -> int:
"""Handle run command."""
print("Running application")
return 0
if __name__ == "__main__":
sys.exit(main())
```
## Type Hints for Distribution
```python
# src/mypackage/py.typed
# This file signals that the package supports type hints (PEP 561)
# Leave empty or add configuration if needed
```
## Version Management
```python
# src/mypackage/_version.py
"""Version information."""
__version__ = "0.1.0"
__version_info__ = tuple(int(i) for i in __version__.split("."))
# src/mypackage/__init__.py
from mypackage._version import __version__, __version_info__
__all__ = ["__version__", "__version_info__"]
```
## Build and Distribution
```bash
# Build package
python -m build
# This creates:
# dist/mypackage-0.1.0-py3-none-any.whl (wheel)
# dist/mypackage-0.1.0.tar.gz (source distribution)
# Test installation locally
pip install dist/mypackage-0.1.0-py3-none-any.whl
# Upload to PyPI
python -m twine upload dist/*
# Upload to Test PyPI first
python -m twine upload --repository testpypi dist/*
```
## Manifest for Non-Python Files
```
# MANIFEST.in - Include additional files in source distribution
include README.md
include LICENSE
include CHANGELOG.md
include pyproject.toml
recursive-include src/mypackage *.json
recursive-include src/mypackage *.yaml
recursive-include src/mypackage/templates *.html
recursive-include tests *.py
global-exclude __pycache__
global-exclude *.py[co]
```
## Development Installation
```bash
# Install in editable mode with dev dependencies
pip install -e ".[dev]"
# Or with all optional dependencies
pip install -e ".[all]"
# This allows you to edit code without reinstalling
```
## GitHub Actions for Publishing
```yaml
# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install build twine
- name: Build package
run: python -m build
- name: Check distribution
run: twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: twine upload dist/*
```
## ❌ Anti-Patterns
```python
# ❌ Using setup.py instead of pyproject.toml
# setup.py is legacy, use pyproject.toml
# ❌ No version pinning in dependencies
dependencies = ["fastapi"] # Which version?
# ✅ Better: Pin compatible versions
dependencies = ["fastapi>=0.109.0,<1.0.0"]
# ❌ Flat package structure
mypackage.py # Single file, hard to scale
# ✅ Better: Proper package structure
src/mypackage/__init__.py
# ❌ No py.typed marker
# Users can't benefit from type hints
# ✅ Better: Include py.typed
src/mypackage/py.typed
# ❌ Not specifying python_requires
# Package might be installed on incompatible Python
# ✅ Better: Specify Python version
requires-python = ">=3.11"
# ❌ No optional dependencies
dependencies = ["pytest", "sphinx"] # Forces install!
# ✅ Better: Use optional-dependencies
[project.optional-dependencies]
dev = ["pytest"]
docs = ["sphinx"]
```
## Best Practices Checklist
- ✅ Use pyproject.toml for configuration
- ✅ Use src/ layout for packages
- ✅ Include py.typed for type hints
- ✅ Specify Python version requirement
- ✅ Pin dependency versions
- ✅ Use optional-dependencies for extras
- ✅ Include README, LICENSE, CHANGELOG
- ✅ Define entry points for CLI tools
- ✅ Configure tools in pyproject.toml
- ✅ Test package installation locally
- ✅ Use build and twine for publishing
- ✅ Automate publishing with CI/CD
## Auto-Apply
When creating Python packages:
1. Use pyproject.toml for all configuration
2. Use src/package_name/ layout
3. Include py.typed marker
4. Define optional-dependencies for dev/docs
5. Pin dependency versions
6. Include README and LICENSE
7. Define CLI entry points if needed
8. Configure testing and linting tools
## Related Skills
- `dependency-management` - For managing dependencies
- `type-safety` - For type hints
- `pytest-patterns` - For testing
- `git-workflow-standards` - For releases
- `docs-style` - For documentation