インラインスクリプトのメタデータ#

この仕様では、単一ファイルの Python スクリプトがそのようなスクリプトと相互作用するローンチャー・ IDE ・その他外部ツールを手助けするために、スクリプト内に埋め込むことができるメタデータフォーマットを定義しています。

仕様#

この仕様では、メタデータのコメントブロックの (reStructuredText Directives に大雑把に基づく) フォーマットを定義します。

すべての Python スクリプトについて、トップレベルのコメントブロックがあってもかまいませんが、 (そのようなコメントブロックは) TYPE によって内容物をどのように処理するべきかが決まるような # /// TYPE の行で始まらなければなりません。つまり: 単独の # 、これに後続する単独の空白文字、さらに後続する3個のスラッシュ文字 <forward slash> 、さらに空白文字、メタデータのタイプ。ブロックは # /// の行で終わらなければなりません。つまり: 単独の # 、後続する空白文字、3個のスラッシュ文字。 TYPE は、 ASCII 文字・数字・ハイフンだけで構成されていなければなりません。

これらふたつの行 (# /// TYPE# ///) の間に入る各行は、 # で始まるコメントでなければなりません。もし、 # に文字が続くのであれば、その最初の文字は空白文字でなければなりません。ここに置かれる内容は、2文字目が空白文字であれば最初の2文字を削除して整形されますが、さもなければ最初の文字だけ (つまり、単独の # だけから構成される行) になります。

次の行が上述の埋め込みコメント行として正当なものではない場合に、 (コメントブロックの) 終わりの行である # /// が優先されます。例えば、次に示すものはひとつの完全に正当なブロックです:

# /// some-toml
# embedded-csharp = """
# /// <summary>
# /// text
# ///
# /// </summary>
# public class MyClass { }
# """
# ///

開始行は、他の開始行とその終了行の間に位置してはいけません。そのような場合には、ツール類はエラーを生成しても構いません。閉じられていないブロックは無視されなければなりません。

同じ TYPE が定義されたコメントブロックが複数存在する場合、ツール類はエラーを生成しなければなりません。

組み込みのメタデータを読み取るツール類は、 Python 標準のエンコーディング宣言を尊重しても構いません。もし、そうしないことを選択するのであれば、UTF-8 であるものとしてファイルを扱わなければなりません。

これは、メタデータをパースするのに使っても構わない正統な正規表現です:

(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$

テキストの仕様と正規表現の間に矛盾のある環境下では、テキストの仕様が優先されます。

ツール類は、この仕様によって標準化されていないタイプを伴ったメタデータブロックから読み取ることがあってはなりません。

スクリプトのタイプ#

メタデータブロックのひとつめのタイプは script という名前で、スクリプトメタデータ (依存関係のデータとツールの設定) を格納しています。

このドキュメント (訳註、メタデータブロックのことか) は、トップレベルのフィールドとして dependenciesrequires-python を含むことができ、また、オプションとして [tool] テーブルを含んでいても構いません。

[tool] は、どんなツールでもスクリプトランナーでもその他のものでも、振る舞いを設定するのに使うことができます。pyproject.toml <pyproject-tool-table> 内の [tool] テーブル と同一のセマンティクスを持ちます。

トップレベルのフィールドは:

  • dependencies: スクリプトの動作時の依存関係を指定する文字列のリスト。それぞれの項目は正当な 依存関係指定子 でなければなりません。

  • requires-python: そのスクリプトと互換性のある Python のバージョン(群) を指定する文字列。このフィールドの値は正当な バージョン指定子 でなければなりません。

スクリプトを走らせる側では、指定された dependencies を提供できない場合にはエラーを起こさなければなりません。指定された requires-python で指定されたバージョンの Python を提供できない場合には、スクリプトを走らせる側でエラーを起こすべきです。

#

組み込まれたメタデータを伴うスクリプトの例を以下に示します:

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

import requests
from rich.pretty import pprint

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

参照実装#

3.11 およびそれ以降の Python でどのようにメタデータを読み取るのかについて例を以下に示します。

import re
import tomllib

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def read(script: str) -> dict | None:
    name = 'script'
    matches = list(
        filter(lambda m: m.group('type') == name, re.finditer(REGEX, script))
    )
    if len(matches) > 1:
        raise ValueError(f'Multiple {name} blocks found')
    elif len(matches) == 1:
        content = ''.join(
            line[2:] if line.startswith('# ') else line[1:]
            for line in matches[0].group('content').splitlines(keepends=True)
        )
        return tomllib.loads(content)
    else:
        return None

ツール類は、しばしば、パッケージ管理機構や CI における依存関係更新の自動化機構のように、依存関係を編集するでしょう。以下に、 tomlkit library を使って内容を書き換える際のおおまかな例を示します。

import re

import tomlkit

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def add(script: str, dependency: str) -> str:
    match = re.search(REGEX, script)
    content = ''.join(
        line[2:] if line.startswith('# ') else line[1:]
        for line in match.group('content').splitlines(keepends=True)
    )

    config = tomlkit.parse(content)
    config['dependencies'].append(dependency)
    new_content = ''.join(
        f'# {line}' if line.strip() else f'#{line}'
        for line in tomlkit.dumps(config).splitlines(keepends=True)
    )

    start, end = match.span('content')
    return script[:start] + new_content + script[end:]

この例では、TOML フォーマッティングを保存するライブラリを使っていることに留意してください。これは、決して編集することに対する要求事項というわけではなく、むしろ "あったらいいね <nice to have>" 的なものです。

任意のメタデータブロックのストリームをどのように読み取るかの例を以下に示します。

import re
from typing import Iterator

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def stream(script: str) -> Iterator[tuple[str, str]]:
    for match in re.finditer(REGEX, script):
        yield match.group('type'), ''.join(
            line[2:] if line.startswith('# ') else line[1:]
            for line in match.group('content').splitlines(keepends=True)
        )

推奨事項#

異なるバージョンの Python を管理する機能を持つツール類は、スクリプトの requires-python メタデータと互換性のある Python のバージョンのうちの利用可能な最も新しい版を使うことを試みるべきです。

歴史#

  • 2023年10月: PEP 723 を通じて、この仕様が条件付きで承認されました。

  • 2024年1月: PEP 723 に対する修正を通じて、 pyproject メタデータブロックのタイプが script に改名され、 dependencies キーと require-python キーがトップレベルに昇格する形で [run] テーブルが廃止されました。さらに、この仕様はもはや暫定的なものではなくなりました。