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

Python 增强提案

PEP 386 – 更改 Distutils 中的版本比较模块

作者:
Tarek Ziadé <tarek at ziade.org>
状态:
已取代
类型:
标准跟踪
主题:
打包
创建日期:
2009年6月4日
取代者:
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)

这意味着该分发需要版本大于 3.5.0zope.interface

这也意味着 Python 项目需要遵循与用于安装它们的工具相同的约定,以便它们能够比较版本。

这就是为什么本 PEP 为了互操作性,提议了一个标准的方案来表达版本信息及其比较语义。

此外,这将使操作系统打包者在重新打包符合标准的分发时更容易工作,因为目前很难决定两个分发版本如何比较。

前提条件和当前状态

本 PEP 的范围不包括提供一个通用的版本控制方案,旨在支持所有甚至大多数现有版本控制方案。总会有相互竞争的语法,无论是受发行版或项目策略的强制要求,还是由于我们无法期望改变的历史原因。

建议的方案应能表达常见的版本控制语义,因此可以解析任何替代的版本控制方案并将其转换为符合标准的方案。这就是操作系统打包者通常处理现有版本方案的方式,并且是比支持任意一组版本控制方案更可取的替代方案。

符合常规实践和约定,以及简洁性都是优点,可以促进无摩擦的采用和无痛的过渡。实用性有时胜过纯粹性。

项目有非常不同的版本控制需求,但以下被广泛认为是重要的语义

  1. 应该能够表达多个版本级别(通常表示为主修订版和次修订版,有时也表示微修订版)。
  2. 大量项目需要“预发布”版本的特殊含义(例如“alpha”、“beta”、“rc”),并且这些版本有广泛使用的别名(“a”代表“alpha”,“b”代表“beta”,“c”代表“rc”)。这些预发布版本使得无法使用版本字符串组件的简单字母数字排序。(示例:3.1a1 < 3.1)
  3. 一些项目还需要常规版本的“后发布”,主要用于安装程序工作,否则无法清楚表达。
  4. 开发版本允许未发布作品的打包者避免与后续常规发布发生版本冲突。

对于那些想要进一步并使用工具来管理版本号的人来说,主要的两个是

  • 当前的 Distutils 系统 [1]
  • Setuptools [2]

Distutils

Distutils 目前提供了 StrictVersionLooseVersion 类,可用于管理版本。

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”)用于 RPM 的问题:这意味着任何尝试切换到更典型的“主.次…”版本方案都是有问题的,因为它总是排序小于“20090421”。

最后,- 的含义特定于 Setuptools,而在 Debian 或 Ubuntu 使用的一些打包系统中则避免使用。

新版本算法

在 Pycon 期间,Python、Ubuntu 和 Fedora 社区的成员致力于制定一个所有人都可以接受的版本标准。

它目前被称为 verlib,其原型位于 [9]

支持的伪格式是

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 自身的版本方案兼容。rcc 之后排序

>>> 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 (初稿)
  • 未发布.非官方开发
  • 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

最后修改: 2024-12-15 20:57:19 GMT