webentwicklung-frage-antwort-db.com.de

Wie vergleiche ich Versionsnummern in Python?

Ich gehe ein Verzeichnis durch, das Eier enthält, um diese Eier zum sys.path Hinzuzufügen. Wenn sich zwei Versionen desselben .Eggs im Verzeichnis befinden, möchte ich nur die neueste hinzufügen.

Ich habe einen regulären Ausdruck r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.Egg$, um den Namen und die Version aus dem Dateinamen zu extrahieren. Das Problem besteht darin, die Versionsnummer zu vergleichen, die eine Zeichenfolge wie 2.3.1 Ist.

Da ich Strings vergleiche, sind 2 Sortierungen über 10, aber das ist für Versionen nicht korrekt.

>>> "2.3.1" > "10.1.1"
True

Ich könnte ein bisschen splitten, analysieren, nach int umwandeln usw. und ich würde irgendwann einen Workaround bekommen. Aber das ist Python, nicht Java . Gibt es eine elegante Möglichkeit, Versionszeichenfolgen zu vergleichen?

188
BorrajaX

Verwenden 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 ist ein Dienstprogramm eines Drittanbieters, wird jedoch von setuptools (wahrscheinlich haben Sie es bereits installiert) verwendet und entspricht dem aktuellen PEP 44 ; es wird ein packaging.version.Version wenn die version konform ist und ein packaging.version.LegacyVersion wenn nicht. Letztere werden immer vor gültigen Versionen sortiert.


Eine uralte Alternative, die noch immer von vielen Programmen verwendet wird, ist distutils.version , eingebaut, aber undokumentiert und konform nur mit dem abgelösten 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'

Wie Sie sehen, sieht es gültige PEP 440-Versionen als "nicht streng" an und entspricht daher nicht der Vorstellung, die Python von einer gültigen Version hat.

Wie distutils.version ist undokumentiert, hier sind die relevanten Dokumentzeichenfolgen.

292
ecatmur

setuptools definiert parse_version(). Dies implementiert PEP 0440 - Versionsidentifikation und ist auch in der Lage, Versionen zu analysieren, die nicht dem PEP folgen. Diese Funktion wird von easy_install Und pip für den Versionsvergleich verwendet. Aus dem docs :

Analysiert die Versionszeichenfolge eines Projekts gemäß der Definition in PEP 440. Der zurückgegebene Wert ist ein Objekt, das die Version darstellt. Diese Objekte können miteinander verglichen und sortiert werden. Der Sortieralgorithmus entspricht der Definition von PEP 440 mit dem Zusatz, dass jede Version, die keine gültige PEP 440-Version ist, als kleiner als jede gültige PEP 440-Version betrachtet wird und die ungültigen Versionen weiterhin unter Verwendung des ursprünglichen Algorithmus sortiert werden.

Der "ursprüngliche Algorithmus", auf den verwiesen wurde, wurde in älteren Versionen der Dokumente definiert, bevor PEP 440 existierte.

Semantisch ist das Format eine grobe Kreuzung zwischen den Klassen StrictVersion und LooseVersion von distutils. Wenn Sie Versionen angeben, die mit StrictVersion funktionieren, werden sie auf die gleiche Weise verglichen. Ansonsten sind Vergleiche eher eine "intelligentere" Form von LooseVersion. Es ist möglich, pathologische Versionscodierungsschemata zu erstellen, die diesen Parser täuschen, in der Praxis sollten sie jedoch sehr selten sein.

Die Dokumentation liefert einige Beispiele:

Wenn Sie sicher sein möchten, dass das von Ihnen gewählte Nummerierungsschema so funktioniert, wie Sie es sich vorstellen, können Sie die Funktion pkg_resources.parse_version() verwenden, um verschiedene Versionsnummern zu vergleichen:

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

Wenn Sie setuptools nicht verwenden, teilt das Projekt packaging diese und andere paketbezogene Funktionen in eine separate Bibliothek auf.

from packaging import version
version.parse('1.0.3.dev')

from pkg_resources import parse_version
parse_version('1.0.3.dev')
92
davidism
def versiontuple(v):
    return Tuple(map(int, (v.split("."))))

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

Was ist falsch daran, die Versionszeichenfolge in ein Tupel umzuwandeln und von dort weiterzugehen? Scheint elegant genug für mich

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

Die Lösung von @ kindall ist ein kurzes Beispiel dafür, wie gut der Code aussehen würde.

9
Gabi Purcaru

Es ist das Paket packaging verfügbar, mit dem Sie Versionen gemäß PEP-44 sowie ältere Versionen vergleichen können.

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

Unterstützung älterer Versionen:

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

Vergleich der Vorgängerversion mit der PEP-440-Version.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
7
sashk

Mit dem Paket semver können Sie ermitteln, ob eine Version die Anforderung semantische Version erfüllt. Dies ist nicht dasselbe wie der Vergleich zweier tatsächlicher Versionen, sondern eine Art Vergleich.

Beispielsweise sollte Version 3.6.0 + 1234 mit 3.6.0 identisch sein.

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

Posten meiner vollen Funktion basierend auf der Lösung von Kindall. Ich konnte alle alphanumerischen Zeichen, die mit den Zahlen gemischt wurden, unterstützen, indem ich jeden Versionsabschnitt mit führenden Nullen auffüllte.

Obwohl es sicherlich nicht so hübsch ist wie seine Einzeilerfunktion, scheint es gut mit alphanumerischen Versionsnummern zu funktionieren. (Stellen Sie einfach sicher, dass Sie den zfill(#) -Wert entsprechend einstellen, wenn Sie lange Zeichenfolgen in Ihrem Versionsverwaltungssystem haben.)

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
Phaxmohdem

So wie es setuptools macht, benutzt es das pkg_resources.parse_version Funktion. Es sollte PEP44 kompatibel sein.

Beispiel:

#! /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
1
Tyler Gubala

Ich suchte nach einer Lösung, die keine neuen Abhängigkeiten hinzufügt. Überprüfen Sie die folgende (Python 3) Lösung:

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 __== '__main__':
    VersionManager.test_compare_versions()

EDIT: Variante mit Tupelvergleich hinzugefügt. Natürlich ist die Variante mit Tupel-Vergleich besser, aber ich habe nach der Variante mit Integer-Vergleich gesucht

0
Stefan Saru