内联脚本元数据#

本规范定义了一种元数据格式,该格式可嵌入到单文件 Python 脚本中,以帮助启动器、IDE 和其他可能需要与此类脚本交互的外部工具。

规范#

本规范定义了一种元数据注释块格式(松散地受 reStructuredText 指令 启发)。

任何 Python 脚本都可以具有顶级注释块,该注释块必须以行 # /// TYPE 开头,其中 TYPE 确定如何处理内容。即:一个 #,后跟一个空格,后跟三个正斜杠,后跟一个空格,后跟元数据的类型。块必须以行 # /// 结尾。即:一个 #,后跟一个空格,后跟三个正斜杠。 TYPE 只能由 ASCII 字母、数字和连字符组成。

这两行(# /// TYPE# ///)之间的每一行都必须是以 # 开头的注释。如果 # 后面有字符,则第一个字符必须是空格。嵌入式内容是通过去掉每行的前两个字符(如果第二个字符是空格)或仅去掉第一个字符(这意味着该行仅包含一个 #)来形成的。

当下一行不是如上所述的有效嵌入式内容行时,将给出结束行 # /// 的优先级。例如,以下是单个完全有效的块

# /// 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 中的 [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.pythonlang.cn/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])

参考实现#

以下是关于如何在 Python 3.11 或更高版本上读取元数据的示例。

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 修改内容的粗略示例。

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 格式的库。这绝不是编辑的必要条件,而是一种“很高兴有”的功能。

以下是读取任意元数据块流的示例。

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[run] 表格已删除,使 dependenciesrequires-python 键成为顶级键。此外,此规范不再是临时的。