Як порівняти номери версій в 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 . Чи є елегантний спосіб порівняння рядків версій?

Відповіді:


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 ; він поверне a, 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недокументовано, ось відповідні документації.


2
Схоже, NormalizedVersion не буде надходити, оскільки його було замінено, і LooseVersion та StrictVersion тому більше не застаріли.
Taywee

12
Це плач сором distutils.versionнезадокументований.
Джон Y

знайшли це за допомогою пошукової системи та безпосередньо знаходження version.pyвихідного коду. Дуже красиво поставлено!
Joël

@Taywee вони кращі, оскільки вони не відповідають стандартам PEP 440.
літаючі вівці

2
Імхо 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.

Семантично формат є грубим схрещуванням між дистрибутивами 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

57
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

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

10
Інші відповіді знаходяться у стандартній бібліотеці та відповідають стандартам PEP.
Кріс

1
У такому випадку ви можете map()повністю видалити функцію, оскільки результат вжеsplit() є рядками. Але ви все одно не хочете цього робити, оскільки вся причина змінити їх у тому, щоб вони порівнювались як числа. Інакше . int"10" < "2"
kindall

6
Це не вдасться для чогось подібного versiontuple("1.0") > versiontuple("1"). Версії однакові, але створені кортежі(1,)!=(1,0)
dawg

3
У якому сенсі версія 1 та версія 1.0 однакові? Номери версій не плаваючі.
kindall

12
Ні, це не повинна бути прийнятою відповіддю. На щастя, це не так. Надійний аналіз специфікаторів версій у загальному випадку нетривіальний (якщо не практично неможливий). Не винаходити колесо, а потім переходити до розбиття. Як пропонує Екатмур вище , просто використовуйте 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

3
Для тих, хто цікавиться різницею між packaging.version.Versionі packaging.version.parse: "[ version.parse] бере версію рядка версії і розбере її як a, Versionякщо версія є дійсною версією PEP 440, інакше вона розбере її як a 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

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

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()

EDIT: доданий варіант із порівнянням кортежу. Звичайно, варіант з порівнянням кортежу приємніший, але я шукав варіант із цілим порівнянням


Мені цікаво, в якій ситуації це уникає додавання залежностей? Чи вам не потрібна бібліотека упаковки (яка використовується setuptools) для створення пакету python?
Йосія Л.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.