mcpField
P
python-packaging

python-packaging

github.com/ovachiever開発5650 回インストール49 週間前に更新
リポジトリ

python-packaging について

Python 開発者向けに、パッケージの構成、ビルド、配布方法を解説するガイドです。現代的な設定、バージョン管理、PyPI への公開手順を扱います。

主な機能

  1. 01pyproject.toml による現代的な設定
  2. 02src レイアウトとフラットレイアウト
  3. 03CLI エントリーポイントの設定
  4. 04wheel とソース配布物のビルド
  5. 05PyPI やプライベートインデックスへの公開

活用シーン

  1. 01配布可能な Python ライブラリの作成
  2. 02Python CLI ツールのパッケージ化
  3. 03PyPI 公開の準備
  4. 04複数パッケージプロジェクトの構成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ovachiever/droid-tings --skill python-packaging

インストール後、Claude Code で/python-packagingを入力して読み込まれたか確認します。

詳細

Python パッケージング

最新のパッケージングツールや pyproject.toml を使った Python パッケージの作成、構成、配布、および PyPI への公開に関する包括的なガイドです。

このスキルを使用する場面

  • 配布する Python ライブラリを作成するとき
  • エントリーポイントを使ったコマンドラインツールを構築するとき
  • PyPI またはプライベートリポジトリにパッケージを公開するとき
  • Python プロジェクトの構成を設定するとき
  • 依存関係を含むインストール可能なパッケージを作成するとき
  • wheel とソースディストリビューションをビルドするとき
  • Python パッケージのバージョン管理とリリースを行うとき
  • 名前空間パッケージを作成するとき
  • パッケージのメタデータと分類子を実装するとき

主な概念

1. パッケージ構成
  • ソースレイアウト:src/package_name/(推奨)
  • フラットレイアウト:package_name/(より簡単ですが、柔軟性は低くなります)
  • パッケージのメタデータ:pyproject.toml、setup.py、または setup.cfg
  • 配布形式:wheel(.whl)とソースディストリビューション(.tar.gz)
2. 最新のパッケージング標準
  • PEP 517/518:ビルドシステムの要件
  • PEP 621:pyproject.toml のメタデータ
  • PEP 660:編集可能インストール
  • pyproject.toml:設定を一元管理するファイル
3. ビルドバックエンド
  • setuptools:従来型で広く使われています
  • hatchling:最新で、明確な方針を持つツールです
  • flit:軽量で、純粋な Python パッケージ向けです
  • poetry:依存関係管理とパッケージングに対応します
4. 配布
  • PyPI:Python Package Index(公開)
  • TestPyPI:本番環境への公開前のテスト用
  • プライベートリポジトリ:JFrog、AWS CodeArtifact など

クイックスタート

最小限のパッケージ構成
my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│   └── my_package/
│       ├── __init__.py
│       └── module.py
└── tests/
    └── test_module.py
最小限の pyproject.toml
[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 = "[email protected]"}]
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
    "requests>=2.28.0",
]

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

パッケージ構成のパターン

パターン 1:ソースレイアウト(推奨)
my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── src/
│   └── my_package/
│       ├── __init__.py
│       ├── core.py
│       ├── utils.py
│       └── py.typed          # For type hints
├── tests/
│   ├── __init__.py
│   ├── test_core.py
│   └── test_utils.py
└── docs/
    └── index.md

利点:

  • ソースから誤ってインポートするのを防ぎます
  • テスト時のインポートがすっきりします
  • 分離性が向上します

ソースレイアウト用の pyproject.toml:

[tool.setuptools.packages.find]
where = ["src"]
パターン 2:フラットレイアウト
my-package/
├── pyproject.toml
├── README.md
├── my_package/
│   ├── __init__.py
│   └── module.py
└── tests/
    └── test_module.py

より簡単ですが、次の点に注意してください:

  • インストールしなくてもパッケージをインポートできます
  • ライブラリとしてはプロフェッショナルさに欠けます
パターン 3:複数パッケージのプロジェクト
project/
├── pyproject.toml
├── packages/
│   ├── package-a/
│   │   └── src/
│   │       └── package_a/
│   └── package-b/
│       └── src/
│           └── package_b/
└── tests/

pyproject.toml の完全な例

パターン 4:機能を網羅した pyproject.toml
[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 = "[email protected]"},
]
maintainers = [
    {name = "Maintainer Name", email = "[email protected]"},
]
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"]

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

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

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

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

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

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

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
]
パターン 5:動的なバージョン管理
[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__"}

# Or use setuptools-scm for git-based versioning
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"

init.py 内:

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

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

コマンドラインインターフェース(CLI)のパターン

パターン 6:Click を使った CLI
# 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()

pyproject.toml に登録:

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

使用方法:

pip install -e .
my-tool greet World
my-tool greet Alice --greeting="Hi"
my-tool repeat --count=3
パターン 7:argparse を使った CLI
# 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()

ビルドと公開

パターン 8:パッケージをローカルでビルド
# Install build tools
pip install build twine

# Build distribution
python -m build

# This creates:
# dist/
#   my-package-1.0.0.tar.gz (source distribution)
#   my_package-1.0.0-py3-none-any.whl (wheel)

# Check the distribution
twine check dist/*
パターン 9:PyPI への公開
# Install publishing tools
pip install twine

# Test on TestPyPI first
twine upload --repository testpypi dist/*

# Install from TestPyPI to test
pip install --index-url https://test.pypi.org/simple/ my-package

# If all good, publish to PyPI
twine upload dist/*

API トークンを使用(推奨):

# Create ~/.pypirc
[distutils]
index-servers =
    pypi
    testpypi

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

[testpypi]
username = __token__
password = pypi-...your-test-token...
パターン 10: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/*

高度なパターン

パターン 11:データファイルを含める
[tool.setuptools.package-data]
my_package = [
    "data/*.json",
    "templates/*.html",
    "static/css/*.css",
    "py.typed",
]

データファイルへのアクセス:

# src/my_package/loader.py
from importlib.resources import files
import json

def load_config():
    """Load configuration from package data."""
    config_file = files("my_package").joinpath("data/config.json")
    with config_file.open() as f:
        return json.load(f)

# Python 3.9+
from importlib.resources import files

data = files("my_package").joinpath("data/file.txt").read_text()
パターン 12:名前空間パッケージ

複数のリポジトリに分割された大規模プロジェクト向け:

# Package 1: company-core
company/
└── core/
    ├── __init__.py
    └── models.py

# Package 2: company-api
company/
└── api/
    ├── __init__.py
    └── routes.py

名前空間ディレクトリ(company/)には init.py を含めないでください:

# company-core/pyproject.toml
[project]
name = "company-core"

[tool.setuptools.packages.find]
where = ["."]
include = ["company.core*"]

# company-api/pyproject.toml
[project]
name = "company-api"

[tool.setuptools.packages.find]
where = ["."]
include = ["company.api*"]

使用方法:

# Both packages can be imported under same namespace
from company.core import models
from company.api import routes
パターン 13:C 拡張
[build-system]
requires = ["setuptools>=61.0", "wheel", "Cython>=0.29"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
ext-modules = [
    {name = "my_package.fast_module", sources = ["src/fast_module.c"]},
]

または setup.py を使用:

# setup.py
from setuptools import setup, Extension

setup(
    ext_modules=[
        Extension(
            "my_package.fast_module",
            sources=["src/fast_module.c"],
            include_dirs=["src/include"],
        )
    ]
)

バージョン管理

パターン 14:セマンティックバージョニング
# src/my_package/__init__.py
__version__ = "1.2.3"

# Semantic versioning: MAJOR.MINOR.PATCH
# MAJOR: Breaking changes
# MINOR: New features (backward compatible)
# PATCH: Bug fixes

依存関係におけるバージョン制約:

dependencies = [
    "requests>=2.28.0,<3.0.0",  # Compatible range
    "click~=8.1.0",              # Compatible release (~= 8.1.0 means >=8.1.0,<8.2.0)
    "pydantic>=2.0",             # Minimum version
    "numpy==1.24.3",             # Exact version (avoid if possible)
]
パターン 15:Git ベースのバージョン管理
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
dynamic = ["version"]

[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
version_scheme = "post-release"
local_scheme = "dirty-tag"

次のようなバージョンが作成されます:

  • 1.0.0(git タグから)
  • 1.0.1.dev3+g1234567(タグから 3 コミット後)

インストールのテスト

パターン 16:編集可能インストール
# Install in development mode
pip install -e .

# With optional dependencies
pip install -e ".[dev]"
pip install -e ".[dev,docs]"

# Now changes to source code are immediately reflected
パターン 17:分離環境でのテスト
# Create virtual environment
python -m venv test-env
source test-env/bin/activate  # Linux/Mac
# test-env\Scripts\activate  # Windows

# Install package
pip install dist/my_package-1.0.0-py3-none-any.whl

# Test it works
python -c "import my_package; print(my_package.__version__)"

# Test CLI
my-tool --help

# Cleanup
deactivate
rm -rf test-env

ドキュメント

パターン 18:README.md のテンプレート
# My Package

[![PyPI version](https://badge.fury.io/py/my-package.svg)](https://pypi.org/project/my-package/)
[![Python versions](https://img.shields.io/pypi/pyversions/my-package.svg)](https://pypi.org/project/my-package/)
[![Tests](https://github.com/username/my-package/workflows/Tests/badge.svg)](https://github.com/username/my-package/actions)

Brief description of your package.

## Installation

```bash
pip install my-package

クイックスタート

from my_package import something

result = something.do_stuff()

機能

  • 機能 1
  • 機能 2
  • 機能 3

ドキュメント

ドキュメント全文:https://my-package.readthedocs.io

開発

git clone https://github.com/username/my-package.git
cd my-package
pip install -e ".[dev]"
pytest

ライセンス

MIT


## Common Patterns

### Pattern 19: Multi-Architecture Wheels

```yaml
# .github/workflows/wheels.yml
name: Build wheels

on: [push, pull_request]

jobs:
  build_wheels:
    name: Build wheels on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]

    steps:
      - uses: actions/checkout@v3

      - name: Build wheels
        uses: pypa/[email protected]

      - uses: actions/upload-artifact@v3
        with:
          path: ./wheelhouse/*.whl
パターン 20:プライベートパッケージインデックス
# Install from private index
pip install my-package --index-url https://private.pypi.org/simple/

# Or add to pip.conf
[global]
index-url = https://private.pypi.org/simple/
extra-index-url = https://pypi.org/simple/

# Upload to private index
twine upload --repository-url https://private.pypi.org/ dist/*

ファイルテンプレート

Python パッケージ用 .gitignore
# Build artifacts
build/
dist/
*.egg-info/
*.egg
.eggs/

# Python
__pycache__/
*.py[cod]
*$py.class
*.so

# Virtual environments
venv/
env/
ENV/

# IDE
.vscode/
.idea/
*.swp

# Testing
.pytest_cache/
.coverage
htmlcov/

# Distribution
*.whl
*.tar.gz
MANIFEST.in
# MANIFEST.in
include README.md
include LICENSE
include pyproject.toml

recursive-include src/my_package/data *.json
recursive-include src/my_package/templates *.html
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

公開前のチェックリスト

  • コードのテストが完了している(pytest が成功する)
  • ドキュメントが完成している(README、docstring)
  • バージョン番号を更新している
  • CHANGELOG.md を更新している
  • ライセンスファイルを含めている
  • pyproject.toml が完成している
  • パッケージをエラーなくビルドできる
  • クリーンな環境でインストールをテストしている
  • CLI ツールが動作する(該当する場合)
  • PyPI のメタデータが正しい(classifier、keyword)
  • GitHub リポジトリをリンクしている
  • まず TestPyPI でテストしている
  • リリース用の Git タグを作成している

リソース

ベストプラクティスのまとめ

  1. src/ レイアウトを使用することで、パッケージ構造を整理する
  2. pyproject.toml を使用することで、最新のパッケージングに対応する
  3. build-system.requires でビルド依存関係を固定する
  4. セマンティックバージョニングに従って適切にバージョンを付ける
  5. すべてのメタデータ(classifier、URL など)を含める
  6. クリーンな環境でインストールをテストする
  7. PyPI に公開する前にTestPyPI を使用する
  8. README と docstring で十分にドキュメントを整備する
  9. LICENSE ファイルを含める
  10. CI/CD で公開を自動化する

コメント