内联脚本元数据#
本规范定义了一种元数据格式,该格式可嵌入到单文件 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
,其中包含脚本元数据(依赖项数据和工具配置)。
本文档可以包括顶级字段 dependencies
和 requires-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 版本(如果已定义)。