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?

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.


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

57
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

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

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 já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.


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

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

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

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

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


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.
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.