Como eu comparo os números de versão no Python?

236

Estou andando em um diretório que contém ovos para adicioná-los ao sys.path. Se houver duas versões do mesmo .egg no diretório, quero adicionar apenas a mais recente.

Eu tenho uma expressão regular r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$para extrair o nome e a versão do nome do arquivo. O problema está comparando o número da versão, que é uma string semelhante 2.3.1.

Como estou comparando strings, 2 classifica acima de 10, mas isso não está correto para as versões.

>>> "2.3.1" > "10.1.1"
True

Eu poderia dividir, analisar, converter para int etc., e acabaria obtendo uma solução alternativa. Mas isso é Python, não Java . Existe uma maneira elegante de comparar seqüências de caracteres de versão?

BorrajaX
fonte

Respostas:

367

Use 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é um utilitário de terceiros, mas é usado pelo setuptools (então você provavelmente já o instalou) e está em conformidade com o PEP 440 atual ; retornará a packaging.version.Versionse a versão for compatível e a packaging.version.LegacyVersionse não for. O último sempre será classificado antes das versões válidas.

Nota : a embalagem foi vendida recentemente em setuptools .


Uma alternativa antiga ainda usada por muitos softwares é distutils.versionincorporada, mas não documentada e compatível apenas com o PEP 386 substituído ;

>>> 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 você pode ver, vê versões válidas do PEP 440 como "não rigorosas" e, portanto, não corresponde à noção moderna do Python sobre o que é uma versão válida.

Como não distutils.versionestá documentado, aqui estão os documentos relevantes.

ecatmur
fonte
2
Parece que NormalVersion não virá, como foi substituído, e LooseVersion e StrictVersion não serão mais preteridos.
Taywee
12
É uma vergonha distutils.versionnão documentada.
John Y
encontrou-o usando o mecanismo de pesquisa e localizou diretamente o version.pycódigo-fonte. Muito bem colocado!
Joël
@ Taywee eles são melhores, já que não são compatíveis com PEP 440.
ovelha voadora
2
O imho packaging.version.parsenão pode ser confiável para comparar versões. Tente parse('1.0.1-beta.1') > parse('1.0.0')por exemplo.
Trondh 16/08/19
104

A biblioteca de embalagens contém utilitários para trabalhar com versões e outras funcionalidades relacionadas a embalagens. Isso implementa o PEP 0440 - Identificação da versão e também é capaz de analisar versões que não seguem o PEP. É usado pelo pip e outras ferramentas comuns do Python para fornecer análise e comparação de versões.

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

Isso foi separado do código original em setuptools e pkg_resources para fornecer um pacote mais leve e mais rápido.


Antes da biblioteca de empacotamento existir, essa funcionalidade era (e ainda pode ser) encontrada em pkg_resources, um pacote fornecido pelo setuptools. No entanto, isso não é mais preferido, pois não é mais garantido que o setuptools seja instalado (outras ferramentas de empacotamento existem) e o pkg_resources ironicamente usa muitos recursos quando importado. No entanto, todos os documentos e discussões ainda são relevantes.

Dos parse_version()documentos :

Analisou a sequência de versões de um projeto, conforme definido pelo PEP 440. O valor retornado será um objeto que representa a versão. Esses objetos podem ser comparados entre si e classificados. O algoritmo de classificação é definido pelo PEP 440, com a adição de que qualquer versão que não seja uma versão válida do PEP 440 será considerada menor que qualquer versão válida do PEP 440 e as versões inválidas continuarão sendo classificadas usando o algoritmo original.

O "algoritmo original" mencionado foi definido em versões mais antigas dos documentos, antes da existência do PEP 440.

Semanticamente, o formato é um cruzamento aproximado entre distutils StrictVersione LooseVersionclasses; se você fornecer versões com as quais funcionaria StrictVersion, elas serão comparadas da mesma maneira. Caso contrário, as comparações são mais como uma forma "mais inteligente" de LooseVersion. É possível criar esquemas de codificação de versão patológica que enganarão esse analisador, mas na prática eles devem ser muito raros.

A documentação fornece alguns exemplos:

Se quiser ter certeza de que o esquema de numeração escolhido funciona da maneira que você pensa, poderá usar a pkg_resources.parse_version() função para comparar diferentes números de versão:

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

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
kindall
fonte
10
As outras respostas estão na biblioteca padrão e seguem os padrões do PEP.
Chris
1
Nesse caso, você pode remover map()completamente a função, pois o resultado split() é de strings. Mas você não quer fazer isso de qualquer maneira, porque todo o motivo para alterá-los é para que eles sejam comparados corretamente como números. Caso contrário . int"10" < "2"
Kindall
6
Isso irá falhar para algo como versiontuple("1.0") > versiontuple("1"). As versões são as mesmas, mas as tuplas criado(1,)!=(1,0)
dawg
3
Em que sentido as versões 1 e 1.0 são iguais? Os números de versão não são flutuantes.
Kindall
12
Não, essa não deve ser a resposta aceita. Felizmente, não é. A análise confiável de especificadores de versão não é trivial (se não for praticamente inviável) no caso geral. Não reinvente a roda e prossiga para quebrá-la. Como ecatmur sugere acima , basta usar distutils.version.LooseVersion. É para isso que serve.
Cecil Curry
12

O que há de errado em transformar a string de versão em uma tupla e partir daí? Parece elegante o suficiente para mim

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

A solução da @ kindall é um exemplo rápido de quão bom o código seria.

Gabi Purcaru
fonte
1
Eu acho que essa resposta pode ser expandida fornecendo código que executa a transformação de uma string PEP440 em uma tupla. Eu acho que você vai achar que não é uma tarefa trivial. Eu acho que é melhor deixar para o pacote que executa essa tradução setuptools, o que é pkg_resources.
@TylerGubala, esta é uma ótima resposta em situações em que você sabe que a versão é e sempre será "simples". O pkg_resources é um pacote grande e pode fazer com que um executável distribuído fique inchado.
Erik Aronesty 30/09/19
@ Erik Aronesty Acho que o controle de versão dentro de executáveis ​​distribuídos é um pouco fora do escopo da questão, mas concordo, pelo menos geralmente. Penso, no entanto, que há algo a ser dito sobre a reutilização de pkg_resources, e que suposições de nomes simples de pacotes nem sempre podem ser ideais.
Funciona muito bem para garantir sys.version_info > (3, 6)ou o que quer.
Gqqnbig 04/02
7

Há um pacote de embalagem disponível, que permitirá comparar versões conforme PEP-440 , bem como versões herdadas.

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

Suporte à versão herdada:

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

Comparando a versão herdada com a versão PEP-440.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
sashk
fonte
3
Para aqueles que se perguntam sobre a diferença entre packaging.version.Versione packaging.version.parse: "[ version.parse] pega uma string de versão e a analisa como Versionse a versão for uma versão válida do PEP 440, caso contrário, ela será analisada como a LegacyVersion." (enquanto que version.Versionelevaria InvalidVersion; source )
Braham Snyder
5

Você pode usar o pacote semver para determinar se uma versão atende a um requisito de versão semântica . Não é o mesmo que comparar duas versões reais, mas é um tipo de comparação.

Por exemplo, a versão 3.6.0 + 1234 deve ser a mesma 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
fonte
3

Publicando minha função completa com base na solução de Kindall. Consegui suportar qualquer caractere alfanumérico misturado com os números preenchendo cada seção da versão com zeros à esquerda.

Embora certamente não seja tão bonito quanto sua função de uma linha, parece funcionar bem com números de versão alfanuméricos. (Apenas certifique-se de definir o zfill(#)valor adequadamente se você tiver longas seqüências de caracteres em seu sistema de controle de versão.)

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

Da maneira que setuptoolsfaz isso, ele usa a pkg_resources.parse_versionfunção Deve ser PEP440 compatível com .

Exemplo:

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

fonte
pkg_resourcesfaz parte do setuptoolsqual depende packaging. Veja outras respostas que discutem packaging.version.parse, com uma implementação idêntica à pkg_resources.parse_version.
Jed
0

Eu estava procurando uma solução que não adicionasse novas dependências. Confira a seguinte solução (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: variante adicionada com comparação de tupla. Claro que a variante com comparação de tupla é melhor, mas eu estava procurando a variante com comparação de números inteiros

Stefan Saru
fonte
Estou curioso em que situação isso evita adicionar dependências? Você não precisa da biblioteca de empacotamento (usada pelo setuptools) para criar um pacote python?
Josiah L.