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

236

Я иду каталог, который содержит яйца, чтобы добавить эти яйца в 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 . Есть ли элегантный способ сравнения строк версии?

BorrajaX
источник

Ответы:

367

Использование 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если нет. Последний всегда будет сортировать перед верными версиями.

Примечание : упаковка была недавно продана в setuptools .


Древняя альтернатива, все еще используемая многими программами 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 «не строгими» и поэтому не соответствует представлениям современного Python о том, что такое действительная версия.

Как distutils.versionэто недокументировано, вот соответствующие строки документации.

ecatmur
источник
2
Похоже, что NormalizedVersion не придет, поскольку он был заменен, и поэтому LooseVersion и StrictVersion больше не являются устаревшими.
Taywee
12
Плачущий стыд distutils.versionбез документов.
Джон Y
нашел его с помощью поисковой системы и непосредственно найдя version.pyисходный код. Очень красиво положено!
Жоэль
@ Так они лучше, так как они не соответствуют PEP 440.
летающие овцы
2
imho packaging.version.parseнельзя доверять для сравнения версий. Попробуйте, parse('1.0.1-beta.1') > parse('1.0.0')например.
Тронх
104

Библиотека упаковки содержит утилиты для работы с версиями и другие функции, связанные с упаковкой. Это реализует PEP 0440 - Идентификация версии, а также может анализировать версии, которые не соответствуют PEP. Он используется pip и другими распространенными инструментами Python для анализа и сравнения версий.

$ pip install packaging
from packaging.version import parse as parse_version
version = parse_version('1.0.3.dev')

Это было отделено от исходного кода в setuptools и pkg_resources, чтобы обеспечить более легкий и быстрый пакет.


До появления библиотеки упаковки эта функциональность была (и все еще может быть) найдена в пакете pkg_resources, предоставляемом setuptools. Однако это больше не является предпочтительным, так как больше не гарантируется установка setuptools (существуют другие инструменты упаковки), и pkg_resources по иронии судьбы использует довольно много ресурсов при импорте. Тем не менее, все документы и обсуждения по-прежнему актуальны.

Из parse_version()документов :

Проанализировал строку версии проекта в соответствии с определением 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
davidism
источник
57
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
Kindall
источник
10
Другие ответы находятся в стандартной библиотеке и соответствуют стандартам PEP.
Крис
1
В этом случае вы можете удалить map()функцию целиком, как результат split()это уже строки. Но вы все равно не хотите этого делать, потому что причина их изменения в intтом, чтобы они правильно сравнивались как числа. В противном случае "10" < "2".
любезно
6
Это не удастся для чего-то вроде versiontuple("1.0") > versiontuple("1"). Версии одинаковые, но кортежи созданы(1,)!=(1,0)
dawg
3
В каком смысле версия 1 и версия 1.0 совпадают? Номера версий не являются плавающими.
добро пожаловать
12
Нет, это не должен быть принятый ответ. К счастью, это не так. Надежный разбор спецификаторов версий в общем случае является нетривиальным (если не практически невозможным). Не изобретайте колесо, а затем продолжайте его ломать. Как подсказывает ecatmur выше , просто используйте distutils.version.LooseVersion. Вот для чего это.
Сесил Карри
12

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

>>> (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 - быстрый пример того, как хорошо будет выглядеть код.

Габи Пуркару
источник
1
Я думаю, что этот ответ можно расширить, предоставив код, который выполняет преобразование строки PEP440 в кортеж. Я думаю, вы найдете, что это не тривиальная задача. Я думаю, что лучше оставить пакет, который выполняет этот перевод, для setuptoolsкоторого pkg_resources.
@TylerGubala - это отличный ответ в ситуациях, когда вы знаете, что версия есть и всегда будет «простой». pkg_resources - это большой пакет, который может привести к довольно раздутому распределенному исполняемому файлу.
Эрик Аронести
@Erik Aronesty Я думаю, что управление версиями в распределенных исполняемых файлах несколько выходит за рамки вопроса, но я согласен, по крайней мере, в общем. Тем не менее, я думаю, что есть что сказать о возможности повторного использования pkg_resources, и что предположения о простом именовании пакетов не всегда могут быть идеальными.
Это прекрасно работает, чтобы убедиться sys.version_info > (3, 6)или что-то еще.
Gqqnbig
7

Существует упаковка пакет , доступный, который позволит вам сравнить версии в соответствии с 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
sashk
источник
3
Для тех, кто интересуется разницей между packaging.version.Versionи packaging.version.parse: «[ version.parse] берет строку версии и будет анализировать ее как, Versionесли версия является действительной версией PEP 440, в противном случае она будет анализироваться как LegacyVersion». (тогда как version.Versionподнимет InvalidVersion; источник )
Брахам Снайдер
5

Вы можете использовать пакет 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
Prikkeldraad
источник
3

Публикация моей полной функции на основе решения 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
Phaxmohdem
источник
2

То, как это 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

источник
pkg_resourcesявляется частью, от setuptoolsкоторой зависит packaging. Смотрите другие ответы, которые обсуждают packaging.version.parse, который имеет аналогичную реализацию для pkg_resources.parse_version.
Джед
0

Я искал решение, которое не добавило бы никаких новых зависимостей. Проверьте следующее (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()

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

Стефан Сару
источник
Мне интересно, в какой ситуации это позволяет избежать добавления зависимостей? Вам не понадобится библиотека пакетов (используемая setuptools) для создания пакета Python?
Иосия Л.