PEP 582 – Python 本地包目录
- 作者:
- Kushal Das <mail at kushaldas.in>, Steve Dower <steve.dower at python.org>, Donald Stufft <donald at stufft.io>, Alyssa Coghlan <ncoghlan at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2018年5月16日
- Python 版本:
- 3.12
- 发布历史:
- 2019年3月1日
- 决议:
- Discourse 消息
摘要
本 PEP 提议扩展用于设置 `sys.path` 的现有机制,使其除了现有位置外,还能包含一个新的 `__pypackages__` 目录。新目录将添加到 `sys.path` 的开头,位于当前工作目录之后,系统 site-packages 之前,以便让安装在该处的包比其他位置具有更高的优先级。
这类似于添加当前目录(或脚本所在的目录)的现有机制,但通过使用子目录,可以将其他库与用户的项目分开。
动机
新的 Python 程序员可以从学习将单个项目的依赖项与其系统环境隔离的价值中受益。然而,现有的实现此目的的机制——虚拟环境——对初学者来说是复杂且容易出错的。在尝试让一群初学者设置好时,解释虚拟环境通常会分散注意力——平台和 shell 环境的差异需要单独协助,并且在新的 shell 会话中需要激活,使得学生在休息后回来工作时很容易犯错。本提案提供了一种轻量级的解决方案,可以在用户无需理解更高级概念的情况下实现隔离。
此外,独立的 Python 应用程序通常需要第三方库才能运行。通常,它们要么设计为从虚拟环境中运行(其中依赖项与应用程序一起安装到环境中),要么将依赖项捆绑在一个子目录中,并在应用程序启动时修改 `sys.path`。虚拟环境虽然是常见且有效的解决方案(例如 `pipx` 工具使用),但设置和管理起来有些麻烦,并且不可重定位。另一方面,手动操作 `sys.path` 是开发人员需要正确处理的样板代码,并且(作为运行时行为)像 linters 和 type checkers 这样的工具无法理解它。`__pypackages__` 提案将“捆绑依赖项”位置的想法形式化,避免了样板代码,并提供了一个开发工具可以被教授识别的标准位置。
需要注意的是,通常情况下,Python 库不能简单地在机器、平台之间,甚至不一定在 Python 版本之间复制。本提案不会改变这一事实,尽管将脚本及其 `__pypackages__` 捆绑起来被认为是分发应用程序的一种机制,但这**不**是本提案的目标。开发者仍然对代码的可移植性负责。
基本原理
虽然 `sys.path` 可以在运行时进行操作,但默认值很重要,因为它建立了一个用户和工具可以达成一致的通用基线。当前默认值不包含一个可以被视为“当前项目私有”的位置,然而这是一个有用的概念。
这类似于 JavaScript 社区中流行的 npm `node_modules` 目录,熟悉该生态系统的开发者经常要求 Python 提供类似的功能。
规范
本 PEP 提议在启动时计算 `sys.path` 的过程中添加一个新的步骤。
当交互式解释器启动时,如果在当前工作目录中找到 `__pypackages__` 目录,它将被添加到 `sys.path` 中,位于当前工作目录条目之后,系统 site-packages 之前。
当解释器运行脚本时,Python 将尝试在脚本所在的目录中查找 `__pypackages__`。如果找到(以及其内部的当前 Python 版本目录),则将使用它,否则 Python 将像目前一样运行。
行为应与添加当前工作目录或脚本目录到 `sys.path` 的现有机制完全相同。例如,如果设置了 `-P` 选项或 `PYTHONSAFEPATH` 环境变量,则会忽略 `__pypackages__`。
为了被识别,`__pypackages__` 目录必须根据 `sysconfig` 模块中的新 `localpackages` 方案进行布局。具体来说,`purelib` 和 `platlib` 目录都必须存在,并使用以下代码确定这些目录的位置
scheme = "localpackages"
purelib = sysconfig.get_path("purelib", scheme, vars={"base": "__pypackages__", "platbase": "__pypackages__"})
platlib = sysconfig.get_path("platlib", scheme, vars={"base": "__pypackages__", "platbase": "__pypackages__"})
`sys.path` 将添加这两个位置,`__pypackages__` 目录中的其他目录或文件将被静默忽略。路径将基于 Python 版本。
注意
有一个可能的选项是提供一个独立的、新的 API,它记录在 问题 #3013 中。
示例
以下示例展示了一个项目目录结构,以及 Python 可执行文件和任何脚本的行为方式。此示例适用于类 Unix 系统——在 Windows 上子目录会有所不同。
foo
__pypackages__
lib
python3.10
site-packages
bottle
myscript.py
/> python foo/myscript.py
sys.path[0] == 'foo'
sys.path[1] == 'foo/__pypackages__/lib/python3.10/site-packages/'
cd foo
foo> /usr/bin/ansible
#! /usr/bin/env python3
foo> python /usr/bin/ansible
foo> python myscript.py
foo> python
sys.path[0] == '.'
sys.path[1] == './__pypackages__/lib/python3.10/site-packages'
foo> python -m bottle
我们有一个名为 `foo` 的项目目录,其中包含一个 `__pypackages__`。我们在 `__pypackages__/lib/python3.10/site-packages/` 中安装了 `bottle`,并在项目目录中有一个 `myscript.py` 文件。我们已经使用了通常用于在该位置安装 `bottle` 的工具。
对于调用脚本,Python 将尝试在脚本所在的目录中查找 `__pypackages__` [1],`/usr/bin`。在最后一个示例中,当我们从 `foo` 目录中执行 `/usr/bin/ansible` 时,情况也是如此。在这两种情况下,它**不会**使用当前工作目录中的 `__pypackages__`。
同样,如果我们调用第一个示例中的 `myscript.py`,它将使用位于 `foo` 目录中的 `__pypackages__` 目录。
如果我们进入 `foo` 目录并启动 Python 可执行文件(解释器),它将在当前工作目录中找到 `__pypackages__` 目录并将其用于 `sys.path`。如果我们尝试使用 `-m` 并使用模块,情况也是如此。在我们的例子中,`bottle` 模块将在 `__pypackages__` 目录中找到。
以上两个示例是当前工作目录中的 `__pypackages__` 被使用的唯一情况。
在另一个示例场景中,Python 类的培训师可以这样说:“今天我们将学习如何使用 Twisted!首先,请签出我们的示例项目,进入该目录,然后运行给定命令来安装 Twisted。”
这将把 Twisted 安装到一个与 `python3` 分离的目录中。无需讨论虚拟环境、全局或用户安装等,因为安装默认是本地的。培训师然后可以继续让他们使用 `python3`,而无需任何激活步骤等。
与虚拟环境的关系
本质上,本提案只是修改 `sys.path` 默认值的计算,与虚拟环境机制无关。然而,`__pypackages__` 可以被视为提供了隔离能力,从这个意义上说,它与虚拟环境“竞争”。
然而,存在显著的差异
- 虚拟环境与系统环境隔离,而 `__pypackages__` 只是添加到系统环境。
- 虚拟环境包含一个完整的“安装方案”,包含二进制文件、C 头文件等目录,而 `__pypackages__` 仅用于 Python 库代码。
- 虚拟环境在“激活”时效果最好。本提案无需激活。
本提案应被视为独立于虚拟环境,而不是与之竞争。充其量,一些目前仅由虚拟环境服务的用例也可以被 `__pypackages__` 服务(可能更好)。
需要注意的是,安装在 `__pypackages__` 中的库在虚拟环境中是可见的。这可能打破了虚拟环境的隔离,但原则上与当前目录出现在 `sys.path` 上(或 `PYTHONPATH` 环境变量等机制)没有区别。唯一的区别在于程度,因为人们预期会更普遍地在 `__pypackages__` 中安装包。另一种选择是显式检测虚拟环境并在该情况下禁用 `__pypackages__`——但这将破坏具有捆绑依赖项的脚本。PEP 作者认为,使用虚拟环境的开发人员应该足够有经验,能够理解这个问题并预期和避免任何问题。
安全注意事项
理论上,可以将一个库添加到 `__pypackages__` 目录中,该库会覆盖一个标准库模块或已安装的第三方库。对于与脚本关联的 `__pypackages__`,这被认为不是一个重大问题,因为除非一个人能够写入脚本本身,否则不太可能能够写入 `__pypackages__`。
对于当前工作目录中的 `__pypackages__` 目录,交互式解释器可能会受到影响。然而,这与某人在当前目录中拥有一个 `math.py` 模块的现有问题没有显著区别,并且(就像那样的情况一样)它可能会引起用户混淆,但不会引入任何新的安全影响。
运行脚本时,当前工作目录中的任何 `__pypackages__` 目录都将被忽略。这与 Python 添加当前工作目录到 `sys.path` 的方法相同,并确保通过修改当前目录中的文件来改变脚本的行为是不可能的。
此外,`__pypackages__` 目录仅在当前(或脚本)目录中被识别。解释器**不会**在父目录中扫描 `__pypackages__`。这样做会带来安全风险,如果父目录的权限不同。特别是,`bin` 目录或 `__pypackages__`(`sysconfig` 中的 `scripts` 位置)中的可执行脚本对安装在 `__pypackages__` 中的库没有特殊访问权限。本提案不支持将可执行脚本放在 `bin` 目录中。
如何教授此内容
本提案的原始动机是使 Python 的初学者教学更加容易。为此,它需要易于解释且简单易用。
在最基本的层面上,这与现有机制类似,即脚本目录被添加到 `sys.path`,并且可以以类似的方式教授。然而,对于其预期的“轻量级隔离”用途,它可能被教授为“放在 `__pypackages__` 目录中的内容对你的脚本是私有的”。PEP 作者的经验表明,这比当前的替代方案——引入虚拟环境——要容易得多。
对工具的影响
由于该功能 intended 用途是在新目录中安装第三方库,因此工具,特别是安装程序,必须了解如何管理 `__pypackages__` 至关重要。
希望工具会引入一个专门的“pypackages”安装模式,该模式**保证**在所有情况下都符合预期的布局。然而,如何最好地支持 `__pypackages__` 布局的问题最终留给各个工具维护者考虑和决定。
定位包而不实际运行 Python 代码的工具(IDE、linters、type checkers 等)需要更新以识别 `__pypackages__`。在没有此类更新的情况下,`__pypackages__` 目录将与当前添加到运行时 `sys.path` 的目录类似(即,该工具可能会忽略它)。
向后兼容性
目录名 `__pypackages__` 的选择是因为它不太可能被普遍使用。确实,那些选择将该名称用于自身目的的用户会受到影响,但在编写本 PEP 时,这被认为是相对低风险的。
不幸的是,在本 PEP 讨论期间,一些工具选择实现了与本提案类似的变体,但它们并不都与 PEP 的最终形式兼容。因此,冲突的风险现在比最初预期的要高。
通过选择一个**不同**的名称,希望像 `__pypackages__` 最初那样不常见,可以减轻这种情况。但现实地说,任何兼容性问题都可以被视为人们尝试实现草案提案而没有努力跟踪提案变更的后果。因此,保留 `__pypackages__` 名称是合理的,并将解决兼容性问题的负担推给实现了草案版本的工具。
对其他 Python 实现的影响
其他 Python 实现需要复制解释器引导程序的新行为,包括查找 `__pypackages__` 目录并将其添加到 `sys.path` 的 site packages 之前,如果存在的话。这与其他任何 Python 更改无异。
参考实现
此处提供了一个小型脚本,可以为 CPython 和 PyPy 实现该功能。
被拒绝的想法
- 备选名称,如 `__pylocal__` 和 `python_modules`。最终,名称是任意的,而选定的名称已经足够好。
- 虚拟环境的附加功能。本提案不是虚拟环境的替代品,因此这些功能不属于本提案的范围。
- 我们不会扫描任何父目录来查找 `__pypackages__`。如果我们想执行 `~/bin/` 目录中的脚本,那么 `__pypackages__` 目录必须在 `~/bin/` 目录中。对 `__pypackages__`(解释器或脚本)进行任何此类扫描都会带来安全隐患,并增加启动时间。
- 如果在 `__pypackages__` 中存在意外的文件或目录,则引发错误。这被认为过于严格,特别是考虑到像 `pip install --prefix` 这样的过渡性方法可以在 `__pypackages__` 中创建其他文件。
- 使用不同的 `sysconfig` 方案,或专门的 `pypackages` 方案。虽然这在理论上很有吸引力,但它使得过渡更加困难,因为在工具实现显式支持之前,将没有易于获得的安装到 `__pypackages__` 的方法。虽然 PEP 作者希望并假设会添加此类支持,但让提案依赖于此类支持才能使用似乎是不可接受的风险。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0582.rst