在Python中对版本排序

2024-06-06 04:49:12 发布

您现在位置:Python中文网/ 问答频道 /正文

我试着让1.7.0在1.7.0.rc0之后,但在1.8.0之前,如果你对版本进行排序的话。我认为loosversion的关键是它正确地处理了这种事情的排序和比较。

>>> 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')]

Tags: infromimport版本for排序versionversions
3条回答

主要编辑:旧答案太不通俗。这里有两个更漂亮的解决方案。

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

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

一。旧式命令式排序

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”

如果允许您的1.7.0.rc01.7.0a01.7.0b0的形式写入,包distutils.version也有另一个类StrictVersion来执行任务,该类记录alpha或beta版本。

即:

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模块完成从一个表单到另一个表单的转换。

三。扩展版本类

先前解决方案的明显问题是StrictVersion缺乏灵活性。更改version_re类属性以使用rc而不是ab,即使它接受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自带的电池或推出自己的电池。

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

>>> 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']

从文件中

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.
...
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").

所以你看特别对待“rc”是不明智的

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

>>> LooseVersion('1.7.0rc0').version
[1, 7, 0, 'rc', 0]

我像这样使用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)

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

相关问题 更多 >