Bagaimana cara membandingkan nomor versi dengan Python?

236

Saya sedang berjalan direktori yang berisi telur untuk menambahkan telur ke sys.path. Jika ada dua versi .egg yang sama di direktori, saya hanya ingin menambahkan yang terbaru.

Saya memiliki ekspresi reguler r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$untuk mengekstrak nama dan versi dari nama file. Masalahnya adalah membandingkan nomor versi, yang merupakan string seperti 2.3.1.

Karena saya membandingkan string, 2 macam di atas 10, tapi itu tidak benar untuk versi.

>>> "2.3.1" > "10.1.1"
True

Saya bisa melakukan beberapa pemisahan, penguraian, casting ke int, dll, dan saya akhirnya akan mendapatkan solusi. Tapi ini Python, bukan Java . Apakah ada cara yang elegan untuk membandingkan string versi?

BorrajaX
sumber

Jawaban:

367

Gunakan 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.parseadalah utilitas pihak ketiga tetapi digunakan oleh setuptools (jadi Anda mungkin sudah menginstalnya) dan sesuai dengan PEP 440 saat ini ; itu akan mengembalikan packaging.version.Versionjika versi tersebut sesuai dan packaging.version.LegacyVersionjika tidak. Yang terakhir akan selalu mengurutkan sebelum versi yang valid.

Catatan : kemasan baru-baru ini dibatalkan ke dalam setuptools .


Alternatif kuno yang masih digunakan oleh banyak perangkat lunak adalah distutils.version, dibangun tetapi tidak berdokumen dan hanya sesuai dengan PEP 386 yang digantikan ;

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

Seperti yang Anda lihat itu melihat versi PEP 440 yang valid sebagai "tidak ketat" dan karenanya tidak cocok dengan gagasan Python modern tentang apa versi yang valid.

Seperti distutils.versiontidak berdokumen, inilah dokumen yang relevan.

ecatmur
sumber
2
Sepertinya NormalizedVersion tidak akan datang, seperti yang digantikan, dan LooseVersion dan StrictVersion karenanya tidak lagi ditinggalkan.
Taywee
12
Sayang sekali menangis distutils.versiontidak berdokumen.
John Y
menemukannya menggunakan mesin pencari, dan menemukan langsung version.pykode sumber. Sangat bagus!
Joël
@ Talay mereka lebih baik, karena mereka tidak memenuhi PEP 440.
domba terbang
2
saya packaging.version.parsetidak bisa dipercaya untuk membandingkan versi. Coba parse('1.0.1-beta.1') > parse('1.0.0')misalnya.
Trondh
104

The kemasan perpustakaan berisi utilitas untuk bekerja dengan versi dan fungsi-terkait kemasan lainnya. Ini mengimplementasikan PEP 0440 - Identifikasi Versi dan juga dapat mengurai versi yang tidak mengikuti PEP. Ini digunakan oleh pip, dan alat Python umum lainnya untuk menyediakan parsing dan perbandingan versi.

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

Ini dipisahkan dari kode asli di setuptools dan pkg_resources untuk menyediakan paket yang lebih ringan dan lebih cepat.


Sebelum pustaka pengemasan ada, fungsi ini (dan masih dapat) ditemukan di pkg_resources, paket yang disediakan oleh setuptools. Namun, ini tidak lagi disukai karena setuptools tidak lagi dijamin untuk diinstal (alat kemasan lain ada), dan pkg_resources ironisnya menggunakan sumber daya yang cukup banyak ketika diimpor. Namun, semua dokumen dan diskusi masih relevan.

Dari parse_version()dokumen :

Parsing string versi proyek seperti yang didefinisikan oleh PEP 440. Nilai yang dikembalikan akan menjadi objek yang mewakili versi. Objek-objek ini dapat dibandingkan satu sama lain dan diurutkan. Algoritma pengurutan seperti yang didefinisikan oleh PEP 440 dengan tambahan bahwa setiap versi yang bukan versi PEP 440 yang valid akan dianggap kurang dari versi PEP 440 yang valid dan versi yang tidak valid akan terus menyortir menggunakan algoritma asli.

"Algoritma asli" yang dirujuk didefinisikan dalam versi dokumen yang lebih lama, sebelum PEP 440 ada.

Secara semantik, formatnya adalah persilangan kasar antara distutil StrictVersiondan LooseVersionkelas; jika Anda memberikan versi yang dapat digunakan StrictVersion, maka mereka akan membandingkan dengan cara yang sama. Kalau tidak, perbandingan lebih seperti bentuk "cerdas" LooseVersion. Dimungkinkan untuk membuat skema pengkodean versi patologis yang akan membodohi pengurai ini, tetapi dalam praktiknya sangat jarang.

The dokumentasi memberikan beberapa contoh:

Jika Anda ingin memastikan bahwa skema penomoran pilihan Anda berfungsi seperti yang Anda pikirkan, Anda dapat menggunakan pkg_resources.parse_version() fungsi ini untuk membandingkan nomor versi yang berbeda:

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

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
baik hati
sumber
10
Jawaban lainnya ada di perpustakaan standar dan mengikuti standar PEP.
Chris
1
Dalam hal ini Anda dapat menghapus map()fungsi sepenuhnya, karena hasilnya sudahsplit() berupa string. Tetapi Anda tidak ingin melakukan itu, karena seluruh alasan untuk mengubahnya adalah agar mereka membandingkannya dengan benar sebagai angka. Jika tidak . int"10" < "2"
kindall
6
Ini akan gagal untuk sesuatu seperti versiontuple("1.0") > versiontuple("1"). Versi-versinya sama, tetapi tupel dibuat(1,)!=(1,0)
dawg
3
Dalam arti apakah versi 1 dan versi 1.0 sama? Nomor versi tidak mengapung.
hati
12
Tidak, ini seharusnya bukan jawaban yang diterima. Untungnya tidak. Penguraian penentu versi yang andal adalah non-sepele (jika tidak praktis tidak layak) dalam kasus umum. Jangan menemukan kembali roda dan kemudian lanjutkan untuk merusaknya. Seperti yang disarankan ecatmur di atas , gunakan saja distutils.version.LooseVersion. Untuk itulah ia ada.
Cecil Curry
12

Apa yang salah dengan mengubah string versi menjadi tuple dan beralih dari sana? Tampak cukup elegan untukku

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

Solusi @ kindall adalah contoh cepat tentang seberapa baik kode akan terlihat.

Gabi Purcaru
sumber
1
Saya pikir jawaban ini dapat diperluas dengan menyediakan kode yang melakukan transformasi dari string PEP440 menjadi sebuah tuple. Saya pikir Anda akan menemukan itu bukan tugas yang sepele. Saya pikir lebih baik diserahkan kepada paket yang melakukan terjemahan itusetuptools , yaitu pkg_resources.
@ TylerGubala ini adalah jawaban yang bagus dalam situasi di mana Anda tahu bahwa versinya selalu dan akan "sederhana". pkg_resources adalah paket besar dan dapat menyebabkan executable terdistribusi menjadi agak membengkak.
Erik Aronesty
@Erik Aronesty Saya pikir kontrol versi di dalam executable terdistribusi agak berlebihan dari ruang lingkup pertanyaan, tapi saya setuju, umumnya setidaknya. Saya pikir meskipun ada sesuatu yang bisa dikatakan tentang usabilitas ulang pkg_resources, dan bahwa asumsi penamaan paket sederhana mungkin tidak selalu ideal.
Ini bekerja dengan baik untuk memastikan sys.version_info > (3, 6)atau apa pun.
Gqqnbig
7

Ada paket kemasan yang tersedia, yang akan memungkinkan Anda untuk membandingkan versi sesuai PEP-440 , serta versi lama.

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

Dukungan versi lama:

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

Membandingkan versi lawas dengan versi PEP-440.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
sashk
sumber
3
Bagi mereka yang bertanya-tanya tentang perbedaan antara packaging.version.Versiondan packaging.version.parse: "[ version.parse] mengambil string versi dan akan menguraikannya sebagai Versionjika versi tersebut adalah versi PEP 440 yang valid, jika tidak maka akan menguraikannya sebagai LegacyVersion." (padahal version.Versionakan memunculkan InvalidVersion; sumber )
Braham Snyder
5

Anda dapat menggunakan paket semver untuk menentukan apakah suatu versi memenuhi persyaratan versi semantik . Ini tidak sama dengan membandingkan dua versi aktual, tetapi merupakan jenis perbandingan.

Misalnya, versi 3.6.0 + 1234 harus sama dengan 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
sumber
3

Memposting fungsi lengkap saya berdasarkan solusi Kindall. Saya dapat mendukung setiap karakter alfanumerik yang dicampur dengan angka-angka dengan mengisi setiap bagian versi dengan angka nol di depan.

Meskipun tentu saja tidak secantik fungsi one-liner-nya, tampaknya berfungsi dengan baik dengan nomor versi alpha-numeric. (Pastikan untuk menetapkan zfill(#)nilai dengan tepat jika Anda memiliki string panjang di sistem versi Anda.)

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

Cara setuptoolsmelakukannya, ia menggunakan pkg_resources.parse_versionfungsi. Itu harus PEP440 sesuai dengan .

Contoh:

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

sumber
pkg_resourcesadalah bagian dari setuptools, yang tergantung pada packaging. Lihat jawaban lain yang membahas packaging.version.parse, yang memiliki implementasi yang identik dengan pkg_resources.parse_version.
Jed
0

Saya sedang mencari solusi yang tidak akan menambah dependensi baru. Lihat solusi (Python 3) berikut:

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: menambahkan varian dengan perbandingan tuple. Tentu saja varian dengan perbandingan tuple lebih bagus, tetapi saya mencari varian dengan perbandingan integer

Stefan Saru
sumber
Saya ingin tahu dalam situasi apa hal ini menghindari penambahan ketergantungan? Tidakkah Anda memerlukan pustaka pengemasan (digunakan oleh setuptools) untuk membuat paket python?
Josiah L.