Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

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 中找到。

×

请参阅 PEP 1,了解如何提出更改建议。

摘要

本 PEP 提出将 tomllib 模块添加到标准库中,用于解析 TOML(Tom’s Obvious Minimal Language,https://toml.cn)。

动机

TOML 是 Python 打包的首选格式,正如 PEP 517PEP 518PEP 621 所证明的那样。这为 Python 构建工具带来了一个引导问题,迫使它们将 TOML 解析包进行捆绑或采用其他不可取的解决方法,并给重新打包者和其他下游消费者带来严重问题。在标准库中包含 TOML 支持将很好地解决所有这些问题。

此外,许多 Python 工具现在可以通过 TOML 进行配置,例如 blackmypypytesttoxpylintisort。许多没有使用 TOML 配置的工具,例如 flake8,将缺少标准库支持作为 主要原因。鉴于 TOML 在 Python 生态系统中的特殊地位,将其包含为内置库是有意义的。

最后,TOML 作为一种格式越来越受欢迎(原因在 PEP 518 中概述),各种 Python TOML 库在 PyPI 上拥有大约 2000 个反向依赖项(相比之下,requests 拥有大约 28000 个反向依赖项)。因此,即使考虑了 Python 打包和相关工具的需求之外,这也有可能成为一个普遍有用的补充。

理由

本 PEP 建议基于第三方库 tomligithub.com/hukkin/tomli)来构建标准库对读取 TOML 的支持。

许多项目最近已切换到使用 tomli,例如 pipbuildpytestmypyblackflitcoveragesetuptools-scmcibuildwheel

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 dictfp 参数必须具有一个 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 对象(strintboolfloatdatetime.{datetime,date,time}listdict,其键为字符串)以及 parse_float 的结果。

如果 TOML 无效,则会引发 tomllib.TOMLDecodeError

请注意,本 PEP 并没有提出 tomllib.dumptomllib.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 模仿了其他成熟的文件格式库,如 jsonpickle。文档中将解释缺乏 dump 函数的原因,并提供指向相关第三方库(例如 tomlkittomli-wpytomlpp)的链接。

参考实现

提议的实现可以在 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 类型,例如 strdict。此外,它极大地复杂了 API 的设计。

即使不考虑样式保留,在如何设计写入 API 方面也有太多自由度。例如,库应该使用什么默认样式(缩进、垂直和水平间距、引号等)来进行输出,以及用户应该对其进行多少控制?库应该如何处理输入和输出验证?它应该支持自定义类型的序列化吗?如果是,应该如何处理?虽然存在解决这些问题的合理选择,但标准库的性质决定了我们只有一次机会获得成功。

目前,没有 CPython 核心开发人员表示愿意维护写入 API,或赞助包含写入 API 的 PEP。由于更改或删除标准库中的内容很困难,因此目前最好谨慎地选择排除,并在将来重新审视此问题。

因此,编写 TOML 则留给第三方库。如果将来找到适合的 API 和相关用例,可以在未来的 PEP 中添加写入支持。

各种 API 细节

tomllib.load 的第一个参数接受的类型

PyPI 上的 toml 库允许将路径(以及路径类对象的列表,忽略缺失的文件并将文档合并到单个对象中)传递给它的 load 函数。但是,在这里允许这样做将与 json.loadpickle.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 这个名字。这类似于 plistlibxdrlib,这两个是标准库中的另外两个文件格式模块,以及其他模块,例如 pathlibcontextlibgraphlib

其他已考虑但被拒绝的名称包括

  • tomlparser。这类似于 configparser,但在将来如果包含写入 API 时,可能不太合适。
  • tomli。这假设我们使用 tomli 作为实现的基础。
  • 在某些命名空间下的 toml,例如 parser.toml。但是,这很尴尬,尤其是考虑到像 jsonpicklexmlhtml 等等现有的解析库不会包含在命名空间中。

之前讨论

附录 A:建议的 API 与 toml 之间的差异

本附录涵盖了本 PEP 中提出的 API 与第三方包 toml 的 API 之间的差异。这些差异与理解如果我们使用 toml 作为标准库模块的名称,我们可能遇到的代码中断程度,以及更好地理解设计空间相关。请注意,此列表可能并不详尽。

  1. 不建议包含写入 API(无 toml.dump[s]

    本 PEP 目前建议不包含写入 API;也就是说,不会有类似于 toml.dumptoml.dumps 的功能,如 包含写入 TOML 的 API 中所述。

    如果我们包含了写入 API,将可以相当容易地将大多数使用 toml 的代码转换为新的标准库模块(承认这与兼容的 API 非常不同,因为它仍然需要代码更改)。

    根据比较 “toml.load” 的出现次数“toml.dump” 的出现次数,相当一部分 toml 用户依赖于此功能。

  2. 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 用户依赖于此功能。

  3. 错误

    toml 抛出 TomlDecodeError,而建议使用符合 PEP 8TOMLDecodeError

    根据 “TomlDecodeError” 的出现次数,相当一部分 toml 用户依赖于此功能。

  4. toml.load[s] 接受 _dict 参数

    控制 tomllib.load[s] 返回的映射类型 中所述。

    正如那里提到的,几乎所有使用都包含 _dict=OrderedDict,这在 Python 3.7 及更高版本中不再必要。

  5. toml.load[s] 支持一个未记录的 decoder 参数

    似乎预期用例是为了实现注释保留。记录的信息不足以保留样式来对 TOML 文档进行循环处理,实现存在已知错误,该功能未记录,我们只在 https://grep.app 上找到一个使用实例。

    公开的 toml.TomlDecoder 接口 远非简单,包含九种方法。

    用户可能最好使用更完整的样式保留解析和写入实现。

  6. toml.dump[s] 支持 encoder 参数

    请注意,我们目前建议不包含写入 API;但是,如果要更改,这些差异可能会变得相关。

    encoder 参数支持两种用例

    • 控制自定义类型如何序列化,以及
    • 控制输出的格式。

    第一个是合理的;但是,我们只在 https://grep.app 上找到这两个实例中的一个。这两个实例中有一个使用了这种能力来添加对转储 decimal.Decimal 的支持,而潜在的标准库实现将开箱即用地支持该能力。如果需要其他类型,可以通过类似于 json.dump 中的 default 参数的方式来很好地解决此用例。

    第二个用例是通过允许用户指定 toml.TomlEncoder 的子类并覆盖方法来指定 TOML 写入过程的各个部分来实现的。该 API 包含五种方法,并公开了大量的实现细节。

    https://grep.app 上有一些使用 encoder API 的例子;但是,似乎它只占 toml 总使用量的很小一部分。

  7. 时区

    toml 使用并公开自定义的 toml.tz.TomlTz 时区对象。建议的实现使用标准库中的 datetime.timezone 对象。


来源:https://github.com/python/peps/blob/main/peps/pep-0680.rst

上次修改:2023-10-10 15:15:34 GMT