Как сравнить номера версий в 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) и с трупами