nikkie-ftnextの日記

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

小ネタ:inline script metadataをサポートしたツールで実行するPythonスクリプトは、同じディレクトリのモジュールをimportできる

はじめに

グルクン🐟、くくるちゃんを幸せそうな表情にしたので、しゅき... nikkieです

PyCon JP 2024でPEP 723(inline script metadata)の話をしてきました。
発表後に気づいた小ネタです。

目次

別のPythonファイルからのimportをサポートしている!

あまりオススメしないですが、できることに気づきました1

.
├── script.py
└── mylib.py

script.py(richとmylib.pyを使う)

# /// script
# dependencies = ["httpx", "rich"]
# ///

from rich.pretty import pprint

from mylib import fetch_peps

data = fetch_peps()
pprint([(k, v["title"]) for k, v in data.items()][:5])

mylib.py(httpxを使う)

import httpx


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

この構成で、hatch, uv, pipx, pdmと試したところ2、すべて動作しました3

% hatch run example/import-case/script.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')
]

種明かし:スクリプトのあるディレクトリがsys.pathに追加されている

importできるポイントは、script.pyと同じディレクトリにmylib.pyがあることです。

inline script metadataをサポートしたツールは、Python処理系を呼び出しています。
対話モードに入る方法を調べる中で知りました。

python example/import-case/script.pyのように、処理系にスクリプトのパスを指定して立ち上げたとき、
https://docs.python.org/ja/3/using/cmdline.html#cmdarg-script

スクリプト名が Python ファイルを直接指定していた場合、そのファイルを含むディレクトリが sys.path の先頭に追加され、そのファイルは __main__ モジュールとして実行されます。

つまり、script.pyがあるディレクトリがsys.pathの先頭に追加される4ので、mylibモジュール(mylib.py)が見つかり、importできるわけですね!

% PYTHONINSPECT=1 hatch run example/import-case/script.py
>>> import sys
>>> sys.path[0]
'/.../pep723/example/import-case'

inline script metadataをサポートしたツールは渡したスクリプトごとに仮想環境を作りますから、metadataは渡すスクリプトに全ての依存を書いています。

  • ツールはscript.pyに対して仮想環境を作る
  • その仮想環境が有効な状態でimport mylibされるので、mylib.pyが使うライブラリもscript.pyのmetadataに書いて、仮想環境にインストールしておく

思うに、1つのスクリプトを分割したくなったら、プロジェクトへの移行を考えたい

最初に「あまりオススメしない」と書きましたが、inline script metadataをサポートしたツールの出番でなくなっているように思います。
例えば、さらにscript2.pyを追加したとすると

.
├── script.py
├── script2.py
└── mylib.py

mylibの依存ライブラリをmetadataに繰り返し書かねばなりません。

# /// script
# dependencies = ["httpx"]
# ///

from mylib import fetch_peps

これをやるよりは、2つのスクリプト用に共通の仮想環境を1つ作ったほうがよいと思います(少なくともこれはやりたい)

.
├── .venv/  # httpxやrichをインストール
├── script.py
├── script2.py
└── mylib.py

また個人的には、Pythonプロジェクトにしたいタイミングですね。
PEP 723に書かれているユースケース5の1つに、単一スクリプトをプロジェクトにする際にmetadataが役立つとあります。

終わりに

inline script metadataをサポートしたツールで実行するスクリプトの中で、別のPythonファイルをimportできます。

Pythonスクリプトを使った開発の中でimportできるとき・できないときがあるな〜という経験をしてきましたが、処理系に渡したスクリプトのあるディレクトリがsys.pathに追加されることを知って謎が解けた感覚です。


  1. ソースコード
  2. 自作したpep723も同様に動作しています✌️
  3. hatch, uv, pdmにはrequires-pythonも指定できます
  4. sys.pathを扱った過去記事
  5. A script that desires to transition to a directory-type project.https://peps.python.org/pep-0723/#rationale