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?
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.
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
etLooseVersion
de distutils; si vous lui donnez des versions qui fonctionneraient avecStrictVersion
, elles se compareront de la même manière. Sinon, les comparaisons ressemblent davantage à une forme "plus intelligente" deLooseVersion
. 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')
def versiontuple(v):
return Tuple(map(int, (v.split("."))))
>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
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.
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
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
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
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
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.