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 的明显极简语言,https://toml.io)。
动机
TOML 是 Python 打包的首选格式,如 PEP 517、PEP 518 和 PEP 621 所示。这为 Python 构建工具带来了引导问题,迫使它们捆绑 TOML 解析包或采用其他不必要的变通方法,并给重新打包者和其他下游消费者带来了严重问题。在标准库中包含 TOML 支持将完美解决所有这些问题。
此外,许多 Python 工具现在可以通过 TOML 进行配置,例如 black、mypy、pytest、tox、pylint 和 isort。许多不支持的工具,例如 flake8,将缺乏标准库支持作为 主要原因。鉴于 TOML 在 Python 生态系统中已经占据特殊地位,将其作为内置电池是合理的。
最后,TOML 作为一种格式越来越受欢迎(原因如 PEP 518 中所述),各种 Python TOML 库在 PyPI 上大约有 2000 个反向依赖(相比之下,requests 大约有 28000 个反向依赖)。因此,即使超越 Python 打包和相关工具的需求,这也很可能是一个普遍有用的补充。
基本原理
本 PEP 提议将标准库中对 TOML 读取的支持基于第三方库 tomli (github.com/hukkin/tomli)。
许多项目最近已切换到使用 tomli,例如 pip、build、pytest、mypy、black、flit、coverage、setuptools-scm 和 cibuildwheel。
tomli 得到积极维护并经过充分测试。它大约有 800 行代码,100% 测试覆盖率,并通过了 提议的官方 TOML 合规性测试套件 中的所有测试,以及 更完善的 BurntSushi/toml-test 套件。
规范
一个新的模块 tomllib 将被添加到 Python 标准库,暴露以下公共函数
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 参数必须具有与 io.RawIOBase.read() 具有相同 API 的 read() 方法。
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 的稳定性
TOML 1.0.0 于 2021 年 1 月发布,表明 TOML 格式现在应被正式视为稳定。从经验来看,即使在 TOML 1.0.0 发布之前,TOML 也已被证明是一种稳定的格式。从 更新日志 中,我们可以看到 TOML 自 2020 年 4 月以来没有重大更改,并且在过去五年(2017-2021)中发布了两次。
如果 TOML 规范发生更改,我们可以将次要修订视为错误修复并原地更新实现。如果发生重大破坏性更改,我们应保留对 TOML 1.x 的支持。
提议实现的维护性
提议的实现 (tomli) 是纯 Python 的,经过充分测试,代码量不到 1000 行。它极简,与其他 TOML 实现相比,API 表面积更小。
tomli 的作者愿意帮助将 tomli 集成到标准库中并帮助维护它,根据此帖子。此外,Python 核心开发者 Petr Viktorin 已表示愿意维护一个读取 API,根据此帖子。
目前没有必要用 C 重写解析器。TOML 解析很少成为应用程序的瓶颈,有更高性能需求的用户可以使用第三方库(就像 JSON 常常遇到的情况一样,尽管 Python 提供了标准库 C 扩展模块)。
TOML 支持为其他事物开了一个先例
如 动机 部分所述,TOML 在 Python 生态系统中占据特殊地位,用于读取 PEP 518 pyproject.toml 打包和工具配置文件。将 TOML 包含在标准库中的主要原因不适用于其他格式,例如 YAML 或 MessagePack。
此外,TOML 的简洁性使其区别于 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 提供了一些非常有限且大部分未使用的功能,用于保留输入样式。有关其与本 PEP 的 API 差异的更多详细信息,请参阅 附录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 旨在作为人类可读和可编辑的配置格式,因此保留注释、格式和其他标记非常重要。这需要一个其输出包含样式相关元数据的解析器,从而使得输出普通的 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 文件必须是有效的 UTF-8 编码的 Unicode 文档。
tomllib.loads 不打算加载 TOML 文件,而是加载文件存储的文档。Unicode 文档在 Python 中最自然的表示是 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 及以后版本中应该是不必要的。我们发现了两个相关的用例:在一个案例中,为了更友好的 KeyError 传递了一个自定义类;在另一个案例中,自定义类具有几个额外的查找和变异方法(例如,帮助解析点式键)。
这样的参数对于 动机 部分中概述的核心用例来说不是必需的。缺少此功能可以通过包装类、转换函数或第三方库轻松解决。最后,支持可以在以后以向后兼容的方式添加。
移除对 tomllib.load[s] 中 parse_float 的支持
此选项并非严格必要,因为 TOML 浮点数应实现为“IEEE 754 binary64 值”,这在大多数架构上等同于 Python float。
然而,TOML 规范使用了“SHOULD”一词,这意味着一个可以因有效原因而被忽略的建议。以不同方式解析浮点数,例如解析为 decimal.Decimal,允许用户获得超出 TOML 格式承诺的额外精度。根据 tomli 作者的经验,这在科学和金融应用中特别有用。对于需要更高精度或最终用户包括非开发人员且可能不知道 binary64 浮点数限制的其他情况,这也很实用。
还有一些小众架构,其中 Python float 不是 IEEE 754 binary64 值。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 上存在一些
encoderAPI 的用法;然而,它似乎只占toml总体使用量的一小部分。- 时区
toml使用并暴露自定义toml.tz.TomlTz时区对象。提议的实现使用标准库中的datetime.timezone对象。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0680.rst