PEP 650 – 为 Python 项目指定安装程序要求
- 作者:
- Vikram Jayanthi <vikramjayanthi at google.com>, Dustin Ingram <di at python.org>, Brett Cannon <brett at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2020年7月16日
- 发布历史:
- 2021年1月14日
摘要
Python 包安装程序之间并不完全可互操作。虽然 pip 是使用最广泛的安装程序和事实上的标准,但像 Poetry 或 Pipenv 这样的其他安装程序也很受欢迎,因为它们提供了独特的功能,这些功能最适合某些工作流程,并且不直接符合 pip 的工作方式。
虽然安装程序选项的丰富性对有特定需求的最终用户来说是好事,但它们之间缺乏互操作性使得支持所有潜在安装程序变得困难。特别是,缺乏用于声明依赖关系的标准化需求文件意味着每个工具都必须显式使用才能安装以其各自格式指定的依赖关系。否则,工具必须发出需求文件,这会导致安装程序丢失潜在信息,并增加开发者工作流程中的导出步骤。
通过提供一个可用于调用兼容安装程序的标准化 API,我们可以解决这个问题,而无需解决不同安装程序及其锁定文件之间的个体问题、独特要求和不兼容性。
实现该规范的安装程序可以以统一的方式调用,使用户能够像直接调用一样使用他们选择的安装程序。
术语
- 安装程序接口
- 安装程序后端和通用安装程序交互的接口。
- 通用安装程序
- 一个通过调用安装程序接口的可选调用方法来调用安装程序后端的安装程序。这也可以被认为是安装程序前端,类似于 build 项目用于 PEP 517。
- 安装程序后端
- 实现安装程序接口的安装程序,允许它被通用安装程序调用。安装程序后端也可以是通用安装程序,但这不是必需的。与 PEP 517 相比,这将是 Flit。安装程序后端可以是包装某个后端安装程序的包装器包,例如,Poetry可以选择不支持此 API,但一个包可以作为包装器调用 Poetry,以适当地使用 Poetry 来执行安装。
- 依赖组
- 一组相关联的依赖项,需要同时安装才能达到某种目的。例如,“test”依赖组可能包括运行测试套件所需的依赖项。依赖组的指定方式取决于安装程序后端。
动机
该规范允许任何人调用和交互实现指定接口的安装程序后端,从而在现有工具特定的安装过程之上提供一个普遍支持的层。
反过来,这将使得所有实现指定接口的安装程序都可以在支持单个通用安装程序的环境中使用,只要该安装程序也实现了此规范。
下面,我们确定了适用于 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 中的 replace 错误处理程序进行解码)。如果输出流是终端,安装程序后端负责准确地呈现其输出,就像在终端中运行的任何程序一样。
如果钩子抛出异常或导致进程终止,则表示出现错误。
强制性钩子
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