PEP 396 – 模块版本号
- 作者:
- Barry Warsaw <barry at python.org>
- 状态:
- 已撤回
- 类型:
- 信息性
- 主题:
- 打包
- 创建日期:
- 2011年3月16日
- 发布历史:
- 2011年4月5日
摘要
鉴于为 Python 模块指定版本号既有用又常见,并且考虑到社区中已自然衍生出多种方法来执行此操作,因此有必要建立模块作者应遵循和引用的标准约定。本信息性 PEP 描述了希望定义其 Python 模块版本号的模块作者的最佳实践。
遵循此 PEP 是可选的;但是,其他 Python 工具(如 distutils2 [1])可能会改编以使用此处定义的约定。
PEP 拒绝/撤回
此 PEP 于 2021 年 4 月 14 日被正式拒绝。自本 PEP 初次撰写以来,打包生态系统已发生显著变化,并且 importlib.metadata.version() [11] 等 API 提供了更好的体验。
此拒绝于 2024 年 10 月 21 日被重新归类为撤回,因为之前的状态被误解 [12],暗示 *不应* 有任何模块定义 __version__ 属性,但这绝非事实。
模块仍可根据其选择定义 __version__。但是,选择 *不* 这样做不会妨碍查找已安装分发包的版本信息,因此信息性 PEP 不是记录有关模块 __version__ 属性使用的社区惯例的正确工具(这些惯例最好在 Python Packaging User Guide 中涵盖)。
用户故事
Alice 正在编写一个名为 alice 的新模块,她希望与其他 Python 开发人员共享。 alice 是一个简单的模块,位于一个文件中 alice.py。Alice 希望指定一个版本号,以便她的用户可以知道他们正在使用哪个版本。由于她的模块完全位于一个文件中,因此她希望将版本号添加到该文件中。
Bob 编写了一个名为 bob 的模块,并与许多用户共享。 bob.py 包含一个版本号,以方便用户。Bob 了解 Cheeseshop [2],并使用经典的 distutils 添加了一些简单的打包,以便他可以将 *The Bob Bundle* 上传到 Cheeseshop。由于 bob.py 已经指定了一个用户可以以编程方式访问的版本号,因此他希望即使他的用户现在从 Cheeseshop 获取该版本号,相同的 API 也能继续工作。
Carol 维护着几个命名空间包,每个包都经过独立开发和分发。为了让她的用户能够正确指定对其包正确版本的依赖项,她在命名空间包的 setup.py 文件中指定了版本号。由于 Carol 希望每个包只更新一个版本号,因此她在模块中指定版本号,并在构建 *sdist* 存档时由 setup.py 提取模块版本号。
David 维护着标准库中的一个包,并且还为其他 Python 版本制作独立版本。标准库副本在模块中定义了版本号,并且相同的版本号也用于独立分发。
基本原理
Python 模块(包括标准库和第三方提供的模块)长期以来一直包含版本号。有描述版本号的既定事实标准,多年来也自然衍生出许多临时的做法。通常,可以通过导入模块并检查属性来以编程方式检索模块的版本号。经典的 Python distutils setup() 函数 [3] 描述了一个 version 参数,可以在其中指定发行版的版本号。 PEP 8 描述了使用名为 __version__ 的模块属性来记录使用关键字扩展的“Subversion, CVS, 或 RCS”版本字符串。在 PEP 作者自己的电子邮件存档中,独立模块开发者使用 __version__ 模块属性的最早例子可以追溯到 1995 年。
版本信息的另一个例子是 sqlite3 [5] 模块,它具有 sqlite_version_info、version 和 version_info 属性。可能不清楚哪个属性包含模块的版本号,哪个属性包含底层 SQLite3 库的版本号。
本信息性 PEP 规范了既定实践,并推荐了描述模块版本号的标准方法,以及包含或 *不* 包含它们的某些用例。模块作者是否采用纯属自愿;标准库中的打包工具将为本文档定义的标准提供可选支持,Python 宇宙中的其他工具也可能遵守。
规范
- 一般来说,标准库中的模块 *不应* 包含版本号。它们隐式承载其包含的 Python 发行版的版本号。
- 根据具体情况,同时以独立形式为其他 Python 版本发布的标准库模块,在包含在标准库中时 *可以* 包含模块版本号,并且在单独打包时 *应* 包含版本号。
- 当模块(或包)包含版本号时,版本号 *应* 可在
__version__属性中访问。 - 对于位于命名空间包内的模块,模块 *应* 包含
__version__属性。命名空间包本身 *不应* 包含其自己的__version__属性。 __version__属性的值 *应* 为字符串。- 模块版本号 *应* 符合 PEP 386 中指定的规范化版本格式。
- 模块版本号 *不应* 包含版本控制系统提供的修订号,或任何其他语义上不同的版本号(例如底层库的版本号)。
- 经典 distutils
setup.py文件中的version属性,或 PEP 345Version元数据字段 *应* 源自__version__字段,反之亦然。
示例
从第三方包检索版本号
>>> import bzrlib
>>> bzrlib.__version__
'2.3.0'
从也作为独立模块分发的标准库包中检索版本号
>>> import email
>>> email.__version__
'5.1.0'
命名空间包的版本号
>>> import flufl.i18n
>>> import flufl.enum
>>> import flufl.lock
>>> print flufl.i18n.__version__
1.0.4
>>> print flufl.enum.__version__
3.1
>>> print flufl.lock.__version__
2.1
>>> import flufl
>>> flufl.__version__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute '__version__'
>>>
推导
模块版本号可以出现在至少两个地方,有时甚至更多。例如,根据本 PEP,它们可以通过模块的 __version__ 属性以编程方式访问。在经典的 distutils setup.py 文件中,setup() 函数接受一个 version 参数,而 distutils2 setup.cfg 文件有一个 version 键。版本号还必须进入 PEP 345 元数据,最好是在构建 *sdist* 存档时。模块作者最好只指定一次版本号,并让所有其他用法都源自此单一定义。
这可以通过多种方式完成,其中一些方式概述如下。这些仅供说明之用,并非旨在具有决定性、完整性或包容性。其他方法也是可能的,并且下面包含的一些方法可能存在限制,导致它们在某些情况下无法使用。
假设 Elle 将此属性添加到她的模块文件 elle.py 中
__version__ = '3.1.1'
经典 distutils
在经典 distutils 中,将版本字符串添加到 setup.py 中 setup() 函数的最简单方法是这样做:
from elle import __version__
setup(name='elle', version=__version__)
然而,根据 PEP 作者的经验,这在某些情况下可能会失败,例如当模块通过 2to3 程序使用自动 Python 3 转换时(因为 setup.py 在 elle 模块转换之前由 Python 3 执行)。
在这种情况下,编写一些代码来解析文件中的 __version__ 而不是导入它,难度并不大。不提供太多细节,像 distutils2 这样的模块很可能会提供一种从文件中解析版本字符串的方法。例如:
from distutils2 import get_version
setup(name='elle', version=get_version('elle.py'))
Distutils2
由于 distutils2 风格的 setup.cfg 是声明式的,我们无法运行任何代码来提取 __version__ 属性,无论是通过导入还是通过解析。
与 distutils-sig [9] 协商后,提出了两个选项。两者都包含将版本号保存在一个文件中,并在 setup.cfg 中声明该文件。当文件内容包含版本号时,将使用 version-file 键。
[metadata]
version-file: version.txt
当版本号包含在更大的文件中时,例如 Python 代码中,因此必须解析该文件以提取版本,将使用键 version-from-file。
[metadata]
version-from-file: elle.py
将对冒号后面的文件名执行类似上述的解析方法。具体的实现方法将在相应的 distutils2 开发论坛中讨论。
另一种选择是仅在 setup.cfg 中定义版本号,并使用 pkgutil 模块 [8] 以编程方式提供它。例如,在 elle.py 中:
from distutils2._backport import pkgutil
__version__ = pkgutil.get_distribution('elle').metadata['version']
PEP 376 元数据
PEP 376 定义了静态元数据的标准,但没有描述创建此元数据的过程。最好在构建时而不是安装时将派生的版本信息放入 PEP 376 .dist-info 元数据中。这样,即使代码未安装,元数据也可以用于内省。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0396.rst