PEP 650 – 为 Python 项目指定安装程序要求
- 作者:
- Vikram Jayanthi <vikramjayanthi at google.com>, Dustin Ingram <di at python.org>, Brett Cannon <brett at python.org>
- 讨论至:
- Discourse 主题
- 状态:
- 已撤回
- 类型:
- 标准追踪
- 主题:
- 打包
- 创建时间:
- 2020-07-16
- 发布历史:
- 2021-01-14
摘要
Python 软件包安装程序彼此之间并不完全互操作。虽然 pip 是使用最广泛的安装程序并且是事实上的标准,但其他安装程序(如 Poetry 或 Pipenv)也很流行,因为它们提供了针对某些工作流程的独特功能,这些功能与 pip 的操作方式不完全一致。
虽然安装程序选项丰富对具有特定需求的最终用户来说是一件好事,但它们之间缺乏互操作性使得支持所有潜在的安装程序变得很困难。具体来说,缺乏用于声明依赖关系的标准需求文件意味着必须显式地使用每个工具来安装使用各自格式指定的依赖关系。否则,工具必须发出一个需求文件,这会导致安装程序潜在的信息丢失,以及在开发人员工作流程中添加额外的导出步骤。
通过提供一个标准化的 API,该 API 可用于调用兼容的安装程序,我们可以解决这个问题,而无需解决不同安装程序及其锁定文件之间各个问题、独特要求和不兼容性。
实现该规范的安装程序可以通过统一的方式调用,允许用户像直接调用它们一样使用自己选择的安装程序。
术语
- 安装程序接口
- 安装程序后端和通用安装程序相互作用的接口。
- 通用安装程序
- 一个可以调用安装程序后端的安装程序,通过调用安装程序接口的可选调用方法。这也可以被认为是安装程序前端,类似于 build 项目对于 PEP 517。
- 安装程序后端
- 一个实现安装程序接口的安装程序,允许它被通用安装程序调用。一个安装程序后端也可以是一个通用安装程序,但这不是必需的。与 PEP 517 相比,这将是 Flit。安装程序后端可能是围绕底层安装程序的包装包,例如,Poetry 可以选择不支持此 API,但一个包可以作为包装程序来适当地调用 Poetry 以使用 Poetry 执行安装。
- 依赖组
- 一组相关且需要同时安装才能实现某些目的的依赖关系。例如,“测试”依赖组可以包括运行测试套件所需的依赖关系。依赖组的指定方式由安装程序后端决定。
动机
该规范允许任何人调用和与实现指定接口的安装程序后端进行交互,从而在现有的工具特定安装流程之上提供一个普遍支持的层。
这反过来将使所有实现指定接口的安装程序能够用于支持单个通用安装程序的环境中,只要该安装程序也实现了此规范。
下面,我们确定了适用于 Python 社区中的利益相关者以及与 Python 软件包安装程序进行交互的任何人的各种用例。对于开发者或公司来说,此 PEP 将允许使用 Python 软件包安装程序获得更高的功能和灵活性。
提供者
提供者是向与 Python 打包以及因此与 Python 软件包安装程序进行交互的服务或软件工具提供服务的方(组织、个人、社区等)。考虑两种不同的提供者类型
平台/基础设施提供者
平台提供者(云环境、应用程序托管等)和基础设施服务提供者需要为其用户支持软件包安装程序以安装 Python 依赖项。大多数只支持 pip,但是用户对其他 Python 安装程序有需求。大多数提供者不想维护对多个安装程序的支持,因为这会增加其软件或服务的复杂性,并需要投入更多资源来完成这些工作。
通过此规范,我们可以启用一个提供者支持的通用安装程序来调用用户想要的安装程序后端,而无需提供者的平台需要了解所述后端。这意味着,如果 Poetry 实现了此 PEP 提出的安装程序后端 API(或其他一些围绕 Poetry 的包来提供 API),那么平台提供者将隐式地支持 Poetry。
IDE 提供者
集成开发环境可能会与 Python 软件包安装和管理进行交互。大多数只支持 pip 作为 Python 软件包安装程序,用户需要找到解决方法才能使用其他软件包安装程序来安装其依赖项。类似于 PaaS 和 IaaS 提供者的情况,IDE 提供者也不想维护对 N 种不同 Python 安装程序的支持。相反,安装程序接口的实现者(安装程序后端)可以通过 IDE 作为通用安装程序来调用。
开发者
开发者是编写和使用 Python 软件包安装程序和 Python 软件包的团队、个人或社区。考虑三种不同的开发者类型
使用 PaaS 和 IaaS 提供者的开发者
大多数 PaaS 和 IaaS 提供者只支持一个 Python 软件包安装程序:pip。(一些例外包括 Heroku 的 Python buildpack,它支持 pip 和 Pipenv)。这决定了开发者在使用这些提供者时可以使用哪些安装程序,而这些安装程序可能并不适合其应用程序或工作流程。
采用此 PEP 来成为安装程序后端的安装程序将允许用户使用第三方平台/基础设施,而无需担心他们需要使用哪个 Python 软件包安装程序,只要提供者使用通用安装程序即可。
使用 IDE 的开发者
大多数 IDE 只支持 pip 或一些 Python 软件包安装程序。因此,如果开发者使用不受支持的软件包安装程序,则必须使用解决方法或 hacky 方法来安装其依赖项。
如果 IDE 使用/提供通用安装程序,它将允许使用开发者想要用来安装依赖项的任何安装程序后端,从而使他们免去任何额外工作来安装依赖项,以便更紧密地集成到 IDE 的工作流程中。
与其他开发者合作的开发者
开发者希望能够在与其他开发者合作时使用自己选择的安装程序,但目前必须同步他们的安装程序选择以确保依赖项安装的兼容性。如果所有首选安装程序改为实现指定的接口,它将允许交叉使用安装程序,允许开发者选择一个安装程序,而无需考虑其合作者的偏好。
升级者和软件包基础设施提供者
软件包升级者和 CI/CD 中的软件包基础设施(如 Dependabot、PyUP 等)目前支持一些安装程序。它们通过直接解析和编辑特定于安装程序的依赖项文件(如 requirements.txt
或 poetry.lock
)来工作,其中包含相关软件包信息(如升级、降级或新哈希值)。与平台和 IDE 提供者类似,大多数这些提供者不想支持 N 种不同的 Python 软件包安装程序,因为这将需要支持 N 种不同的文件类型。
目前,这些服务/机器人必须分别实现对每个软件包安装程序的支持。不可避免地,最流行的安装程序首先得到支持,而不太流行的工具往往得不到支持。通过实现此规范,这些服务/机器人可以支持任何(符合规范的)安装程序,允许用户选择他们想要的工具。这将允许该领域有更多创新,因为平台和 IDE 不再被迫过早地选择“获胜者”。
开源社区
指定安装程序要求并采用此 PEP 将减少 Python 软件包安装程序与人们工作流程之间的摩擦。因此,它将减少 Python 软件包安装程序与第三方基础设施/技术(如 PaaS 或 IDE)之间的摩擦。总的来说,它将允许更轻松地开发、部署和维护 Python 项目,因为 Python 软件包安装变得更简单,并且更具互操作性。
指定要求和为安装程序创建接口还可以加快安装程序领域的创新速度。这将允许安装程序进行实验并添加独特的功能,而无需要求生态系统中的其他部分也这样做。无论添加的功能和写入依赖项的格式如何,新安装程序的支持变得更容易,并且可能性更大,同时减少了开发人员所需的时间和资源。
规范
与 PEP 517 指定构建系统的方式类似,安装系统信息将位于 pyproject.toml
文件中的 install-system
表中。
[install-system]
install-system 表用于存储与安装系统相关的數據和信息。此表需要多个键:requires
和 install-backend
。 requires
键保存执行安装后端的最低要求,这些要求将由通用安装器安装。 install-backend
键保存安装后端入口点的名称。这将允许通用安装器安装执行安装后端本身所需的条件(而不是安装后端本身将安装的条件),以及调用安装后端。
如果缺少或为空任何必需的键,则通用安装器应引发错误。
与该接口交互的所有软件包名称假定遵循 PEP 508 的“Python 软件包的依赖项规范”格式。
示例 install-system
表
#pyproject.toml
[install-system]
#Eg : pipenv
requires = ["pipenv"]
install-backend = "pipenv.api:main"
安装程序要求
requires
键指定的依赖项必须在 PEP 517 指定的约束范围内。具体来说,不允许依赖循环,并且通用安装器应拒绝安装依赖项,如果检测到循环。
其他参数或工具特定数据
其他参数或工具(安装后端)数据也可以存储在 pyproject.toml
文件中。这将位于 PEP 518 指定的“tool.*”表中。例如,如果安装后端是 Poetry,并且您想指定多个依赖组,则 tool.poetry 表格可能如下所示
[tool.poetry.dev-dependencies]
dependencies = "dev"
[tool.poetry.deploy]
dependencies = "deploy"
数据也可以以安装后端认为合适的方式存储(例如,单独的配置文件)。
安装程序接口
安装器接口包含强制和可选钩子。符合标准的安装后端必须实现强制钩子,并且可以实现可选钩子。通用安装器可以自行实现任何安装后端钩子,以充当通用安装器和安装后端,但这并非必需。
所有钩子都接受 **kwargs
任意参数,这些参数是安装后端可能需要的,但尚未指定,允许向后兼容。如果将意外参数传递给安装后端,它应该忽略它们。
以下信息类似于 PEP 517 中相应部分的信息。可以将关键字参数传递给钩子,因此实现它们的安装后端应注意确保其签名与以上参数的顺序和名称匹配。
所有钩子都可以将任意信息文本打印到 stdout
和 stderr
。它们不得从 stdin
读取,并且通用安装器可以在调用钩子之前关闭 stdin
。
通用安装器可以捕获来自后端的 stdout
和/或 stderr
。如果后端检测到输出流不是终端/控制台(例如,不是 sys.stdout.isatty()
),它应确保写入该流的任何输出都是 UTF-8
编码的。如果捕获的输出不是有效的 UTF-8,则通用安装器不得失败,但它可能无法保留所有信息(例如,它可以使用 Python 中的替换错误处理程序进行解码)。如果输出流是终端,则安装后端负责准确地呈现其输出,就像在终端中运行的任何程序一样。
如果钩子引发异常或导致进程终止,则表示错误。
强制钩子
invoke_install
安装依赖项
def invoke_install(
path: Union[str, bytes, PathLike[str]],
*,
dependency_group: str = None,
**kwargs
) -> int:
...
path
: 安装后端应从中调用的绝对路径(例如,pyproject.toml
所在的目录)。dependency_group
: 一个可选标志,指定安装后端应该安装的依赖组。如果依赖组不存在,安装将报错。如果安装后端支持依赖组,则用户可以通过调用get_dependency_groups()
找到所有依赖组。**kwargs
: 安装后端可能需要的任意参数,但尚未指定,允许向后兼容。- 返回 : 退出代码(int)。成功为 0,不成功为任何正整数。
通用安装器将使用退出代码来确定安装是否成功,并且应返回退出代码本身。
可选钩子
invoke_uninstall
卸载指定的依赖项
def invoke_uninstall(
path: Union[str, bytes, PathLike[str]],
*,
dependency_group: str = None,
**kwargs
) -> int:
...
path
: 安装后端应从中调用的绝对路径(例如,pyproject.toml
所在的目录)。dependency_group
: 一个可选标志,指定安装后端应该卸载的依赖组。**kwargs
: 安装后端可能需要的任意参数,但尚未指定,允许向后兼容。- 返回 : 退出代码(int)。成功为 0,不成功为任何正整数。
通用安装器必须在与通用安装器本身调用相同的路径上调用安装后端。
通用安装器将使用退出代码来确定卸载是否成功,并且应返回退出代码本身。
get_dependencies_to_install
返回 invoke_install(...)
将安装的依赖项。这允许包升级器(例如,Dependabot)在不解析依赖项文件的情况下检索尝试安装的依赖项
def get_dependencies_to_install(
path: Union[str, bytes, PathLike[str]],
*,
dependency_group: str = None,
**kwargs
) -> Sequence[str]:
...
path
: 安装后端应从中调用的绝对路径(例如,pyproject.toml
所在的目录)。dependency_group
: 指定一个依赖组,以获取invoke_install(...)
将为该依赖组安装的依赖项。**kwargs
: 安装后端可能需要的任意参数,但尚未指定,允许向后兼容。- 返回 : 要安装的依赖项列表(PEP 508 字符串)。
如果指定了组,则安装后端必须返回与提供的依赖组相对应的依赖项。如果指定的组不存在,或者安装后端不支持依赖组,则安装后端必须引发错误。
如果未指定组,并且安装后端提供了默认/未指定组的概念,则安装后端可以返回默认/未指定组的依赖项,但否则必须引发错误。
get_dependency_groups
返回可安装的依赖组。这允许通用安装器枚举安装后端知道的所有依赖组
def get_dependency_groups(
path: Union[str, bytes, PathLike[str]],
**kwargs
) -> AbstractSet[str]:
...
path
: 安装后端应从中调用的绝对路径(例如,pyproject.toml
所在的目录)。**kwargs
: 安装后端可能需要的任意参数,但尚未指定,允许向后兼容。- 返回 : 已知依赖组的集合,以字符串形式表示。空集表示没有依赖组。
update_dependencies
根据输入的软件包列表输出依赖项文件
def update_dependencies(
path: Union[str, bytes, PathLike[str]],
dependency_specifiers: Iterable[str],
*,
dependency_group=None,
**kwargs
) -> int:
...
path
: 安装后端应从中调用的绝对路径(例如,pyproject.toml
所在的目录)。dependency_specifiers
: 作为 PEP 508 字符串的依赖项的迭代器,正在更新,例如:["requests==2.8.1", ...]
。可选地用于特定的依赖组。dependency_group
: 软件包列表所属的依赖组。**kwargs
: 安装后端可能需要的任意参数,但尚未指定,允许向后兼容。- 返回 : 退出代码(int)。成功为 0,不成功为任何正整数。
示例
让我们考虑实现一个安装后端,该后端使用 pip 及其要求文件用于依赖组。实现可能(非常粗略地)如下所示
import subprocess
import sys
def invoke_install(path, *, dependency_group=None, **kwargs):
try:
return subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"-r",
dependency_group or "requirements.txt",
],
cwd=path,
).returncode
except subprocess.CalledProcessError as e:
return e.returncode
如果我们将此软件包命名为 pep650pip
,那么我们可以在 pyproject.toml
中指定
[install-system]
#Eg : pipenv
requires = ["pep650pip", "pip"]
install-backend = "pep650pip:main"
基本原理
所有钩子都接受 **kwargs
,以允许向后兼容,并允许使用特定于工具的安装后端功能,这些功能需要用户提供钩子不需要的附加信息。
虽然安装后端必须是 Python 软件包,但它们在被调用时所做的事情是该工具的实现细节。例如,安装后端可以充当平台包管理器(例如,apt
)的包装器。
该接口不会以任何方式尝试指定安装后端应该如何工作。这是有意的,以便允许安装后端以自己的方式进行创新和解决问题。这也意味着本 PEP 不对操作系统打包采取任何立场,因为这将是安装后端的领域。
在 Python 中定义 API 确实意味着最终需要执行一些 Python 代码。但这并不排除使用非 Python 安装后端(例如 mamba),因为它们可以作为 Python 代码中的子进程执行。
向后兼容性
本 PEP 不会对现有代码和功能产生任何影响,因为它只为通用安装器添加了新功能。任何现有安装程序都应保持其现有功能和用例,因此不会出现向后兼容性问题。只有旨在利用此新功能的代码才会有动机对其现有代码进行更改。
安全影响
恶意用户在添加标准化安装程序规范后,不会获得更多能力或更容易地访问任何东西。可以通过本 PEP 中指定的接口由通用安装器调用的安装程序将由用户明确声明。如果用户选择了一个恶意安装程序,那么使用通用安装程序调用它与用户直接调用安装程序没有什么不同。恶意安装程序成为安装后端不会赋予它额外的权限或能力。
拒绝的方案
标准化的锁文件
一个标准化的锁定文件将解决指定安装程序要求将解决的许多相同问题。例如,它将允许 PaaS/IaaS 只支持一个安装程序,该安装程序可以读取标准化的锁定文件,而不管创建它的安装程序是什么。标准化锁定文件的问题是 Python 包安装程序之间的需求差异,以及通过锁定文件创建可重复环境的基本问题(主要优点之一)。
安装程序之间依赖项文件中存储的需求和信息差异很大,并且取决于安装程序的功能。例如,Poetry 这样的 Python 包安装程序需要所有 Python 版本和平台的信息,并计算相应的哈希值,而 pip 则不会。此外,pip 无法保证重建相同的环境(安装完全相同的依赖项),因为它超出了其功能范围。这使得标准化的锁定文件更难实现,并且似乎更适合将锁定文件设置为特定于工具的。
让安装程序后端支持创建虚拟环境
由于安装后端很可能会有关于虚拟环境的概念以及如何安装到虚拟环境中的概念,因此曾短暂地考虑过让它们也支持创建虚拟环境。但最终,它被认为是一个正交的想法。
未解决的问题
dependency_group
参数是否应该接受可迭代对象?
这将允许在一个调用中指定不重叠的依赖组,例如“docs”和“test”组,它们具有独立的依赖项,但开发人员可能希望在进行开发时同时安装它们。
安装程序后端是在进程内执行的吗?
如果安装后端在进程内执行,那么它极大地简化了知道为哪个环境/安装到哪个环境的流程,因为可以查询实时 Python 环境以获取相应的信息。
进程外执行允许最大程度地减少被安装到其中的环境与安装后端(以及可能通用安装器)之间潜在冲突的问题。
强制执行从提议接口中获取的结果馈送到其他部分?
例如,来自 get_dependencies_to_install()
和 get_dependency_groups()
的结果可以传递给 invoke_install()
。这将防止所提议接口的各个部分之间出现偏差,但它使得更多接口成为必需的,而不是可选的。
对于失败条件,引发异常而不是退出代码
有人建议,API 应该抛出异常,而不是返回退出代码。如果您将此 PEP 视为帮助将当前安装程序转换为安装程序后端,那么依赖退出代码是有意义的。此外,API 没有特定的返回值,因此传递退出代码不会干扰函数的返回值。
将此与在出错时抛出异常进行比较。这可能会提供一种更结构化的错误抛出方法,虽然为了能够捕获错误,需要将异常类型指定为接口的一部分。
版权
本文档放置在公共领域或 CC0-1.0-Universal 许可下,以更宽松的许可为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0650.rst
最后修改时间:2023-09-09 17:39:29 GMT