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?

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.


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

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

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

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.


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

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

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

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

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


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.,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.