web-dev-qa-db-fra.com

Comment comparer les numéros de version en Python?

Je parcours un répertoire contenant des œufs pour ajouter ces œufs au sys.path. S'il existe deux versions du même .Egg dans le répertoire, je souhaite ajouter uniquement la dernière.

J'ai une expression régulière r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.Egg$ pour extraire le nom et la version du nom de fichier. Le problème est de comparer le numéro de version, qui est une chaîne comme 2.3.1.

Puisque je compare les chaînes, 2 tris au-dessus de 10, mais ce n'est pas correct pour les versions.

>>> "2.3.1" > "10.1.1"
True

Je pouvais faire quelques scissions, analyses, casting dans int, etc., et je finirais par trouver une solution de contournement. Mais ceci est Python, pas Java . Existe-t-il un moyen élégant de comparer les chaînes de version?

188
BorrajaX

Utilisez 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_ est un utilitaire tiers, mais il est utilisé par setuptools (il est donc probablement déjà installé) et est conforme à la version actuelle PEP 44 ; il retournera un _packaging.version.Version_ si la version est conforme et un _packaging.version.LegacyVersion_ sinon. Ce dernier sera toujours trié avant les versions valides.


Une ancienne alternative encore utilisée par de nombreux logiciels est distutils.version , intégrée mais non documentée et conforme uniquement à la version remplacée 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'
_

Comme vous pouvez le constater, les versions valides du PEP 440 sont considérées comme "non strictes" et ne correspondent donc pas à la notion moderne de Python.

Comme _distutils.version_ est non documenté, ici est la docstrings pertinente.

292
ecatmur

setuptools définit parse_version(). Ceci implémente PEP 0440 - Identification de version et est également capable d'analyser des versions qui ne suivent pas le PEP. Cette fonction est utilisée par easy_install et pip pour gérer la comparaison de version. De la docs :

Analyser la chaîne de version d'un projet telle que définie par PEP 440. La valeur renvoyée sera un objet qui représente la version. Ces objets peuvent être comparés les uns aux autres et triés. L'algorithme de tri est tel que défini par PEP 440 avec l'ajout que toute version qui n'est pas une version valide de PEP 440 sera considérée comme inférieure à toute version valide de PEP 440 et les versions non valides continueront à trier en utilisant l'algorithme d'origine.

"L'algorithme d'origine" référencé a été défini dans les versions antérieures de la documentation, avant que le PEP 440 n'existe.

Sémantiquement, le format est un croisement approximatif entre les classes StrictVersion et LooseVersion de distutils; si vous lui donnez des versions qui fonctionneraient avec StrictVersion, elles se compareront de la même manière. Sinon, les comparaisons ressemblent davantage à une forme "plus intelligente" de LooseVersion. Il est possible de créer des schémas de codage de version pathologique qui vont tromper cet analyseur, mais ils devraient être très rares dans la pratique.

La documentation fournit quelques exemples:

Si vous voulez être certain que le schéma de numérotation choisi fonctionne comme vous le pensez, vous pouvez utiliser la fonction pkg_resources.parse_version() pour comparer différents numéros de version:

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

Si vous n'utilisez pas setuptools, le projet packaging divise cette fonctionnalité et d'autres fonctionnalités liées à l'emballage dans une bibliothèque séparée.

from packaging import version
version.parse('1.0.3.dev')

from pkg_resources import parse_version
parse_version('1.0.3.dev')
92
davidism
def versiontuple(v):
    return Tuple(map(int, (v.split("."))))

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

Quel est le problème avec la transformation de la chaîne de version en un tuple et à partir de là? Semble assez élégant pour moi

>>> (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 solution de @ kindall est un exemple rapide de la qualité du code.

9
Gabi Purcaru

Le paquet emballage est disponible, ce qui vous permettra de comparer les versions selon PEP-44 , ainsi que les versions héritées.

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

Prise en charge de la version précédente:

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

Comparaison de la version existante avec la version PEP-440.

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

Vous pouvez utiliser le package semver pour déterminer si une version satisfait à une exigence de version sémantique . Ce n'est pas la même chose que comparer deux versions réelles, mais c'est un type de comparaison.

Par exemple, la version 3.6.0 + 1234 devrait être identique à 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
4
Prikkeldraad

Publier toutes mes fonctions sur la base de la solution Kindall. J'ai été en mesure de prendre en charge tous les caractères alphanumériques mélangés aux chiffres en complétant chaque section de version par des zéros non significatifs.

Bien que ce ne soit certainement pas aussi joli que sa fonction one-liner, il semble bien fonctionner avec les numéros de version alphanumériques. (Veillez simplement à définir correctement la valeur zfill(#) si vous avez de longues chaînes dans votre système de gestion des versions.)

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
Phaxmohdem

Comme setuptools le fait, il utilise la fonction pkg_resources.parse_version. Il devrait être PEP44 conforme.

Exemple:

#! /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
1
Tyler Gubala

Je cherchais une solution qui n’ajouterait aucune nouvelle dépendance. Découvrez la solution suivante (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 __== '__main__':
    VersionManager.test_compare_versions()

EDIT: ajout de la variante avec comparaison des tuples. Bien sûr, la variante avec comparaison des tuples est plus intéressante, mais je recherchais la variante avec comparaison des nombres entiers.

0
Stefan Saru