mcpField
P
python-packaging

python-packaging

github.com/hermeticormus開発12476 回インストール5 週間前に更新
リポジトリ

python-packaging について

python-packagingは、Pythonライブラリやコマンドラインツールの作者向けのパッケージ化・リリースガイドです。pyproject.tomlや現代的なビルド規格、公開手順を扱い、構成や設定の例を掲載しています。

主な機能

  1. 01プロジェクト構成とpyproject.toml設定
  2. 02ビルドバックエンドと配布形式の選択
  3. 03コマンドラインエントリーポイントの設定
  4. 04PyPI向けのビルドと公開
  5. 05バージョン管理とインストールテスト

活用シーン

  1. 01配布可能なPythonライブラリの作成
  2. 02Pythonプロジェクトへのコマンドラインツール追加
  3. 03PyPIまたはプライベートリポジトリへの公開
  4. 04パッケージ化、バージョン、リリース手順の整備

インストール

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

$npx skills add https://github.com/hermeticormus/libreuiux-claude-code --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 で公開を自動化する

コメント