nikkie-ftnextの日記

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

Pythonスクリプトをpipx runで実行した後に対話モードに入るには、PYTHONINSPECT環境変数を指定する

はじめに

『ぬ』という本をつくりました! nikkieです。

pipx runを使い倒していく中で見つけたtipsを取り上げます。

目次

前回: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: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 -cPythonコードを渡せます。
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.execvpepythonに渡したコード中の変数aが参照できます!

["python", "-i", "-c", (略)]と変えると、pipx-suburiで対話モードに入るので、PYTHONINSPECT環境変数の指定が-iとして機能しています。

仕組みとしては、PYTHONINSPECT=1 pipx-suburiと実行したとき、envPYTHONINSPECTというキーを持ちます(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の実装読みメモ


  1. pipx runがPEP 723をサポートを知った元記事によると./script.pyが必要ということなのですが、script.pyでも動くようです(宿題事項)
  2. https://formulae.brew.sh/formula/pipx
  3. このブログでは過去にsubprocess.run()を取り上げています