nikkie-ftnextの日記

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

inline script metadataをサポートしたツールでスクリプトを実行したときにもPythonの対話モードに入りたい! 環境変数 PYTHONINSPECT が有効か調査しました

はじめに

真珠星ちゃんとなるちゃん、もうギスギスだよ...(でもかわいい) nikkieです。

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

目次

私はPythonスクリプトを対話モードに入りながら書き進めたい

仮想環境を用意してスクリプトを開発しているとき、私は頻繁に対話モードに入って途中のデータを確認しながら書き上げます。
python -i script.pyとすると、スクリプトが実行された後に対話モードに入ります1

例えば

import httpx

resp = httpx.get("https://peps.python.org/api/peps.json")
data = resp.json()

ここまで書いて、python -i script.pyと実行して動くことを確認しつつ、先を書き進めるためにdataの形式を確認します。

>>> type(data)
<class 'dict'>
>>> data.keys()  # PEPの番号がキーと知る
dict_keys(['1', '2', '3', ..., '0'])
>>> data["1"].keys()
dict_keys(['number', 'title', 'authors', ..., 'url'])

inline script metadata(PEP 723)が採択され、ツールもサポートするように機能追加されています。
これらのツールは実際のところ、スクリプトに必要な仮想環境を管理してくれるのでとても便利です。

私のスクリプト開発もPEP 723サポートツールに移行しつつ、そのツールとPythonの対話モードの両方を使うことを模索しています。
現在期待しているのはPYTHONINSPECT環境変数
https://docs.python.org/ja/3/using/cmdline.html#envvar-PYTHONINSPECT

この変数に空でない文字列を設定するのは -i オプションを指定するのと等価です。

PEP 723をサポートするいくつかのツールと、PYTHONINSPECT環境変数で対話モードに入りたいが両立するのか、調査ログを残します。

pipx

条件付きで両立します。
条件付きと言っているのは、pipxがinline script metadataに沿って仮想環境を作っているとき、PYTHONINSPECTの指定によりハングしてしまうからです2
pipx runはmetadataを元に作った仮想環境をキャッシュする3ため、一度仮想環境を作った後PYTHONINSPECTを指定して対話モードに入れます。

今回はこのようなスクリプトを実行しています。

# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "rich"]
# ///
import httpx
from rich.pretty import pprint

resp = httpx.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:2])

区別のため、ツールごとにprintする件数を変えています。
※pipxはdependencies以外をまだサポートしていないように思われますが、どのツールでも揃えるために書いています

% pipx --version
1.7.1
% PYTHONINSPECT=1 pipx run example_pipx.py 
[('1', 'PEP Purpose and Guidelines'), ('2', 'Procedure for Adding New Modules')]
>>> len(data)
656

uv

両立すると思います(反例を知っている方は教えてください)

% uv --version
uv 0.4.12 (Homebrew 2024-09-18)
% PYTHONINSPECT=1 uv run example_uv.py
Reading inline script metadata from: example_uv.py
Installed 11 packages in 25ms
[
│   ('1', 'PEP Purpose and Guidelines'),
│   ('2', 'Procedure for Adding New Modules'),
│   ('3', 'Guidelines for Handling Bug Reports'),
│   ('4', 'Deprecation of Standard Modules')
]
>>> len(data)
656

uv run script.pyuv run python script.pyと同じ4とリファレンスにありました。
https://docs.astral.sh/uv/reference/cli/#uv-run
しかし、uv run python -i example_uv.py

ModuleNotFoundError: No module named 'httpx'

となります。

Rustの実装は今回は読みに行けていないのですが、後述するHatchと似たような感じなのかなと妄想しています(むふふ)

Hatch

両立すると思います(反例を知っている方は教えてください)

% hatch --version
Hatch, version 1.12.0
% PYTHONINSPECT=1 hatch run example_hatch.py 
[
│   ('1', 'PEP Purpose and Guidelines'),
│   ('2', 'Procedure for Adding New Modules'),
│   ('3', 'Guidelines for Handling Bug Reports'),
│   ('4', 'Deprecation of Standard Modules'),
│   ('5', 'Guidelines for Language Evolution')
]
>>> len(data)
656

これができる実装が気になって覗いてみたところ、pythonコマンドを実行していそうでした。
https://github.com/pypa/hatch/blob/hatch-v1.12.0/src/hatch/cli/run/__init__.py#L116
スクリプト実行時に環境変数PYTHONINSPECTが効くわけですね。

pipxと違って仮想環境をPython処理系を使わずにどのように作っているのかは気になるところです

PDM

両立しなさそうでした

% pdm --version
PDM, version 2.19.1

inline script metadataに沿って環境を作った後に、PYTHONINSPECT=1が効いて対話モードに入るようです

% PYTHONINSPECT=1 pdm run example_pdm.py   
>>> # NameError: name 'data' is not defined

Ctrl+Dで抜けると、このタイミングでスクリプトが実行され、入りたかった対話モードとなりました。

[
│   ('1', 'PEP Purpose and Guidelines'),
│   ('2', 'Procedure for Adding New Modules'),
│   ('3', 'Guidelines for Handling Bug Reports'),
│   ('4', 'Deprecation of Standard Modules'),
│   ('5', 'Guidelines for Language Evolution'),
│   ('6', 'Bug Fix Releases'),
│   ('7', 'Style Guide for C Code')
]
>>> len(data)
656

Ctrl+Dで抜けると「SystemExit: 0」が現れ、三度対話モード

SystemExit: 0
>>> # NameError: name 'data' is not defined

pipxが仮想環境を作るときにPYTHONINSPECTが指定されているとハングするのと同様で、PDMのinline script metadataサポートはPython処理系を複数回呼び出すことで実現しているようです。
処理系が呼び出されるたびにPYTHONINSPECTが効いて、対話モードに入ってしまうととらえています。

inline script metadataを持つスクリプト実行はsubprocess.Popenのようでした。
https://github.com/pdm-project/pdm/blob/2.19.1/src/pdm/cli/commands/run.py#L291
仮想環境を作る実装なども確認できれば、より説明できそうです

終わりに

PEP 723(inline script metadata)をサポートするツールがPYTHONINSPECT環境変数をサポートしているかを調査しました。
PYTHONINSPECTを指定して、ツールでスクリプトを実行したとき、対話モードに入るのは

  • uv
  • Hatch
  • 一度仮想環境を作っているpipx
    • 仮想環境を作っていない場合はハング

です。
PDMはPYTHONINSPECT環境変数とは別の仕組みを探したほうがよさそうです。

この記事で取り上げた挙動は、2024/09/24時点のものです(ツールのバージョンも示しました)。
これらのツールはすべてOSSですから、対話モードに入るとすごく捗る私としてはプルリクチャンスです🙌(一人だけだと手が回らないので、「私もほしいな」と思った方は一緒にプルリク送りましょう)


  1. https://docs.python.org/ja/3/using/cmdline.html#cmdoption-i
  2. pipx run --no-cache script.pyでキャッシュを使いません
  3. When used with a file ending in .py, the file will be treated as a script and run with a Python interpreter, i.e., uv run file.py is equivalent to uv run python file.py. 続きは If the script contains inline dependency metadata, it will be installed into an isolated, ephemeral environment.