vxSDK/vxsdk/core/pkg/version.py

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