Come faccio a confrontare i numeri di versione in Python?


236

Sto percorrendo una directory che contiene uova per aggiungere quelle uova al file sys.path. Se ci sono due versioni dello stesso .egg nella directory, voglio aggiungere solo l'ultima.

Ho un'espressione regolare r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$per estrarre il nome e la versione dal nome file. Il problema sta confrontando il numero di versione, che è una stringa simile 2.3.1.

Dal momento che sto confrontando le stringhe, 2 ordina sopra 10, ma non è corretto per le versioni.

>>> "2.3.1" > "10.1.1"
True

Potrei fare un po 'di divisione, analisi, casting in int, ecc., E alla fine otterrei una soluzione alternativa. Ma questo è Python, non Java . Esiste un modo elegante per confrontare le stringhe di versione?

Risposte:


367

Usa 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è un'utilità di terze parti ma viene utilizzata da setuptools (quindi probabilmente è già installata) ed è conforme all'attuale PEP 440 ; restituirà a packaging.version.Versionse la versione è conforme e a in packaging.version.LegacyVersioncaso contrario. Quest'ultimo ordinerà sempre prima delle versioni valide.

Nota : il packaging è stato recentemente venduto in setuptools .


Un'antica alternativa ancora utilizzata da molti software è distutils.version, integrata ma non documentata e conforme solo al PEP 386 sostituito ;

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

Come puoi vedere, vede le versioni PEP 440 valide come "non rigorose" e quindi non corrisponde alla moderna nozione di Python di cosa sia una versione valida.

Come distutils.versionnon documentato, ecco i documenti pertinenti.


2
Sembra che NormalizedVersion non arriverà, poiché è stato sostituito e LooseVersion e StrictVersion non sono più deprecati.
Taywee,

12
È un peccato piangere distutils.versionè senza documenti.
John Y,

trovato tramite il motore di ricerca e trovato direttamente il version.pycodice sorgente. Molto ben messo!
Joël,

@Tay sono meglio, dal momento che non sono conformi PEP 440.
volo di pecore il

2
packaging.version.parseNon posso fidarmi di imho per confrontare le versioni. Prova parse('1.0.1-beta.1') > parse('1.0.0')ad esempio.
Trondh,

104

La libreria di packaging contiene utilità per lavorare con le versioni e altre funzionalità relative al packaging. Questo implementa PEP 0440 - Identificazione della versione ed è anche in grado di analizzare le versioni che non seguono il PEP. Viene utilizzato da pip e da altri strumenti Python comuni per fornire analisi e confronto delle versioni.

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

Questo è stato separato dal codice originale in setuptools e pkg_resources per fornire un pacchetto più leggero e veloce.


Prima che esistesse la libreria di packaging, questa funzionalità era (e può ancora essere) trovata in pkg_resources, un pacchetto fornito da setuptools. Tuttavia, questo non è più preferito poiché setuptools non è più garantito per l'installazione (esistono altri strumenti di packaging) e pkg_resources ironicamente utilizza molte risorse durante l'importazione. Tuttavia, tutti i documenti e le discussioni sono ancora pertinenti.

Dai parse_version()documenti :

Analizzato la stringa di versione di un progetto come definita da PEP 440. Il valore restituito sarà un oggetto che rappresenta la versione. Questi oggetti possono essere confrontati tra loro e ordinati. L'algoritmo di ordinamento è definito da PEP 440 con l'aggiunta che qualsiasi versione che non è una versione PEP 440 valida verrà considerata inferiore a qualsiasi versione PEP 440 valida e le versioni non valide continueranno l'ordinamento utilizzando l'algoritmo originale.

L '"algoritmo originale" a cui si fa riferimento era definito nelle versioni precedenti dei documenti, prima che esistesse PEP 440.

Semanticamente, il formato è un incrocio approssimativo tra distutils ' StrictVersione LooseVersionclassi; se gli dai versioni che funzionerebbero StrictVersion, si confronteranno allo stesso modo. Altrimenti, i confronti sono più simili a una forma "più intelligente" di LooseVersion. È possibile creare schemi di codifica di versione patologica che inganneranno questo parser, ma dovrebbero essere molto rari nella pratica.

La documentazione fornisce alcuni esempi:

Se vuoi essere certo che il tuo schema di numerazione scelto pkg_resources.parse_version() funzioni come pensi che possa fare, puoi usare la funzione per confrontare diversi numeri di versione:

>>> 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
Le altre risposte sono nella libreria standard e seguono gli standard PEP.
Chris,

1
In tal caso, è possibile rimuovere completamente la map()funzione, poiché il risultato split()è già stringhe. Ma non vuoi farlo comunque, perché l'intero motivo per cambiarli intè in modo che si confrontino correttamente come numeri. In caso contrario "10" < "2".
kindall

6
Questo fallirà per qualcosa del genere versiontuple("1.0") > versiontuple("1"). Le versioni sono le stesse, ma le tuple create(1,)!=(1,0)
dawg

3
In che senso sono la stessa versione 1 e 1.0? I numeri di versione non sono float.
kindall

12
No, questa non dovrebbe essere la risposta accettata. Per fortuna, non lo è. L'analisi affidabile degli identificatori di versione non è banale (se non praticamente impossibile) nel caso generale. Non reinventare la ruota e quindi procedere alla sua rottura. Come ecatmur suggerisce sopra , basta usare distutils.version.LooseVersion. Ecco a cosa serve.
Cecil Curry

12

Cosa c'è di sbagliato nel trasformare la stringa di versione in una tupla e andare da lì? Sembra abbastanza elegante per me

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

La soluzione di @ kindall è un rapido esempio di quanto sarebbe bello il codice.


1
Penso che questa risposta possa essere ampliata fornendo il codice che esegue la trasformazione di una stringa PEP440 in una tupla. Penso che troverai che non è un compito banale. Penso che sia meglio lasciare al pacchetto che esegue quella traduzione per setuptools, che è pkg_resources.

@TylerGubala questa è un'ottima risposta in situazioni in cui sai che la versione è e sarà sempre "semplice". pkg_resources è un grosso pacchetto e può far gonfiare un eseguibile distribuito.
Erik Aronesty,

@Erik Aronesty Penso che il controllo della versione all'interno degli eseguibili distribuiti sia in qualche modo esterno allo scopo della domanda, ma sono d'accordo, almeno in generale. Penso però che ci sia qualcosa da dire sulla riusabilità di pkg_resources, e che i presupposti della semplice denominazione dei pacchetti potrebbero non essere sempre l'ideale.

Funziona benissimo per esserne certi sys.version_info > (3, 6)o altro.
Gqqnbig

7

È disponibile un pacchetto di packaging che ti consentirà di confrontare le versioni secondo PEP-440 , nonché le versioni legacy.

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

Supporto versione legacy:

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

Confronto tra la versione legacy e la versione PEP-440.

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

3
Per coloro che si chiedono la differenza tra packaging.version.Versione packaging.version.parse: "[ version.parse] accetta una stringa di versione e la analizzerà come Versionse la versione fosse una versione PEP 440 valida, altrimenti la analizzerà come una LegacyVersion." (mentre version.Versionaumenterebbe InvalidVersion; fonte )
Braham Snyder

5

È possibile utilizzare il pacchetto semver per determinare se una versione soddisfa un requisito di versione semantica . Questo non equivale a confrontare due versioni effettive, ma è un tipo di confronto.

Ad esempio, la versione 3.6.0 + 1234 dovrebbe essere uguale alla 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

Pubblicando la mia funzione completa basata sulla soluzione di Kindall. Sono stato in grado di supportare qualsiasi carattere alfanumerico mischiato ai numeri riempiendo ogni sezione della versione con zeri iniziali.

Anche se certamente non è carino come la sua funzione a linea singola, sembra funzionare bene con i numeri di versione alfanumerici. (Assicurati di impostare il zfill(#)valore in modo appropriato se hai stringhe lunghe nel tuo sistema di controllo delle versioni.)

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

Il modo in cui lo setuptoolsfa, utilizza la pkg_resources.parse_versionfunzione. Dovrebbe essere PEP440 conforme a .

Esempio:

#! /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_resourcesfa parte di setuptools, che dipende da packaging. Vedi altre risposte che discutono packaging.version.parse, che ha un'implementazione identica a pkg_resources.parse_version.
Jed,

0

Stavo cercando una soluzione che non aggiungesse nuove dipendenze. Dai un'occhiata alla seguente soluzione (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: aggiunta variante con confronto tupla. Naturalmente la variante con il confronto delle tuple è più bella, ma stavo cercando la variante con il confronto dei numeri interi


Sono curioso di sapere in quale situazione questo evita di aggiungere dipendenze? Non è necessaria la libreria di packaging (utilizzata da setuptools) per creare un pacchetto python?
Josiah L.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.