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