Как сравнить номера версий в Python?

Я иду по каталогу, который содержит яйца, чтобы добавить эти яйца в sys.path, Если в каталоге есть две версии одного и того же.egg, я хочу добавить только самую последнюю.

У меня есть регулярное выражение r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$ извлечь имя и версию из имени файла. Проблема в сравнении номера версии, которая представляет собой строку типа 2.3.1,

Так как я сравниваю строки, 2 сорта выше 10, но это не правильно для версий.

>>> "2.3.1" > "10.1.1"
True

Я мог бы сделать некоторое разбиение, разбор, приведение к int и т. Д., И я в конечном итоге нашел бы обходной путь. Но это Python, а не Java. Есть ли элегантный способ сравнить строки версий?

17 ответов

Решение

Использование distutils.version или же packaging.version.parse,

>>> from distutils.version import LooseVersion, StrictVersion
>>> LooseVersion("2.3.1") < LooseVersion("10.1.2")
True
>>> StrictVersion("2.3.1") < StrictVersion("10.1.2")
True
>>> StrictVersion("1.3.a4")
Traceback (most recent call last):
...
ValueError: invalid version number '1.3.a4'

>>> from packaging import version
>>> version.parse("2.3.1") < version.parse("10.1.2")
True
>>> version.parse("1.3.a4") < version.parse("10.1.2")
True

Различия между двумя вариантами:

  • distutils.version встроен, но не имеет документов и соответствует только замененному PEP 386;
  • packaging.version.parse является сторонней утилитой, но используется setuptools (так что вы, вероятно, уже установили ее) и соответствует текущему PEP 440; он также обрабатывает "свободные" и "строгие" версии в одной функции (хотя "унаследованные" версии всегда сортируются перед действительными версиями).

Как distutils.version недокументировано, вот соответствующие строки документации (на основе Python 3.3) для справки (взято из источника):

Каждый класс номера версии реализует следующий интерфейс:

  • метод 'parse' берет строку и анализирует ее в некотором внутреннем представлении; если строка является недопустимым номером версии, 'parse' вызывает ValueError исключение
  • конструктор класса принимает необязательный строковый аргумент, который, если он указан, передается в 'parse'
  • __str__ реконструирует строку, которая была передана в 'parse' (или эквивалентную строку - т. е. ту, которая сгенерирует экземпляр с эквивалентным номером версии)
  • __repr__ генерирует код Python для воссоздания экземпляра номера версии
  • _cmp сравнивает текущий экземпляр с другим экземпляром того же класса или строкой (который будет проанализирован с экземпляром того же класса, поэтому должен следовать тем же правилам)

StrictVersion

Нумерация версий для анальных хранителей и идеалистов программного обеспечения. Реализует стандартный интерфейс для классов номеров версий, как описано выше. Номер версии состоит из двух или трех числовых компонентов, разделенных точками, с необязательным тегом "pre-release" на конце. Предварительная версия тега состоит из буквы "a" или "b", за которой следует число. Если числовые компоненты двух номеров версий равны, то один с тегом предварительной версии всегда будет считаться более ранним (меньшим), чем один без.

Ниже приведены действительные номера версий (показанные в порядке, который будет получен путем сортировки в соответствии с предоставленной функцией cmp):

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

Ниже приведены примеры неверных номеров версий:

1
2.7.2.2
1.3.a4
1.3pl1
1.3c4

Обоснование этой системы нумерации версий будет объяснено в документации distutils.


LooseVersion

Нумерация версий для анархистов и программных реалистов. Реализует стандартный интерфейс для классов номеров версий, как описано выше. Номер версии состоит из серии чисел, разделенных точками или строками букв. При сравнении номеров версий числовые компоненты будут сравниваться численно, а алфавитные компоненты лексически. Ниже приведены действительные номера версий в произвольном порядке:

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

На самом деле, нет такой вещи, как недопустимый номер версии по этой схеме; правила сравнения просты и предсказуемы, но не всегда дают желаемые результаты (для некоторого определения "хочу").

setuptools определяет parse_version(), Это реализует PEP 0440 - Идентификация версии, а также может анализировать версии, которые не соответствуют PEP. Эта функция используется easy_install а также pip обрабатывать сравнение версий. Из документов:

Проанализировал строку версии проекта в соответствии с определением PEP 440. Возвращаемым значением будет объект, представляющий версию. Эти объекты можно сравнивать друг с другом и сортировать. Алгоритм сортировки определен в PEP 440 с добавлением, что любая версия, которая не является действительной версией PEP 440, будет считаться меньшей, чем любая действительная версия PEP 440, и недействительные версии будут продолжать сортировку с использованием исходного алгоритма.

Указанный "оригинальный алгоритм" был определен в более старых версиях документов, до появления PEP 440.

Семантически, формат представляет собой грубую помесь между distutils StrictVersion а также LooseVersion классы; если вы дадите ему версии, которые будут работать с StrictVersion, тогда они будут сравнивать так же. В противном случае сравнения больше похожи на "умную" форму LooseVersion, Можно создать патологические схемы кодирования версий, которые будут обманывать этот синтаксический анализатор, но на практике они должны быть очень редкими.

Документация содержит несколько примеров:

Если вы хотите быть уверены, что выбранная вами схема нумерации работает так, как вы думаете, вы можете использовать pkg_resources.parse_version() функция для сравнения разных номеров версий:

>>> from pkg_resources import parse_version
>>> parse_version('1.9.a.dev') == parse_version('1.9a0dev')
True
>>> parse_version('2.1-rc2') < parse_version('2.1')
True
>>> parse_version('0.6a9dev-r41475') < parse_version('0.6a9')
True

Если вы не используете setuptools, проект упаковки разделяет эту и другие функции, связанные с упаковкой, в отдельную библиотеку.

from packaging import version
version.parse('1.0.3.dev')

from pkg_resources import parse_version
parse_version('1.0.3.dev')
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False

Что плохого в преобразовании строки версии в кортеж и в дальнейшем? Кажется достаточно элегантным для меня

>>> (2,3,1) < (10,1,1)
True
>>> (2,3,1) < (10,1,1,1)
True
>>> (2,3,1,10) < (10,1,1,1)
True
>>> (10,3,1,10) < (10,1,1,1)
False
>>> (10,3,1,10) < (10,4,1,1)
True

Решение @kindall - это быстрый пример того, как хорошо будет выглядеть код.

То, что setuptools делает это, он использует pkg_resources.parse_version функция. Это должно быть PEP440- совместимым.

Пример:

#! /usr/bin/python
# -*- coding: utf-8 -*-
"""Example comparing two PEP440 formatted versions
"""
import pkg_resources

VERSION_A = pkg_resources.parse_version("1.0.1-beta.1")
VERSION_B = pkg_resources.parse_version("v2.67-rc")
VERSION_C = pkg_resources.parse_version("2.67rc")
VERSION_D = pkg_resources.parse_version("2.67rc1")
VERSION_E = pkg_resources.parse_version("1.0.0")

print(VERSION_A)
print(VERSION_B)
print(VERSION_C)
print(VERSION_D)

print(VERSION_A==VERSION_B) #FALSE
print(VERSION_B==VERSION_C) #TRUE
print(VERSION_C==VERSION_D) #FALSE
print(VERSION_A==VERSION_E) #FALSE

Доступен упаковочный пакет, который позволит вам сравнивать версии согласно PEP-440, а также унаследованные версии.

>>> from packaging.version import Version, LegacyVersion
>>> Version('1.1') < Version('1.2')
True
>>> Version('1.2.dev4+deadbeef') < Version('1.2')
True
>>> Version('1.2.8.5') <= Version('1.2')
False
>>> Version('1.2.8.5') <= Version('1.2.8.6')
True

Поддержка устаревшей версии:

>>> LegacyVersion('1.2.8.5-5-gdeadbeef')
<LegacyVersion('1.2.8.5-5-gdeadbeef')>

Сравнение устаревшей версии с версией PEP-440.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True

Публикация моей полной функции на основе решения Kindall. Я смог поддержать любые буквенно-цифровые символы, смешанные с числами, добавив в каждую версию раздела начальные нули.

Хотя он, конечно, не так хорош, как его однострочная функция, похоже, он хорошо работает с буквенно-цифровыми номерами версий. (Просто не забудьте установить zfill(#) оценивать соответствующим образом, если в вашей системе управления версиями есть длинные строки.)

def versiontuple(v):
   filled = []
   for point in v.split("."):
      filled.append(point.zfill(8))
   return tuple(filled)

,

>>> versiontuple("10a.4.5.23-alpha") > versiontuple("2a.4.5.23-alpha")
True


>>> "10a.4.5.23-alpha" > "2a.4.5.23-alpha"
False

Вы можете использовать пакет semver, чтобы определить, удовлетворяет ли версия требованиям семантической версии. Это не то же самое, что сравнение двух реальных версий, но это тип сравнения.

Например, версия 3.6.0+1234 должна совпадать с 3.6.0.

import semver
semver.match('3.6.0+1234', '==3.6.0')
# True

from packaging import version
version.parse('3.6.0+1234') == version.parse('3.6.0')
# False

from distutils.version import LooseVersion
LooseVersion('3.6.0+1234') == LooseVersion('3.6.0')
# False

Я искал решение, которое не добавило бы никаких новых зависимостей. Проверьте следующее (Python 3) решение:

class VersionManager:

    @staticmethod
    def compare_version_tuples(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):

        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as tuples)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        tuple_a = major_a, minor_a, bugfix_a
        tuple_b = major_b, minor_b, bugfix_b
        if tuple_a > tuple_b:
            return 1
        if tuple_b > tuple_a:
            return -1
        return 0

    @staticmethod
    def compare_version_integers(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):
        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as integers)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        # --
        if major_a > major_b:
            return 1
        if major_b > major_a:
            return -1
        # --
        if minor_a > minor_b:
            return 1
        if minor_b > minor_a:
            return -1
        # --
        if bugfix_a > bugfix_b:
            return 1
        if bugfix_b > bugfix_a:
            return -1
        # --
        return 0

    @staticmethod
    def test_compare_versions():
        functions = [
            (VersionManager.compare_version_tuples, "VersionManager.compare_version_tuples"),
            (VersionManager.compare_version_integers, "VersionManager.compare_version_integers"),
        ]
        data = [
            # expected result, version a, version b
            (1, 1, 0, 0, 0, 0, 1),
            (1, 1, 5, 5, 0, 5, 5),
            (1, 1, 0, 5, 0, 0, 5),
            (1, 0, 2, 0, 0, 1, 1),
            (1, 2, 0, 0, 1, 1, 0),
            (0, 0, 0, 0, 0, 0, 0),
            (0, -1, -1, -1, -1, -1, -1),  # works even with negative version numbers :)
            (0, 2, 2, 2, 2, 2, 2),
            (-1, 5, 5, 0, 6, 5, 0),
            (-1, 5, 5, 0, 5, 9, 0),
            (-1, 5, 5, 5, 5, 5, 6),
            (-1, 2, 5, 7, 2, 5, 8),
        ]
        count = len(data)
        index = 1
        for expected_result, major_a, minor_a, bugfix_a, major_b, minor_b, bugfix_b in data:
            for function_callback, function_name in functions:
                actual_result = function_callback(
                    major_a=major_a, minor_a=minor_a, bugfix_a=bugfix_a,
                    major_b=major_b, minor_b=minor_b, bugfix_b=bugfix_b,
                )
                outcome = expected_result == actual_result
                message = "{}/{}: {}: {}: a={}.{}.{} b={}.{}.{} expected={} actual={}".format(
                    index, count,
                    "ok" if outcome is True else "fail",
                    function_name,
                    major_a, minor_a, bugfix_a,
                    major_b, minor_b, bugfix_b,
                    expected_result, actual_result
                )
                print(message)
                assert outcome is True
                index += 1
        # test passed!


if __name__ == '__main__':
    VersionManager.test_compare_versions()

РЕДАКТИРОВАТЬ: добавлен вариант со сравнением кортежей. Конечно, вариант с кортежным сравнением лучше, но я искал вариант с целочисленным сравнением

Если вы хотите создать фильтр для версии библиотеки, вы можете использовать__version__атрибут (здесь пример с библиотекой jwt):

      from packaging import version
import jwt

if version.parse(jwt.__version__) < version.parse('2.0.0'):
    # TODO: your code

... и возвращаясь к простоте... для простых сценариев, которые вы можете использовать:

import sys
needs = (3, 9) # or whatever
pvi = sys.version_info.major, sys.version_info.minor    

позже в вашем коде

try:
    assert pvi >= needs
except:
    print("will fail!")
    # etc.

Это компактный код для сравнения трех номеров версий. Обратите внимание, что здесь сравнение строк не выполняется для всех пар.

      from itertools import permutations

for v1, v2 in permutations(["3.10.21", "3.10.3", "3.9.9"], 2):
    print(f"\nv1 = {v1}, v2 = {v2}")
    print(f"v1 < v2      version.parse(v1) < version.parse(v2)")
    print(f"{v1 < v2}         {version.parse(v1) < version.parse(v2)}")

Это дает нам:

      v1='3.10.21', v2='3.10.3'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.10.21', v2='3.9.9'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.10.3', v2='3.10.21'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

v1='3.10.3', v2='3.9.9'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.9.9', v2='3.10.21'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

v1='3.9.9', v2='3.10.3'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

permutations(iterable, 2)дает нам все 2-кратные перестановки итерации. Так например

      list(permutations('ABC', 2))

дает нам [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')].

Чтобы увеличить версию с помощью python

      def increment_version(version):
    version = version.split('.')
    if int(version[len(version) - 1]) >= 99:
        version[len(version) - 1] = '0'
        version[len(version) - 2] = str(int(version[len(version) - 2]) + 1)
    else:
        version[len(version) - 1] = str(int(version[len(version) - 1]) + 1)
    return '.'.join(version)

version = "1.0.0"
version_type_2 = "1.0"
print("old version",version ,"new version",increment_version(version))
print("old version",version_type_2 ,"new version",increment_version(version_type_2))

Вот что будет работать, если ваши семантические версии "чисты" (например, x.x.x), и у вас есть список версий, которые нужно отсортировать.

      # Here are some versions
versions = ["1.0.0", "1.10.0", "1.9.0"]

# This does not work
versions.sort() # Result: ['1.0.0', '1.10.0', '1.9.0']

# So make a list of tuple versions
tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]

# And sort the string list based on the tuple list
versions = [x for _, x in sorted(zip(tuple_versions, versions))] # Result: ['1.0.0', '1.9.0', '1.10.0']

Чтобы получить последнюю версию, вы можете просто выбрать последний элемент в списке. versions[-1]или сортировать в обратном порядке с помощью reverseатрибут sorted(), установив его на True, и получение [0]элемент.

Конечно, вы могли бы обернуть все это в удобную функцию для повторного использования.

      def get_latest_version(versions):
    """
    Get the latest version from a list of versions.
    """
    try:
        tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]
        versions = [x for _, x in sorted(zip(tuple_versions, versions), reverse=True)]
        latest_version = versions[0]
    except Exception as e:
        print(e)
        latest_version = None

    return latest_version

print(get_latest_version(["1.0.0", "1.10.0", "1.9.0"]))

простой малострочный:

      import sys
if (sys.version_info.major, sys.version_info.minor) >= (3, 9):
    ...
else:
    ...

аналогичен стандартному strverscmp и аналогичен этому решению Марка Байерса, но с использованием findall вместо split, чтобы избежать пустого регистра.

      import re
num_split_re = re.compile(r'([0-9]+|[^0-9]+)')

def try_int(i, fallback=None):
    try:
        return int(i)
    except ValueError:
        pass
    except TypeError:
        pass
    return fallback

def ver_as_list(a):
    return [try_int(i, i) for i in num_split_re.findall(a)]

def strverscmp_lt(a, b):
    a_ls = ver_as_list(a)
    b_ls = ver_as_list(b)
    return a_ls < b_ls

Я остановлюсь больше на параметре touple, делая тест, используя LooseVersion, я получаю в своем тесте второй по величине тест (возможно, я делаю что-то не так, поскольку я впервые использую эту библиотеку)

import itertools
from distutils.version import LooseVersion, StrictVersion

lista_de_frameworks = ["1.1.1", "1.2.5", "10.5.2", "3.4.5"]

for a, b in itertools.combinations(lista_de_frameworks, 2):
    if LooseVersion(a) < LooseVersion(b):
        big = b
print big

list_test = []
for a in lista_de_frameworks:
    list_test.append( tuple(map(int, (a.split(".")))))

print max(list_test)

и вот что я получил:

3.4.5 с Loose

(10, 5, 2) и с трупами

Другие вопросы по тегам