いろいろやってきた1ファイルPythonだけど、もうあったw
このPEPは、Pythonファイル内に必要なPythonバージョンと依存モジュールを埋め込めるようにする「インライン スクリプト メタデータ」の書式を定義している。
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "Jinja2==3.1.6",
# "MarkupSafe==3.0.3",
# ]
# ///
今のところ script というタイプのみ定義されているけれど、それ以外のタイプについても後発のPEPによる拡充に期待、というところなのかな。
PEP 723をサポートしたツールはいくつかあるが、その中でもいろんなところでおすすめされている uv を試してみた。
uvとは何か
- 仮想環境を作るvenvやその中のモジュールを管理するpip等のツールを代替する統合ツール
- Rust製で同種の他のツールよりめっちゃ速い
- Pythonのバージョンも管理できる
- pyproject.toml プロジェクトの雛型を作ってモジュール管理したりできる
- インライン スクリプト メタデータを処理してスクリプトを実行できる
←ココ重要!
最後の項目が、1ファイルPythonに効く!
PEP 723の書式で依存関係を埋め込んだPythonスクリプトを uv で実行すると、自動的に仮想環境を作ってモジュールをインストールして実行してくれる。
つまり、前記事で頑張って自作してたところをまんまやってくれる素敵ツールです。
使ってみる
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "Jinja2==3.1.6",
# ]
# ///
import jinja2
print(jinja2.Template("Hello {{ name }}!").render(name="World"))
これを実行してみる。
>uv run --script test.py
Installed 2 packages in 9ms
Hello World!
>uv run --script test.py
Hello World!
1度目は仮想環境を作ってモジュールをインストールする時間が少しかかるが、2度目は作成済みの仮想環境ですぐに実行してくれる。
ちなみにWindows環境では上記のように実行する必要があるが、Linux/Mac環境ならもちろんファイルに実行権限を付けて直接実行できる。
$ chmod +x test.py
$ ./test.py
Installed 2 packages in 9ms
Hello World!
$ ./test.py
Hello World!
仮想環境はどこに?
これが一番困ったところ。
PyCharmに仮想環境のインタープリタを設定しないと、自動補完が効かないどころか import 文はじめモジュールに関する部分が全部エラー表示になる。
インタラクティブにPythonを動かすこともできない。
仮想環境のパスを指定するオプションもない。
ということでいろいろ調べていたら、ホームディレクトリにありました。
| プラットフォーム | 仮想環境のパス |
|---|---|
| Linux | ~/.cache/uv/environments-v2/test-<16進数16桁> |
| macOS | ~/.cache/uv/environments-v2/test-<16進数16桁> |
| Windows | %LOCALAPPDATA%\uv\cache\environments-v2\test-<16進数16桁> |
スクリプトのファイル名(拡張子を除く)に16進数16桁の文字列が付いた名前になっている。
<16進数16桁> のところはランダムっぽいけど、スクリプトや依存関係を変更しても、仮想環境のディレクトリを削除して再生成しても変化せず、スクリプトを別ディレクトリに移動したら変化した。
スクリプトのパスのハッシュか何かかも??
ともあれ無事に、仮想環境をアクティベートしたりいろいろできました。
pipがない!
この仮想環境、pipがインストールされていません。
これも一瞬「???」となったけど、でも大丈夫。uvがpipの機能も持っています。
これでいける。
>%LOCALAPPDATA%\uv\cache\environments-v2\test-60316609c68e53ea\Scripts\Activate.bat
(test.py) >uv pip list
Package Version
---------- -------
jinja2 3.1.6
markupsafe 3.0.3
ちゃんとアクティベートできて、モジュールもインストールされてます。
Gitリポジトリの依存関係
自分の環境では、一般的な処理を自作パッケージに入れて、それをBitbucketの非公開リポジトリに置いている。
依存関係でそれを指定する場合、以下のように指定できる。
# /// script
# dependencies = [
# ...
# "mylib @ git+ssh://git@bitbucket.org/myaccount/mylib.git",
# ]
# ///
公開リポジトリならこれだけで十分だけど、非公開なので認証が必要。
Linux/Mac環境では、予め ssh-add でSSHエージェントにキーを登録していればすんなりインストールしてくれる。
WindowsでPageantを使用している場合はもうひと手間必要で、PageantにSSHキーを登録したうえで、環境変数 GIT_SSH を設定しておく必要がある。
SET GIT_SSH=<path_to_putty>\plink.exe
これで、非公開リポジトリからもインストールできる。
ただ、これでやると毎回インストールが走って、起動時に待たされる(といってもちょっと引っかかる程度だが)。
バージョンを指定しない場合は、最新が入ってるかどうかの確認が毎回行われるらしい。
依存関係でバージョンを明示してあげると、この動作は抑制できた。
# /// script
# dependencies = [
# ...
# "mylib @ git+ssh://git@bitbucket.org/myaccount/mylib.git@664181eb07c572d9df0124c25a70b9eb27b94d05",
# ]
# ///
この辺の書き方、ググっても出てくるのは大抵 GitHub の書き方で、 Bitbucket では微妙に動作しないのが多いのも辛いところ。。( ≖_≖)=3
モジュールインストール以外の前処理もしたい
元々このスクリプトでやりたかったのは、VirtualBox の Python バインディングによる仮想マシンの作成の自動化だった。
なので、これができないと俺的には何も解決しない。
VirtualBox の Python バインディングを使うには Virtaul Box SDK が必要だが、このインストール方法がプラットフォームごとに異なる。
| プラットフォーム | 処理 |
|---|---|
| Windows | pywin32 を入れる%VBOX_MSI_INSTALL_PATH%\sdk を仮想環境にコピー<venv>\sdk\installer\python\vboxapi をインストール |
| macOS |
/Applications/VirtualBox.app/Contents/MacOS/sdk を仮想環境にコピー<venv>/sdk/installer/python/vboxapi をインストール |
| Linux |
https://www.virtualbox.org/wiki/Downloads から VirtualBox SDK をダウンロードして仮想環境に展開<venv>/sdk/installer/python/vboxapi をインストール |
これを、以前はシェルスクリプト部分で仮想環境作成の一環として pip install の前に実行していた。
シェルスクリプトはこういう「何でもアリ」の柔軟性がいいんだが、uv ではどう実現する?
というところは Gemini さんが素敵なアイデア出してくれました。
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ...
# dependencies = [
# ...
# "pywin32==311 ; sys.platform == 'win32'",
# ...
# ]
# ///
def setup_env(venv_path):
done_mark_path = venv_path / "setup_env.done"
if done_mark_path.exists():
return
import subprocess, ...
...
subprocess.run(["uv", "pip", "install", str(sdk_install_path / "installer" / "python" / "vboxapi")])
done_mark_path.touch()
import pathlib
import sys
setup_env(pathlib.Path(sys.prefix))
...(以下、普通の実装)
まず、Windowsのみインストールする pywin32 は requirement.txt の普通の書き方でいける。
仮想環境のパスは sys.prefix で取れる。
モジュールインストール前の事前処理は初回起動時に Python 内でやってしまって、uv pip install で普通にインストールしてしまえというもの。
まあ、以前シェルスクリプト部分でやってたことをそのまんまPythonでやっただけです。
スクリプト本体が少しごちゃつくというのはあるが、それでも前記事の「sh+py」的カオスよりははるかにマシ、というかマトモ。
それに、前記事ではシェルスクリプトで Linux/Mac 共用にしていたものの、Windows 用の環境準備・起動スクリプトは結局別に bat ファイルを作っていて、内容はほぼシェルスクリプトのコピーになっていた。
それをPythonで書き直すことにより共通化できたというのは何気に大きなポイント。
とは言え Windows 専用ファイルがなくなったわけじゃない。
けどここまで縮小できた。
@ECHO OFF
SETLOCAL
SET GIT_SSH=<path-to-putty>\plink.exe
uv run --script "%~dpn0.py" %*
つまりGIT_SSH の定義と、シェバンの代替処理だけやっている。
まあこのくらいなら許容範囲かなと。
蛇足:ところで env -S ってなんぞ???
PEP 723 + uv でできたことはおおむね以上なんだけど、気になったことを一つ。
シェバンについてはこの辺を参考にしてたんだけど、ここで /usr/bin/env -S が使われている。
#!/usr/bin/env -S uv run --script
print("Hello, world!")
これ何?ってことで調べたところ、env は2つの働きをしてくれるらしい。
- PATH環境変数でコマンドを探して実行する。
uvは通常~/.local/binにインストールされるので、絶対パスで指定すると可搬性が著しく悪化するが、シェバンでは絶対パスを指定する必要がある。envを使うと、env自身がPATH環境変数からコマンドを探してくれる。
- シェバンでは、コマンドの引数を1つしか指定できない。
複数指定すると、すべての引数がスペース区切りでつながった1つの文字列がコマンドに渡される。envで-Sオプションを指定すると、この「すべてが連結された1つの引数」を分解してコマンドを実行してくれるようになるため、複数の引数(runと--script)を渡すことができるようになる。
シェバンにそんな制約あったこと自体知らんかったw
もしかして *nix でオプションに長い版 --long と短い版 -l があって短い版が複数連結できる -xzvf のって、この制約回避のためだったりするのかね??
さらに蛇足:コマンドのパスいろいろ
| プラットフォーム | パス | 有無 |
|---|---|---|
| Ubuntu | /bin/sh /usr/bin/sh /bin/env /usr/bin/env |
有 有 有 有 |
| macOS | /bin/sh /usr/bin/sh /bin/env /usr/bin/env |
有 無 無 有 |
Ubuntu(Linuxはディストリビューションによるかもしれないので一応Ubuntu表記)については、/bin が usr/bin のシンボリックリンクなので当然っちゃ当然。
maxOS は独立したディレクトリなので、片方にしか入ってないものが多いみたい。
なので、シェバンによって「Linuxでは動いたのにぃぃぃ!?」がちょこちょこ発生した。
Ubuntuのほうが楽だけど、ここは mac のほうがマトモでLinuxが緩すぎるだけなのかも??
知らんけど。