217 lines
6.9 KiB
Python
217 lines
6.9 KiB
Python
import os
|
|
import re
|
|
import subprocess
|
|
|
|
__all__ = [
|
|
'VxVersion',
|
|
'version_is_valid',
|
|
'version_get'
|
|
]
|
|
|
|
class VxVersion(object):
|
|
r"""Represent a Version object.
|
|
|
|
This version mecanism is a strong part of the package managing because If
|
|
the 'version' is detected to respect the correct semantic versioning, you
|
|
can perform version operations: caret (^), tilde (~) and wildcard (*)
|
|
|
|
Caret requirements (^)
|
|
^1.2.3 := >=1.2.3, <2.0.0
|
|
^1.2 := >=1.2.0, <2.0.0
|
|
^1 := >=1.0.0, <2.0.0
|
|
^0.2.3 := >=0.2.3, <0.3.0
|
|
^0.2 := >=0.2.0, <0.3.0
|
|
^0.0.3 := >=0.0.3, <0.0.4
|
|
^0.0 := >=0.0.0, <0.1.0
|
|
^0 := >=0.0.0, <1.0.0
|
|
|
|
Tilde requirements (~)
|
|
~1.2.3 := >=1.2.3, <1.3.0
|
|
~1.2 := >=1.2.0, <1.3.0
|
|
~1 := >=1.0.0, <2.0.0
|
|
|
|
Wildcard requirements (*)
|
|
* := >=0.0.0
|
|
1.* := >=1.0.0, <2.0.0
|
|
1.2.* := >=1.2.0, <1.3.0
|
|
|
|
Note that it's possible that a package can have two same versions (one
|
|
for branch name and another for a tag), by default, the tag is always
|
|
selected, but here the display information will explicitly describe if
|
|
the version is a tag and / or a branch.
|
|
|
|
|
|
TODO:
|
|
> Use 'releases' section too ?
|
|
> Define explicitely version type (tag, branch, releases ?)
|
|
"""
|
|
def __init__(self, name, type=None, source=None):
|
|
self._type = type
|
|
self._name = name
|
|
self._source_list = [source]
|
|
|
|
def __repr__(self):
|
|
return '<' + self._name + ',(' + self._type + ')>'
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
@property
|
|
def type(self):
|
|
return self._type
|
|
@property
|
|
def sources(self):
|
|
return self._source_list
|
|
|
|
|
|
def validate(self, pattern):
|
|
r""" Try to check if the version validate the pattern operation.
|
|
|
|
As explainned in the class docstring, we can have 3 type of operation,
|
|
caret (^), tilde (~) and wildcard (*) and the pattern should follow the
|
|
semantic requirement for each operator.
|
|
|
|
Note that the pattern can be None, in this case, the validation step is
|
|
always success. Beside, the pattern can be a non-valid version semantic
|
|
request, in this case the version name should match the entiere pattern.
|
|
|
|
Args:
|
|
> pattern (None, str) - pattern used to check if the version is valid
|
|
|
|
Return:
|
|
> True if the version validate the pattern, False otherwise
|
|
"""
|
|
return version_is_valid(self.name, pattern)
|
|
|
|
def compare(self, version):
|
|
r"""Compare two version
|
|
|
|
This methods will compare versionning inforation and return an interger
|
|
indicating the result of the comparison, as follow:
|
|
|
|
> 0 - the two version match
|
|
> a negative value - self is less than 'version'
|
|
> a positive value - self is greater than 'version'
|
|
"""
|
|
try:
|
|
s1 = tuple([int(d) for d in self.name.split(".") if d] + [0,0,0])[:3]
|
|
s2 = tuple([int(d) for d in version.name.split(".") if d] + [0,0,0])[:3]
|
|
|
|
if s1[0] - s2[0] != 0: return s1[0] - s2[0]
|
|
if s1[1] - s2[1] != 0: return s1[1] - s2[1]
|
|
return s1[2] - s2[2]
|
|
except Exception as _:
|
|
return 0
|
|
|
|
def addSource(self, source):
|
|
self._source_list.append(source)
|
|
|
|
|
|
def version_is_valid(version, pattern):
|
|
r"""Check if the version validate a pattern
|
|
|
|
If the 'version' is detected to respect the correct semantic versioning, you
|
|
can perform version operations: caret (^), tilde (~) and wildcard (*)
|
|
|
|
Caret requirements (^)
|
|
^1.2.3 := >=1.2.3, <2.0.0
|
|
^1.2 := >=1.2.0, <2.0.0
|
|
^1 := >=1.0.0, <2.0.0
|
|
^0.2.3 := >=0.2.3, <0.3.0
|
|
^0.2 := >=0.2.0, <0.3.0
|
|
^0.0.3 := >=0.0.3, <0.0.4
|
|
^0.0 := >=0.0.0, <0.1.0
|
|
^0 := >=0.0.0, <1.0.0
|
|
|
|
Tilde requirements (~)
|
|
~1.2.3 := >=1.2.3, <1.3.0
|
|
~1.2 := >=1.2.0, <1.3.0
|
|
~1 := >=1.0.0, <2.0.0
|
|
|
|
Wildcard requirements (*)
|
|
* := >=0.0.0
|
|
1.* := >=1.0.0, <2.0.0
|
|
1.2.* := >=1.2.0, <1.3.0
|
|
|
|
Note that if the pattern argument is None, then the version is always
|
|
validate and the function will return True
|
|
|
|
Args:
|
|
> version (str) - version to check
|
|
> pattern (str, None) - version pattern
|
|
|
|
Return:
|
|
> True if the version validate the pattern, False otherwise
|
|
"""
|
|
if not pattern:
|
|
return True
|
|
|
|
# Parse a version string of the form "M", "M.m", or "M.m.p" into a triplet
|
|
def _parse_version(version_string):
|
|
digits = [int(d) for d in version_string.split(".") if d]
|
|
return tuple(digits + [0,0,0])[:3]
|
|
|
|
# Index of first nonzero component
|
|
def _first_nonzero(v, default):
|
|
return next((i for i, value in enumerate(v) if value), default)
|
|
|
|
# Increment at specified position
|
|
def _increment_at(v, position):
|
|
padding = len(v) - position - 1
|
|
return tuple(list(v[:position]) + [v[position]+1] + [0] * padding)
|
|
|
|
# Parse a spec like ^1.2.3, ~1.2 or 1.2.* into a min/max pair
|
|
def _parse_spec(spec):
|
|
regex = re.compile(r'[~^]?(\d+\.){,2}\d+|(\d+\.){,2}\*')
|
|
if not regex.fullmatch(spec):
|
|
return None
|
|
|
|
spec_length = spec.count(".") + 1
|
|
v = _parse_version(
|
|
spec.replace("^","").replace("~","").replace("*","")
|
|
)
|
|
M, m, p = v
|
|
|
|
if spec[0] == '^':
|
|
return v, _increment_at(v, _first_nonzero(v, spec_length-1))
|
|
elif spec[0] == '~':
|
|
return v, _increment_at(v, min(spec_length-1, 1))
|
|
elif spec == "*":
|
|
return ((0, 0, 0), None)
|
|
elif "*" in spec:
|
|
return v, _increment_at(v, spec_length-2)
|
|
else:
|
|
return (M,m,p), (M,m,p+1)
|
|
|
|
# Check if version is between requested bounds
|
|
def _version_is_suitable(version, min_version, max_version):
|
|
return (min_version is None or min_version <= version) \
|
|
and (max_version is None or version < max_version)
|
|
|
|
# function entry
|
|
try:
|
|
pair = _parse_spec(pattern)
|
|
if not pair:
|
|
return version == pattern
|
|
fmt_version = _parse_version(version)
|
|
return _version_is_suitable(fmt_version, pair[0], pair[1])
|
|
except:
|
|
return False
|
|
|
|
def version_get(path):
|
|
saved_pwd = os.getcwd()
|
|
os.chdir(path)
|
|
ret = subprocess.run(
|
|
['git', 'describe', '--tags', '--exact-match'],
|
|
capture_output=True, check=False
|
|
)
|
|
if ret.returncode != 0:
|
|
ret = subprocess.run(
|
|
['git', 'branch', '--show-current'],
|
|
capture_output=True, check=False
|
|
)
|
|
os.chdir(saved_pwd)
|
|
if ret.returncode != 0:
|
|
return 'unknown'
|
|
return ret.stdout.decode("utf8").strip()
|