PEP 766 – 多个索引之间明确的优先级选择
- 作者:
- Michael Sarahan <msarahan at gmail.com>
- 发起人:
- Barry Warsaw <barry at python.org>
- PEP 代理人:
- Paul Moore <p.f.moore at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 草案
- 类型:
- 信息性
- 主题:
- 打包
- 创建日期:
- 2024年11月18日
- 发布历史:
- 2024年11月18日
摘要
包解析是Python用户体验的关键部分,也是扩展Python核心功能的方式。包解析的体验在大多数情况下是理所当然的,直到有人遇到包安装程序做了他们不期望的事情。安装程序在处理多个索引时的行为已成为意外行为的常见来源。通过其无处不在的特性,pip长期以来定义了生态系统中其他工具的标准预期行为,但Python安装程序在处理多个索引方面正在出现分歧。这种分歧的核心在于是在解析分发之前合并索引内容,还是按顺序单独处理每个索引。pip在匹配分发之前合并所有索引,而uv在匹配一个索引上的分发之后才移动到下一个索引。每种方法都有优点和缺点。本PEP旨在描述这些行为,分别称为“版本优先级”和“索引优先级”,以便社区讨论和故障排除可以共享通用词汇,并且工具可以根据这些描述实现可预测的行为。
动机
Python包用户经常发现自己需要指定PyPI以外的索引或包源。外部索引存在的原因有很多
- PyPI上的文件大小/配额限制
- 实现变体,例如PyTorch中不同的GPU库构建
- 组织内部共享的包的本地构建
- 本地包具有远程依赖项的情况,用户希望优先使用本地包而不是远程依赖项,同时在需要时仍然回退到远程依赖项
在大多数这些情况下,完全放弃PyPI是不可取的。相反,用户通常希望PyPI仍然是包的来源,但优先级较低。不幸的是,pip当前的設計排除了这种优先级概念。一些Python安装程序工具已经开发了处理多个索引的替代方法,这些方法包含了表达索引优先级的机制,例如uv和PDM。
创新和定制的潜力令人兴奋,但它带来了进一步碎片化Python打包生态系统的风险,而这已被认为是Python的弱点之一。本PEP的动机是鼓励安装程序提供更多关于它们如何处理多个索引的洞察,并提供一个可以为更广泛社区通用的词汇。
规范
“版本优先级”
这种行为的特点是安装程序总是获取包的“最佳”版本,无论它来自哪个索引。“最佳”由安装程序优化包各种特性的算法定义,同时也考虑用户输入(例如只优先使用二进制文件,或不使用二进制文件)。虽然安装程序在优化标准和用户选项上可能有所不同,但所有版本优先级安装程序共享的共同特点是,在候选选择之前,索引内容会进行整理。
当所有配置的索引都同样可信且在分发互换性假设方面表现良好时,版本优先级最为有用。镜像在这方面表现特别好。正是这种互换性假设使得比较给定包的分发变得有意义。没有它,安装程序就不再是“苹果与苹果”的比较。实际上,不同的索引拥有与其它索引内容不同的文件是很常见的,例如针对特殊硬件的构建,或相同包的不同元数据。版本优先级行为在这些情况下可能导致不理想、意外的结果,这就是用户通常寻求某种索引优先级的地方。此外,当索引之间存在信任差异时,版本优先级无法提供一种方式来优先选择更受信任的索引而不是较不受信任的索引。这已被依赖混淆攻击所利用,并且PEP 708被提议作为一种将受信任的外部索引概念硬编码到索引中的方式。
“版本优先级”这个名称是新词,新术语的引入应始终最小化。本PEP参考了uv项目,该项目将其版本优先级行为的实现称为“unsafe-best-match
”。这里的命名确实很难。一方面,称pip的默认行为本质上是“不安全的”并不准确。可能是恶意的索引的添加才引入了对这种行为的担忧。PEP 708增加了一种方法来限制安装程序从意外的、可能不安全的索引中提取包。另一方面,“最佳匹配”这个术语在技术上是正确的,但也具有误导性。“最佳匹配”因用户和应用程序而异。“最佳”在技术上是正确的,因为它根据上面指定的匹配标准是全局最优的,但这不一定符合用户眼中的“最佳”。“版本优先级”是一个提议的术语,它避免了uv术语的担忧,同时以最易于用户识别的方式近似地描述了包的比较行为。
“索引优先级”
在索引优先级中,解析器一次为一个索引查找候选包。解析器仅在当前包请求没有可行的候选包时才继续处理后续索引。索引优先级不会将多个索引组合成一个全局的扁平命名空间。由于索引是按顺序搜索的,因此来自较早索引的包将优先于来自较晚索引的包,无论较晚索引是否与安装程序的优化标准有更好的匹配。对于给定的安装程序,优化标准和选择算法对于索引优先级和版本优先级都应该相同。不同之处仅在于对多个索引的处理:版本优先级将所有索引一起处理,而索引优先级则单独处理。
索引的指定顺序决定了它们在查找过程中的优先级。因此,安装程序加载索引配置的方式必须是可预测和可重现的。本PEP不规定任何特定的机制,只是说安装程序应该提供一种对其来源集合进行排序的方式。安装程序还应该理想地提供可选的调试输出,以深入了解正在考虑的索引。
每个包的查找器都应从索引列表的开头开始,因此每个包都从索引列表重新开始。换句话说,如果一个包在第一个索引上没有有效的候选者,但在第二个索引上找到了,那么后续的包仍然应该从第一个索引开始搜索,而不是从第二个索引开始。
索引优先级策略所隐含的一个理想行为是,不会出现“意外”更新,即较低优先级索引上的版本升级胜过精心策划、批准的较高优先级索引。这与PEP 708的安全改进有关,其中包可以限制分发来源的外部索引,但索引优先级对最终用户来说更具可配置性。包安装预计仅在较高优先级索引或索引优先级配置更改时才会发生更改。这种稳定性和可预测性使得将索引配置为环境的更持久属性而不是一次性安装命令的参数变得更可行。
缓存键
由于索引优先级承认不同索引可能对给定包具有不同内容的可能性,因此缓存和锁文件现在应该包含下载分发包的索引。如果没有这方面,则在更改已配置索引列表后,缓存或锁文件可能会提供来自较低优先级索引的类似名称的分发包。如果每个索引都遵循建议的行为,即为给定文件名提供跨索引的相同文件,则这不是问题。然而,该建议不易强制执行,并且用源索引增强缓存键将是一个明智的防御性更改。
请求降级到较低优先级索引的方式
- 较高优先级索引中完全不存在包名称
- 由于版本指定符、兼容的Python版本、平台标签、取消或其它原因,所有来自较高优先级索引的分发包都被过滤掉
- 安装程序的黑名单配置指定应在给定索引上忽略特定的包名称
- 较高优先级的索引无法访问(例如,被防火墙规则阻止,由于维护暂时不可用,其他各种临时网络问题)。这是一个不太明确的细节,应该由用户控制。一方面,这种行为将导致不可预测、可能无法重现的结果,因为会意外地降级到较低优先级的索引。另一方面,对于某些用户来说,优雅的回退可能更有价值,特别是如果他们可以安全地假设所有索引都同样可信。pip当前的行为是优雅的回退:如果索引出现连接问题,您会看到警告,但安装将继续使用任何其他可用的索引。由于索引优先级可以传达索引之间的不同信任级别,因此实现索引优先级的安装程序应默认在网络问题时引发错误并中止。安装程序可以选择提供一个标志,以允许在网络错误时降级到较低优先级的索引。
在给定索引中的处理遵循现有行为,但止于一个索引的范围,仅当该索引中的所有优先级偏好都已耗尽后才转向下一个索引。这意味着在降级到较低优先级索引之前,统一包集合中的现有优先级适用于每个索引。
在优化标准的每个层面都需要进行权衡
- 版本:索引优先级将使用来自较高优先级索引的旧版本,即使在另一个索引上有较新版本可用。
- wheel vs sdist:安装程序是否应该在尝试较低优先级索引中的wheel之前使用较高优先级索引中的sdist?
- 更具体的平台轮子优先于不那么具体的轮子:安装程序是否应该在尝试使用较低优先级索引中的更具体的轮子之前,使用较高优先级索引中不那么具体的轮子?
- 诸如pip的
--prefer-binary
之类的标志:安装程序是否应该在考虑较低优先级索引上的wheel之前使用较高优先级索引中的sdist?
安装程序可以自由地以不同的方式实现这些优先级,但它们应该记录其优化标准以及如何处理降级到较低优先级索引。例如,安装程序可以说--prefer-binary
不应安装sdist,除非它已遍历所有已配置的索引并且未找到任何可安装的二进制候选者。
镜像
如前所述,索引优先级方案打破了多个索引URL提供相同内容的用例。此类镜像可用于缓解网络问题或以其他方式提高可靠性。安装程序可以采取的一种方法是在添加索引优先级的同事保留镜像功能,那就是添加用户可定义的索引组概念,其中组中的每个索引都被认为是等效的。这与Poetry的包源概念有关,但它允许任意数量的可优先分组,并且假设组的成员是镜像。在每个组内,内容可以合并,或者每个成员可以并发获取。最快响应的索引将代表该组。
向后兼容性
本PEP不规定任何安装程序必须进行任何更改,因此只有当工具选择采用不同于其当前实现的索引行为时,才会引入兼容性问题。
本PEP的语言与现有工具(包括pip和uv)并不完全一致。本PEP的语言可以在本PEP审查期间更改,或者如果本PEP的语言受到青睐,其他项目可以遵守它。提议这些术语的唯一目标是创建一个核心的、通用的词汇,使用户更容易了解其他安装程序。
由于某些工具依赖于其中一种或另一种行为,因此可能会出现一些潜在问题,即为特定行为定制可用资源/包可能会损害依赖其他行为的用户的体验。
- 不同的索引可能具有不同的元数据。例如,不能假定索引“A”上的包“something”的元数据与索引“B”上的“something”具有相同的依赖项。这打破了版本优先级的基本假设,但索引优先级可以处理这种情况。当安装程序在搜索顺序中降级到较低优先级的索引时,这意味着从新索引刷新包元数据。这既是改进,也是复杂化。从复杂化的角度来看,缓存的元数据条目必须由包名称和索引URL同时作为键,而不仅仅是包名称。从潜在改进的角度来看,只要包的不同实现变体将其分发包分离到不同的索引中,它们就可以在依赖项方面有所不同。
- 使用索引优先级时,用户可能无法像预期那样获得更新,因为某些更高优先级的索引尚未更新/与PyPI同步以获取最新包。如果更高优先级的索引有有效候选包,则不会找到较新的包。这将需要详细说明,因为它与pip已建立的行为相悖。
- 通过增加索引优先级,安装程序将提高选择哪个索引的可预测性,索引主机可能会滥用此机制,以拥有内容不同的同名文件。在使用版本优先级时,这违反了关键的包互换性假设,并将导致混乱。索引优先级将更具可行性,但这种情况仍有很大的混淆潜力。开发支持安装程序识别这些混淆问题的工具将很有帮助。这些工具可以独立于安装程序进程运行,作为验证一组索引的合理性的一种手段。根据这些工具的时间成本,安装程序可以在其进程中运行它们。当然,用户可以自行承担风险忽略建议。
安全隐患
索引优先级为用户创建了一种明确指定其索引之间信任层次结构的机制。因此,它限制了依赖混淆攻击的可能性。PEP 708拒绝了索引优先级作为依赖混淆攻击的解决方案。本PEP请求重新考虑该拒绝,将索引优先级用于不同的目的。本PEP主要由支持实现变体的愿望驱动,这是另一个讨论的主题,希望能促成一个PEP。它与PEP 708并非互斥,也不建议撤销或取消PEP 708。它是针对我们如何允许用户在比“每次安装”更细粒度的级别上选择使用哪个索引的答案。
有关PEP 708拒绝索引优先级的更详细讨论,请参阅本PEP的discuss.python.org帖子。
如何教授此内容
一开始,目标并非改变pip或任何其他工具的默认优先级行为。最好的教学方式也许是观察留言板、GitHub问题跟踪器和聊天频道,留意索引优先级可以帮助解决的问题。有几项长期存在的讨论会是宣传这些概念的良好起点。这两种官方支持行为的主题需要文档,我们,本PEP的作者,将在本PEP的审查期间开发这些文档。这些文档可能包括对几个索引的添加,以及在安装程序之间交叉链接这些概念。至少,我们期望添加到PyPUG和pip的文档中。
安装程序宣传活跃行为非常重要,尤其是在错误消息中,这将为用户提供有关这些行为的资源。
uv 用户已经在使用索引优先级。uv 很好地记录了这种行为,但总是有可能提高该文档在命令行中的可发现性,而用户实际上会在命令行中遇到意外行为。
参考实现
uv 项目以其默认行为展示了索引优先级。但是,uv 是用 Rust 实现的,因此如果需要一个基于 Python 工具的参考实现,我们,本 PEP 的作者,将提供一个。对于 pip 来说,我们认为实施计划如下:
- 对于不使用
--extra-index-url
或--find-links
的用户,将不会有任何变化,也不需要迁移。 - pip用户可以通过CLI和
pip.conf
中的新配置设置来选择索引优先级行为。本提案不建议将任何策略作为任何安装程序的默认策略。它只建议记录工具提供的策略。 - 为使用多个索引的任何 pip 操作启用额外的info级别输出。在此输出中,说明当前的策略设置,并简要概述隐含行为,以及指向描述不同选项的文档的链接
- 添加调试输出,详细识别每个步骤中使用的索引,包括文件在配置层次结构中的位置,以及它包含的位置(通过配置文件、环境变量或CLI标志)。
- 通过整个pip安装过程,跟踪哪个索引用于哪个包/分发。存储此信息,以便像
pip freeze
这样的工具可以使用它 - 补充PEP 751(锁文件)以捕获包/分发来源的索引
被拒绝的想法
- 告诉用户设置代理/镜像,例如devpi或Artifactory,如果存在本地文件则提供本地文件,如果本地文件不匹配则转发到另一个服务器(PyPI)
这与本提案的行为非常接近,只是这种方法需要托管一些服务器,并且在某些环境中可能对用户不可访问或无法配置。同样重要的是要考虑到,对于运营自己的索引的组织(例如,为了克服 PyPI 大小限制),这并不能解决最终用户对
--extra-index-url
或代理/镜像的需求。也就是说,除非组织将 PyPI 作为一个整体进行代理/镜像,并让用户将其代理/镜像配置为唯一的索引,否则这种方法不会带来任何改进。 - 构建标签和/或本地版本指定符足够吗?
构建标签和本地版本指定符将优先于没有这些标签和/或本地版本指定符的包。在一组包中,在PyPI以外的服务器上托管的具有这些附加内容的构建将优先于PyPI上的包,而PyPI很少使用构建标签,并且禁止本地版本指定符。当包提供者希望提供自己的本地覆盖时,例如为用户提供优化构建的HPC维护者,这种方法是可行的。在某些方面,它的可行性较低,例如构建标签不会出现在
pip freeze
元数据中,以及PyPI上不允许本地版本指定符。构建和维护具有本地构建标签变体的包集合也需要大量工作。https://discuss.python.org/t/dependency-notation-including-the-index-url/5659/21
- 那PEP 708呢?这还不够吗?
PEP 708专门旨在解决依赖混淆攻击,并未解决索引之间实现变体的可能性。它是一种过滤外部URL并将外部索引的允许列表编码到索引元数据中的方法。它不改变当前渠道之间缺乏优先级或偏好。
- 命名空间
命名空间是一种指定包的方式,其中包的Python使用方式不变,但包安装会限制包的来源。PEP 752最近提出了一种通过保留前缀作为分组元素来在扁平包命名空间(例如PyPI)中多路复用包所有者的方法。NPM的“范围”概念也被提出作为这种可能性的一种很好的例子。本PEP不同之处在于,它针对的是多个索引,而不是扁平包命名空间。就可预测地选择特定包源而言,最终效果大致相同,只是命名空间方法更依赖于使用这些命名空间前缀来命名包,而本PEP则不那么细粒度,它会从用户指定的任何更高优先级的索引中拉取包。命名空间方法依赖于所有已配置的索引以类似方式处理给定命名空间,这留下了通常的担忧,即并非所有已配置的索引都同样受到信任。命名空间思想与本PEP并不冲突,但它也没有像本PEP那样改进索引信任的表达。
未解决的问题
[仍在决定/讨论中的任何观点。]
致谢
这项工作得到了NVIDIA的资助,通过聘用作者。NVIDIA的同事们通过他们的投入极大地改进了本PEP。Astral Software开创了索引优先级的行为,从而为本文奠定了基础。pip的作者们值得高度赞扬,感谢他们对版本优先级行为的始终如一的指导和耐心的沟通,尤其是在面临有争议的安全问题时。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0766.rst