版本控制¶
本讨论涵盖了 Python 包版本控制的所有方面。
有效版本号¶
不同的 Python 项目可能会根据特定项目的需求使用不同的版本控制方案,但为了与 pip 等工具兼容,所有项目都必须遵守一种灵活的版本标识符格式,其权威参考是版本说明符规范。以下是一些版本号示例[1]
一个简单版本(最终发布):
1.2.0一个开发版本:
1.2.0.dev1一个 Alpha 版本:
1.2.0a1一个 Beta 版本:
1.2.0b1一个候选发布版本:
1.2.0rc1一个后发布版本:
1.2.0.post1一个 Alpha 版本的后发布版本(可能,但不推荐):
1.2.0a1.post1一个只有两个组件的简单版本:
23.12一个只有一个组件的简单版本:
42一个带纪元的版本:
1!1.0
项目可以使用预发布周期来支持用户在最终发布之前进行测试。按顺序,步骤是:Alpha 发布、Beta 发布、候选发布、最终发布。Pip 和其他现代 Python 包安装程序在决定安装哪个版本的依赖项时,默认会忽略预发布版本,除非明确请求(例如,使用 pip install pkg==1.1a3 或 pip install --pre pkg)。
开发版本的目的是支持在开发周期早期发布的版本,例如,每日构建或 Linux 发行版中最新源的构建。
后发布版本用于解决最终版本中不影响已分发软件的微小错误,例如纠正发布说明中的错误。它们不应用于错误修复;这些应该通过新的最终发布来完成(例如,在使用语义版本控制时递增第三个组件)。
最后,纪元是一个很少使用的功能,用于在更改版本控制方案时修复排序顺序。例如,如果一个项目使用日历版本控制,版本号如 23.12,并切换到语义版本控制,版本号如 1.0,则 1.0 和 23.12 之间的比较将出错。为了纠正这一点,新的版本号应该有一个明确的纪元,如“1!1.0”,以便被视为比旧版本号更新。
语义版本控制 vs. 日历版本控制¶
版本控制方案是一种形式化的方法,用于解释版本号的各个部分,并决定包新发布的下一个版本号。Python 包通常使用两种版本控制方案:语义版本控制和日历版本控制。
注意
选择哪个版本号由项目维护者决定。这实际上意味着版本更新反映了维护者的观点。该观点可能与最终用户对所述形式化版本控制方案所承诺的理解有所不同。
选择下一个版本号时存在已知的例外。维护者可能会有意识地选择打破最后一个版本段仅包含向后兼容更改的假设。其中一个案例是需要解决安全漏洞。安全发布通常以补丁版本形式出现,但不可避免地包含破坏性更改。
语义版本控制¶
语义版本控制(或 SemVer)的思想是使用三部分版本号,即 主版本号.次版本号.补丁版本号,项目作者在以下情况下递增:
主版本号:当他们进行不兼容的 API 更改时,
次版本号:当他们以向后兼容的方式添加功能时,以及
补丁版本号:当他们进行向后兼容的错误修复时。
大多数 Python 项目都使用类似于语义版本控制的方案。然而,大多数项目,尤其是大型项目,并没有严格遵守语义版本控制,因为许多更改在技术上是破坏性更改,但只影响一小部分用户。这些项目倾向于在不兼容性高或表示项目发生转变时递增主版本号,而不是因为任何微小的不兼容性[2]。相反,有时主版本号的递增用于表示重大但向后兼容的新功能。
对于那些确实使用严格语义版本控制的项目,此方法允许用户使用带有 ~= 运算符的兼容发布版本说明符。例如,name ~= X.Y 大致等同于 name >= X.Y, == X.*,即它要求至少发布 X.Y,并允许任何具有更大 Y 的后续发布,只要 X 相同。同样,name ~= X.Y.Z 大致等同于 name >= X.Y.Z, == X.Y.*,即它要求至少发布 X.Y.Z,并允许具有相同 X 和 Y 但更高 Z 的后续发布。
采用语义版本控制的 Python 项目应遵守 语义版本控制 2.0.0 规范的第 1-8 条。
流行的 Sphinx 文档生成器是使用严格语义版本控制的项目示例(Sphinx 版本控制策略)。著名的 NumPy 科学计算包明确使用“宽松”语义版本控制,其中递增次要版本的发布可能包含向后不兼容的 API 更改(NumPy 版本控制策略)。
日历版本控制¶
语义版本控制并非适用于所有项目,例如那些具有定期基于时间的发布节奏和在功能移除前提供多次发布警告的弃用过程的项目。
基于日期的版本控制或日历版本控制(CalVer)的一个主要优点是,只需版本号即可直接判断特定版本的基线功能集有多旧。
日历版本号通常采用 年.月 的形式(例如,2023 年 12 月为 23.12)。
Pip,标准的 Python 包安装程序,使用日历版本控制。
其他方案¶
序列版本控制是指最简单的版本控制方案,它由一个每次发布递增的数字组成。虽然序列版本控制作为开发者非常容易管理,但作为最终用户却最难跟踪,因为序列版本号几乎不提供关于 API 向后兼容性的信息。
上述方案的组合是可能的。例如,一个项目可以将基于日期的版本控制与序列版本控制结合起来,创建一种 年.序列 编号方案,该方案可以很容易地传达发布的大致年龄,但不会承诺一年内特定的发布节奏。
本地版本标识符¶
公共版本标识符旨在支持通过 PyPI 进行分发。Python 打包工具还支持本地版本标识符的概念,该标识符可用于标识不打算发布的本地开发版本,或由重新分发者维护的已发布版本的修改变体。
本地版本标识符的形式为公共版本标识符,后跟“+”和一个本地版本标签。例如,应用了 Fedora 特定补丁的包的版本可以是“1.2.1+fedora.4”。另一个例子是 setuptools-scm 计算的版本,setuptools-scm 是一个从 Git 数据读取版本的 setuptools 插件。在一个自最新发布以来有一些提交的 Git 仓库中,setuptools-scm 会生成一个像“0.5.dev1+gd00980f”这样的版本,如果仓库有未跟踪的更改,则会生成像“0.5.dev1+gd00980f.d20231217”这样的版本。
在运行时访问版本信息¶
当前环境中所有本地可用的分发包的版本信息可以在运行时使用标准库的 importlib.metadata.version() 函数获取
>>> importlib.metadata.version("cryptography")
'41.0.7'
许多项目还选择通过提供包级 __version__ 属性来版本化其顶级导入包
>>> import cryptography
>>> cryptography.__version__
'41.0.7'
这种技术对于希望确保版本查询调用(例如 pip -V)尽快运行的 CLI 应用程序来说特别有价值。
希望确保其报告的分发包和导入包版本彼此一致的包发布者可以查阅单源项目版本的讨论,了解实现这一目标的潜在方法。
由于导入包和模块不要求以这种方式发布运行时版本信息(参见PEP 396 中已撤回的提案),因此 __version__ 属性应该要么只通过已知提供它的接口进行查询(例如项目查询其自己的版本或其直接依赖项的版本),要么查询代码应该设计成能够处理该属性缺失的情况[3]。
有些项目可能需要发布外部 API 的版本信息,而不是模块本身的版本。此类项目应定义自己的项目特定方法,以在运行时获取相关信息。例如,标准库的 ssl 模块提供了多种方式来访问底层 OpenSSL 库版本
>>> ssl.OPENSSL_VERSION
'OpenSSL 3.2.2 4 Jun 2024'
>>> ssl.OPENSSL_VERSION_INFO
(3, 2, 0, 2, 0)
>>> hex(ssl.OPENSSL_VERSION_NUMBER)
'0x30200020'