Как сравнить номера версий в 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. Есть ли элегантный способ сравнения строк версии?
Ответы
Ответ 1
Использовать packaging.version.parse
.
>>> 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
>>> isinstance(version.parse("1.3.a4"), version.Version)
True
>>> isinstance(version.parse("1.3.xy123"), version.LegacyVersion)
True
>>> version.Version("1.3.xy123")
Traceback (most recent call last):
...
packaging.version.InvalidVersion: Invalid version: '1.3.xy123'
packaging.version.parse
- это сторонняя утилита, но она используется в setuptools (так что вы, вероятно, уже установили ее) и соответствует текущему PEP 440; он вернет packaging.version.Version
если версия соответствует, и packaging.version.LegacyVersion
если нет. Последний всегда будет сортировать перед действительными версиями.
Древняя альтернатива, все еще используемая многими программами, - это distutils.version
, встроенный, но недокументированный и соответствующий только замененному PEP 386;
>>> 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'
Как вы можете видеть, он считает действительные версии PEP 440 "не строгими" и поэтому не соответствует современным представлениям Pythons о том, что такое действительная версия.
Поскольку distutils.version
является недокументированным, здесь приведены соответствующие строки документов.
Ответ 2
setuptools определяет parse_version()
. Это реализует PEP 0440 - Идентификация версии, а также может анализировать версии, которые не соответствуют PEP. Эта функция используется easy_install
и pip
для сравнения версий. Из документов:
Проанализировал строку версии проекта в соответствии с определением PEP 440. Возвращаемым значением будет объект, представляющий версию. Эти объекты можно сравнивать друг с другом и сортировать. Алгоритм сортировки определен в PEP 440 с добавлением, что любая версия, которая не является действительной версией PEP 440, будет считаться меньшей, чем любая действительная версия PEP 440, и недопустимые версии будут продолжать сортировку с использованием исходного алгоритма.
Упомянутый "оригинальный алгоритм" был определен в более старых версиях документов, до появления PEP 440.
Семантически, формат представляет собой грубое скрещивание между StrictVersion
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')
Ответ 3
def versiontuple(v):
return tuple(map(int, (v.split("."))))
>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
Ответ 4
Что случилось с преобразованием строки версии в кортеж и оттуда? Кажется достаточно элегантным для меня
>>> (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 - это быстрый пример того, насколько хороший код будет выглядеть.
Ответ 5
Существует packaging, который позволит вам сравнивать версии по 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
Ответ 6
Вы можете использовать пакет 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
Ответ 7
Публикация моей полной функции на основе решения 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
Ответ 8
То, как это делает 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
Ответ 9
Я искал решение, которое не добавило бы никаких новых зависимостей. Проверьте следующее (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()
РЕДАКТИРОВАТЬ: добавлен вариант со сравнением кортежей. Конечно, вариант с кортежным сравнением лучше, но я искал вариант с целочисленным сравнением
Ответ 10
я поеду больше для опции 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) и с кортежами