Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 794 – 导入名称元数据

作者:
Brett Cannon <brett at python.org>
讨论至:
Discourse 帖子
状态:
已接受
类型:
标准跟踪
主题:
打包
创建日期:
2025年6月5日
发布历史:
2025年5月2日, 2025年6月5日
决议:
2025年9月5日

目录

摘要

本PEP提议扩展Python打包的核心元数据规范,以包含两个新的、可重复的字段,名为 Import-NameImport-Namespace,用于记录项目安装后提供的导入名称。名为 import-namesimport-namespaces 的新键将添加到 pyproject.toml 中的 [project] 表中,以提供新的核心元数据字段的值。这还将引入核心元数据版本2.5。

动机

在Python打包中,不要求项目名称与您可以为该项目导入的名称匹配。因此,没有一种清晰、简单、准确的方法可以从导入名称到项目名称,反之亦然。这可能会使工具难以帮助人们在知道导入名称时发现要安装的正确项目,或者难以知道项目安装后将提供哪些导入名称。

例如,代码编辑器可能会检测到用户在选定的虚拟环境中存在未满足的导入。但是,由于无法可靠地知道各个项目提供哪些导入名称,代码编辑器无法准确地向用户提供要安装的潜在项目列表以满足该导入要求(例如,import PIL 很可能意味着用户希望安装 Pillow 项目,这一点并不明显)。这也适用于用户模糊地记住项目名称但忘记导入名称,并且在看到包提供的导入名称列表时会回想起。最后,工具将能够通知用户在安装项目后哪些导入名称将可用。

也没有简单的方法可以知道安装两个项目是否会因它们提供的导入名称而相互冲突。例如,如果两个不同的项目都有一个 _utils 模块,安装这两个项目将导致冲突,因为一个项目的 _utils 模块将通过覆盖另一个项目的文件而优先于另一个项目;这个问题已经在实际中出现过

它可能还有助于垃圾邮件检测。如果一个项目指定了与一个非常流行的项目相同的导入名称,则可以作为仔细检查不太受欢迎项目有效性的信号。一个被发现谎报其提供的导入名称的项目将是另一个信号。

基本原理

本PEP提议扩展打包的核心元数据规范,以便项目所有者可以指定项目在某个平台上安装后提供的最高级别导入名称。

将此元数据放入核心元数据意味着数据(可能)由索引服务器提供,独立于任何 sdist 或 wheel。这避免了需要想出一种将元数据暴露给工具的方法,以避免必须下载整个例如 wheel。

所有发布工件都具有相同的元数据将允许项目只需检查单个文件的核心元数据即可获取所有可能的导入名称,而不是检查所有已发布的文件。这也意味着在读取核心元数据时无需担心文件是否丢失,或者如果提供了元数据,则可以仅从 sdist 工作。此外,通过使其在整个项目版本中保持一致,而不是针对同一版本的每个已发布文件都是唯一的,它简化了 pyproject.tomlproject.import-namesproject.import-namespaces 键的设置。

包含模块和包的分发文件可以具有模块/包级别的公共和私有API的任意组合。分发文件也可以不包含任何类型的模块或包。能够区分这些情况都具有对用户有益的各种工具用途。例如,了解所有导入名称(无论它们是公共的还是私有的)有助于在安装时检测冲突。但了解哪些是显式公共或私有的允许编辑器等工具不建议将私有导入名称作为自动完成的一部分。

本PEP故意对拟议元数据中要(不)列出的内容没有过于严格。鉴于Python导入系统的灵活性,让构建后端验证项目是否准确遵循某种严格规定可列出内容的规范几乎不可能做到正确。因此,本PEP只要求使用有效的导入名称,并且项目不能撒谎(并且承认后一个要求无法通过编程验证)。但是,项目确实需要考虑它们列出的名称的所有级别(例如,您不能列出 a.b.c 而不考虑 aa.b)。

已经进行了各种其他尝试来解决这个问题,但它们都必须做出权衡。例如,可以下载每个项目版本的每个 wheel,并查看通过二进制分发格式提供哪些文件,但这对于静态信息来说需要大量的CPU和带宽(尽管可以使用技巧来减少数据请求,例如使用HTTP范围请求只读取zip文件的目录)。这种计算目前由每个人独立重复进行,而不是由像PyPI这样的中央索引服务器托管元数据。它也不适用于sdists,因为wheel的结构尚不清楚,因此无法推断所安装代码的结构。此外,这些解决方案不一定准确,因为它们基于推断而不是项目所有者明确提供。所有这些准确性问题甚至影响到索引托管信息以避免收集信息的计算成本。

规范

由于本PEP引入了核心元数据的新字段,因此它将最新的核心元数据版本升级到2.5。

Import-NameImport-Namespace 字段是“多用途”字段。这两个字段的每个条目都必须是有效的导入名称,或者在 Import-Name 的情况下可以为空。当项目在某个平台安装时,指定的所有名称都必须是可导入的,并且与项目版本相同(例如,元数据必须在项目发布的所有 sdists 和 wheels 中保持一致)。这确实意味着信息不特定于其所在的分发工件,而是特定于该分发工件所属的发布版本。

导入名称后面可以跟着一个分号和“private”一词(例如 ; private)。这向工具发出信号,表明该导入名称不属于项目的公共API。允许分号 ; 周围有任意数量的空格。

Import-Name 列出了项目安装后将独占提供的导入名称(即,如果安装了两个在 Import-Name 中列出相同导入名称的项目,则其中一个项目将覆盖另一个项目的名称)。Import-Namespace 列出了安装后将由项目提供但非独占的导入名称(即,所有在 Import-Namespace 中列出相同导入名称的项目一起安装不会覆盖这些共享名称)。

pyproject.toml 规范将增加一个 import-names 键。它将是一个字符串数组,用于存储写入 Import-Name 的内容。如果用户在 project.dynamic 中声明了该键,构建后端可以根据需要支持为用户动态计算值。同样适用于 Import-Namespaceimport-namespaces

项目应列出项目独占提供的所有最短导入名称,这些名称应涵盖所有导入名称场景。如果任何最短名称是点分名称,则从该名称到顶级名称的所有中间名称也应在 Import-Namespace 和/或 Import-Name 中适当列出。例如,一个名为 spam 且具有多个子模块的单一包项目将只列出 project.import-names = ["spam"]。一个列出 spam.bacon.eggs 的项目也需要适当考虑 spamspam.baconimport-namesimport-namespaces 中。列出所有名称作为检查导入名称的意图是否符合预期。此外,项目应使用 ; private 修饰符适当列出所有导入名称,无论是公共的还是私有的。

如果一个项目在 Import-NameImport-Namespace 中列出相同的名称,则工具必须因歧义而引发错误;这同样适用于 import-namesimport-namespaces

当即将由工具安装的两个项目在彼此的 Import-Name 条目中列出的名称重叠时(即在相同的命令/操作中安装),工具应引发错误。这是为了避免项目意外地覆盖另一个项目的代码。当项目在 Import-Name 中有一个条目与另一个项目的 Import-Namespace 条目重叠时,也适用相同的情况。这不适用于重叠的 Import-Namespace 条目,因为那是命名空间包的目的。当将项目安装到现有环境中,并且与已安装的项目存在导入名称重叠时,工具可以警告或引发错误。这是一个“可以”而不是“应该”,因为有些用户在分多个步骤安装时会故意覆盖导入名称(例如,在同一环境中使用不同的安装程序)。

项目可以在 pyproject.toml 文件中将 import-names 设置为空数组,并且根本不设置 import-namespaces (例如 import-names = [])。为了匹配这一点,项目可以在其元数据中有一个空的 Import-Name 字段。这代表一个没有导入名称,无论是公共还是私有的项目(即,分发文件中没有任何Python模块)。

由于项目可能没有 Import-Name 元数据(要么是因为项目使用了旧的元数据版本,要么是因为它没有指定任何元数据),因此工具无法获取项目提供哪些名称的信息。然而,实际上大多数项目的项目名称都与其导入名称相匹配。因此,如果需要某种答案,假设可以将项目名称以某种方式标准化为导入名称(例如 packaging.utils.canonicalize_name(name, validate=True).replace("-", "_"))是合理的。

项目可以设置 import-namesimport-namespaces ——以及 Import-NameImport-Namespace ——为与项目名称(标准化与否)匹配的导入名称,以明确声明项目的名称也是导入名称。

示例

对于 scikit-learn 1.7.0

[project]
import-names = ["sklearn"]

对于 pytest 8.3.5 预计会有3个条目

[project]
# The pytest docs list code out of all of these modules, so it isn't
# obvious whether they would mark any as private.
import-names = ["_pytest", "py", "pytest"]

对于 azure-mgmt-search 9.1.0,应该有两个命名空间条目和一个名称条目,用于 azure.mgmt.search

[project]
import-names = ["azure.mgmt.search"]
import-namespaces = ["azure", "azure.mgmt"]

向后兼容性

由于这是核心元数据的新字段和新的核心元数据版本,因此不应存在向后兼容性问题。

安全隐患

工具应将元数据视为可能不准确。因此,任何基于提供的元数据做出的决策都应假定存在某种恶意。

如何教授此内容

项目所有者应该被告知,他们现在可以记录其项目为导入提供的名称。如果他们的项目名称与他们的项目提供的模块或包名称匹配,他们不需要做任何事情。但是,如果存在差异,他们应该记录其项目提供的所有导入名称,使用尽可能短的名称。如果任何名称是隐式命名空间,则这些名称将放入 pyproject.toml 中的 project.import-namespaces,否则该名称将放入 project.import-names

项目用户不一定需要了解此新元数据。虽然他们可能会通过工具接触到它,但这些数据的来源细节并不重要。如果索引服务器公开了它(例如,列出 Import-Name 的值并标记文件结构是否支持元数据的主张),他们可能会偶然发现它,但这仍然不需要用户了解此PEP的技术细节。

参考实现

https://github.com/brettcannon/packaging/tree/pep-794 是一个分支,用于更新“packaging”以支持此PEP。

被拒绝的想法

推断 Import-Namespace 的值

此PEP的先前版本根据 Import-Name 中的点分名称推断 Import-Namespace 的值。最终决定最好明确说明,不仅可以避免因意外列出被解释为隐式命名空间的内容而犯错误,而且还使数据更具自文档性。

要求 Import-Namespace 中列出的名称永远不应包含在 Import-Name 中的名称中

Python 的导入系统默认工作方式意味着不可能让导入名称包含命名空间。但是 Python 的导入系统足够灵活,用户代码可以实现这一点。因此,删除了要求工具在导入名称包含命名空间名称时出错的要求——import-names = ["spam"]import-namespaces = ["spam.bacon"]

重新利用 Provides 字段

在元数据版本1.1中引入并在1.2中弃用的 Provides 字段旨在提供类似的信息,但它指的是项目提供的**所有**名称,而不是像本PEP提议那样区分命名空间。基于这一差异以及 Provides 已弃用且可能被现有代码忽略的事实,决定采用新字段。

将字段命名为 Namespace

虽然从导入的角度来看,“命名空间”这个术语在技术上是准确的,但它可能与隐式命名空间包混淆。

提供 RECORD 文件

讨论此PEP的预版本时,有人建议从索引服务器提供 wheels 的 RECORD 文件,而不是此新元数据。这将具有立即可实现的优点。但为了提供等效信息,需要根据 wheel 将安装的文件结构进行推断。这可能导致信息不准确。它也不支持 sdists。

最后进行了一次投票,本PEP采取的方法获胜。

更规范地规定项目所指定的内容

本PEP的早期版本对于 Import-Name 中可以放入的内容更为严格。这包括将一些“SHOULD”准则变为“MUST”要求,并具体说明如何计算项目“拥有”的内容。最终决定这过于限制,并有被错误实施或规范意外过于严格的风险。

由于元数据从未期望是详尽无遗的,因为它无法验证,因此选择了本PEP中当前更宽松的规范。

未解决的问题

不适用

致谢

感谢HeeJae Chang ~~抱怨~~经常提出此元数据将提供的有用性。感谢Josh Cannon(无亲属关系)审阅了本PEP的草稿并提供了反馈。此外,感谢所有参与之前关于此主题的讨论的人。


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

最后修改:2025-09-05 19:53:27 GMT