Python'daki sürüm numaralarını nasıl karşılaştırırım?


236

Ben yumurta eklemek için yumurta içeren bir dizin yürüyoruz sys.path. Dizinde aynı .egg'in iki sürümü varsa, yalnızca en son sürümü eklemek istiyorum.

r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$Dosya adından adı ve sürümü ayıklamak için normal bir ifade var . Sorun, bir dize olan sürüm numarasını karşılaştırmaktır 2.3.1.

Dizeleri karşılaştırdığım için, 10'un üstünde 2 çeşit var, ancak bu sürümler için doğru değil.

>>> "2.3.1" > "10.1.1"
True

Bazı bölme, ayrıştırma, int, vb döküm yapabilir ve sonunda bir geçici çözüm elde ediyorum. Ama bu Python, Java değil . Sürüm dizelerini karşılaştırmanın zarif bir yolu var mı?

Yanıtlar:


367

Kullanın 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üçüncü taraf bir yardımcı programdır, ancak setuptools tarafından kullanılır (bu nedenle muhtemelen zaten yüklemişsinizdir) ve mevcut PEP 440 ile uyumludur ; packaging.version.Versionsürüm uyumlu ise a ve değilse döndürür packaging.version.LegacyVersion. İkincisi her zaman geçerli sürümlerden önce sıralanır.

Not : ambalaj son zamanlarda kurulum araçlarına satıldı .


Halen pek çok yazılım tarafından kullanılan eski bir alternatif distutils.version, yerleşik ancak belgelenmemiş ve yalnızca yerini alan PEP 386'ya uygundur ;

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

Gördüğünüz gibi geçerli PEP 440 versiyonlarını “katı değil” olarak görüyor ve bu nedenle modern Python'un geçerli bir versiyonun ne olduğu fikrine uymuyor.

Gibi distutils.versionbelgelenmemiş, burada 'ilgili Docstringler s.


2
Görünüşe göre NormalizedVersion yerine geçmeyecek ve LooseVersion ve StrictVersion artık kullanılmıyor.
Taywee

12
Ağlayan bir utanç distutils.versionbelgelenmemiş.
John Y

arama motorunu kullanarak ve doğrudan version.pykaynak kodunu buldu. Çok güzel koy!
Joël

@ Peki, onlar daha iyi, çünkü PEP 440 uyumlu değiller.
uçan koyun

2
packaging.version.parseSürümleri karşılaştırmak için imho'ya güvenilemez. parse('1.0.1-beta.1') > parse('1.0.0')Örneğin deneyin .
Trondh

104

Ambalaj kütüphanesi için araçları içerir sürümleri ile çalışan ve diğer ambalaj alakalı işlevsellik. Bu, PEP 0440 - Sürüm Tanımlama'yı uygular ve ayrıca PEP'i takip etmeyen sürümleri ayrıştırabilir. Pip ve sürüm ayrıştırma ve karşılaştırma sağlamak için diğer yaygın Python araçları tarafından kullanılır.

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

Bu, daha hafif ve daha hızlı bir paket sağlamak için setuptools ve pkg_resources içindeki orijinal koddan ayrıldı.


Paketleme kitaplığı var olmadan önce, bu işlevsellik, setuptools tarafından sağlanan bir paket olan pkg_resources içinde bulunmuştu (ve hala bulunabilir). Bununla birlikte, artık kurulum araçlarının kurulacağı garanti edilmediğinden (diğer paketleme araçları mevcuttur) ve pkg_resources, içe aktarıldığında ironik bir şekilde çok fazla kaynak kullandığı için bu artık tercih edilmemektedir. Ancak, tüm dokümanlar ve tartışma hala geçerlidir.

Gönderen parse_version()docs :

Bir projenin sürüm dizesini PEP 440 tarafından tanımlanan şekilde çözümledi. Döndürülen değer sürümü temsil eden bir nesne olacaktır. Bu nesneler birbirleriyle karşılaştırılabilir ve sıralanabilir. Sıralama algoritması, geçerli bir PEP 440 sürümü olmayan herhangi bir sürümün herhangi bir geçerli PEP 440 sürümünden daha az kabul edileceği ve geçersiz sürümlerin orijinal algoritmayı kullanarak sıralamaya devam edeceği ek olarak PEP 440 tarafından tanımlandığı gibidir.

Referans verilen "orijinal algoritma" PEP 440 mevcut olmadan dokümanların eski versiyonlarında tanımlanmıştır.

Anlamsal olarak, biçim, dağıtımcılar StrictVersionve LooseVersionsınıflar arasında kabaca bir çarpıdır ; eğer onunla çalışacak versiyonlar verirseniz StrictVersion, aynı şekilde karşılaştırırlar. Aksi takdirde, karşılaştırmalar daha "akıllı" bir biçime benzer LooseVersion. Bu ayrıştırıcıyı kandıracak patolojik versiyon kodlama şemaları oluşturmak mümkündür, ancak pratikte çok nadir olmalıdırlar.

Dokümantasyon bazı örnekler verilmiştir:

Seçtiğiniz numaralandırma şemasının düşündüğünüz şekilde çalıştığından emin olmak istiyorsanız, pkg_resources.parse_version() işlevi farklı sürüm numaralarını karşılaştırmak için kullanabilirsiniz :

>>> 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
Diğer cevaplar standart kütüphanedir ve PEP standartlarına uygundur.
Chris

1
Bu durumda map(), sonucu zaten dizeler split()olduğu için işlevi tamamen kaldırabilirsiniz . Ama yine de bunu yapmak istemezsiniz, çünkü onları değiştirmenin tüm nedeni , sayılarla düzgün bir şekilde karşılaştırılmalarıdır. Aksi takdirde . int"10" < "2"
kindall

6
Bu gibi bir şey için başarısız olacaktır versiontuple("1.0") > versiontuple("1"). Sürümler aynı, ancak oluşturulan tuples(1,)!=(1,0)
dawg

3
Sürüm 1 ve sürüm 1.0 ne anlamda aynıdır? Sürüm numaraları kayan değil.
tür

12
Hayır, bu olmalıdır değil kabul cevap. Neyse ki öyle değil. Sürüm belirteçlerinin güvenilir ayrıştırılması genel durumda önemsiz değildir (pratik olarak mümkün değilse). Tekerleği yeniden icat etmeyin ve kırmaya devam edin. As ecatmur anlaşılacağı yukarıdaki , sadece kullanmak distutils.version.LooseVersion. Bunun için orada.
Cecil Curry

12

Sürüm dizesini bir demet haline getirip oradan gitmenin nesi yanlış? Benim için yeterince zarif görünüyor

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

@ kindall'ın çözümü, kodun ne kadar iyi görüneceğine hızlı bir örnektir.


1
Bence bu cevap, bir PEP440 dizesinin bir tuple dönüşümünü gerçekleştiren kod sağlayarak genişletilebilir . Bence bu önemsiz bir görev değil. Bence bu çeviriyi yapan pakete bırakılsa iyi setuptoolsolur pkg_resources.

@TylerGubala bu sürümün "basit" olduğunu ve her zaman olacağını bildiğiniz durumlarda harika bir cevaptır. pkg_resources büyük bir pakettir ve dağıtılmış bir yürütülebilir dosyanın oldukça şişirilmesine neden olabilir.
Erik Aronesty

@Erik Aronesty Bence dağıtılmış yürütülebilir dosyaların içindeki sürüm kontrolü, sorunun kapsamının biraz dışındadır, ama genel olarak en azından katılıyorum. Yine de, yeniden kullanılabilirliği hakkında söylenecek bir şey pkg_resourcesolduğunu ve basit paket adlandırma varsayımlarının her zaman ideal olmayabileceğini düşünüyorum.

Emin olmak sys.version_info > (3, 6)ya da her şeyi yapmak için harika çalışıyor .
Gqqnbig

7

Orada ambalaj sen gereğince sürümlerini karşılaştırmak sağlayacak kullanılabilir paketi, PEP-440 , yanı sıra eski sürümleri.

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

Eski sürüm desteği:

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

Eski sürüm ile PEP-440 sürümünün karşılaştırılması.

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

3
Arasındaki fark hakkında merak edenler için packaging.version.Versionve packaging.version.parse: "[ version.parse] sürüm dize alır ve bir olarak ayrıştırmak olacak Version, aksi takdirde olarak ayrıştırmak olacak, sürüm geçerli bir PEP 440 versiyonu ise LegacyVersion." (oysa version.Versionartacaktır InvalidVersion; kaynak )
Braham Snyder

5

Bir sürümün semantik sürüm gereksinimini karşılayıp karşılamadığını belirlemek için semver paketini kullanabilirsiniz . Bu, iki gerçek sürümü karşılaştırmakla aynı şey değildir, ancak bir tür karşılaştırmadır.

Örneğin, 3.6.0 + 1234 sürümleri 3.6.0 ile aynı olmalıdır.

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

Kindall'ın çözümüne dayanarak tüm fonksiyonumu yayınlıyorum. Her sürüm bölümünü baştaki sıfırlarla doldurarak rakamlarla karıştırılmış alfasayısal karakterleri destekleyebildim.

Kesinlikle tek katmanlı işlevi kadar güzel olmasa da, alfa-sayısal sürüm numaraları ile iyi çalışıyor gibi görünüyor. ( zfill(#)Sürüm sisteminizde uzun dizeler varsa değeri uygun şekilde ayarladığınızdan emin olun .)

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

Bunu setuptoolsyaptığı gibi, pkg_resources.parse_versionişlevi kullanır . PEP440 uyumlu olmalıdır .

Misal:

#! /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_resourcessetuptoolsbağlı olan bir parçasıdır packaging. packaging.version.parseAynı uygulaması olan tartışan diğer cevaplara bakın pkg_resources.parse_version.
Jed

0

Yeni bağımlılıklar eklemeyecek bir çözüm arıyordum. Aşağıdaki (Python 3) çözümüne göz atın:

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: grup karşılaştırması ile varyant eklendi. Tabii ki tuple karşılaştırmalı varyant daha güzel, ama tamsayı karşılaştırmalı varyantı arıyordum


Bu durumun bağımlılık eklemekten kaçındığını merak ediyorum. Bir python paketi oluşturmak için ambalaj kütüphanesine (setuptools tarafından kullanılan) ihtiyacınız olmayacak mı?
Josiah L.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.