nikkie-ftnextの日記

イベントレポートや読書メモを発信

inline script metadataをサポートしたツールが仮想環境を再利用する様子を、振る舞いやソースコードから調べる

はじめに

未完成のポラリス、ありがとう〜!! nikkieです

inline script metadataをサポートしたツール(pipx, uv, Hatch, PDM)について、より使い倒すためにまたまた調べます。

目次

ツールは仮想環境をどのくらいの頻度で一から用意する?

先日のみんなのPython勉強会で、inline script metadataについてLTしました。

いただいた反応から確かめたいことが。

  • ツールは仮想環境をどの単位で用意する?
  • ツールは仮想環境にどのくらいの頻度で依存をインストールする?
    • 毎回ゼロから? 一度インストールして再利用?

pipx -> uvで半年くらい使った体感は

  • スクリプトごとに仮想環境を用意してそう
  • 依存は最初の一度だけインストール
  • metadataにdependenciesが追加されたら、それもインストール

というものです。
ここについて、もう少し確信を得たく調べてみます。

pipx

依存が同じであれば同一の仮想環境を使うようです

% pipx --version
1.7.1

pipx runでinline script metadataを持ったスクリプトを初回実行すると、ターミナルにアニメーションで出力されます

仮想環境の名前を決めていると思われる部分
https://github.com/pypa/pipx/blob/1.7.1/src/pipx/commands/run.py#L86-L90

Note that the environment name is based on the identified requirements, and not on the script name.
This is deliberate, as it ensures that two scripts with the same requirements can use the same environment, which means fewer environments need to be managed.

ソースも見ましたが、pipxはdependenciesのみサポートなので、上記のrequirementsはdependenciesの意味だと思います

pipx runPYTHONINSPECTでハングしてしまってCtrl+Cで抜けた後に、再度同じスクリプトを渡す(pipx run script.py)と入る仮想環境には依存がなくて落ちる経験1をしていたのですが、requirementsごとに仮想環境を作っているというのは説明になるなと思いました

uv

仮想環境をキャッシュしていそうなことは分かりました。
同一スクリプトを別名でコピーしてuv runを実行した感じ、metadataが同じであれば同じ仮想環境が使われているように見えます(が、果たして...)

% uv --version
uv 0.4.12 (Homebrew 2024-09-18)

ターミナルの出力を手がかりに挑みますが、Rustは読み慣れてないのでムズいです。

Reading inline script metadata from
https://github.com/astral-sh/uv/blob/0.4.12/crates/uv/src/commands/project/run.rs#L107

仮想環境を用意していそう
CachedEnvironment::get_or_create
https://github.com/astral-sh/uv/blob/0.4.12/crates/uv/src/commands/project/run.rs#L206

その実装
https://github.com/astral-sh/uv/blob/0.4.12/crates/uv/src/commands/project/environment.rs#L30 resolve_environment()あたりなのかな🤯
https://github.com/astral-sh/uv/blob/0.4.12/crates/uv/src/commands/project/mod.rs#L668

Hatch

スクリプトごとに仮想環境を用意するようです。
実行前にmetadataの依存と同期しています。
毎回全ての依存をインストールするわけでなく、追加された依存を都度インストールということですね

% hatch --version
Hatch, version 1.12.0

Hatchもinline script metadataを持ったスクリプト実行時のアニメーションを手がかりに見ていきます。
アニメーションのテキストは、ドキュメントに明示されています:https://hatch.pypa.io/latest/how-to/run/python-scripts/

inline script metadataを持つスクリプトhatch runで実行するとき、
https://github.com/pypa/hatch/blob/hatch-v1.12.0/src/hatch/cli/run/__init__.py#L69
hatch run script.pyと渡ったスクリプトのパスはHatchが定義するPathpathlib.Pathを継承)にインスタンス化されます。
https://github.com/pypa/hatch/blob/hatch-v1.12.0/src/hatch/utils/fs.py#L34
HatchのPathidを持ち、「SyB4bPbL」のような値を返します2

スクリプト絶対パスによるID3を使って、Hatchのenvを登録し、そのenvでスクリプトを実行していると読みました。

# https://github.com/pypa/hatch/blob/hatch-v1.12.0/src/hatch/cli/run/__init__.py#L115-L116
for context in app.runner_context([script.id]):
    context.add_shell_command(['python', first_arg, *args[1:]])

PDM

スクリプトごとに仮想環境を用意するようです。
毎回全ての依存をインストールするわけでなく、追加された依存を都度インストールという動きではないかと思われます(※読み切れていないところもある)

% pdm --version
PDM, version 2.19.1

pdm run script.pyと実行したとき、os.path.expanduser()でパスの~を展開してスクリプトのパスを得ます。
それを_get_script_env()に渡します。
https://github.com/pdm-project/pdm/blob/2.19.1/src/pdm/cli/commands/run.py#L234-L235
_get_script_env()ではスクリプトのパスからハッシュ値を得て仮想環境の名前としています。
https://github.com/pdm-project/pdm/blob/2.19.1/src/pdm/cli/commands/run.py#L167

この仮想環境はpdm run--recreateが渡されない限りは再利用されます。

依存のインストールはinstall_requirements()を呼び出して。
https://github.com/pdm-project/pdm/blob/2.19.1/src/pdm/cli/commands/run.py#L179
install_requirements(reqs, env, clean=True)という呼び出しで、依存をインストールしています。
差分更新になっているのかな?

clean=Truesynchronizerに渡りますが、これはクリーンインストールでなく「clean unneeded packages」でした

終わりに

inline script metadataをサポートしたツールがどれくらいの頻度で仮想環境を一から用意するか、コマンド実行 + ソースリーディングで確認しました。
今回読んだ範囲では以下のように理解しています。

  • Hatch(やPDM)
    • スクリプトの単位で仮想環境を用意
    • スクリプト実行時にdependenciesの同期を実施している(差分があればインストールされる)
  • pipx
    • inline script metadataのdepencenciesが同じであれば同一の仮想環境を使う(「two scripts with the same requirements can use the same environment」)
    • スクリプトにdependenciesを追加したら(キャッシュになければ)新たな仮想環境を作ると思われる
  • uv(ソースは読み切れず)
    • 振る舞いから、metadataが同じであれば同じ仮想環境が使われているように見える(pipxに近い感じ)

もともとスクリプト開発で都度人力で仮想環境を作っていました。
inline script metadataをサポートしたツールに任せた場合、スクリプト単位やmetadataの単位で仮想環境が作られます
依存インストールの頻度は人力と変わらず、差分があるときと思われます4


  1. --no-cacheを指定してpipx runすると、requirementsを見て仮想環境を作り直すので、解消します
  2. 実装 https://github.com/pypa/hatch/blob/hatch-v1.12.0/src/hatch/utils/fs.py#L44-L45
  3. resolve()しています。「Ensure consistent IDs for storage」 ref: https://github.com/pypa/hatch/blob/hatch-v1.12.0/src/hatch/cli/run/__init__.py#L72-L73
  4. ただしpipxは同一の仮想環境を使い回せるメリットがある一方、裏返しとしてこの点不利かも