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

11 KiB

name, description, category
name description category
python-packaging Automatically applies when configuring Python project packaging. Ensures proper pyproject.toml setup, project layout, build configuration, metadata, and distribution best practices. 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

# 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

# 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

# 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

# src/mypackage/py.typed
# This file signals that the package supports type hints (PEP 561)
# Leave empty or add configuration if needed

Version Management

# 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

# 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

# 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

# .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

# ❌ 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
  • dependency-management - For managing dependencies
  • type-safety - For type hints
  • pytest-patterns - For testing
  • git-workflow-standards - For releases
  • docs-style - For documentation