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