¿Cómo comparo los números de versión en Python?

236

Estoy caminando por un directorio que contiene huevos para agregar esos huevos al sys.path. Si hay dos versiones del mismo .egg en el directorio, quiero agregar solo la última.

Tengo una expresión regular r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$para extraer el nombre y la versión del nombre del archivo. El problema es comparar el número de versión, que es como una cadena 2.3.1.

Como estoy comparando cadenas, 2 ordena por encima de 10, pero eso no es correcto para las versiones.

>>> "2.3.1" > "10.1.1"
True

Podría hacer un poco de división, análisis, conversión a int, etc., y eventualmente obtendría una solución. Pero esto es Python, no Java . ¿Hay una manera elegante de comparar cadenas de versiones?

BorrajaX
fuente

Respuestas:

367

Uso 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.parsees una utilidad de terceros pero es utilizada por setuptools (por lo que probablemente ya lo tenga instalado) y cumple con el PEP 440 actual ; devolverá a packaging.version.Versionsi la versión es compatible y a packaging.version.LegacyVersionsi no. Este último siempre se ordenará antes que las versiones válidas.

Nota : el paquete se ha enviado recientemente a las herramientas de configuración .


Una alternativa antigua aún utilizada por una gran cantidad de software es distutils.version, incorporada pero no documentada y conforme solo a la PEP 386 reemplazada ;

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

Como puede ver, ve versiones válidas de PEP 440 como "no estrictas" y, por lo tanto, no coincide con la noción moderna de Python de lo que es una versión válida.

Como distutils.versionno está documentado, aquí están las cadenas de documentos relevantes.

ecatmur
fuente
2
Parece que NormalizedVersion no vendrá, ya que fue reemplazado, y LooseVersion y StrictVersion ya no están en desuso.
Taywee
12
Es una vergüenza distutils.versionindocumentada.
John Y
Lo encontré usando el motor de búsqueda y buscando directamente el version.pycódigo fuente. Muy bien puesto!
Joël
@Taywee, es mejor, ya que no cumplen con PEP 440.
ovejas voladoras
2
en mi humilde opinión packaging.version.parse, no se puede confiar para comparar las versiones. Prueba parse('1.0.1-beta.1') > parse('1.0.0')por ejemplo.
Trondh
104

La biblioteca de empaque contiene utilidades para trabajar con versiones y otras funcionalidades relacionadas con el empaque. Esto implementa PEP 0440 - Identificación de versión y también es capaz de analizar versiones que no siguen la PEP. Lo utiliza pip y otras herramientas comunes de Python para proporcionar análisis y comparación de versiones.

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

Esto se separó del código original en setuptools y pkg_resources para proporcionar un paquete más ligero y rápido.


Antes de que existiera la biblioteca de empaquetado, esta funcionalidad se encontraba (y aún se puede encontrar) en pkg_resources, un paquete proporcionado por setuptools. Sin embargo, esto ya no se prefiere ya que ya no se garantiza la instalación de las herramientas de configuración (existen otras herramientas de empaque) y, irónicamente, pkg_resources utiliza muchos recursos cuando se importa. Sin embargo, todos los documentos y la discusión siguen siendo relevantes.

De los parse_version()documentos :

Analizó la cadena de versión de un proyecto según lo definido por PEP 440. El valor devuelto será un objeto que representa la versión. Estos objetos pueden compararse entre sí y ordenarse. El algoritmo de clasificación es el definido por PEP 440 con la adición de que cualquier versión que no sea una versión válida de PEP 440 se considerará menor que cualquier versión válida de PEP 440 y las versiones no válidas continuarán clasificando utilizando el algoritmo original.

El "algoritmo original" al que se hace referencia se definió en versiones anteriores de los documentos, antes de que existiera PEP 440.

Semánticamente, el formato es un cruce aproximado entre distutils StrictVersiony LooseVersionclases; si le das versiones con las que funcionaría StrictVersion, se compararán de la misma manera. De lo contrario, las comparaciones son más como una forma de "más inteligente" LooseVersion. Es posible crear esquemas de codificación de versiones patológicas que engañarán a este analizador, pero deberían ser muy raros en la práctica.

La documentación proporciona algunos ejemplos:

Si desea estar seguro de que su esquema de numeración elegido funciona de la manera que cree, puede usar la pkg_resources.parse_version() función para comparar diferentes números de versión:

>>> 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
davidismo
fuente
57
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
un poco
fuente
10
Las otras respuestas están en la biblioteca estándar y siguen los estándares PEP.
Chris
1
En ese caso, podría eliminar la map()función por completo, ya que el resultado yasplit() es cadenas. Pero de todos modos no desea hacer eso, porque la razón completa para cambiarlos es para que se comparen correctamente como números. De lo contrario . int"10" < "2"
poco
66
Esto fallará por algo así versiontuple("1.0") > versiontuple("1"). Las versiones son las mismas, pero las tuplas creadas(1,)!=(1,0)
dawg
3
¿En qué sentido son iguales la versión 1 y la versión 1.0? Los números de versión no son flotantes.
poco
12
No, esta no debería ser la respuesta aceptada. Afortunadamente no lo es. El análisis confiable de los especificadores de versión no es trivial (si no es prácticamente inviable) en el caso general. No reinvente la rueda y luego proceda a romperla. Como ecatmur sugiere anteriormente , solo use distutils.version.LooseVersion. Para eso está ahí.
Cecil Curry
12

¿Qué hay de malo en transformar la cadena de versión en una tupla e ir desde allí? Parece lo suficientemente elegante para mi

>>> (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 solución de @ kindall es un ejemplo rápido de lo bien que se vería el código.

Gabi Purcaru
fuente
1
Creo que esta respuesta podría ampliarse proporcionando código que realice la transformación de una cadena PEP440 en una tupla. Creo que encontrarás que no es una tarea trivial. Creo que es mejor dejarlo al paquete que realiza esa traducción setuptools, que es pkg_resources.
@TylerGubala esta es una gran respuesta en situaciones en las que sabes que la versión es y siempre será "simple". pkg_resources es un gran paquete y puede hacer que un ejecutable distribuido se hinche bastante.
Erik Aronesty
@ Erik Aronesty Creo que el control de versiones dentro de los ejecutables distribuidos es algo fuera del alcance de la pregunta, pero estoy de acuerdo, en general al menos. Sin embargo, creo que hay algo que decir sobre la reutilización de pkg_resources, y que los supuestos de simple denominación de paquetes pueden no ser siempre ideales.
Funciona muy bien para asegurarse sys.version_info > (3, 6)o lo que sea.
Gqqnbig
7

Hay un paquete de empaque disponible, que le permitirá comparar versiones según PEP-440 , así como versiones heredadas.

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

Soporte de versiones heredadas:

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

Comparación de la versión heredada con la versión PEP-440.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
sashk
fuente
3
Para aquellos que se preguntan sobre la diferencia entre packaging.version.Versiony packaging.version.parse: "[ version.parse] toma una cadena de versión y la analizará como Versionsi la versión fuera una versión PEP 440 válida, de lo contrario, la analizará como a LegacyVersion". (mientras que version.Versionsubiría InvalidVersion; fuente )
Braham Snyder
5

Puede usar el paquete semver para determinar si una versión cumple un requisito de versión semántica . Esto no es lo mismo que comparar dos versiones reales, pero es un tipo de comparación.

Por ejemplo, la versión 3.6.0 + 1234 debería ser la misma que 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
fuente
3

Publicar mi función completa basada en la solución de Kindall. Pude admitir cualquier carácter alfanumérico mezclado con los números al rellenar cada sección de la versión con ceros a la izquierda.

Aunque ciertamente no es tan bonito como su función de una línea, parece funcionar bien con números de versión alfanuméricos. (Solo asegúrese de establecer el zfill(#)valor de manera adecuada si tiene cadenas largas en su sistema de versiones).

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

De la manera que lo setuptoolshace, usa la pkg_resources.parse_versionfunción. Debe ser compatible con PEP440 .

Ejemplo:

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

fuente
pkg_resourceses parte de setuptools, que depende de packaging. Vea otras respuestas que discuten packaging.version.parse, que tiene una implementación idéntica a pkg_resources.parse_version.
Jed
0

Estaba buscando una solución que no agregara ninguna dependencia nueva. Consulte la siguiente solución (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()

EDITAR: variante agregada con comparación de tuplas. Por supuesto, la variante con comparación de tuplas es mejor, pero estaba buscando la variante con comparación de enteros

Stefan Saru
fuente
Tengo curiosidad sobre en qué situación evita esto agregar dependencias. ¿No necesitará la biblioteca de empaquetado (utilizada por setuptools) para crear un paquete de Python?
Josiah L.