python-packaging

Par wshobson · agents

Créez des packages Python distribuables avec une structure de projet appropriée, setup.py/pyproject.toml, et la publication sur PyPI. À utiliser pour packager des bibliothèques Python, créer des outils CLI ou distribuer du code Python.

npx skills add https://github.com/wshobson/agents --skill python-packaging

Empaquetage Python

Guide complet pour créer, structurer et distribuer des packages Python en utilisant les outils d'empaquetage modernes, pyproject.toml et la publication sur PyPI.

Quand utiliser cette compétence

  • Créer des bibliothèques Python pour la distribution
  • Construire des outils en ligne de commande avec des points d'entrée
  • Publier des packages sur PyPI ou dans des dépôts privés
  • Configurer la structure d'un projet Python
  • Créer des packages installables avec des dépendances
  • Construire des wheels et des distributions source
  • Gérer la versioning et les releases de packages Python
  • Créer des namespace packages
  • Implémenter les métadonnées et classifieurs de packages

Concepts clés

1. Structure du package

  • Source layout : src/package_name/ (recommandé)
  • Flat layout : package_name/ (plus simple mais moins flexible)
  • Métadonnées du package : pyproject.toml, setup.py ou setup.cfg
  • Formats de distribution : wheel (.whl) et distribution source (.tar.gz)

2. Standards modernes d'empaquetage

  • PEP 517/518 : Exigences du système de build
  • PEP 621 : Métadonnées dans pyproject.toml
  • PEP 660 : Installations éditables
  • pyproject.toml : Source unique de configuration

3. Build backends

  • setuptools : Traditionnel, largement utilisé
  • hatchling : Moderne, affirmé
  • flit : Léger, pour du pur Python
  • poetry : Gestion des dépendances + empaquetage

4. Distribution

  • PyPI : Python Package Index (public)
  • TestPyPI : Test avant la production
  • Dépôts privés : JFrog, AWS CodeArtifact, etc.

Démarrage rapide

Structure minimale du package

my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│   └── my_package/
│       ├── __init__.py
│       └── module.py
└── tests/
    └── test_module.py

pyproject.toml minimal

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
version = "0.1.0"
description = "A short description"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
    "requests>=2.28.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "black>=22.0",
]

Modèles de structure du package

Modèle 1 : Source Layout (Recommandé)

my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── src/
│   └── my_package/
│       ├── __init__.py
│       ├── core.py
│       ├── utils.py
│       └── py.typed          # Pour les type hints
├── tests/
│   ├── __init__.py
│   ├── test_core.py
│   └── test_utils.py
└── docs/
    └── index.md

Avantages :

  • Empêche d'importer accidentellement depuis la source
  • Imports de tests plus propres
  • Meilleure isolation

pyproject.toml pour source layout :

[tool.setuptools.packages.find]
where = ["src"]

Modèle 2 : Flat Layout

my-package/
├── pyproject.toml
├── README.md
├── my_package/
│   ├── __init__.py
│   └── module.py
└── tests/
    └── test_module.py

Plus simple, mais :

  • Permet d'importer le package sans l'installer
  • Moins professionnel pour les bibliothèques

Modèle 3 : Projet multi-package

project/
├── pyproject.toml
├── packages/
│   ├── package-a/
│   │   └── src/
│   │       └── package_a/
│   └── package-b/
│       └── src/
│           └── package_b/
└── tests/

Exemples complets de pyproject.toml

Modèle 4 : pyproject.toml complet

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my-awesome-package"
version = "1.0.0"
description = "An awesome Python package"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
    {name = "Your Name", email = "you@example.com"},
]
maintainers = [
    {name = "Maintainer Name", email = "maintainer@example.com"},
]
keywords = ["example", "package", "awesome"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]

dependencies = [
    "requests>=2.28.0,<3.0.0",
    "click>=8.0.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "black>=23.0.0",
    "ruff>=0.1.0",
    "mypy>=1.0.0",
]
docs = [
    "sphinx>=5.0.0",
    "sphinx-rtd-theme>=1.0.0",
]
all = [
    "my-awesome-package[dev,docs]",
]

[project.urls]
Homepage = "https://github.com/username/my-awesome-package"
Documentation = "https://my-awesome-package.readthedocs.io"
Repository = "https://github.com/username/my-awesome-package"
"Bug Tracker" = "https://github.com/username/my-awesome-package/issues"
Changelog = "https://github.com/username/my-awesome-package/blob/main/CHANGELOG.md"

[project.scripts]
my-cli = "my_package.cli:main"
awesome-tool = "my_package.tools:run"

[project.entry-points."my_package.plugins"]
plugin1 = "my_package.plugins:plugin1"

[tool.setuptools]
package-dir = {"" = "src"}
zip-safe = false

[tool.setuptools.packages.find]
where = ["src"]
include = ["my_package*"]
exclude = ["tests*"]

[tool.setuptools.package-data]
my_package = ["py.typed", "*.pyi", "data/*.json"]

# Configuration Black
[tool.black]
line-length = 100
target-version = ["py38", "py39", "py310", "py311"]
include = '\.pyi?$'

# Configuration Ruff
[tool.ruff]
line-length = 100
target-version = "py38"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]

# Configuration MyPy
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true

# Configuration Pytest
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --cov=my_package --cov-report=term-missing"

# Configuration Coverage
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*"]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
]

Modèle 5 : Versioning dynamique

[build-system]
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
dynamic = ["version"]
description = "Package with dynamic version"

[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}

# Ou utiliser setuptools-scm pour le versioning basé sur git
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"

Dans init.py :

# src/my_package/__init__.py
__version__ = "1.0.0"

# Ou avec setuptools-scm
from importlib.metadata import version
__version__ = version("my-package")

Modèles d'interface en ligne de commande (CLI)

Modèle 6 : CLI avec Click

# src/my_package/cli.py
import click

@click.group()
@click.version_option()
def cli():
    """My awesome CLI tool."""
    pass

@cli.command()
@click.argument("name")
@click.option("--greeting", default="Hello", help="Greeting to use")
def greet(name: str, greeting: str):
    """Greet someone."""
    click.echo(f"{greeting}, {name}!")

@cli.command()
@click.option("--count", default=1, help="Number of times to repeat")
def repeat(count: int):
    """Repeat a message."""
    for i in range(count):
        click.echo(f"Message {i + 1}")

def main():
    """Entry point for CLI."""
    cli()

if __name__ == "__main__":
    main()

Enregistrer dans pyproject.toml :

[project.scripts]
my-tool = "my_package.cli:main"

Utilisation :

pip install -e .
my-tool greet World
my-tool greet Alice --greeting="Hi"
my-tool repeat --count=3

Modèle 7 : CLI avec argparse

# src/my_package/cli.py
import argparse
import sys

def main():
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        description="My awesome tool",
        prog="my-tool"
    )

    parser.add_argument(
        "--version",
        action="version",
        version="%(prog)s 1.0.0"
    )

    subparsers = parser.add_subparsers(dest="command", help="Commands")

    # Add subcommand
    process_parser = subparsers.add_parser("process", help="Process data")
    process_parser.add_argument("input_file", help="Input file path")
    process_parser.add_argument(
        "--output", "-o",
        default="output.txt",
        help="Output file path"
    )

    args = parser.parse_args()

    if args.command == "process":
        process_data(args.input_file, args.output)
    else:
        parser.print_help()
        sys.exit(1)

def process_data(input_file: str, output_file: str):
    """Process data from input to output."""
    print(f"Processing {input_file} -> {output_file}")

if __name__ == "__main__":
    main()

Building et Publishing

Modèle 8 : Construire le package localement

# Installer les outils de build
pip install build twine

# Construire la distribution
python -m build

# Cela crée :
# dist/
#   my-package-1.0.0.tar.gz (source distribution)
#   my_package-1.0.0-py3-none-any.whl (wheel)

# Vérifier la distribution
twine check dist/*

Modèle 9 : Publier sur PyPI

# Installer les outils de publication
pip install twine

# Tester d'abord sur TestPyPI
twine upload --repository testpypi dist/*

# Installer depuis TestPyPI pour tester
pip install --index-url https://test.pypi.org/simple/ my-package

# Si tout va bien, publier sur PyPI
twine upload dist/*

Utiliser les API tokens (recommandé) :

# Créer ~/.pypirc
[distutils]
index-servers =
    pypi
    testpypi

[pypi]
username = __token__
password = pypi-...your-token...

[testpypi]
username = __token__
password = pypi-...your-test-token...

Modèle 10 : Publication automatisée avec GitHub Actions

# .github/workflows/publish.yml
name: Publish to PyPI

on:
  release:
    types: [created]

jobs:
  publish:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - 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 package
        run: twine check dist/*

      - name: Publish to PyPI
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
        run: twine upload dist/*

Pour les modèles avancés incluant les fichiers de données, les namespace packages, les extensions C, la gestion de version, les tests d'installation, les templates de documentation et les workflows de distribution, voir references/advanced-patterns.md

Skills similaires