はじめに
『ぬ』という本をつくりました! nikkieです。
pipx run
を使い倒していく中で見つけたtipsを取り上げます。
目次
詳細は上の記事を見ていただきたいのですが、かいつまんでご紹介。
Pythonスクリプトの冒頭にコメントとしてメタデータを書きます。
このスクリプトを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
はインスペクトモードなのですね。
例えば、以下のように実装途中のスクリプトがあるとき、
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:subprocess.runで
python
コマンドを呼び出す
- Windows以外:os.execvpeで
python
コマンドにプロセスが変わる
検証
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
- vは可変長個の引数(argsはリストやタプル)
- pはfileを見つけるうえで環境変数
PATH
を使う
- eは辞書型のenv引数で環境変数を定義
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のユースケースほどうまくいかなかったという苦情の共有、お待ちしています
補足: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()
です