파이썬에서 버전 번호를 비교하려면 어떻게 해야 하나요?
에그들을 추가하기 위해 에그들을 포함하는 디렉토리를 걷고 있다. 만약 디렉토리에 같은 .egg의 두 버전이 있다면, 나는 최신 버전만 추가하고 싶다.
나는 파일 이름에서 이름과 버전을 추출하는 정규 표현을 가지고 있다. 문제는 와 같은 문자열인 버전 번호를 비교하는 것입니다.
제가 스트링을 비교하니까 10보다 2개가 더 높게 정렬되는데, 그건 버전에 맞지 않아요.
>>> "2.3.1" > "10.1.1"
True
분할, 파싱, 캐스팅, int 등을 할 수 있었고 결국 해결 방법을 찾을 수 있었습니다. 하지만 이것은 파이썬입니다. 버전 문자열을 비교할 수 있는 우아한 방법이 있을까요?
버전 문자열을 튜플(tuple)로 변환하고 거기서부터 가는 게 뭐가 문제야? 나에게 충분히 우아해 보인다.
>>> (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's solution is a quick example of how good the code would look.
def versiontuple(v):
return tuple(map(int, (v.split("."))))
>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
>>> # pip install packaging
>>> 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
is a third-party utility but is used by setuptools (so you probably already have it installed) and is conformant to the current PEP 440; it will return a packaging.version.Version
if the version is compliant and a packaging.version.LegacyVersion
if not. The latter will always sort before valid versions.
Note: packaging has recently been vendored into setuptools.
An ancient and now deprecated method you might encounter is distutils.version
, it's undocumented and conforms only to the superseded 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'
As you can see it sees valid PEP 440 versions as “not strict” and therefore doesn’t match modern Python’s notion of what a valid version is.
As distutils.version
is undocumented, here are the relevant docstrings.
The packaging library contains utilities for working with versions and other packaging-related functionality. This implements PEP 0440 -- Version Identification and is also able to parse versions that don't follow the PEP. It is used by pip, and other common Python tools to provide version parsing and comparison.
$ pip install packaging
from packaging.version import parse as parse_version
version = parse_version('1.0.3.dev')
This was split off from the original code in setuptools and pkg_resources to provide a more lightweight and faster package.
Before the packaging library existed, this functionality was (and can still be) found in pkg_resources, a package provided by setuptools. However, this is no longer preferred as setuptools is no longer guaranteed to be installed (other packaging tools exist), and pkg_resources ironically uses quite a lot of resources when imported. However, all the docs and discussion are still relevant.
From the parse_version()
docs:
Parsed a project's version string as defined by PEP 440. The returned value will be an object that represents the version. These objects may be compared to each other and sorted. The sorting algorithm is as defined by PEP 440 with the addition that any version which is not a valid PEP 440 version will be considered less than any valid PEP 440 version and the invalid versions will continue sorting using the original algorithm.
The "original algorithm" referenced was defined in older versions of the docs, before PEP 440 existed.
Semantically, the format is a rough cross between distutils'
StrictVersion
andLooseVersion
classes; if you give it versions that would work withStrictVersion
, then they will compare the same way. Otherwise, comparisons are more like a "smarter" form ofLooseVersion
. It is possible to create pathological version coding schemes that will fool this parser, but they should be very rare in practice.
The documentation provides some examples:
If you want to be certain that your chosen numbering scheme works the way you think it will, you can use the
pkg_resources.parse_version()
function to compare different version numbers:>>> 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
There is packaging package available, which will allow you to compare versions as per PEP-440, as well as legacy versions.
>>> 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
Legacy version support:
>>> LegacyVersion('1.2.8.5-5-gdeadbeef')
<LegacyVersion('1.2.8.5-5-gdeadbeef')>
Comparing legacy version with PEP-440 version.
>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
Posting my full function based on Kindall's solution. I was able to support any alphanumeric characters mixed in with the numbers by padding each version section with leading zeros.
While certainly not as pretty as his one-liner function, it seems to work well with alpha-numeric version numbers. (Just be sure to set the zfill(#)
value appropriately if you have long strings in your versioning system.)
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
You can use the semver package to determine if a version satisfies a semantic version requirement. This is not the same as comparing two actual versions, but is a type of comparison.
For example, version 3.6.0+1234 should be the same as 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
I was looking for a solution which wouldn't add any new dependencies. Check out the following (Python 3) solution:
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: added variant with tuple comparison. Of course the variant with tuple comparison is nicer, but I was looking for the variant with integer comparison
The way that setuptools
does it, it uses the pkg_resources.parse_version
function. It should be PEP440 compliant.
Example:
#! /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
... and getting back to easy ... for simple scripts you can use:
import sys
needs = (3, 9) # or whatever
pvi = sys.version_info.major, sys.version_info.minor
later in your code
try:
assert pvi >= needs
except:
print("will fail!")
# etc.
similar to standard strverscmp and similar to this solution by Mark Byers but using findall instead of split to avoid empty case.
import re
num_split_re = re.compile(r'([0-9]+|[^0-9]+)')
def try_int(i, fallback=None):
try:
return int(i)
except ValueError:
pass
except TypeError:
pass
return fallback
def ver_as_list(a):
return [try_int(i, i) for i in num_split_re.findall(a)]
def strverscmp_lt(a, b):
a_ls = ver_as_list(a)
b_ls = ver_as_list(b)
return a_ls < b_ls
Here is something that will work assuming your semantic versions are "clean" (e.g. x.x.x
) and you have a list of versions you need to sort.
# Here are some versions
versions = ["1.0.0", "1.10.0", "1.9.0"]
# This does not work
versions.sort() # Result: ['1.0.0', '1.10.0', '1.9.0']
# So make a list of tuple versions
tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]
# And sort the string list based on the tuple list
versions = [x for _, x in sorted(zip(tuple_versions, versions))] # Result: ['1.0.0', '1.9.0', '1.10.0']
To get the latest version you could just select the last element in the list versions[-1]
or reverse sort by using the reverse
attribute of sorted()
, setting it to True
, and getting the [0]
element.
You could of course then wrap all this up in a convenient function for reuse.
def get_latest_version(versions):
"""
Get the latest version from a list of versions.
"""
try:
tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]
versions = [x for _, x in sorted(zip(tuple_versions, versions), reverse=True)]
latest_version = versions[0]
except Exception as e:
print(e)
latest_version = None
return latest_version
print(get_latest_version(["1.0.0", "1.10.0", "1.9.0"]))
To increment version using python
def increment_version(version):
version = version.split('.')
if int(version[len(version) - 1]) >= 99:
version[len(version) - 1] = '0'
version[len(version) - 2] = str(int(version[len(version) - 2]) + 1)
else:
version[len(version) - 1] = str(int(version[len(version) - 1]) + 1)
return '.'.join(version)
version = "1.0.0"
version_type_2 = "1.0"
print("old version",version ,"new version",increment_version(version))
print("old version",version_type_2 ,"new version",increment_version(version_type_2))
This is a compact code for comparing three version numbers. Note that the string comparison fails for all pairs here.
from itertools import permutations
for v1, v2 in permutations(["3.10.21", "3.10.3", "3.9.9"], 2):
print(f"\nv1 = {v1}, v2 = {v2}")
print(f"v1 < v2 version.parse(v1) < version.parse(v2)")
print(f"{v1 < v2} {version.parse(v1) < version.parse(v2)}")
That gives us:
v1='3.10.21', v2='3.10.3'
v1 < v2 version.parse(v1) < version.parse(v2)
True False
v1='3.10.21', v2='3.9.9'
v1 < v2 version.parse(v1) < version.parse(v2)
True False
v1='3.10.3', v2='3.10.21'
v1 < v2 version.parse(v1) < version.parse(v2)
False True
v1='3.10.3', v2='3.9.9'
v1 < v2 version.parse(v1) < version.parse(v2)
True False
v1='3.9.9', v2='3.10.21'
v1 < v2 version.parse(v1) < version.parse(v2)
False True
v1='3.9.9', v2='3.10.3'
v1 < v2 version.parse(v1) < version.parse(v2)
False True
permutations(iterable, 2)
gives us all the 2-length permutations of an iterable. So for example
list(permutations('ABC', 2))
gives us [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
.
If you want to create a filter on a library version, you may use the __version__
attribute (here an example with the jwt library):
from packaging import version
import jwt
if version.parse(jwt.__version__) < version.parse('2.0.0'):
# TODO: your code
'개발하자' 카테고리의 다른 글
유형스크립트 키-값 관계를 보존하는 Object.entries 유형 (0) | 2022.11.12 |
---|---|
TypeScript를 사용하는 React 구성 요소의 기본 속성 값 (0) | 2022.11.12 |
Event.target이 Typescript에서 Element가 아닌 이유는 무엇입니까? (0) | 2022.11.11 |
Kubernetes 메트릭-서버 검색 확인 실패 (0) | 2022.11.11 |
선언 또는 문에 javascript/typescript가 필요합니다. (0) | 2022.11.11 |