PEP 680 – tomllib:标准库中解析 TOML 的支持
- 作者:
- Taneli Hukkinen,Shantanu Jain <hauntsaninja at gmail.com>
- 赞助商:
- Petr Viktorin <encukou at gmail.com>
- 讨论至:
- Discourse 主题
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2022 年 1 月 1 日
- Python 版本:
- 3.11
- 历史记录:
- 2021 年 12 月 9 日,2022 年 1 月 27 日
- 决议:
- Python-Dev 主题
摘要
本 PEP 提出将 tomllib
模块添加到标准库中,用于解析 TOML(Tom’s Obvious Minimal Language,https://toml.cn)。
动机
TOML 是 Python 打包的首选格式,正如 PEP 517、PEP 518 和 PEP 621 所证明的那样。这为 Python 构建工具带来了一个引导问题,迫使它们将 TOML 解析包进行捆绑或采用其他不可取的解决方法,并给重新打包者和其他下游消费者带来严重问题。在标准库中包含 TOML 支持将很好地解决所有这些问题。
此外,许多 Python 工具现在可以通过 TOML 进行配置,例如 black
、mypy
、pytest
、tox
、pylint
和 isort
。许多没有使用 TOML 配置的工具,例如 flake8
,将缺少标准库支持作为 主要原因。鉴于 TOML 在 Python 生态系统中的特殊地位,将其包含为内置库是有意义的。
最后,TOML 作为一种格式越来越受欢迎(原因在 PEP 518 中概述),各种 Python TOML 库在 PyPI 上拥有大约 2000 个反向依赖项(相比之下,requests
拥有大约 28000 个反向依赖项)。因此,即使考虑了 Python 打包和相关工具的需求之外,这也有可能成为一个普遍有用的补充。
理由
本 PEP 建议基于第三方库 tomli
(github.com/hukkin/tomli)来构建标准库对读取 TOML 的支持。
许多项目最近已切换到使用 tomli
,例如 pip
、build
、pytest
、mypy
、black
、flit
、coverage
、setuptools-scm
和 cibuildwheel
。
tomli
积极维护且经过良好测试。它大约有 800 行代码,拥有 100% 的测试覆盖率,并且通过了 提议的官方 TOML 兼容性测试套件 中的所有测试,以及 更成熟的 BurntSushi/toml-test 测试套件。
规范
将向 Python 标准库添加一个新模块 tomllib
,公开以下公共函数
def load(
fp: SupportsRead[bytes],
/,
*,
parse_float: Callable[[str], Any] = ...,
) -> dict[str, Any]: ...
def loads(
s: str,
/,
*,
parse_float: Callable[[str], Any] = ...,
) -> dict[str, Any]: ...
tomllib.load
将包含 TOML 文档的二进制类文件对象反序列化为 Python dict
。fp
参数必须具有一个 read()
方法,该方法与 io.RawIOBase.read()
的 API 相同。
tomllib.loads
将包含 TOML 文档的 str
实例反序列化为 Python dict
。
parse_float
参数是一个可调用对象,它以 TOML 浮点数的原始字符串表示形式作为输入,并返回相应的 Python 对象(类似于 json.load
中的 parse_float
)。例如,用户可以传递一个返回 decimal.Decimal
的函数,用于需要精确度的用例。默认情况下,TOML 浮点数将解析为 Python float
类型的实例。
返回的对象仅包含基本 Python 对象(str
、int
、bool
、float
、datetime.{datetime,date,time}
、list
、dict
,其键为字符串)以及 parse_float
的结果。
如果 TOML 无效,则会引发 tomllib.TOMLDecodeError
。
请注意,本 PEP 并没有提出 tomllib.dump
或 tomllib.dumps
函数;有关详细信息,请参阅 包含用于写入 TOML 的 API。
维护影响
TOML 的稳定性
2021 年 1 月发布 TOML 1.0.0 版本表明 TOML 格式现在应正式被视为稳定版本。从经验上讲,即使在发布 TOML 1.0.0 之前,TOML 也已证明是一种稳定的格式。从 变更日志 中,我们可以看到 TOML 自 2020 年 4 月以来没有重大更改,并且在过去五年(2017-2021)中发布了两个版本。
如果 TOML 规范发生更改,我们可以将次要修订视为错误修复,并更新现有实现。如果发生重大破坏性更改,我们应保留对 TOML 1.x 的支持。
建议的实现的可维护性
建议的实现(tomli
)是纯 Python,经过良好测试,代码量不到 1000 行。它非常简洁,提供的 API 表面积小于其他 TOML 实现。
tomli
的作者愿意帮助将 tomli
整合到标准库中并帮助维护它,参见这篇帖子。此外,Python 核心开发人员 Petr Viktorin 已表示愿意维护一个读 API,参见这篇帖子。
目前不需要用 C 重写解析器。TOML 解析很少成为应用程序的瓶颈,而性能需求更高的用户可以使用第三方库(就像 JSON 已经经常发生的那样,尽管 Python 提供了一个标准库 C 扩展模块)。
TOML 支持其他事物的滑坡
正如在 动机 部分中讨论的那样,TOML 在 Python 生态系统中占有特殊地位,用于读取 PEP 518 pyproject.toml
打包和工具配置文件。将 TOML 包含在标准库中的这个主要原因不适用于其他格式,例如 YAML 或 MessagePack。
此外,TOML 的简洁性使其与其他格式(如 YAML)区分开来,YAML 非常复杂,难以构建和解析。
但是,将来可能会在另一个 PEP 中添加用于写入 TOML 的 API。
向后兼容性
本提案在标准库中没有任何向后兼容性问题,因为它描述了一个新模块。任何现有的名为 tomllib
的第三方模块都将被破坏,因为 import tomllib
将导入标准库模块。但是,tomllib
未在 PyPI 上注册,因此不太可能存在广泛使用的同名模块。
请注意,我们避免使用更直观的名称 toml
,以避免对已固定当前 toml
PyPI 包版本的用户的向后兼容性影响。有关详细信息,请参阅 模块的备选名称 部分。
安全影响
实现中的错误可能导致潜在的安全问题。但是,解析器的输出仅限于简单数据类型;无法加载任意类可以避免更“强大”的格式(如 pickle 和 YAML)中常见的安全问题。此外,实现将使用纯 Python,这减少了 C 中常见的安全问题,例如缓冲区溢出。
如何教授
tomllib
的 API 模仿了其他成熟的文件格式库,如 json
和 pickle
。文档中将解释缺乏 dump
函数的原因,并提供指向相关第三方库(例如 tomlkit
、tomli-w
、pytomlpp
)的链接。
参考实现
提议的实现可以在 https://github.com/hukkin/tomli 中找到。
被拒绝的建议
基于另一个 TOML 实现
存在多个潜在的替代实现。
tomlkit
已经建立,积极维护,并支持 TOML 1.0.0。一个重要的区别是tomlkit
支持样式往返。因此,它具有更复杂的 API 和实现(代码量大约是tomli
的 5 倍)。它的作者认为tomlkit
不是标准库的合适选择。toml
是一个使用非常广泛的库。但是,它没有得到积极维护,不支持 TOML 1.0.0,并且存在一些已知的错误。它的 API 比tomli
更复杂。它允许通过复杂的编码器 API 自定义输出样式,以及一些非常有限且几乎未使用功能,通过未记录的解码器 API 保留输入样式。有关其 API 与此 PEP 的区别的更多详细信息,请参阅 附录 A。pytomlpp
是 C++ 项目toml++
的 Python 包装器。纯 Python 库比扩展模块更容易维护。rtoml
是 Rust 项目toml-rs
的 Python 包装器,因此与pytomlpp
有类似的缺点。此外,它不支持 TOML 1.0.0。- 从头开始编写实现。目前尚不清楚我们能从中得到什么;
tomli
满足我们的需求,并且作者愿意帮助将其纳入标准库。
包含用于写入 TOML 的 API
不包括编写 TOML 的 API 有几个原因。
对于激励此 PEP 的用例,不需要编写 TOML 的功能:核心 Python 打包工具,以及需要读取 TOML 配置文件的项目。
编辑现有 TOML 文件(而不是编写全新的 TOML 文件)的用例,最好使用样式保留库。TOML 旨在作为一种人类可读和可编辑的配置格式,因此保留注释、格式和其他标记非常重要。这需要一个解析器,其输出包括样式相关的元数据,使其不适合输出简单的 Python 类型,例如 str
和 dict
。此外,它极大地复杂了 API 的设计。
即使不考虑样式保留,在如何设计写入 API 方面也有太多自由度。例如,库应该使用什么默认样式(缩进、垂直和水平间距、引号等)来进行输出,以及用户应该对其进行多少控制?库应该如何处理输入和输出验证?它应该支持自定义类型的序列化吗?如果是,应该如何处理?虽然存在解决这些问题的合理选择,但标准库的性质决定了我们只有一次机会获得成功。
目前,没有 CPython 核心开发人员表示愿意维护写入 API,或赞助包含写入 API 的 PEP。由于更改或删除标准库中的内容很困难,因此目前最好谨慎地选择排除,并在将来重新审视此问题。
因此,编写 TOML 则留给第三方库。如果将来找到适合的 API 和相关用例,可以在未来的 PEP 中添加写入支持。
各种 API 细节
tomllib.load
的第一个参数接受的类型
PyPI 上的 toml
库允许将路径(以及路径类对象的列表,忽略缺失的文件并将文档合并到单个对象中)传递给它的 load
函数。但是,在这里允许这样做将与 json.load
、pickle.load
和其他标准库函数的行为不一致。如果我们一致认为这里的统一性是可取的,那么允许路径将超出此 PEP 的范围。这可以在用户代码中轻松明确地解决,或者使用第三方库。
提议的 API 使用二进制文件,而 toml.load
使用文本文件,json.load
使用二者。使用二进制文件可以确保使用 UTF-8 编码(确保在其他默认编码平台(如 Windows)上正确解析),并避免由于文本模式中的通用换行符而错误地将包含单行回车的文件解析为有效的 TOML。
tomllib.loads
的第一个参数接受的类型
虽然 tomllib.load
使用二进制文件,但 tomllib.loads
使用文本字符串。乍一看,这可能显得不一致。
引用 TOML v1.0.0 规范。
TOML 文件必须是有效的 UTF-8 编码 Unicode 文档。
tomllib.loads
的目的不是加载 TOML 文件,而是加载文件存储的文档。在 Python 中,Unicode 文档最自然的表示是 str
,而不是 bytes
。
如果需要,将来可以添加 bytes
支持,但我们不知道任何使用它的用例。
控制 tomllib.load[s]
返回的映射类型
PyPI 上的 toml
库在它的 load[s]
函数中接受一个 _dict
参数,其工作方式类似于 json.load[s]
中的 object_hook
参数。在 https://grep.app 上发现了 _dict
的几个使用场景;但是,几乎所有场景都传递了 _dict=OrderedDict
,从 Python 3.7 开始,这应该是不必要的。我们发现了两个相关使用场景的实例:在一个场景中,传递了一个自定义类来实现更友好的 KeyErrors;在另一个场景中,自定义类具有几个额外的查找和修改方法(例如,帮助解析带点的键)。
对于 动机 部分概述的核心用例,这样的参数是不必要的。可以使用包装类、转换函数或第三方库轻松解决它的缺失。最后,可以在将来以向后兼容的方式添加支持。
删除 tomllib.load[s]
中对 parse_float
的支持
此选项并非严格必要,因为 TOML 浮点数应该实现为“IEEE 754 二进制 64 值”,这与大多数架构上的 Python float
等效。
TOML 规范使用“SHOULD”一词,但这暗示了一种可以出于正当理由忽略的建议。以不同方式解析浮点数,例如解析为 decimal.Decimal
,允许用户获得超过 TOML 格式承诺的额外精度。根据 tomli
作者的经验,这在科学和金融应用中特别有用。这也适用于需要更高精度,或者最终用户包括可能不知道二进制 64 浮点数限制的非开发人员的其他场景。
也存在一些利基架构,其中 Python float
不是 IEEE 754 二进制 64 值。parse_float
参数允许用户即使在这些架构上也能获得正确的 TOML 语义。
模块的备选名称
理想情况下,我们能够使用 toml
模块名。
但是,PyPI 上的 toml
包使用非常广泛,因此存在向后兼容性问题。由于标准库优先于第三方包,因此当前依赖于 toml
包的库和应用程序,在升级 Python 版本时可能会发生故障,因为 附录 A 中列出了许多 API 不兼容性,即使它们固定了依赖项版本。
为了进一步说明,这里最关心的是具有固定依赖项的应用程序。即使我们能够获得对 toml
PyPI 包名的控制权,并将其重新用于提议的新模块的移植版本,我们仍然会让使用新 Python 版本的用户发生故障,这些版本在标准库中包含它,无论他们是否固定了现有 toml
包的旧版本。这是不幸的,因为固定可能是对通过将 toml
包重新用作移植版本(与今天的 toml
不兼容)而引入的重大更改的常见响应。
最后,PyPI 上的 toml
包没有得到积极维护,但到目前为止,请求作者添加其他维护者的努力 一直没有成功,因此这里可能需要在没有作者同意的情况下采取行动。
相反,此 PEP 提议使用 tomllib
这个名字。这类似于 plistlib
和 xdrlib
,这两个是标准库中的另外两个文件格式模块,以及其他模块,例如 pathlib
、contextlib
和 graphlib
。
其他已考虑但被拒绝的名称包括
tomlparser
。这类似于configparser
,但在将来如果包含写入 API 时,可能不太合适。tomli
。这假设我们使用tomli
作为实现的基础。- 在某些命名空间下的
toml
,例如parser.toml
。但是,这很尴尬,尤其是考虑到像json
、pickle
、xml
、html
等等现有的解析库不会包含在命名空间中。
之前讨论
附录 A:建议的 API 与 toml
之间的差异
本附录涵盖了本 PEP 中提出的 API 与第三方包 toml
的 API 之间的差异。这些差异与理解如果我们使用 toml
作为标准库模块的名称,我们可能遇到的代码中断程度,以及更好地理解设计空间相关。请注意,此列表可能并不详尽。
- 不建议包含写入 API(无
toml.dump[s]
)本 PEP 目前建议不包含写入 API;也就是说,不会有类似于
toml.dump
或toml.dumps
的功能,如 包含写入 TOML 的 API 中所述。如果我们包含了写入 API,将可以相当容易地将大多数使用
toml
的代码转换为新的标准库模块(承认这与兼容的 API 非常不同,因为它仍然需要代码更改)。根据比较 “toml.load” 的出现次数 与 “toml.dump” 的出现次数,相当一部分
toml
用户依赖于此功能。 toml.load
的第一个参数不同toml.load
具有以下签名def load( f: Union[SupportsRead[str], str, bytes, list[PathLike | str | bytes]], _dict: Type[MutableMapping[str, Any]] = ..., decoder: TomlDecoder = ..., ) -> MutableMapping[str, Any]: ...
这与本 PEP 中提出的第一个参数
SupportsRead[bytes]
相当不同。回顾一下之前在 tomllib.load 的第一个参数所接受的类型 中提到的原因
- 允许路径(甚至路径列表)作为参数与标准库中其他类似函数不一致。
- 使用
SupportsRead[bytes]
使我们能够确保 UTF-8 是使用的编码,并避免错误地将单个回车符解析为有效的 TOML。
根据手动检查 “toml.load” 的出现次数,相当一部分
toml
用户依赖于此功能。- 错误
toml
抛出TomlDecodeError
,而建议使用符合 PEP 8 的TOMLDecodeError
。根据 “TomlDecodeError” 的出现次数,相当一部分
toml
用户依赖于此功能。 toml.load[s]
接受_dict
参数如 控制 tomllib.load[s] 返回的映射类型 中所述。
正如那里提到的,几乎所有使用都包含
_dict=OrderedDict
,这在 Python 3.7 及更高版本中不再必要。toml.load[s]
支持一个未记录的decoder
参数似乎预期用例是为了实现注释保留。记录的信息不足以保留样式来对 TOML 文档进行循环处理,实现存在已知错误,该功能未记录,我们只在 https://grep.app 上找到一个使用实例。
公开的 toml.TomlDecoder 接口 远非简单,包含九种方法。
用户可能最好使用更完整的样式保留解析和写入实现。
toml.dump[s]
支持encoder
参数请注意,我们目前建议不包含写入 API;但是,如果要更改,这些差异可能会变得相关。
encoder
参数支持两种用例- 控制自定义类型如何序列化,以及
- 控制输出的格式。
第一个是合理的;但是,我们只在 https://grep.app 上找到这两个实例中的一个。这两个实例中有一个使用了这种能力来添加对转储
decimal.Decimal
的支持,而潜在的标准库实现将开箱即用地支持该能力。如果需要其他类型,可以通过类似于json.dump
中的default
参数的方式来很好地解决此用例。第二个用例是通过允许用户指定 toml.TomlEncoder 的子类并覆盖方法来指定 TOML 写入过程的各个部分来实现的。该 API 包含五种方法,并公开了大量的实现细节。
在 https://grep.app 上有一些使用
encoder
API 的例子;但是,似乎它只占toml
总使用量的很小一部分。- 时区
toml
使用并公开自定义的toml.tz.TomlTz
时区对象。建议的实现使用标准库中的datetime.timezone
对象。
版权
本文档放置在公共领域或根据 CC0-1.0-Universal 许可证发布,以更宽松的许可证为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0680.rst