Initial commit
This commit is contained in:
501
.claude/skills/python-packaging/SKILL.md
Normal file
501
.claude/skills/python-packaging/SKILL.md
Normal file
@@ -0,0 +1,501 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user