Jak porównać numery wersji w Pythonie?

236

Idę do katalogu zawierającego jajka, aby dodać te jajka do sys.path. Jeśli w katalogu są dwie wersje tego samego pliku .egg, chcę dodać tylko najnowszą.

Mam wyrażenie regularne, r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$aby wyodrębnić nazwę i wersję z nazwy pliku. Problem polega na porównaniu numeru wersji, który jest ciągiem podobnym do 2.3.1.

Ponieważ porównuję ciągi, 2 sortuje powyżej 10, ale to nie jest poprawne dla wersji.

>>> "2.3.1" > "10.1.1"
True

Mógłbym podzielić, parsować, rzutować na int, itp., I w końcu dostałem obejście. Ale to jest Python, a nie Java . Czy istnieje elegancki sposób porównywania ciągów wersji?

BorrajaX
źródło

Odpowiedzi:

367

Zastosowanie 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.parsejest narzędziem innej firmy, ale jest używane przez setuptools (więc prawdopodobnie już go masz) i jest zgodne z obecnym PEP 440 ; zwróci a, packaging.version.Versionjeśli wersja jest zgodna, a packaging.version.LegacyVersionjeśli nie. Te ostatnie zawsze będą sortować przed ważnymi wersjami.

Uwaga : opakowanie zostało ostatnio sprzedane w setuptools .


Starożytna alternatywa wciąż używana przez wiele programów jest distutils.versionwbudowana, ale nieudokumentowana i zgodna tylko z zastąpionym 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'

Jak widać, widzi prawidłowe wersje PEP 440 jako „nie ścisłe”, a zatem nie pasuje do współczesnego rozumienia Pythona co do prawidłowej wersji.

Jak distutils.versionnieudokumentowane, oto odpowiednie dokumenty.

ecatmur
źródło
2
Wygląda na to, że NormalizedVersion nie nadejdzie, ponieważ został zastąpiony, dlatego LooseVersion i StrictVersion nie są już przestarzałe.
Taywee,
12
To płacz wstyd distutils.versionjest nieudokumentowane.
John Y
znalazłem go za pomocą wyszukiwarki i znajdując bezpośrednio version.pykod źródłowy. Bardzo ładnie ułożone!
Joël
@Taywee są lepsze, ponieważ nie są zgodne z PEP 440.
latające owce
2
IMHO packaging.version.parsenie można ufać porównać wersje. Spróbuj parse('1.0.1-beta.1') > parse('1.0.0')na przykład.
Trondh
104

Pakowanie biblioteka zawiera narzędzia do pracy z wersjami oraz inne funkcje związane z opakowań. To implementuje PEP 0440 - Identyfikacja wersji, a także może analizować wersje, które nie są zgodne z PEP. Jest używany przez pip i inne popularne narzędzia Pythona do analizowania wersji i porównywania.

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

Zostało to oddzielone od oryginalnego kodu w setuptools i pkg_resources, aby zapewnić lżejszy i szybszy pakiet.


Zanim istniała biblioteka pakietów, ta funkcjonalność została (i nadal może być) znaleziona w pkg_resources, pakiecie dostarczanym przez setuptools. Jednak nie jest to już preferowane, ponieważ nie można już zagwarantować, że setuptools zostanie zainstalowany (istnieją inne narzędzia do pakowania), a pkg_resources ironicznie zużywa sporo zasobów podczas importowania. Jednak wszystkie dokumenty i dyskusje są nadal aktualne.

Z parse_version()dokumentów :

Analizowano ciąg wersji projektu zgodnie z definicją PEP 440. Zwrócona wartość będzie obiektem reprezentującym wersję. Obiekty te mogą być ze sobą porównywane i sortowane. Algorytm sortowania jest zdefiniowany przez PEP 440 z tym, że każda wersja, która nie jest prawidłową wersją PEP 440, będzie uważana za mniejszą niż dowolna ważna wersja PEP 440, a nieprawidłowe wersje będą kontynuować sortowanie przy użyciu oryginalnego algorytmu.

Wspomniany „oryginalny algorytm” został zdefiniowany w starszych wersjach dokumentów, zanim istniał PEP 440.

Semantycznie format ten jest szorstkim skrzyżowaniem między destutils StrictVersioni LooseVersionklasami; jeśli podasz wersje, które by działały StrictVersion, będą one porównywać w ten sam sposób. W przeciwnym razie porównania są bardziej jak „mądrzejsza” forma LooseVersion. Możliwe jest stworzenie schematów kodowania wersji patologicznej, które oszukają ten parser, ale w praktyce powinny one występować bardzo rzadko.

Dokumentacja podaje kilka przykładów:

Jeśli chcesz mieć pewność, że wybrany schemat numerowania działa tak, jak myślisz, możesz użyć pkg_resources.parse_version() funkcji do porównania różnych numerów wersji:

>>> 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
dawidyzm
źródło
57
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
kindall
źródło
10
Pozostałe odpowiedzi znajdują się w standardowej bibliotece i są zgodne ze standardami PEP.
Chris
1
W takim przypadku możesz map()całkowicie usunąć funkcję, ponieważ wynik split()jest już ciągiem. Ale i tak nie chcesz tego robić, ponieważ jedynym powodem, aby je zmienić, intjest prawidłowe porównywanie liczb. Inaczej "10" < "2".
uprzejmie
6
To się nie powiedzie dla czegoś takiego versiontuple("1.0") > versiontuple("1"). Wersje są takie same, ale utworzone krotki(1,)!=(1,0)
dawg
3
W jakim sensie wersja 1 i wersja 1.0 są takie same? Numery wersji nie są zmiennoprzecinkowe.
uprzejmie
12
Nie, to nie powinna być zaakceptowana odpowiedź. Na szczęście tak nie jest. Wiarygodne analizowanie specyfikatorów wersji nie jest trywialne (jeśli nie jest praktycznie niemożliwe) w ogólnym przypadku. Nie wymyślaj koła na nowo, a następnie przystąp do jego łamania. Jak sugeruje powyżej ecatmur , po prostu użyj . Po to jest. distutils.version.LooseVersion
Cecil Curry
12

Co jest złego w przekształcaniu ciągu wersji w krotkę i stamtąd? Wydaje mi się wystarczająco elegancki

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

Rozwiązanie @ kindall to szybki przykład tego, jak dobrze wyglądałby kod.

Gabi Purcaru
źródło
1
Myślę, że tę odpowiedź można rozszerzyć, udostępniając kod, który dokonuje transformacji łańcucha PEP440 w krotkę. Myślę, że przekonasz się, że nie jest to trywialne zadanie. Myślę, że lepiej zostawić do pakietu, który wykonuje, że tłumaczenie setuptools, co jest pkg_resources.
@TylerGubala to świetna odpowiedź w sytuacjach, w których wiesz, że wersja jest i zawsze będzie „prosta”. pkg_resources jest dużym pakietem i może powodować, że rozproszony plik wykonywalny jest raczej rozdęty.
Erik Aronesty
@Erik Aronesty Myślę, że kontrola wersji w rozproszonych plikach wykonywalnych jest nieco poza zakresem pytania, ale generalnie się zgadzam. Myślę jednak, że jest coś do powiedzenia na temat możliwości ponownego użycia pkg_resourcesi że założenia dotyczące prostego nazewnictwa pakietów nie zawsze są idealne.
Działa świetnie, aby upewnić się sys.version_info > (3, 6)czy cokolwiek.
Gqqnbig
7

Dostępny jest pakiet opakowań , który pozwoli ci porównać wersje zgodnie z PEP-440 , a także wersje starsze.

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

Obsługa starszych wersji:

>>> LegacyVersion('1.2.8.5-5-gdeadbeef')
<LegacyVersion('1.2.8.5-5-gdeadbeef')>

Porównanie starszej wersji z wersją PEP-440.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
sashk
źródło
3
Dla tych, którzy zastanawiają się nad różnicą między : packaging.version.Versiona packaging.version.parse„[ version.parse] bierze ciąg wersji i parsuje go tak, Versionjakby wersja była prawidłową wersją PEP 440, w przeciwnym razie parsuje go jako LegacyVersion.” (mając na uwadze version.Version, że powstanie InvalidVersion; źródło )
Braham Snyder
5

Możesz użyć pakietu semver , aby ustalić, czy wersja spełnia wymagania wersji semantycznej . Nie jest to to samo, co porównywanie dwóch rzeczywistych wersji, ale jest rodzajem porównania.

Na przykład wersja 3.6.0 + 1234 powinna być taka sama jak wersja 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
źródło
3

Publikowanie mojej pełnej funkcji opartej na rozwiązaniu Kindall. Byłem w stanie obsłużyć dowolne znaki alfanumeryczne pomieszane z liczbami, wypełniając każdą sekcję wersji wiodącymi zerami.

Chociaż z pewnością nie jest tak ładna, jak jego funkcja jednowarstwowa, wydaje się, że dobrze współpracuje z alfanumerycznymi numerami wersji. (Pamiętaj tylko, aby odpowiednio ustawić zfill(#)wartość, jeśli masz długie ciągi w systemie kontroli wersji.)

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
źródło
2

Sposób, w jaki to setuptoolsrobi, wykorzystuje pkg_resources.parse_versionfunkcję. Powinien być zgodny z PEP440 .

Przykład:

#! /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

źródło
pkg_resourcesjest częścią setuptools, która zależy od packaging. Zobacz inne omawiane odpowiedzi packaging.version.parse, które mają identyczną implementację jak pkg_resources.parse_version.
Jed
0

Szukałem rozwiązania, które nie dodałoby żadnych nowych zależności. Sprawdź następujące rozwiązanie (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()

EDYCJA: dodano wariant z porównaniem krotek. Oczywiście wariant z porównywaniem krotek jest ładniejszy, ale szukałem wariantu z porównaniem liczb całkowitych

Stefan Saru
źródło
Jestem ciekawy, w jakiej sytuacji unika to dodawania zależności? Czy nie potrzebujesz biblioteki pakietów (używanej przez setuptools) do utworzenia pakietu python?
Josiah L.,