Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 722 – 单文件脚本的依赖项规范

作者:
Paul Moore <p.f.moore at gmail.com>
PEP 代理人:
Brett Cannon <brett at python.org>
讨论至:
Discourse 帖子
状态:
已拒绝
类型:
标准跟踪
主题:
打包
创建日期:
2023年7月19日
发布历史:
2023年7月19日
取代者:
723
决议:
2023年10月21日

目录

Warning

此 PEP 已被 PEP 723 取代。

×

摘要

此 PEP 规范了一种在单文件 Python 脚本中包含第三方依赖项的格式。

动机

并非所有 Python 代码都组织成一个“项目”,即拥有自己的目录,包含 pyproject.toml 文件,并构建成可安装的分发包。Python 也经常被用作脚本语言,Python 脚本是 shell 脚本、批处理文件等的(更好的)替代品。在用于创建脚本时,Python 代码通常存储为单个文件,通常位于专门用于此类“实用脚本”的目录中,这些目录可能是多种语言的混合体,而 Python 只是其中一种可能性。此类脚本可能会被共享,通常通过电子邮件或指向 Github gist 等 URL 的链接来共享。但它们通常不是在正常工作流程中被“分发”或“安装”的。

以这种方式使用 Python 作为脚本语言时的一个问题是如何在包含脚本所需的所有第三方依赖项的环境中运行该脚本。目前没有标准工具来解决这个问题,而本 PEP试图定义一个。然而,任何解决此问题的工具都需要知道脚本需要哪些第三方依赖项。通过定义一种存储此类数据的标准格式,现有工具以及任何未来工具都将能够获取该信息,而无需用户在脚本中包含特定于工具的元数据。

基本原理

由于关键要求是编写单文件脚本,并通过提供脚本副本即可简单共享,因此 PEP 定义了一种机制,用于将依赖项数据嵌入到脚本本身,而不是外部文件。

我们定义了依赖项块的概念,其中包含有关脚本依赖哪些第三方包的信息。

为了识别依赖项块,脚本可以像读取文本文件一样读取。这是故意的,因为 Python 语法会随着时间而改变,因此尝试将脚本解析为 Python 代码需要选择特定的 Python 语法版本。此外,很可能至少一些工具不会用 Python 编写,并且期望它们实现 Python 解析器将负担过重。

但是,为了避免对核心 Python 进行更改,该格式被设计为对 Python 解析器显示为注释。有可能编写一个依赖项块被解释为注释的代码(例如,将其嵌入到 Python 多行字符串中),但此类用法是不被鼓励的,并且可以很容易地避免,除非您故意尝试创建一个病态的示例。

对其他语言如何允许脚本指定其依赖项的审查表明,“结构化注释”是一种常用的方法。

规范

本节内容将作为标题为“在脚本文件中嵌入元数据”的文档发布在 Python 打包用户指南的 PyPA 规范部分。

任何 Python 脚本都可以包含一个依赖项块。通过将脚本作为文本文件读取(即,不将文件解析为 Python 源代码)来识别依赖项块,查找形式为

# Script Dependencies:

哈希字符必须位于行首,前面没有任何空格。“Script Dependencies”文本不区分大小写,空格表示任意空格(但至少必须有一个空格)。以下正则表达式可以识别依赖项块标题行

(?i)^#\s+script\s+dependencies:\s*$

读取依赖项块的工具可以遵循标准的 Python 编码声明。如果它们选择不这样做,则必须将文件作为 UTF-8 处理。

在标题行之后,文件中的所有行直到第一个不以 # 开头的行都被视为依赖项行,并按以下方式处理

  1. 开头的 # 符号将被剥离。
  2. 如果行包含“ # ”(空格井号空格)序列,则这些字符及其后的所有字符都将被丢弃。这允许依赖项块包含行内注释。
  3. 丢弃剩余文本的开头和结尾处的空格。
  4. 如果行现在为空,则忽略它。
  5. 该行的内容现在必须是一个有效的 PEP 508 依赖项说明符。

行内注释中 # 前后需要空格是为了区分它们与 PEP 508 URL 说明符(可能包含井号,但周围没有空格)的一部分。

消费者必须至少验证所有依赖项是否以 PEP 508 中定义的name开头,并且可以验证所有依赖项是否完全符合 PEP 508。如果发现无效的说明符,它们必须以错误形式失败。

示例

以下是一个包含嵌入式依赖项块的脚本示例

# In order to run, this script needs the following 3rd party libraries
#
# Script Dependencies:
#    requests
#    rich     # Needed for the output
#
#    # Not needed - just to show that fragments in URLs do not
#    # get treated as comments
#    pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686

import requests
from rich.pretty import pprint

resp = requests.get("https://peps.pythonlang.cn/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])

向后兼容性

由于依赖项块采用结构化注释的形式,因此可以在不改变现有代码含义的情况下添加它们。

可能已经存在一个注释,其形式与依赖项块匹配。虽然“Script Dependencies”的标识标题文本旨在最大限度地减少此风险,但仍然有可能。

在极少数情况下,现有注释可能被错误地解释为依赖项块,可以通过在代码更早的位置添加一个实际的依赖项块(如果脚本没有依赖项,则该块可以为空)来解决。

安全隐患

如果使用自动安装依赖项的工具运行包含依赖项块的脚本,这可能会导致任意代码被下载并安装到用户的环境中。

这里的风险是使用该工具运行脚本的功能的一部分,因此应该由该工具本身解决。此 PEP 引入的唯一额外风险是,当运行一个不受信任的带有依赖项块的脚本时,可能会安装一个潜在恶意的依赖项。此风险通过在运行代码之前审查代码的正常良好实践来解决。

如何教授此内容

该格式旨在接近开发人员在说明性注释中指定脚本依赖项的方式。必需的结构是故意最小化的,以便易于学习格式规则。

用户需要知道如何编写 Python 依赖项说明符。这由 PEP 508 涵盖,但对于简单的示例(这对于没有经验的用户来说预计是常态),语法只是一个包名,或者是一个名称和版本限制,这是相当容易理解的语法。

用户还将知道如何使用解释依赖项数据的工具运行脚本。这不在此 PEP 的范围内,因为此类工具如何使用由该工具自行负责记录。

请注意,核心 Python 解释器解释依赖项块。这可能是一个让初学者感到困惑的地方,他们尝试运行 python some_script.py 并且不明白为什么会失败。然而,这与当前的现状没有区别,在现状下,运行一个缺少依赖项的脚本也会产生错误。

总的来说,我们假设,如果初学者收到一个带有依赖项的脚本(无论是否在依赖项块中指定),提供脚本的人应该解释如何运行该脚本,如果这涉及使用脚本运行器工具,则应予以说明。

建议

本节是非规范性的,仅描述使用依赖项块时的“最佳实践”。

虽然允许工具对需求进行最小程度的验证,但实际上它们应该尽可能多地进行“健全性检查”验证,即使它们无法完全检查 PEP 508 语法。这有助于确保未正确终止的依赖项块能够尽早报告。介于仅检查需求是否以名称开头和完整 PEP 508 验证之间的折衷方案是检查裸露名称,或名称后跟可选空格,然后是 [(额外)、@(urlspec)、;(标记)或 (<!=>~(版本)之一。

一般来说,脚本应该将依赖项块放在文件的顶部,紧随任何 shebang 行之后,或紧随脚本文档字符串之后。特别是,依赖项块应始终放在文件中的任何可执行代码之前。这使得人类读者易于找到它。

参考实现

用 Python 实现此提案的代码相当直接,因此可以将其作为参考实现包含在此处。

import re
import tokenize
from packaging.requirements import Requirement

DEPENDENCY_BLOCK_MARKER = r"(?i)^#\s+script\s+dependencies:\s*$"

def read_dependency_block(filename):
    # Use the tokenize module to handle any encoding declaration.
    with tokenize.open(filename) as f:
        # Skip lines until we reach a dependency block (OR EOF).
        for line in f:
            if re.match(DEPENDENCY_BLOCK_MARKER, line):
                break
        # Read dependency lines until we hit a line that doesn't
        # start with #, or we are at EOF.
        for line in f:
            if not line.startswith("#"):
                break
            # Remove comments. An inline comment is introduced by
            # a hash, which must be preceded and followed by a
            # space.
            line = line[1:].split(" # ", maxsplit=1)[0]
            line = line.strip()
            # Ignore empty lines
            if not line:
                continue
            # Try to convert to a requirement. This will raise
            # an error if the line is not a PEP 508 requirement
            yield Requirement(line)

一种类似于此处提出的格式已经在 pipxpip-run 中得到支持。

被拒绝的想法

为何不包含其他元数据?

此提案所解决的核心用例是识别独立脚本成功运行所需的依赖项。这是一个常见的实际问题,目前通过脚本运行器工具使用特定于实现的存储数据的方式来解决。标准化存储格式通过不将脚本绑定到特定运行器来提高互操作性。

虽然可以说其他形式的元数据可能在独立脚本中有用,但目前这种需求在很大程度上是理论上的。在实际操作中,脚本要么不使用其他元数据,要么使用现有的、广泛使用的(因此是事实上的标准)格式来存储它们。例如,需要 README 风格文本的脚本通常使用标准的 Python 模块文档字符串,而需要声明版本的脚本则使用具有 __version__ 变量的常见约定。

在此 PEP 的讨论中提出的一种情况是,能够声明脚本运行所需的最低 Python 版本,类似于软件包的 Requires-Python 核心元数据项。与包不同,脚本通常只由一个用户或在一个环境中运行,在这些环境中,多个 Python 版本是不常见的。因此,这种元数据的需求在脚本情况下不那么关键。作为进一步证据,目前可用的两个主要脚本运行器 pipxpip-run 并没有提供在此类脚本中包含此数据的手段。

创建标准的“元数据容器”格式可以统一各种方法,但实际上并不需要统一,而且中断可能会延迟采用,或者更有可能导致脚本作者忽略该标准。

因此,本提案选择仅关注一个有明确需求、不存在现有标准或通用实践的用例。

为何不每行使用一个标记?

除了使用带有标题的注释块之外,另一种可能性是在每行使用一个标记,类似于

# Script-Dependency: requests
# Script-Dependency: click

虽然这使得单独解析行更容易,但它存在一些问题。首先是它相当冗长,可读性较差。这明显受所选关键字的影响,但所有建议的选项(在作者看来)都比块注释形式的可读性差。

更重要的是,这种形式故意使得无法要求所有依赖项说明符都集中在一个块中。因此,人类读者在不仔细检查整个文件的情况下,无法确定他们已经识别了所有依赖项。有关此问题的更多讨论,请参阅下面的问题“为何不允许使用多个依赖项块并合并它们?”。

最后,正如参考实现所示,解析“注释块”形式在实践中并不比解析此形式困难得多。

为何不使用一种独特的注释形式来表示依赖项块?

本提案的一个早期版本使用了 ## 来标识依赖项块。然而,不幸的是,flake8 linter 实现了一条规则,要求注释必须在开头的 # 符号后有一个空格。虽然 PEP 作者认为该规则是错误的,但它是默认启用的,因此在遇到依赖项块时会导致检查失败。

此外,black 格式化程序虽然允许 ## 形式,但对于大多数其他形式的注释会在 # 之后添加空格。这意味着如果我们选择 #% 等替代方案,自动重新格式化将损坏依赖项块。包含空格的形式,例如 # # 是可能的,但对于普通用户来说不够自然(省略空格是一个显而易见的错误)。

虽然有可能更改 linter 和格式化程序以识别新标准,但拥有专用前缀的好处似乎不足以证明迁移成本或用户可能使用旧工具的风险是值得的。

为何不允许使用多个依赖项块并合并它们?

因为人类读者很容易忽略第二个依赖项块的事实。这可能导致脚本运行器意外下载额外的包,甚至可能是一种将恶意包偷渡到用户机器上的方法(通过将第二个依赖项块“隐藏”在脚本正文中)。

虽然“不运行不受信任的代码”的原则在此处适用,但其好处不足以弥补风险。

为何不使用更标准的数据格式(例如 TOML)?

首先,另一种格式的唯一实际选择是 TOML。Python 打包已将 TOML 标准化用于结构化数据,使用不同的格式,如 YAML 或 JSON,将增加复杂性和混乱,而没有实际的好处。

因此,问题本质上是“为什么不使用 TOML?”

“依赖项块”格式背后的关键思想是定义一种在脚本中自然地读取为注释的内容。依赖项数据对工具和人类读者都有用,因此拥有人类可读的格式是有益的。另一方面,TOML 必然有其自身的语法,这会分散人们对底层数据的注意力。

重要的是要记住,编写 Python 脚本的开发人员通常不是 Python 或 Python 打包方面的专家。他们通常是系统管理员或数据分析师,可能只是将 Python 用作“更好的批处理文件”。对于这些用户来说,TOML 格式极有可能不熟悉,其语法对他们来说是晦涩的,并且不太直观。这些开发人员可能会从 Stack Overflow 等来源复制代码说明符,而没有真正理解它们。将这种要求嵌入 TOML 结构中是一种额外的复杂性——重要的是要记住,目标是让这些用户轻松使用第三方库。

此外,TOML 本质上是一种灵活的格式,旨在支持非常通用的数据结构。在 TOML 中编写简单字符串列表有许多方法,对于没有经验的用户来说,不清楚使用哪种形式。

另一个潜在的问题是,使用通用的 TOML 解析器有时导致可衡量的性能开销。启动时间通常被认为是运行小型脚本时的一个问题,因此这对于以高性能为目标的脚本运行器来说可能是一个问题。

最后,将会有期望写入依赖项数据的工具——例如,一个 IDE,它有一个功能可以在您引用库函数时自动添加 import 和依赖项说明符。虽然存在允许编辑 TOML 数据的库,但它们并不总是擅长保留用户的布局。即使存在能有效完成此工作的库,期望所有工具都使用此类库也是对支持此 PEP 的代码的重大负担。

通过选择一种简单的、基于行的格式,不带引号规则,依赖项数据易于阅读(对人类和工具)且易于编写。该格式不具备像 TOML 那样的灵活性,但用例根本不需要那种灵活性。

为何不使用(可能受限的)Python 语法?

这通常涉及将依赖项存储为具有约定名称的(运行时)列表变量,例如

__requires__ = [
    "requests",
    "click",
]

其他建议包括静态多行字符串,或将依赖项包含在脚本的文档字符串中。

此提案最显著的问题是它要求所有依赖项数据的使用者都实现 Python 解析器。即使语法受到限制,脚本的其余部分也将使用完整的 Python 语法,并且尝试定义一个可以在与周围代码隔离的情况下成功解析的语法可能会非常困难且容易出错。

此外,Python 的语法在每个版本中都会发生变化。如果提取依赖项数据需要 Python 解析器,则解析器需要知道脚本是为哪个 Python 版本编写的,而通用工具拥有能够处理多个 Python 版本的解析器的开销是无法承受的。

即使上述问题得到解决,该格式也会给人一种数据可以在运行时更改的印象。然而,通常并非如此,试图这样做的代码将遇到意外和令人困惑的行为。

最后,没有证据表明在运行时提供依赖项数据具有任何实际用途。如果发现此类用途,通过解析源来获取数据很简单——read_dependency_block(__file__)

不过,值得注意的是,pip-run 工具确实实现了(此方法的扩展形式)。关于 pip-run 设计的进一步讨论可在项目的问题跟踪器上找到。

为何不将 pyproject.toml 文件嵌入到脚本中?

首先,pyproject.toml 是基于 TOML 的格式,因此之前围绕 TOML 作为格式的所有顾虑都适用。然而,pyproject.toml 是 Python 打包所使用的标准,重用现有标准是一个合理的建议,值得单独解决。

第一个问题是,该建议很少意味着支持脚本的全部 pyproject.toml。脚本不打算“构建”成任何类型的可分发构件,如 wheel(关于这一点,请参见下文)。因此,pyproject.toml[build-system] 部分意义不大。虽然 pyproject.toml 的工具特定部分可能对脚本有用,但尚不清楚像 ruff 这样的工具是否会希望以这种方式支持每个文件配置,这会导致用户期望它工作但它不起作用时产生混乱。此外,这种工具特定的配置对于大型项目中的单个文件同样有用,因此我们必须考虑将 pyproject.toml 嵌入到具有自己 pyproject.toml 的大型项目中的单个文件中意味着什么。

此外,pyproject.toml 目前侧重于将要构建成 wheel 的项目。正在进行讨论,关于如何使用 pyproject.toml 来处理不打算构建为 wheel 的项目,并且在这些问题得到解决之前(这可能需要一些 PEP),讨论将 pyproject.toml 嵌入到脚本中似乎还为时过早,脚本绝对不打算以这种方式构建和分发。

因此,结论是(在某些情况下明确陈述,在其他情况下不那么明确)本提案旨在嵌入部分 pyproject.toml。通常是来自 PEP 621[project] 部分,甚至是该部分中的 dependencies 项。

此时,第一个问题是,将提案框架化为“嵌入 pyproject.toml”,我们将鼓励前面段落讨论的混淆——开发人员将期望 pyproject.toml 的全部功能,并且在存在差异和限制时感到困惑。因此,最好将此建议视为使用嵌入式 TOML 格式的建议,但专门重用了 pyproject.toml 特定部分的结构。问题就变成了我们如何描述该结构,而不引起熟悉 pyproject.toml 的人的混淆。如果我们引用 pyproject.toml 进行描述,那么链接仍然存在。但如果我们单独描述它,人们会因该结构“相似但不同”的性质而感到困惑。

同样重要的是要记住,此提案的目标受众是仅将 Python 用作“更好的批处理文件”解决方案的开发人员。这些开发人员通常不熟悉 Python 打包及其约定,并且常常是对打包解决方案的“复杂性”和“难度”最持批评态度的人。因此,基于现有解决方案的提案很可能不受该受众欢迎,并且很容易导致人们继续使用现有的临时解决方案,而忽略了旨在让他们生活更轻松的标准。

为何不从 import 语句推断需求?

该想法是自动识别源文件中的 import 语句,并将它们转换为需求列表。

然而,这由于几个原因而不可行。首先,上面关于使语法易于解析的必要性的观点,对于所有 Python 版本,以及对于其他语言编写的工具,同样适用。

其次,PyPI 和其他符合 Simple Repository API 的包存储库不提供从导入的模块名解析包名的机制(另请参阅相关的讨论)。

第三,即使存储库提供了此信息,相同的导入名也可能对应 PyPI 上的多个包。有人可能会争辩说,只有当有多个项目提供相同的导入名时,才需要区分想要哪个包。然而,这很容易让任何人无意中或恶意地破坏正在工作的脚本,通过上传一个提供与现有项目相同导入名的包到 PyPI。另一种选择是,在候选者中,选择第一个在索引中注册的包,这在流行的包与现有的晦涩包具有相同导入名的情况下会令人困惑,如果现有包是恶意上传的,其导入名足够通用,很可能被重复使用,那么甚至是有害的。

一个相关的想法是将需求作为注释附加到 import 语句,而不是将它们收集在一个块中,其语法如下:

import numpy as np # requires: numpy
import rich # requires: rich

这仍然存在解析困难。此外,在多行导入的情况下,注释的位置是模棱两可的,并且可能看起来很丑。

from PyQt5.QtWidgets import (
    QCheckBox, QComboBox, QDialog, QDialogButtonBox,
    QGridLayout, QLabel, QSpinBox, QTextEdit
) # requires: PyQt5

此外,这种语法在所有情况下都不能如直观预期那样运行。考虑

import platform
if platform.system() == "Windows":
    import pywin32 # requires: pywin32

在这里,用户的意图是该包仅在 Windows 上需要,但脚本运行器无法理解这一点(正确的方法是 requires: pywin32 ; sys_platform == 'win32')。

(感谢 Jean Abou-Samra 对此点的清晰讨论)

为何不直接在运行时管理环境?

运行带有依赖项的脚本的另一种方法是简单地在运行时管理这些依赖项。这可以通过使用使包可用的库来完成。有许多选项可以实现此类库,例如直接将它们安装到用户的环境中,或通过修改 sys.path 使它们可以从本地缓存中获得。

这些方法与此 PEP 并不冲突。API,如

env_mgr.install("rich")
env_mgr.install("click")

import rich
import click

...

当然是可行的。然而,这样的库可以在不需要任何新标准的情况下编写,据 PEP 作者所知,这种情况尚未发生。这表明这种方法不像最初看起来那么有吸引力。还有使 env_mgr 库首先可用的引导问题。最后,这种方法实际上并没有提供任何互操作性优势,因为它不使用标准形式的依赖项列表,因此其他工具无法访问数据。

无论如何,这样的库仍然可以从此 PEP 中受益,因为它可以包含读取脚本依赖项块中要安装的包的 API。这将提供相同的功能,同时允许与其他支持此规范的工具进行互操作。

# Script Dependencies:
#     rich
#     click
env_mgr.install_dependencies(__file__)

import rich
import click

...

为何不直接设置一个带有 pyproject.toml 的 Python 项目?

同样,一个关键问题是此提案的目标受众是编写不打算分发的脚本的人。有时脚本会被“共享”,但这比“分发”要不正式得多——通常涉及通过带有如何运行它的书面说明的电子邮件发送脚本,或将链接传递给 gist。

期望这些用户学习 Python 打包的复杂性是一个重大的复杂性提升,几乎肯定会给人一种“Python 对脚本来说太难了”的印象。

此外,如果这里的期望是 pyproject.toml 将以某种方式设计用于就地运行脚本,那么这是标准的一项新功能,目前尚不存在。至少,在关于使用 pyproject.toml 处理不打算分发为 wheels 的项目的当前 Discourse 讨论得到解决(这可能需要一些 PEP),否则这不是一个合理的建议。即使如此,它也未能解决“在 gist 或电子邮件中发送脚本”的用例。

为何不使用 requirements 文件来管理依赖项?

将您的需求放在 requirements 文件中,不需要 PEP。您可以立即这样做,实际上很多临时解决方案可能就是这样做的。然而,没有标准,就无法知道如何定位脚本的依赖项数据。此外,requirements 文件格式是 pip 特有的,因此依赖它的工具依赖于 pip 的实现细节。

因此,为了建立一个标准,需要两件事

  1. 标准化 requirements 文件格式的替代方案。
  2. 关于如何定位给定脚本的 requirements 文件的标准。

第一项是一项重大工程。它已经多次讨论过,但到目前为止还没有人尝试过实际去做。最可能的方法是为当前使用 requirements 文件解决的各个用例制定标准。一种选择是此 PEP 简单地定义一种新文件格式,该格式只是一个包含 PEP 508 要求的文本文件,每行一个。这样就只剩下如何定位该文件的问题了。

这里的“明显”解决方案是做类似的事情,将文件名命名为与脚本相同,但带有 .reqs 扩展名(或类似的)。然而,这仍然需要两个文件,而目前只需要一个文件,因此不符合“更好的批处理文件”模型(shell 脚本和批处理文件通常是独立的)。这要求开发人员记住将这两个文件放在一起,而这并不总是可能的。例如,系统管理策略可能要求某个目录中的所有文件都必须可执行(例如,Linux 文件系统标准要求 /usr/bin)。而一些共享脚本的方法(例如,将其发布在像 Github gist 这样的文本文件共享服务上,或企业内部网)可能不允许从脚本的位置推导出关联的 requirements 文件的位置(像 pipx 这样的工具支持直接从 URL 运行脚本,因此“下载并解压脚本及其依赖项的 zip 包”可能不是一个适用的要求)。

但本质上,这里的问题是明确规定的要求是该格式支持将依赖项数据存储在脚本文件本身中。不这样做(不存储在脚本中)的解决方案只是忽略了这一要求。

脚本是否应该能够指定包索引?

依赖项元数据是关于代码依赖于哪个包,而不是从哪里获取该包。脚本的元数据与分发包的元数据(在 pyproject.toml 中定义)之间没有区别。在这两种情况下,依赖项都以“抽象”形式给出,而没有指定如何获取它们。

当然,一些使用依赖项信息的工具可能需要定位具体的依赖项工件——例如,如果它们期望创建一个包含这些依赖项的环境。但它们选择如何做到这一点将与工具的 UI 紧密相关,本 PEP 不试图规定工具的 UI。

关于这一点,以及特别是 pip-run 工具所做的 UI 选择,在之前提到的 pip-run 问题中有更多讨论。

本地依赖项怎么办?

这些可以通过添加依赖项的位置到 sys.path 来处理,而无需特殊的元数据和工具。此 PEP 对于这种情况根本不需要。另一方面,如果“本地依赖项”是实际已本地发布的发行版,则它们可以像通常一样用 PEP 508 要求指定,并通过在运行工具时使用工具的 UI 来指定本地包索引。

未解决的问题

此时无。


来源:https://github.com/python/peps/blob/main/peps/pep-0722.rst

最后修改:2025-07-08 12:01:15 GMT