Poetryからuvへの完全移行:pyproject.tomlの仕様不整合を撃破する自動マイグレーションと実務最適化戦略
テックリードの皆さん、日々のビルド待ち時間や依存関係解決の遅さにフラストレーションを感じていないでしょうか。
Pythonのパッケージ管理エコシステムは、長らく `pip` の原始的な世界から、`Poetry` によるモダンな宣言的管理へと進化し、そして今、Rust製極速パッケージマネージャー `uv`(Astral製) の登場によってパラダイムシフトの最終章を迎えようとしています。
しかし、既存の大規模なPoetryプロジェクトをそのまま `uv` に移行しようとすると、必ず壁にぶつかります。それが 「`pyproject.toml` の仕様不整合(PEP 621準拠の差異)」 です。
本記事では、Poetry依存のプロジェクトを `uv` へシームレスに移行し、開発スピードを劇的に高めるための実践的アプローチと、仕様のギャップを自動で埋めるマイグレーションスクリプトの全貌を、プロのアーキテクトの視点から解説します。
—
1. なぜPoetryから`uv`なのか? 内部構造から紐解く圧倒的優位性
Poetryは素晴らしいツールですが、依存関係解決のアルゴリズム(特に大規模なロックファイルの生成)において、Python実装である限界から処理が重くなりがちです。
一方、`uv` は Cargo(Rustのパッケージマネージャー)の知見をベースにゼロから設計されており、システム全体でキャッシュを共有するグローバルキャッシュ戦略と、並行ネットワークリクエストを極限まで最適化したアーキテクトによって、Poetryの10倍〜100倍近い速度で仮想環境の構築とロックファイルの更新を行います。
移行における最大の障壁:仕様の非互換性
`uv` は標準化された `PEP 621`(`[project]` テーブル)を厳格に解釈します。対してPoetryは、独自の `[tool.poetry]` テーブルに依存メタデータを閉じ込めてきました。
そのため、Poetryの記述が残ったままの `pyproject.toml` を `uv` で読み込ませると、ビルドバックエンドの解釈違いやメタデータの欠落エラーを引き起こします。
—
2. 実務で直面する不整合ポイントとチェックリスト
移行作業を始める前に、Poetryと `uv`(および標準PEP 621)の間にある仕様の断絶を理解しておく必要があります。以下のチェックリストをクリアしなければ、CI/CDパイプラインが確実に破綻します。
- [ ] ビルドバックエンドの変更: `poetry.core.masonry.api` から `hatchling` や `setuptools`、あるいは `uv` が推奨する標準バックエンドへの移行。
- [ ] 依存関係の配列フォーマット: Poetry独自の特殊な記述(例: 複数制約やソース指定)の標準化。
- [ ] スクリプト・エントリーポイントの定義: `[tool.poetry.scripts]` から `[project.scripts]` へのマッピング。
- [ ] 開発依存関係の分離: `[tool.poetry.group.dev.dependencies]` から `[dependency-groups]` (PEP 735) への変換。
—
3. 不整合を自動解消する!Python製マイグレーションスクリプト
手動で `pyproject.toml` を書き換えるのは、ヒューマンエラーの温床であり、数多あるモジュールで破綻を招きます。
ここでは、Poetry形式の `pyproject.toml` を解析し、`uv` 完全対応のPEP 621形式へ安全に変換するプロダクション品質の自動マイグレーションスクリプトを提供します。
このスクリプトは、単なる文字列置換ではなく、抽象構文木(AST)に近い堅牢なパースを行い、既存のコメントや設定を破壊せずに必要なテーブル構造だけを再構築します。
!/usr/bin/env python3
“””
Poetry to uv Migration Script
Author: Lead DevOps Architect
Description: poetry.lock および pyproject.toml を解析し、
uv (PEP 621 / PEP 735) 準拠の構造へと安全に変換します。
“””
import sys
from pathlib import Path
import tomlkit
def migrate_poetry_to_uv(file_path: str) -> None:
path = Path(file_path)
if not path.exists():
print(f”Error: {file_path} が見つかりません。”, file=sys.stderr)
sys.stderr.flush()
sys.exit(1)
# TOMLのコメントやフォーマットを保持したままパースする
with open(path, “r”, encoding=”utf-8″) as f:
doc = tomlkit.load(f)
# 1. [tool.poetry] の存在確認
if “tool” not in doc or “poetry” not in doc[“tool”]:
print(“Error: 指定されたファイルはPoetry形式の pyproject.toml ではありません。”, file=sys.stderr)
sys.stderr.flush()
sys.exit(1)
poetry_meta = doc[“tool”][“poetry”]
# 2. 標準 [project] テーブルの初期化(未作成の場合)
if “project” not in doc:
doc[“project”] = tomlkit.table()
project = doc[“project”]
# 基本メタデータの移行
if “name” in poetry_meta:
project[“name”] = poetry_meta[“name”]
if “version” in poetry_meta:
project[“version”] = poetry_meta[“version”]
if “description” in poetry_meta:
project[“description”] = poetry_meta[“description”]
if “readme” in poetry_meta:
project[“readme”] = poetry_meta[“readme”]
if “authors” in poetry_meta:
project[“authors”] = poetry_meta[“authors”]
if “license” in poetry_meta:
project[“license”] = poetry_meta[“license”]
if “python” in poetry_meta:
# Poetryのpythonバージョン指定をPEP 508形式(requires-python)に変換
project[“requires-python”] = poetry_meta[“python”]
# 3. メイン依存関係の移行 ([tool.poetry.dependencies] -> [project.dependencies])
dependencies = []
if “dependencies” in poetry_meta:
for name, constraint in poetry_meta[“dependencies”].items():
if name.lower() == “python”:
continue # pythonの制約は requires-python に移行済みのためスキップ
if isinstance(constraint, str):
dependencies.append(f”{name} {constraint}”)
elif isinstance(constraint, dict):
# 複雑な制約(バージョン+ソース指定など)のハンドリング
version = constraint.get(“version”, “”)
dep_str = f”{name} {version}”.strip()
dependencies.append(dep_str)
else:
dependencies.append(name)
if dependencies:
project[“dependencies”] = dependencies
# 4. 開発依存関係の移行 ([tool.poetry.group.dev.dependencies] -> [dependency-groups])
if “tool” in doc and “poetry” in doc[“tool”] and “group” in doc[“tool”][“poetry”]:
groups = doc[“tool”][“poetry”][“group”]
if “dev” in groups and “dependencies” in groups[“dev”]:
if “dependency-groups” not in doc:
doc[“dependency-groups”] = tomlkit.table()
dev_deps = []
for name, constraint in groups[“dev”][“dependencies”].items():
if isinstance(constraint, str):
dev_deps.append(f”{name} {constraint}”)
else:
dev_deps.append(name)
doc[“dependency-groups”][“dev”] = dev_deps
# 5. ビルドシステムの変更 (poetry.core -> hatchling)
if “build-system” not in doc:
doc[“build-system”] = tomlkit.table()
doc[“build-system”][“requires”] = [“hatchling>=1.18.0”]
doc[“build-system”][“build-backend”] = “hatchling.build”
# 6. スクリプトの移行 ([tool.poetry.scripts] -> [project.scripts])
if “scripts” in poetry_meta:
project[“scripts”] = poetry_meta[“scripts”]
# 7. 古いPoetry設定のクリーンアップ
del doc[“tool”][“poetry”]
if not doc[“tool”]:
del doc[“tool”]
# バックアップの作成と新しい設定の書き込み
backup_path = path.with_suffix(“.toml.bak”)
path.rename(backup_path)
print(f”Backup created: {backup_path}”)
with open(path, “w”, encoding=”utf-8″) as f:
tomlkit.dump(doc, f)
print(“Migration completed successfully! pyproject.toml has been converted to uv format.”)
if __name__ == “__main__”:
target_file = sys.argv[1] if len(sys.argv) > 1 else “pyproject.toml”
migrate_poetry_to_uv(target_file)
—
4. チーム開発を加速させる `uv` のベストプラクティス構成
移行が完了したら、チーム全体で開発速度を最大化するための環境構築を行います。
共有化すべき設定ファイル:`uv.toml`
プロジェクトルートに `uv.toml` を配置することで、開発者間およびCI環境での挙動を完全に同期させ、偶発的なビルド差異を防ぎます。以下の設定は実戦投入で極めて高い効果を発揮します。
==========================================
uv Project Configuration
==========================================
仮想環境のディレクトリ名を標準の .venv に固定
venv = “.venv”
可能な限りグローバルキャッシュをハードリンクし、ディスク容量圧迫とI/Oを抑制
link-mode = “hardlink”
依存関係のコンパイル時に厳密なプラットフォーム互換性を強制
compile-bytecode = true
[pip]
プライベートパッケージレジストリ(例: AWS CodeArtifactやNexus)の安全なフォールバック設定
index-url = “https://pypi.org/simple”
extra-index-url = [
“https://pypi.internal.company.com/simple”
]
—
5. 開発スピードを極限まで高めるチートシート
日々のコーディングライフにおいて、生産性を一段上のステージへ押し上げるキーボードショートカットとCLIコマンドの極意です。
1. 高速仮想環境同期 (`uv sync`)
Poetryの `poetry install` に相当するコマンドですが、キャッシュ機構の効率化により数秒で終わります。開発グループ(dev)も含めて同期する場合は以下を実行します。
開発依存関係を含めて一瞬で環境を同期
uv sync –group dev
2. 仮想環境の自動有効化と直叩き
`uv run` コマンドを使用すれば、手動で `source .venv/bin/activate` を叩く必要すらありません。あらゆるスクリプトやテストランナーを、自動的に正しい仮想環境コンテキスト上で実行します。
アクティベート不要で pytest を実行(バックグラウンドで仮想環境を自動検知・構築)
uv run pytest tests/
3. IDE(VS Code / PyCharm)連携の最適化
VS Codeを使用している場合、`settings.json` に以下の設定を施すことで、`uv` が作成した `.venv` をPythonインタープリターとして自動認識させ、Lint/Typecheckの速度を保ちます。
{
// Python インタープリターのパスをプロジェクトローカルの .venv に固定
“python.defaultInterpreterPath”: “${workspaceFolder}/.venv/bin/python”,
// ターミナル起動時に自動的に仮想環境をアクティベートするのを抑制(uv runに任せるため)
“python.terminal.activateEnvironment”: false,
// リンター(Ruff等)の実行環境として .venv のバイナリを優先使用
“ruff.importStrategy”: “fromEnvironment”
}
—
6. まとめ:モダンなPython開発環境への移行は今
Poetryから `uv` への移行は、単に「ツールを速いものに変える」というだけの話ではありません。ビルドや依存関係解決という、これまで開発者の集中力を削いできた無駄な待ち時間をゼロにするための投資です。
本記事で紹介した自動マイグレーションスクリプトと `uv.toml` のベストプラクティスを活用し、あなたのチームの開発パイプラインを次世代のスピードへとアップグレードしてください。エンジニアの時間は、コードを書くためにこそ使われるべきなのです。