python-packaging について
Python 開発者向けに、パッケージの構成、ビルド、配布方法を解説するガイドです。現代的な設定、バージョン管理、PyPI への公開手順を扱います。
主な機能
- 01pyproject.toml による現代的な設定
- 02src レイアウトとフラットレイアウト
- 03CLI エントリーポイントの設定
- 04wheel とソース配布物のビルド
- 05PyPI やプライベートインデックスへの公開
活用シーン
- 01配布可能な Python ライブラリの作成
- 02Python CLI ツールのパッケージ化
- 03PyPI 公開の準備
- 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
[](https://pypi.org/project/my-package/)
[](https://pypi.org/project/my-package/)
[](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 タグを作成している
リソース
- Python パッケージングガイド: https://packaging.python.org/
- PyPI: https://pypi.org/
- TestPyPI: https://test.pypi.org/
- setuptools ドキュメント: https://setuptools.pypa.io/
- build: https://pypa-build.readthedocs.io/
- twine: https://twine.readthedocs.io/
ベストプラクティスのまとめ
- src/ レイアウトを使用することで、パッケージ構造を整理する
- pyproject.toml を使用することで、最新のパッケージングに対応する
- build-system.requires でビルド依存関係を固定する
- セマンティックバージョニングに従って適切にバージョンを付ける
- すべてのメタデータ(classifier、URL など)を含める
- クリーンな環境でインストールをテストする
- PyPI に公開する前にTestPyPI を使用する
- README と docstring で十分にドキュメントを整備する
- LICENSE ファイルを含める
- CI/CD で公開を自動化する
コメント
Markdown 対応