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-05-16
- Python 版本:
- 3.12
- 历史记录:
- 2019-03-01
- 决议:
- Discourse 信息
摘要
本 PEP 提出扩展现有的 sys.path
设置机制,除了现有的位置,还包含一个新的 __pypackages__
目录。新目录将在 sys.path
的开头添加,位于当前工作目录之后,系统 site-packages 之前,以便在此目录中安装的包优先于其他位置。
这类似于将当前目录(或脚本所在目录)添加到 sys.path
的现有机制,但通过使用子目录,额外的库与用户的操作保持分离。
动机
新的 Python 程序员可以从学习将单个项目的依赖关系与系统环境隔离的价值中获益。然而,现有的实现方法,虚拟环境,对于初学者来说是出了名的复杂且容易出错。在尝试让一群初学者完成设置时,解释虚拟环境往往是一个干扰因素——平台和 shell 环境的差异需要单独的帮助,并且需要在每个新的 shell 会话中激活,这使得学生在休息后回来工作时很容易出错。本提案提供了一种轻量级解决方案,它在不使用户需要理解更高级的概念的情况下,提供了隔离功能。
此外,独立的 Python 应用程序通常需要第三方库才能正常运行。通常,它们要么被设计为从虚拟环境运行,在这种环境中,依赖关系与应用程序一起安装到环境中,要么将它们的依赖关系捆绑在一个子目录中,并在应用程序启动时修改 sys.path
。虚拟环境虽然是一种常见且有效的解决方案(例如,由 pipx
工具使用),但设置和管理起来有点繁琐,并且不可移动。另一方面,手动操作 sys.path
是一种开发人员需要正确使用的样板代码,而且(作为运行时行为),它不被诸如 linter 和类型检查器之类的工具理解。 __pypackages__
提案将“捆绑的依赖项”位置的概念形式化,避免了样板代码,并提供了一个标准位置,开发工具可以被教导识别该位置。
需要注意的是,一般来说,Python 库不能简单地在机器、平台之间甚至 Python 版本之间复制。本提案并没有改变这一事实,虽然假设捆绑一个脚本及其 __pypackages__
是一种分发应用程序的机制,但这明确地**不是**本提案的目标。开发人员仍然需要为其代码的可移植性负责。
基本原理
虽然 sys.path
可以由运行时操作,但默认值很重要,因为它建立了用户和工具可以同意的共同基线。当前的默认值不包含一个可以被视为“对当前项目私有”的位置,但这是一个有用的概念。
这类似于 npm 的 node_modules
目录,该目录在 Javascript 社区中很流行,而且熟悉该生态系统的开发人员经常从 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,它记录在 issue #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 将尝试在脚本所在的目录 [1], /usr/bin
中找到一个 __pypackages__
。当我们从 foo
目录中执行 /usr/bin/ansible
时,也会发生同样的情况。在这两种情况下,它**不会**使用当前工作目录中的 __pypackages__
。
同样,如果我们从第一个示例中调用 myscript.py
,它将使用 foo
目录中的 __pypackages__
目录。
如果我们进入 foo
目录并启动 Python 可执行文件(解释器),它将在当前工作目录中找到 __pypackages__
目录,并在 sys.path
中使用它。如果我们尝试使用 -m
并使用模块,也会发生同样的事情。在我们的示例中, bottle
模块将在 __pypackages__
目录中找到。
以上两个示例只是使用当前工作目录中的 __pypackages__
的情况。
在另一个示例场景中,一位 Python 课程的培训师可以说“今天我们将学习如何使用 Twisted!首先,请检出我们的示例项目,转到该目录,然后运行给定的命令来安装 Twisted。”
这将在与 python3
分开的目录中安装 Twisted。无需讨论虚拟环境、全局安装与用户安装等问题,因为安装默认情况下是本地的。然后,培训师可以继续告诉他们使用 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 作者的经验表明,这比目前介绍虚拟环境的替代方法要容易得多。
对工具的影响
由于该功能的预期用途是在新目录中安装第三方库,因此工具(特别是安装程序)必须了解如何管理 __pypackages__
。
希望工具会引入一种专门的“pypackages”安装模式,该模式 *保证* 在所有情况下都与预期的布局相匹配。但是,如何最好地支持 __pypackages__
布局最终留给各个工具维护者考虑和决定。
没有实际运行 Python 代码来定位包的工具(IDE、linter、类型检查器等)需要更新以识别 __pypackages__
。如果没有这些更新, __pypackages__
目录的工作方式将类似于当前在运行时添加到 sys.path
的目录(即,该工具可能会忽略它)。
向后兼容性
目录名 __pypackages__
被选中是因为它不太可能被普遍使用。确实,选择使用该名称的用户将受到影响,但在撰写本 PEP 时,这被认为是一个相对较低的风险。
不幸的是,在本 PEP 讨论期间,许多工具选择实现对这里提出的内容的变体,这些变体并不都与 PEP 的最终形式兼容。因此,冲突的风险现在比最初预期的更高。
可以通过选择一个 *不同* 的名称来缓解这个问题,希望这个名称像 __pypackages__
最初一样不常见。但实际上,任何兼容性问题都可以被视为人们尝试实施草案提案,而没有付出努力来跟踪提案中的更改的结果。因此,保留 __pypackages__
名称,并将解决兼容性问题的负担放在实施草案版本的工具上似乎是合理的。
对其他 Python 实现的影响
其他 Python 实现将需要复制解释器引导的新行为,包括定位 __pypackages__
目录,并在存在的情况下,在站点包之前将它添加到 sys.path
中。这与任何其他 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
最后修改时间:2024-08-20 10:29:32 GMT