PEP 386 – 更改 Distutils 中的版本比较模块
- 作者:
- Tarek Ziadé <tarek at ziade.org>
- 状态:
- 已取代
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建:
- 2009-06-04
- 已取代:
- 440
摘要
注意:此 PEP 已被 PEP 440 中定义的版本标识和依赖项规范方案所取代。
此 PEP 提出了一种新的版本比较方案系统,用于 Distutils。
动机
在 Python 中,关于项目如何管理其版本以及如何递增版本,目前还没有真正的限制。
Distutils 提供了一个 version
分发元数据字段,但它是自由格式的,当前用户(如 PyPI)通常将推送的最新版本视为 latest
版本,而不管预期的语义如何。
Distutils 很快就会扩展其功能,允许分发通过 Requires-Dist
元数据字段(参见 PEP 345)表达对其他分发的依赖关系,并且它将可以选择允许使用该字段将依赖关系限制在一组兼容版本中。请注意,此字段正在替换 Requires
,后者表示对模块和包的依赖关系。
Requires-Dist
字段将允许分发定义对另一个包的依赖关系,并可以选择将此依赖关系限制在一组兼容版本中,因此可以编写
Requires-Dist: zope.interface (>3.5.0)
这意味着分发需要 zope.interface
版本大于 3.5.0
。
这也意味着 Python 项目需要遵循与用于安装它们的工具相同的约定,以便它们能够比较版本。
这就是为什么此 PEP 为了互操作性,提出了一种标准方案来表达版本信息及其比较语义。
此外,这将使 OS 打包人员在重新打包符合标准的分发时更容易,因为截至目前,很难确定两个分发版本如何比较。
先决条件和当前状态
此 PEP 的范围不包括提供一个通用的版本控制方案,该方案旨在支持所有或甚至大多数现有的版本控制方案。总会有竞争的语法,要么由发行版或项目策略强制执行,要么出于我们无法期望改变的历史原因。
建议的方案应该能够表达通常的版本控制语义,因此可以解析任何替代的版本控制方案并将其转换为符合标准的方案。这就是 OS 打包人员通常处理现有版本方案的方式,并且是比支持任意一组版本控制方案更好的替代方案。
符合通常的做法和惯例,以及简单性都是优点,可以促进无摩擦的采用和轻松的过渡。有时,实用性胜过纯洁性。
项目有非常不同的版本控制需求,但以下被广泛认为是重要的语义
- 应该能够表达多个版本控制级别(通常表示为主要和次要修订版,有时也包括微修订版)。
- 大量项目需要为“预发布版”(如“alpha”、“beta”、“rc”)使用特殊含义的版本,并且这些版本有广泛使用的别名(“a”代表“alpha”,“b”代表“beta”,“c”代表“rc”)。并且这些预发布版使得无法对版本字符串组件进行简单的字母数字排序。(示例:3.1a1 < 3.1)
- 一些项目还需要常规版本的“发布后版本”,主要用于安装程序工作,否则无法明确表达。
- 开发版本允许未发布工作的打包人员避免与以后的常规版本发生版本冲突。
对于想要更进一步并使用工具来管理版本号的人员,主要的两个工具是
Distutils
Distutils 目前提供了一个 StrictVersion
类和一个 LooseVersion
类,可用于管理版本。
LooseVersion
类非常宽松。来自 Distutils 文档
Version numbering for anarchists and software realists.
Implements the standard interface for version number classes as
described above. A version number consists of a series of numbers,
separated by either periods or strings of letters. When comparing
version numbers, the numeric components will be compared
numerically, and the alphabetic components lexically. The following
are all valid version numbers, in no particular order:
1.5.1
1.5.2b2
161
3.10a
8.02
3.4j
1996.07.12
3.2.pl0
3.1.1.6
2g6
11g
0.960923
2.2beta29
1.13++
5.5.kw
2.0b1pl0
In fact, there is no such thing as an invalid version number under
this scheme; the rules for comparison are simple and predictable,
but may not always give the results you want (for some definition
of "want").
此类使任何版本字符串都有效,并提供了一种算法来先按数字后按字典顺序对它们进行排序。这意味着任何内容都可以用于对项目进行版本控制
>>> from distutils.version import LooseVersion as V
>>> v1 = V('FunkyVersion')
>>> v2 = V('GroovieVersion')
>>> v1 > v2
False
问题在于,虽然它允许表达任何嵌套级别,但它不允许为版本赋予特殊含义(预发布版和发布后版本以及开发版本),如先决条件 2、3 和 4 中所述。
StrictVersion
类更严格。来自文档
Version numbering for meticulous retentive and software idealists.
Implements the standard interface for version number classes as
described above. A version number consists of two or three
dot-separated numeric components, with an optional "pre-release" tag
on the end. The pre-release tag consists of the letter 'a' or 'b'
followed by a number. If the numeric components of two version
numbers are equal, then one with a pre-release tag will always
be deemed earlier (lesser) than one without.
The following are valid version numbers (shown in the order that
would be obtained by sorting according to the supplied cmp function):
0.4 0.4.0 (these two are equivalent)
0.4.1
0.5a1
0.5b3
0.5
0.9.6
1.0
1.0.4a3
1.0.4b1
1.0.4
The following are examples of invalid version numbers:
1
2.7.2.2
1.3.a4
1.3pl1
1.3c4
此类强制执行一些规则,并成为一个不错的工具来处理版本号
>>> from distutils.version import StrictVersion as V
>>> v2 = V('GroovieVersion')
Traceback (most recent call last):
...
ValueError: invalid version number 'GroovieVersion'
>>> v2 = V('1.1')
>>> v3 = V('1.3')
>>> v2 < v3
True
它添加了预发布版本和一些结构,但缺少一些使其可用的语义元素,例如开发版本或发布后标记,如先决条件 3 和 4 中所述。
此外,请注意,Distutils 版本类已经存在多年,但在社区中并没有真正使用。
Setuptools
Setuptools 提供了另一个版本比较工具 [3],它不强制执行任何版本规则,但尝试提供一个更好的算法将字符串转换为可排序的键,并使用 parse_version
函数。
来自文档
Convert a version string to a chronologically-sortable key
This is a rough cross between Distutils' StrictVersion and LooseVersion;
if you give it versions that would work with StrictVersion, then it behaves
the same; otherwise it acts like a slightly-smarter LooseVersion. It is
*possible* to create pathological version coding schemes that will fool
this parser, but they should be very rare in practice.
The returned value will be a tuple of strings. Numeric portions of the
version are padded to 8 digits so they will compare numerically, but
without relying on how numbers compare relative to strings. Dots are
dropped, but dashes are retained. Trailing zeros between alpha segments
or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
"2.4". Alphanumeric parts are lower-cased.
The algorithm assumes that strings like "-" and any alpha string that
alphabetically follows "final" represents a "patch level". So, "2.4-1"
is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
considered newer than "2.4-1", which in turn is newer than "2.4".
Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
come before "final" alphabetically) are assumed to be pre-release versions,
so that the version "2.4" is considered newer than "2.4a1".
Finally, to handle miscellaneous cases, the strings "pre", "preview", and
"rc" are treated as if they were "c", i.e. as though they were release
candidates, and therefore are not as new as a version string that does not
contain them, and "dev" is replaced with an '@' so that it sorts lower
than any other pre-release tag.
换句话说,parse_version
将为每个版本字符串返回一个元组,该元组与 StrictVersion
兼容,但也可以接受任意版本并处理它们,以便可以比较它们
>>> from pkg_resources import parse_version as V
>>> V('1.2')
('00000001', '00000002', '*final')
>>> V('1.2b2')
('00000001', '00000002', '*b', '00000002', '*final')
>>> V('FunkyVersion')
('*funkyversion', '*final')
在此方案中,实用性优先于纯洁性,但结果是它不强制执行任何策略,并且由于缺乏明确的标准而导致非常复杂的语义。它只是尝试适应广泛使用的约定。
现有系统的警告
所描述的版本比较工具的主要问题是它们过于宽松,同时又无法表达某些所需的语义。PyPI 上的许多版本 [4] 显然不是有用的版本,这使得用户难以理解特定包使用的版本控制并在 PyPI 之上提供工具。
Distutils 类在 Python 项目中并没有真正使用,但 Setuptools 函数却非常广泛,因为它被 easy_install
[6]、pip
[5] 或 zc.buildout
[7] 等工具用于安装给定项目的依赖项。
虽然 Setuptools *确实* 提供了一种比较/排序版本的机制,但如果版本规范允许人类在不运行某些代码的情况下对排序进行合理的尝试,则会更好。
此外,在“主要”版本号(例如版本字符串“20090421”)中使用日期存在问题:这意味着任何尝试切换到更典型的“major.minor…”版本方案都会出现问题,因为它始终排序小于“20090421”。
最后,-
的含义特定于 Setuptools,而在 Debian 或 Ubuntu 等某些打包系统中避免使用它。
新的版本控制算法
在 Pycon 期间,Python、Ubuntu 和 Fedora 社区的成员共同努力制定了一个对每个人都可接受的版本标准。
它目前称为 verlib
,原型位于 [10]。
支持的伪格式为
N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]
真正的正则表达式为
expr = r"""^
(?P<version>\d+\.\d+) # minimum 'N.N'
(?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
(?:
(?P<prerel>[abc]|rc) # 'a' = alpha, 'b' = beta
# 'c' or 'rc' = release candidate
(?P<prerelversion>\d+(?:\.\d+)*)
)?
(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
$"""
一些示例可能使其更清晰
>>> from verlib import NormalizedVersion as V
>>> (V('1.0a1')
... < V('1.0a2.dev456')
... < V('1.0a2')
... < V('1.0a2.1.dev456')
... < V('1.0a2.1')
... < V('1.0b1.dev456')
... < V('1.0b2')
... < V('1.0b2.post345')
... < V('1.0c1.dev456')
... < V('1.0c1')
... < V('1.0.dev456')
... < V('1.0')
... < V('1.0.post456.dev34')
... < V('1.0.post456'))
True
尾随的 .dev123
用于预发布版。.post123
用于发布后版本——显然,许多项目都在使用此版本(例如 Twisted [8])。例如,在 1.2.0
发布 *之后*,可能会有 1.2.0-r678
发布。我们使用 post
而不是 r
,因为 r
在指示预发布版还是发布后版本方面存在歧义。
.post456.dev34
表示发布后版本的开发标记,排序在 .post456
标记之前。这可以用于进行发布后版本的开发版本。
预发布版可以使用 a
表示“alpha”、b
表示“beta”和 c
表示“候选发布版”。rc
是“候选发布版”的另一种表示法,它被添加进来是为了使版本方案与 Python 自身的版本方案兼容。rc
排在 c
之后
>>> from verlib import NormalizedVersion as V
>>> (V('1.0a1')
... < V('1.0a2')
... < V('1.0b3')
... < V('1.0c1')
... < V('1.0rc2')
... < V('1.0'))
True
请注意,c
是第三方项目的首选标记。
verlib
提供了一个 NormalizedVersion
类和一个 suggest_normalized_version
函数。
NormalizedVersion
NormalizedVersion
类用于保存版本并将其与其他版本进行比较。它将字符串作为参数,该字符串包含版本的表示形式
>>> from verlib import NormalizedVersion
>>> version = NormalizedVersion('1.0')
版本可以用字符串表示
>>> str(version)
'1.0'
或与其他版本进行比较
>>> NormalizedVersion('1.0') > NormalizedVersion('0.9')
True
>>> NormalizedVersion('1.0') < NormalizedVersion('1.1')
True
如果您想通过提供构成版本的各个部分来创建实例,可以使用名为from_parts
的类方法。
示例
>>> version = NormalizedVersion.from_parts((1, 0))
>>> str(version)
'1.0'
>>> version = NormalizedVersion.from_parts((1, 0), ('c', 4))
>>> str(version)
'1.0c4'
>>> version = NormalizedVersion.from_parts((1, 0), ('c', 4), ('dev', 34))
>>> str(version)
'1.0c4.dev34'
suggest_normalized_version
suggest_normalized_version
是一个函数,它建议一个接近给定版本字符串的标准化版本。如果您有一个未标准化的版本字符串(即 NormalizedVersion
不喜欢它),那么您可能可以通过此函数获得一个等效的(或接近的)标准化版本。
它根据目前在 PyPI 上使用的版本的观察结果,对给定的字符串进行了一些简单的标准化操作。
根据 2010 年 1 月 6 日的这些版本的转储,该函数在 PyPI 的 8821 个发行版中给出了这些结果
- 7822 个 (88.67%) 已经与
NormalizedVersion
匹配,无需任何更改 - 717 个 (8.13%) 在使用此建议方法时匹配
- 282 个 (3.20%) 完全不匹配。
3.20% 与 NormalizedVersion
不兼容且无法转换为兼容形式的项目,其中大部分是基于日期的版本方案、带有自定义标记的版本或虚拟版本。示例
- 概念验证
- 1 (第一个草稿)
- unreleased.unofficialdev
- 0.1.alphadev
- 2008-03-29_r219
- 等等。
当工具需要处理版本时,一种策略是在版本字符串上使用 suggest_normalized_version
。如果此函数返回 None
,则表示提供的版本与标准方案不符。如果它返回一个与原始版本略有不同的版本,则它是一个建议的标准化版本。最后,如果它返回相同的字符串,则表示该版本与方案匹配。
这是一个用法示例
>>> from verlib import suggest_normalized_version, NormalizedVersion
>>> import warnings
>>> def validate_version(version):
... rversion = suggest_normalized_version(version)
... if rversion is None:
... raise ValueError('Cannot work with "%s"' % version)
... if rversion != version:
... warnings.warn('"%s" is not a normalized version.\n'
... 'It has been transformed into "%s" '
... 'for interoperability.' % (version, rversion))
... return NormalizedVersion(rversion)
...
>>> validate_version('2.4-rc1')
__main__:8: UserWarning: "2.4-rc1" is not a normalized version.
It has been transformed into "2.4c1" for interoperability.
NormalizedVersion('2.4c1')
>>> validate_version('2.4c1')
NormalizedVersion('2.4c1')
>>> validate_version('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in validate_version
ValueError: Cannot work with "foo"
路线图
Distutils 将弃用其现有的版本类,转而使用 NormalizedVersion
。本 PEP 中介绍的 verlib
模块将重命名为 version
并放置到 distutils
包中。
参考文献
致谢
Trent Mick、Matthias Klose、Phillip Eby、David Lyon 以及 Pycon 和 Distutils-SIG 的许多人。
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0386.rst
上次修改时间:2023-09-09 17:39:29 GMT