Wie vergleiche ich Versionsnummern in Python?

235

Ich gehe durch ein Verzeichnis, das Eier enthält, um diese Eier dem hinzuzufügen sys.path. Wenn das Verzeichnis zwei Versionen derselben .egg-Datei enthält, 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, bei der es sich um eine Zeichenfolge handelt 2.3.1.

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

>>> "2.3.1" > "10.1.1"
True

Ich könnte etwas aufteilen, analysieren, in int umwandeln usw., und ich würde irgendwann eine Problemumgehung bekommen. Aber das ist Python, nicht Java . Gibt es eine elegante Möglichkeit, Versionszeichenfolgen zu vergleichen?

BorrajaX
quelle

Antworten:

367

Verwenden Sie 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.parseist ein Dienstprogramm eines Drittanbieters, wird jedoch von setuptools verwendet (sodass Sie es wahrscheinlich bereits installiert haben) und entspricht dem aktuellen PEP 440 . Es wird a zurückgegeben, packaging.version.Versionwenn die Version kompatibel ist, und a, packaging.version.LegacyVersionwenn nicht. Letzteres wird immer vor gültigen Versionen sortiert.

Hinweis : Verpackungen wurden kürzlich in Setuptools verkauft .


Eine alte Alternative, die immer noch von vielen Softwareprogrammen verwendet wird distutils.version, ist eingebaut, aber nicht dokumentiert und entspricht nur 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 können, werden gültige PEP 440-Versionen als „nicht streng“ eingestuft und entsprechen daher nicht der Vorstellung des modernen Python, was eine gültige Version ist.

Wie nicht distutils.versiondokumentiert, finden Sie hier die relevanten Dokumentzeichenfolgen.

ecatmur
quelle
2
Es sieht so aus, als würde NormalizedVersion nicht kommen, da es ersetzt wurde, und LooseVersion und StrictVersion sind daher nicht mehr veraltet.
Taywee
12
Es ist eine Schande, dass distutils.versiones keine Papiere gibt.
John Y
fand es mit einer Suchmaschine und fand direkt den version.pyQuellcode. Sehr schön ausgedrückt!
Joël
@Taywee sind sie besser, da sie nicht PEP 440-konform sind.
fliegende Schafe
2
Imho packaging.version.parsekann nicht vertraut werden, um Versionen zu vergleichen. Versuchen Sie es parse('1.0.1-beta.1') > parse('1.0.0')zum Beispiel.
Trondh
104

Die Verpackungsbibliothek enthält Dienstprogramme zum Arbeiten mit Versionen und anderen verpackungsbezogenen Funktionen. Dies implementiert PEP 0440 - Versionsidentifikation und kann auch Versionen analysieren, die nicht dem PEP folgen. Es wird von pip und anderen gängigen Python-Tools verwendet, um das Parsen und Vergleichen von Versionen zu ermöglichen.

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

Dies wurde vom ursprünglichen Code in setuptools und pkg_resources abgespalten, um ein leichteres und schnelleres Paket bereitzustellen.


Bevor die Verpackungsbibliothek existierte, wurde (und kann) diese Funktionalität in pkg_resources gefunden, einem Paket, das von setuptools bereitgestellt wird. Dies wird jedoch nicht mehr bevorzugt, da die Installation von Setuptools nicht mehr garantiert ist (andere Verpackungswerkzeuge sind vorhanden) und pkg_resources beim Import ironischerweise eine Menge Ressourcen verwendet. Alle Dokumente und Diskussionen sind jedoch weiterhin relevant.

Aus den parse_version()Dokumenten :

Analysiert die Versionszeichenfolge eines Projekts gemäß 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 weniger als jede gültige PEP 440-Version betrachtet wird und die ungültigen Versionen weiterhin mit dem ursprünglichen Algorithmus sortieren.

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

Semantisch ist das Format eine grobe Kreuzung zwischen Distutils StrictVersionund LooseVersionKlassen; Wenn Sie ihm Versionen geben, die funktionieren würden StrictVersion, werden sie auf die gleiche Weise verglichen. Ansonsten sind Vergleiche eher eine "intelligentere" Form vonLooseVersion . Es ist möglich, pathologische Versionscodierungsschemata zu erstellen, die diesen Parser täuschen, aber sie sollten in der Praxis sehr selten sein.

Die Dokumentation enthält 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 pkg_resources.parse_version() Funktion 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
Davidismus
quelle
57
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
irgendwie
quelle
10
Die anderen Antworten befinden sich in der Standardbibliothek und folgen den PEP-Standards.
Chris
1
In diesem Fall könnten Sie die entfernen map()Funktion vollständig, wie das Ergebnis split()ist bereits Saiten. Aber das wollen Sie sowieso nicht, denn der ganze Grund, sie zu ändern, intist, dass sie richtig als Zahlen verglichen werden. Ansonsten "10" < "2".
Kindall
6
Dies wird für so etwas wie scheitern versiontuple("1.0") > versiontuple("1"). Die Versionen sind die gleichen, aber die Tupel erstellt(1,)!=(1,0)
dawg
3
Inwiefern sind Version 1 und Version 1.0 gleich? Versionsnummern sind keine Floats.
Kindall
12
Nein, dies sollte nicht die akzeptierte Antwort sein. Zum Glück ist es nicht. Das zuverlässige Parsen von Versionsspezifizierern ist im allgemeinen Fall nicht trivial (wenn nicht praktisch unmöglich). Erfinden Sie das Rad nicht neu und brechen Sie es dann. Wie ecatmur schlägt oben , verwenden Sie einfach distutils.version.LooseVersion. Dafür ist es da.
Cecil Curry
12

Was ist falsch daran, die Versionszeichenfolge in ein Tupel umzuwandeln und von dort fortzufahren? Scheint mir elegant genug

>>> (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 schnelles Beispiel dafür, wie gut der Code aussehen würde.

Gabi Purcaru
quelle
1
Ich denke, diese Antwort könnte erweitert werden, indem Code bereitgestellt wird, der die Umwandlung eines PEP440- Strings in ein Tupel durchführt. Ich denke, Sie werden feststellen, dass es keine triviale Aufgabe ist. Ich denke, es ist besser, dem Paket zu überlassen, für das diese Übersetzung durchgeführt setuptoolswird pkg_resources.
@ TylerGubala Dies ist eine großartige Antwort in Situationen, in denen Sie wissen, dass die Version "einfach" ist und immer sein wird. pkg_resources ist ein großes Paket und kann dazu führen, dass eine verteilte ausführbare Datei ziemlich aufgebläht ist.
Erik Aronesty
@Erik Aronesty Ich denke, dass die Versionskontrolle in verteilten ausführbaren Dateien etwas außerhalb des Umfangs der Frage liegt, aber ich stimme zumindest im Allgemeinen zu. Ich denke jedoch, dass es etwas zu sagen gibt über die Wiederverwendbarkeit von pkg_resourcesund dass Annahmen einer einfachen Paketbenennung möglicherweise nicht immer ideal sind.
Es funktioniert gut, um sicher zu gehen sys.version_info > (3, 6)oder was auch immer.
Gqqnbig
7

Es ist ein Verpackungspaket verfügbar, mit dem Sie Versionen gemäß PEP-440 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 für ältere Versionen:

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

Vergleich der Legacy-Version mit der PEP-440-Version.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
Schärpe
quelle
3
Für diejenigen, die sich über den Unterschied zwischen packaging.version.Versionund wundern packaging.version.parse: "[ version.parse] nimmt eine Versionszeichenfolge und analysiert sie als, Versionwenn die Version eine gültige PEP 440-Version ist, andernfalls wird sie als analysiert LegacyVersion." (wohingegen version.Versionerhöhen würde InvalidVersion; Quelle )
Braham Snyder
5

Mit dem Semver- Paket können Sie feststellen, ob eine Version eine semantische Versionsanforderung 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 Version 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
Prikkeldraad
quelle
3

Veröffentlichung meiner vollen Funktion basierend auf Kindalls Lösung. Ich konnte alle alphanumerischen Zeichen unterstützen, die mit den Zahlen gemischt waren, indem ich jeden Versionsabschnitt mit führenden Nullen auffüllte.

Obwohl es sicherlich nicht so hübsch ist wie seine Einzeiler-Funktion, scheint es mit alphanumerischen Versionsnummern gut zu funktionieren. (Stellen zfill(#)Sie den Wert nur entsprechend ein, wenn Ihr Versionsverwaltungssystem lange Zeichenfolgen enthält.)

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

So wie setuptoolses geht, nutzt es die pkg_resources.parse_versionFunktion. Es sollte PEP440 sein konform 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

quelle
pkg_resourcesist ein Teil von setuptools, was davon abhängt packaging. Sehen Sie sich andere Antworten an packaging.version.parse, die eine identische Implementierung haben pkg_resources.parse_version.
Jed
0

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

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

BEARBEITEN: Variante mit Tupelvergleich hinzugefügt. Natürlich ist die Variante mit Tupelvergleich schöner, aber ich habe nach der Variante mit ganzzahligem Vergleich gesucht

Stefan Saru
quelle
Ich bin gespannt, in welcher Situation dadurch das Hinzufügen von Abhängigkeiten vermieden wird. Benötigen Sie nicht die Paketbibliothek (von setuptools verwendet), um ein Python-Paket zu erstellen?
Josiah L.