【入門編】Python開発のポイズン・ピルを回避せよ:uvプロジェクトにおけるpyproject.tomlの動的バージョン管理とGitハッシュ連結テクニック – ビルド・パッケージ管理ツール生産性向上バイブル

こんにちは!日々のPython開発、快適に進んでいますか?

「あ、この修正、さっきのバージョンと何が違うんだっけ…?」
「手元でビルドしたパッケージと、CI/CDでビルドしたパッケージでバージョンが衝突してデプロイに失敗した…」

Pythonでの開発中、こんな「バージョン管理の泥沼」に足を取られた経験はありませんか?手動で `__version__ = “1.0.1”` なんて書き換えているうちは、まだ序の口。複数の開発者が同時並行で機能追加を行う現場では、この古典的なアプローチは「ポイズン・ピル(毒薬)」となってプロジェクトの信頼性を内側から蝕んでいきます。

今回は、世界最速のPythonパッケージマネージャー `uv` と、現代の標準仕様である `pyproject.toml` を極限まで使い倒し、「ビルド時にGitのコミットハッシュをバージョンに自動連結する動的バージョン管理テクニック」を徹底解説します。

これをマスターすれば、バージョン管理のヒューマンエラーは根絶され、成果物のトレーサビリティ(追跡可能性)が劇的に向上します。さあ、一緒にモダンでスマートなPython開発の世界へ足を踏み入れましょう!

—

なぜ、従来のバージョン管理は破綻するのか?

まず、私たちが直面している問題の根源をアーキテクトの視点から紐解いておきましょう。

Pythonエコシステムにおいて、パッケージのバージョンは長らく `setup.py` や `__init__.py` の中に静的にハードコーディングされてきました。しかし、このアプローチには致命的な欠陥があります。

1. 人間の記憶と手作業への依存: リリース担当者がバージョンを上げ忘れる、あるいはコンフリクトを起こす。
2. 「誰の、どの状態のコードか」のブラックボックス化: バージョンが `1.0.0` とだけ書かれていても、それがGitHubのどのコミットからビルドされたものか、バイナリ単体では判別不能。

これを解決するのが、「ビルドシステムにバージョン計算を動的に委譲する」という思想です。コードの真実(Single Source of Truth)はGitリポジトリの履歴に一任し、パッケージングの瞬間に関数やスクリプトを走らせて、バージョン文字列を自動生成する。これがプロフェッショナルな現場のスタンダードです。

—

圧倒的スピードを誇る次世代ツール:`uv` とは?

今回主役として使用する `uv` は、Rust製で驚異的な高速動作を実現した、パッケージマネージャー兼ビルドツールです。従来の `pip` や `poetry` が持っていた「依存関係解決の遅さ」というストレスを完全に過去のものにしてくれます。

1. `uv` のインストール

まずは、お使いの環境に `uv` を導入しましょう。公式が提供するインストーラーを使うのが最も安全かつ一瞬です(macOS / Linuxの場合)。

公式のインストーラーシェルスクリプトを安全にダウンロードして実行します
curl -LsSf https://astral.sh/uv/install.sh | sh

インストールが完了したら、新しいターミナルを開いてバージョンを確認してください。

uv –version
出力例: uv 0.x.y (高パフォーマンスなRust製Pythonツールチェイン)

2. プロジェクトの初期化と基礎セットアップ

それでは、動的バージョン管理を実験するための新しいプロジェクト(ここでは `my-ultrafast-app` とします)を `uv` で爆速作成しましょう。

新規ライブラリ形式のプロジェクトを初期化(pyproject.tomlが自動生成されます)
uv init –lib my-ultrafast-app

作成されたディレクトリへ移動
cd my-ultrafast-app

ここで生成される `pyproject.toml` は、PEP 621に準拠したPythonプロジェクトの心臓部です。

—

本丸:`pyproject.toml` とビルドフックによるGitハッシュ連結

ここからが本記事のハイライトです。`uv` のビルドバックエンド(標準の `hatchling` など)を利用し、「Gitの最新コミットハッシュを動的に取得してバージョン末尾に付与する」仕組みを構築します。

ステップ 1: Gitリポジトリの初期化

動的バージョン管理は、ターゲットがGitリポジトリであることを前提とします。まずはこのディレクトリをGit管理下に置き、最初のコミットを行いましょう。

git init
git add .
git commit -m “feat: 初期プロジェクトセットアップ”

ステップ 2: `pyproject.toml` の設定と動的フィールドの宣言

`pyproject.toml` を開き、バージョンを「動的(dynamic)」として宣言し、ビルドシステムにフックを仕掛けます。以下の設定を記述してください。

[project]
name = “my-ultrafast-app”
バージョンをハードコーディングせず、ビルドシステムに動的生成を委任することを宣言
dynamic = [“version”]
description = “Gitハッシュを動的埋め込みする最速Pythonアプリ”
readme = “README.md”
requires-python = “>=3.10”
dependencies = []

[build-system]
ビルドバックエンドとして強力な拡張性を持つ hatchling を採用
requires = [“hatchling”]
build-backend = “hatchling.build”

[tool.hatch.version]
バージョン生成のロジックをカスタムスクリプト(下部で定義)にルーティング
source = “regex”
動的バージョンのフックを有効化するための設定

…おっと、よりエレガントにGitハッシュを埋め込むために、Hatchlingの強力な「プラグイン機構」または「動的ファイル読み込み」を使いましょう。Hatchlingでは、環境変数やカスタムフックを使ってバージョンを上書きできます。

ここでは、最も確実で実用的な「ビルド直前にスクリプトでバージョンファイルを動的生成するアプローチ」を採用します。

ステップ 3: 動的バージョン生成スクリプトの配置

プロジェクトのルートディレクトリに `scripts/set_version.py` を作成します。このスクリプトは、Gitから現在のタグやコミットハッシュを取得し、パッケージ内のバージョンファイルを書き換えます。

scripts/set_version.py
import subprocess
from pathlib import Path

def get_git_version() -> str:
“””
Gitリポジトリから現在のコミットハッシュと直近のタグを取得し、
セマンティックバージョニングに沿った動的バージョン文字列を構築する。
“””
try:
# 直近のGitタグを取得 (例: v0.1.0)
tag = subprocess.check_output(
[“git”, “describe”, “–tags”, “–abbrev=0”],
stderr=subprocess.DEVNULL
).decode(“utf-8”).strip()
# ‘v’ がつい ていれば除去
base_version = tag.lstrip(‘v’)
except subprocess.CalledProcessError:
# タグがまだ存在しない場合のフォールバックベースバージョン
base_version = “0.1.0”

try:
# 現在のGitコミットハッシュ(短縮版: 7桁)を取得
commit_hash = subprocess.check_output(
[“git”, “rev-parse”, “–short”, “HEAD”],
stderr=subprocess.DEVNULL
).decode(“utf-8”).strip()
except subprocess.CalledProcessError:
commit_hash = “unknown”

# 例: 0.1.0+g1a2b3c4 (PEP 440準拠のローカルバージョン識別子)
dynamic_version = f”{base_version}+g{commit_hash}”
return dynamic_version

if __name__ == “__main__”:
version = get_git_version()
version_file = Path(“src/my_ultrafast_app/__init__.py”)

# バージョン情報を書き込むファイルの存在を確認し、__version__ を更新
content = f’__version__ = “{version}”\n’
version_file.write_text(content)
print(f”[Dynamic Versioning] バージョンを ‘{version}’ に動的更新しました。”)

—

動作確認:HelloWorldをビルドして確かめる

それでは、この仕組みが完璧に機能するか、実際にパッケージをビルドして動作確認(HelloWorld)を行いましょう。

1. パッケージ構造の確認とコードの記述

`uv init –lib` によって生成されたソースディレクトリに、簡単な動作確認用コードを記述します。

src/my_ultrafast_app/__init__.py
(このファイルは先ほどのスクリプトによって上書きされます)
__version__ = “0.1.0+g0000000″

def hello() -> str:
return f”Hello from my-ultrafast-app! Version: {__version__}”

2. ビルド前フックの実行と `uv build`

パッケージをホイール(`.whl`)としてビルドする直前に、先ほどのバージョン生成スクリプトを実行し、その後に `uv build` を呼び出します。

1. 確実に最新のGitハッシュをコードに埋め込む
python scripts/set_version.py

出力例:
[Dynamic Versioning] バージョンを ‘0.1.0+g8a3f12b’ に動的更新しました。

2. uv を使って超高速ビルドを実行
uv build

コマンドを実行すると、一瞬でビルドが完了し、`dist/` ディレクトリに以下のようなホイールファイルが生成されます。

  • `dist/my_ultrafast_app-0.1.0+g8a3f12b-py3-none-any.whl`

お気づきでしょうか? ファイル名自体に、現在のGitコミットハッシュ(`g8a3f12b`)が鮮やかに埋め込まれています! これにより、「どのソースコードの正確なスナップショットからビルドされたバイナリなのか」がファイル名を見ただけで一目瞭然になります。

3. 動作確認スクリプトの実行

生成されたパッケージを仮想環境にインストールして、動的バージョンが正しくコード内に反映されているか確認しましょう。

仮想環境を作成して有効化
uv venv
source .venv/bin/activate

ビルドしたパッケージをローカルインストール
uv pip install dist/my_ultrafast_app-0.1.0+g8a3f12b-py3-none-any.whl

Pythonインタプリタで動作確認
python -c “import my_ultrafast_app; print(my_ultrafast_app.hello())”

実行結果:

Hello from my-ultrafast-app! Version: 0.1.0+g8a3f12b

完璧です!手動でのバージョン書き換えの手間は一切なく、Gitのコミット履歴と完全に同期した動的バージョンがアプリケーションの内部まで正確に伝播しています。

—

先輩エンジニアからのメッセージ

今回は、`uv` とカスタムスクリプトを組み合わせた、実践的でロバストな動的バージョン管理テクニックをご紹介しました。

これをCI/CDパイプライン(GitHub Actionsなど)のビルドステップに組み込むだけで、すべてのリリース成果物に自動的にコミットIDが刻印されるようになります。「本番環境で動いているバージョン、どのコードだっけ?」という悪夢から、あなたは今日ですべて解放されるのです。

これをマスターすれば、毎日のコーディングとデプロイメントが劇的に、そして圧倒的に楽になりますよ。ぜひ、あなたの次のプロジェクトから導入してみてください。現場の信頼性が文字通り一段階跳ね上がるのを実感できるはずです!

タイトルとURLをコピーしました