はじめに
『ぬ』という本をつくりました! nikkieです。
pipx runを使い倒していく中で見つけたtipsを取り上げます。
目次
- はじめに
- 目次
- 前回:Inline script metadata(PEP 723)を一部サポートしたpipx
- pipx runにpython -i相当の動きをさせたい
- PYTHONINSPECT環境変数の指定で、なぜうまくいくのか
- 検証
- 終わりに
- 補足:pipx runの実装読みメモ
前回:Inline script metadata(PEP 723)を一部サポートしたpipx
詳細は上の記事を見ていただきたいのですが、かいつまんでご紹介。
Pythonスクリプトの冒頭にコメントとしてメタデータを書きます。
# /// script # dependencies = [ # "requests<3", # "rich", # ] # ///
このスクリプトをpipx run ./script.pyと実行1すると、
- pipxが仮想環境を管理(dependenciesに挙げたライブラリをインストール)
- この仮想環境が有効になった状態でscript.pyが実行される
つまり、このスクリプトを書いた開発者は仮想環境を一切管理しなくていいんです(python -m venvで作る必要からありません)。
pipxが仮想環境を代わりに管理してくれます。
と っ て も 便 利 !!
スクリプトを動かすために仮想環境を作る必要がなくなり、私はめちゃめちゃ捗っています。
なお、pipx runには--python引数があり、スクリプトを実行するPythonのバージョンも指定できます。
pipx runにpython -i相当の動きをさせたい
pipx runを知るまでは、仮想環境を作り、スクリプトを少し書いてはpython -iで実行して動作確認しながら進めていました。
pythonに-iを指定すると、「スクリプトかコマンドを実行した後にインタラクティブモードに入ります。」
https://docs.python.org/ja/3/using/cmdline.html#cmdoption-i
これと同じように、pipx runでもスクリプトを少し書いた後に実行して、動作確認しながら進めたいです。
その方法を調べていて見つけたのは、PYTHONINSPECT環境変数の指定!
ドキュメントによると
https://docs.python.org/ja/3/using/cmdline.html#envvar-PYTHONINSPECT
この変数に空でない文字列を設定するのは
-iオプションを指定するのと等価です。
-iはインスペクトモードなのですね。
例えば、以下のように実装途中のスクリプトがあるとき、
# /// script # dependencies = [ # "requests<3", # "rich", # ] # /// import requests from rich.pretty import pprint
% PYTHONINSPECT=1 pipx run ./script.py
>>> resp = requests.get("https://peps.python.org/api/peps.json")
>>> data = resp.json()
>>> pprint([(k, v["title"]) for k, v in data.items()][:3])
[
│ ('1', 'PEP Purpose and Guidelines'),
│ ('2', 'Procedure for Adding New Modules'),
│ ('3', 'Guidelines for Handling Bug Reports')
]
dependenciesに挙げたrequestsやrichがインストールされた環境で、対話モードに入っています!
動作環境
PYTHONINSPECT環境変数の指定で、なぜうまくいくのか
pipx runの実装はpythonコマンドを呼び出しているから、という理解です。
pipx runの実装は、Windowsとそれ以外で分かれていました。
https://github.com/pypa/pipx/blob/1.5.0/src/pipx/util.py#L376-L389
検証
Windows機は用意できなかったのでmacOSで検証しています。
どちらの実装の場合もPYTHONINSPECT環境変数の指定で対話モードが立ち上がることを、簡単な再現実装を用意して確認しました
. ├── src/ │ └── suburi.py └── pyproject.toml
[project] name = "pipx-suburi" version = "0.1.0" [project.scripts] pipx-suburi = "suburi:main"
os.execvpeとPYTHONINSPECT環境変数
suburi.py
import os def main(): print("Start") env = dict(os.environ) os.execvpe("python", ["python", "-c", "a = 1 + 2; print('Hello World')"], env) print("Finish") # この行は実行されません
python -cはPythonコードを渡せます。
https://docs.python.org/ja/3/using/cmdline.html#cmdoption-c
% PYTHONINSPECT=1 pipx-suburi Start Hello World >>> a 3
python -cの実行(=Hello World出力)後に対話モードに入りました。
os.execvpeでpythonに渡したコード中の変数aが参照できます!
["python", "-i", "-c", (略)]と変えると、pipx-suburiで対話モードに入るので、PYTHONINSPECT環境変数の指定が-iとして機能しています。
仕組みとしては、PYTHONINSPECT=1 pipx-suburiと実行したとき、envがPYTHONINSPECTというキーを持ちます(breakpointを張って確認)。
この環境変数の指定がpythonコマンド実行にも引き継がれているわけです。
os.execvpe(file, args, env)のドキュメントより、関数名の命名規則について
https://docs.python.org/ja/3/library/os.html#os.execvpe
subprocessとPYTHONINSPECT環境変数
suburi.py
import os import sys import subprocess def main(): print("Start") env = dict(os.environ) sys.exit( subprocess.run( ["python", "-c", "a = 1 + 2; print('Hello World')"], env=env, stdout=None, stderr=None, encoding="utf-8", universal_newlines=True, check=False, ).returncode ) print("Finish") # この行は実行されません
引数の説明はドキュメントを参照3
https://docs.python.org/ja/3/library/subprocess.html#subprocess.run
% PYTHONINSPECT=1 pipx-suburi Start Hello World >>> a 3
pipx runで発生するかは未確認ですが、subprocess版の再現実装には副作用がありました。
対話モードをexit()で抜けると
>>> exit()
Traceback (most recent call last):
File "/.../.venv/bin/pipx-suburi", line 8, in <module>
sys.exit(main())
^^^^^^
File "/.../src/suburi.py", line 16, in main
sys.exit(
SystemExit: 0
再度対話モードに入ります(PYTHONINSPECTが指定されているから?)
>>> exit()
もう一度exit()で抜けられます
終わりに
PYTHONINSPECT環境変数を指定してpipx runすると、pipxが管理する仮想環境でpythonの対話モードに入れます!
PEP 723のInline script metadata(のdependencies)の恩恵にあずかって仮想環境の管理から解放されつつ、その仮想環境にも対話モードでアクセスできるという、私としては願ったり叶ったりです。神!
Pythonスクリプトの開発で、開発者が仮想環境を触らなくてよいというのは私にとっては福音です。
サーバレスにならって、スクリプト開発で仮想環境レスと言えるかも(どちらも管理からの解放)。
pipx(やPEP 723をサポートするその他のツール)に寄せていきたく、引き続き探求していきます!
もし興味を持った方がいれば、ぜひお試しあれ!
共感の声、またはnikkieのユースケースほどうまくいかなかったという苦情の共有、お待ちしています
PEP 723対応のpipx runが手に馴染むのですが、python -i相当のことができないかなと思ったのです。
— nikkie / にっきー (@ftnext) 2024年4月21日
os.execvpeでpythonコマンドを実行するプロセスに変わっていたので、PYTHONINSPECT環境変数を設定すればいいみたい。https://t.co/sJysMSEMIp
pipxがあればスクリプトの開発で仮想環境いらない!かも https://t.co/ZMvTuiH4rp
補足:pipx runの実装読みメモ
pipxコマンドの定義はmain.pyのcli()関数cli()関数でrun_pipx_command()関数呼び出しrun_pipx_command()関数でrunサブコマンドの実装(commands/run.pyのrun()関数)呼び出しcommands/run.pyのrun()関数は(おそらく)run_script()関数呼び出しrun_script()関数はutil.pyのexec_app()関数呼び出しexec_app()関数の実装が、上で見たようにWindowsならsubprocess.run()、Windows以外ならos.execvpe()です
-
pipx runがPEP 723をサポートを知った元記事によると./script.pyが必要ということなのですが、script.pyでも動くようです(宿題事項)↩ - https://formulae.brew.sh/formula/pipx↩
-
このブログでは過去に
subprocess.run()を取り上げています ↩