18

我试图让 1.7.0 在 1.7.0.rc0 之后但在 1.8.0 之前得到它,如果你正在对版本进行排序,它应该是这样。我认为 LooseVersion 的全部意义在于它正确地处理了这类事情的排序和比较。

>>> from distutils.version import LooseVersion
>>> versions = ["1.7.0", "1.7.0.rc0", "1.8.0"]
>>> lv = [LooseVersion(v) for v in versions]
>>> sorted(lv, reverse=True)
[LooseVersion ('1.8.0'), LooseVersion ('1.7.0.rc0'), LooseVersion ('1.7.0')]
4

6 回答 6

21
>>> from distutils.version import LooseVersion
>>> versions = ["1.7.0", "1.7.0rc0", "1.11.0"]
>>> sorted(versions, key=LooseVersion)
['1.7.0', '1.7.0rc0', '1.11.0']

从文档

无政府主义者和软件现实主义者的版本编号。实现上述版本号类的标准接口。版本号由一系列数字组成,由句点或字母字符串分隔。比较版本号时,数字组件将按数字进行比较,字母组件将按词汇进行比较。
……
其实这个方案下不存在版本号无效的问题;比较规则简单且可预测,但可能并不总是给出您想要的结果(对于“想要”的某些定义)。

所以你会看到特别对待“rc”没有什么聪明之处

你可以看到版本号是如何分解成这样的

>>> LooseVersion('1.7.0rc0').version
[1, 7, 0, 'rc', 0]
于 2012-09-04T01:04:36.370 回答
15

主要编辑:旧答案太不合情理了。这里有两个更漂亮的解决方案。

所以,我目前看到了三种实现预期排序的方法,在实际发布之前发布候选“rc”。

  1. 我以前的命令式排序
  2. 使用“b”而不是“rc”,以便使用StrictVersion来自同一个包的 ,
  3. 扩展Version类以添加对任意标签和标签排序的支持

1. 旧的命令式排序

from distutils.version import LooseVersion
versions = ["1.7.0", "1.7.0.rc0", "1.8.0"]
lv = [LooseVersion(v) for v in versions]
lv.sort()

sorted_rc = [v.vstring for v in lv]

import re
p = re.compile('rc\\d+$')

i = 0

# skip the first RCs
while i + 1 < len(sorted_rc):
    m = p.search(sorted_rc[i])
    if m:
        i += 1
    else:
        break

while i + 1 < len(sorted_rc):
    tmp = sorted_rc[i]
    m = p.search(sorted_rc[i+1])
    if m and sorted_rc[i+1].startswith(tmp):
        sorted_rc[i] = sorted_rc[i+1]
        sorted_rc[i+1] = tmp
    i += 1

有了这个我得到:

['1.7.0rc0', '1.7.0', '1.11.0']

2. 用“b”代替“rc”

如果您被允许编写为或注明 alpha 或 beta 版本,则该包distutils.version还有另一个类StrictVersion可以完成这项工作。1.7.0.rc01.7.0a01.7.0b0

那是:

from distutils.version import StrictVersion
versions = ["1.7.0", "1.7.0b0", "1.11.0"]
sorted(versions, key=StrictVersion)

这给出了:

['1.7.0b0', '1.7.0', '1.11.0']

可以使用re模块完成从一种形式到另一种形式的翻译。

3.扩展Version类

上一个解决方案的明显问题是缺乏灵活性StrictVersion。更改version_re类属性以使用rc而不是aor b,即使它接受1.7.1rc0,仍将其打印为1.7.1r0(从 python 2.7.3 开始)。

我们可以通过实现我们自己的自定义版本类来做到这一点。这可以像这样完成,至少在某些情况下通过一些单元测试来确保正确性:

#!/usr/bin/python
# file: version2.py

from distutils import version
import re
import functools

@functools.total_ordering
class NumberedVersion(version.Version):
    """
    A more flexible implementation of distutils.version.StrictVersion

    This implementation allows to specify:
    - an arbitrary number of version numbers:
        not only '1.2.3' , but also '1.2.3.4.5'
    - the separator between version numbers:
        '1-2-3' is allowed when '-' is specified as separator
    - an arbitrary ordering of pre-release tags:
        1.1alpha3 < 1.1beta2 < 1.1rc1 < 1.1
        when ["alpha", "beta", "rc"] is specified as pre-release tag list
    """

    def __init__(self, vstring=None, sep='.', prerel_tags=('a', 'b')):
        version.Version.__init__(self) 
            # super() is better here, but Version is an old-style class

        self.sep = sep
        self.prerel_tags = dict(zip(prerel_tags, xrange(len(prerel_tags))))
        self.version_re = self._compile_pattern(sep, self.prerel_tags.keys())
        self.sep_re = re.compile(re.escape(sep))

        if vstring:
            self.parse(vstring)


    _re_prerel_tag = 'rel_tag'
    _re_prerel_num = 'tag_num'

    def _compile_pattern(self, sep, prerel_tags):
        sep = re.escape(sep)
        tags = '|'.join(re.escape(tag) for tag in prerel_tags)

        if tags:
            release_re = '(?:(?P<{tn}>{tags})(?P<{nn}>\d+))?'\
                .format(tags=tags, tn=self._re_prerel_tag, nn=self._re_prerel_num)
        else:
            release_re = ''

        return re.compile(r'^(\d+)(?:{sep}(\d+))*{rel}$'\
            .format(sep=sep, rel=release_re))

    def parse(self, vstring):
        m = self.version_re.match(vstring)
        if not m:
            raise ValueError("invalid version number '{}'".format(vstring))

        tag = m.group(self._re_prerel_tag)
        tag_num = m.group(self._re_prerel_num)

        if tag is not None and tag_num is not None:
            self.prerelease = (tag, int(tag_num))
            vnum_string = vstring[:-(len(tag) + len(tag_num))]
        else:
            self.prerelease = None
            vnum_string = vstring

        self.version = tuple(map(int, self.sep_re.split(vnum_string)))


    def __repr__(self):
        return "{cls} ('{vstring}', '{sep}', {prerel_tags})"\
            .format(cls=self.__class__.__name__, vstring=str(self),
                sep=self.sep, prerel_tags = list(self.prerel_tags.keys()))

    def __str__(self):
        s = self.sep.join(map(str,self.version))
        if self.prerelease:
            return s + "{}{}".format(*self.prerelease)
        else:
            return s

    def __lt__(self, other):
        """
        Fails when  the separator is not the same or when the pre-release tags
        are not the same or do not respect the same order.
        """
        # TODO deal with trailing zeroes: e.g. "1.2.0" == "1.2"
        if self.prerel_tags != other.prerel_tags or self.sep != other.sep:
            raise ValueError("Unable to compare: instances have different"
                " structures")

        if self.version == other.version and self.prerelease is not None and\
                other.prerelease is not None:

            tag_index = self.prerel_tags[self.prerelease[0]]
            other_index = self.prerel_tags[other.prerelease[0]]
            if tag_index == other_index:
                return self.prerelease[1] < other.prerelease[1]

            return tag_index < other_index

        elif self.version == other.version:
            return self.prerelease is not None and other.prerelease is None

        return self.version < other.version

    def __eq__(self, other):
        tag_index = self.prerel_tags[self.prerelease[0]]
        other_index = other.prerel_tags[other.prerelease[0]]
        return self.prerel_tags == other.prerel_tags and self.sep == other.sep\
            and self.version == other.version and tag_index == other_index and\
                self.prerelease[1] == other.prerelease[1]




import unittest

class TestNumberedVersion(unittest.TestCase):

    def setUp(self):
        self.v = NumberedVersion()

    def test_compile_pattern(self):
        p = self.v._compile_pattern('.', ['a', 'b'])
        tests = {'1.2.3': True, '1a0': True, '1': True, '1.2.3.4a5': True,
            'b': False, '1c0': False, ' 1': False, '': False}
        for test, result in tests.iteritems():
            self.assertEqual(result, p.match(test) is not None, \
                "test: {} result: {}".format(test, result))


    def test_parse(self):
        tests = {"1.2.3.4a5": ((1, 2, 3, 4), ('a', 5))}
        for test, result in tests.iteritems():
            self.v.parse(test)
            self.assertEqual(result, (self.v.version, self.v.prerelease))

    def test_str(self):
        tests = (('1.2.3',), ('10-2-42rc12', '-', ['rc']))
        for t in tests:
            self.assertEqual(t[0], str(NumberedVersion(*t)))

    def test_repr(self):
        v = NumberedVersion('1,2,3rc4', ',', ['lol', 'rc'])
        expected = "NumberedVersion ('1,2,3rc4', ',', ['lol', 'rc'])"
        self.assertEqual(expected, repr(v))


    def test_order(self):
        test = ["1.7.0", "1.7.0rc0", "1.11.0"]
        expected = ['1.7.0rc0', '1.7.0', '1.11.0']
        versions = [NumberedVersion(v, '.', ['rc']) for v in test]
        self.assertEqual(expected, list(map(str,sorted(versions))))


if __name__ == '__main__':
    unittest.main()

所以,它可以像这样使用:

import version2
versions = ["1.7.0", "1.7.0rc2", "1.7.0rc1", "1.7.1", "1.11.0"]
sorted(versions, key=lambda v: version2.NumberedVersion(v, '.', ['rc']))

输出:

['1.7.0rc1', '1.7.0rc2', '1.7.0', '1.7.1', '1.11.0']

所以,总而言之,使用 python 随附的电池或推出自己的电池。

关于这个实现:可以通过处理版本中的尾随零来改进它,并记住正则表达式的编译。

于 2012-09-04T01:00:58.957 回答
7

我像这样使用pkg_resources模块:

from pkg_resources import parse_version

def test_version_sorting():
    expected = ['1.0.0dev0',
                '1.0.0dev1',
                '1.0.0dev2',
                '1.0.0dev10',
                '1.0.0rc0',
                '1.0.0rc2',
                '1.0.0rc5',
                '1.0.0rc21',
                '1.0.0',
                '1.1.0',
                '1.1.1',
                '1.1.11',
                '1.2.0',
                '1.3.0',
                '1.23.0',
                '2.0.0', ]
    alphabetical = sorted(expected)
    shuffled = sorted(expected, key=lambda x: random())
    assert expected == sorted(alphabetical, key=parse_version)
    assert expected == sorted(shuffled, key=parse_version)

请注意,从预期版本列表中创建随机排序会使这成为潜在的不稳定单元测试,因为两次运行不会有相同的数据。不过,在这种情况下,这应该没关系……希望如此。

于 2017-04-14T09:33:47.160 回答
5

我发现这很有帮助,而且更简单:

from packaging import version

vers = ["1.7.0", "1.7.0rc2", "1.7.0rc1", "1.7.1", "1.11.0"]

sorted(vers, key=lambda x: version.Version(x))

结果是:

['1.7.0rc1', '1.7.0rc2', '1.7.0', '1.7.1', '1.11.0']

添加reverse=True使它们按“降序”顺序排列,我觉得这很有帮助。

['1.11.0', '1.7.1', '1.7.0', '1.7.0rc2', '1.7.0rc1']

它可以对各种版本风格的数字进行排序(我的测试平台是 Linux 版本 v4.11.16 等)

于 2019-09-20T23:03:14.700 回答
1

我用这个:

#!/usr/bin/python
import re

def sort_software_versions(versions = [], reverse = False):
  def split_version(version):
    def toint(x):
      try:
        return int(x)
      except:
        return x
    return map(toint, re.sub(r'([a-z])([0-9])', r'\1.\2', re.sub(r'([0-9])([a-z])', r'\1.\2', version.lower().replace('-', '.'))).split('.'))
  def compare_version_list(l1, l2):
    def compare_version(v1, v2):
      if isinstance(v1, int):
        if isinstance(v2, int):
          return v1 - v2
        else:
          return 1
      else:
        if isinstance(v2, int):
          return -1
        else:
          return cmp(v1, v2)
    ret = 0
    n1 = len(l1)
    n2 = len(l2)
    if n1 < n2:
      l1.extend([0]*(n2 - n1))
    if n2 < n1:
      l2.extend([0]*(n1 - n2))
    n = max(n1, n2)
    i = 0
    while not ret and i < n:
      ret = compare_version(l1[i], l2[i])
      i += 1
    return ret
  return sorted(versions, cmp = compare_version_list, key = split_version, reverse = reverse)

print(sort_software_versions(['1.7.0', '1.7.0.rc0', '1.8.0']))
['1.7.0.rc0', '1.7.0', '1.8.0']

这样它就可以正确处理 alpha、beta、rc。它可以处理包含连字符的版本,或者当人们将“rc”粘贴到版本时。re.sub 可以使用已编译的正则表达式,但这已经足够了。

于 2014-03-04T08:15:20.503 回答
0

在我的情况下,我想使用“.devX”作为“预发布”标识符,所以这是另一个实现,主要基于distutils.version.StrictVersion

class ArtefactVersion(Version):
    """
    Based on distutils/version.py:StrictVersion
    """
    version_re = re.compile(r'^(\d+) \. (\d+) \. (\d+) (\.dev\d+)?$', re.VERBOSE | re.ASCII)

    def parse(self, vstring):
        match = self.version_re.match(vstring)
        if not match:
            raise ValueError("invalid version number '%s'" % vstring)

        (major, minor, patch, prerelease) = match.group(1, 2, 3, 4)

        self.version = tuple(map(int, [major, minor, patch]))
        if prerelease:
            self.prerelease = prerelease[4:]
        else:
            self.prerelease = None

    def __str__(self):
        vstring = '.'.join(map(str, self.version))

        if self.prerelease:
            vstring = vstring + f".dev{str(self.prerelease)}"

        return vstring

    def _cmp(self, other):
        if isinstance(other, str):
            other = ArtefactVersion(other)

        if self.version != other.version:
            # numeric versions don't match
            # prerelease stuff doesn't matter
            if self.version < other.version:
                return -1
            else:
                return 1

        # have to compare prerelease
        # case 1: neither has prerelease; they're equal
        # case 2: self has prerelease, other doesn't; other is greater
        # case 3: self doesn't have prerelease, other does: self is greater
        # case 4: both have prerelease: must compare them!

        if (not self.prerelease and not other.prerelease):
            return 0
        elif (self.prerelease and not other.prerelease):
            return -1
        elif (not self.prerelease and other.prerelease):
            return 1
        elif (self.prerelease and other.prerelease):
            if self.prerelease == other.prerelease:
                return 0
            elif self.prerelease < other.prerelease:
                return -1
            else:
                return 1
        else:
            assert False, "never get here"
于 2020-05-01T12:25:37.090 回答