import re
from ..errors import InvalidSemanticVersion
from ..utils.py3compat import long
_SEMVER_R = re.compile("""\
(?P<major>\d+)
\.
(?P<minor>\d+)
\.
(?P<patch>\d+)
(?P<pre_release>-[0-9a-zA-Z-\.]+)?
(?P<build>\+[0-9a-zA-Z-\.]+)?
$
""", flags=re.VERBOSE)
_PART_R = re.compile("[0-9a-zA-Z-]+")
def _ensure_no_leading_zero(value, name):
if len(value) > 1 and value.startswith("0"):
msg = "{0} number cannot have leading 0: {1!r}".format(name, value)
raise ValueError(msg)
def _parse_pre_release(s):
if s is not None:
# Remove `-` or `+`
without_prefix_s = s[1:]
return tuple(part for part in without_prefix_s.split("."))
else:
return None
def _parse_build(s):
return _parse_pre_release(s)
def _convert_pre_release(part):
try:
value = int(part)
except ValueError:
return part
else:
_ensure_no_leading_zero(part, "Pre release part")
return value
class _PrereleaseParts(object):
""" Private class used to compare the pre release and build parts. We need
this as an empty tuple need to compare greated than any non empty tuple.
"""
def __init__(self, parts):
self._comparable_parts = tuple(_convert_pre_release(p) for p in parts)
def _compare_parts(self, left_parts, right_parts):
for left, right in zip(left_parts, right_parts):
if left == right:
continue
else:
is_left_int = isinstance(left, (long, int))
is_right_int = isinstance(right, (long, int))
if is_left_int:
if is_right_int:
return left < right
else:
return True
else:
if is_right_int:
return False
else:
return left < right
return len(left_parts) < len(right_parts)
def __hash__(self):
return hash(self._comparable_parts)
def __eq__(self, other):
assert isinstance(other, self.__class__)
return self._comparable_parts == other._comparable_parts
def __ne__(self, other):
return not (self == other)
def __lt__(self, other):
assert isinstance(other, self.__class__)
if self._comparable_parts == other._comparable_parts:
return False
elif len(self._comparable_parts) == 0:
return False
elif len(other._comparable_parts) == 0:
return True
else:
return self._compare_parts(
self._comparable_parts, other._comparable_parts
)
def __le__(self, other):
return self == other or self < other
def __gt__(self, other):
return not (self <= other)
def __ge__(self, other):
return not (self < other)
[docs]class SemanticVersion(object):
""" 'Semver' 2.0 implementation.
This class takes care of parsing and comparing semver objects.
"""
@classmethod
def from_string(cls, s):
m = _SEMVER_R.match(s)
if m is None:
raise InvalidSemanticVersion(s)
else:
d = m.groupdict()
major = d["major"]
minor = d["minor"]
patch = d["patch"]
pre_release = _parse_pre_release(d["pre_release"])
build = _parse_build(d["build"])
try:
_ensure_no_leading_zero(major, "Major")
_ensure_no_leading_zero(minor, "Minor")
_ensure_no_leading_zero(patch, "Patch")
return cls(int(major), int(minor), int(patch), pre_release, build)
except ValueError as exc:
msg = "Invalid semantic version '{0}' ({1})".format(
s, str(exc)
)
raise InvalidSemanticVersion(s, msg)
def __init__(self, major, minor, patch, pre_release=None, build=None):
""" Private constructor, use one of the from_ ctors instead.
"""
self.major = major
self.minor = minor
self.patch = patch
self.pre_release = pre_release or tuple()
self.build = build or tuple()
# We cache _comparable_parts to avoid paying the relatively high cost
# of parsing the pre release parts for versions objects that won't be
# compared.
self._comparable_parts_value = None
@property
def _comparable_parts(self):
if self._comparable_parts_value is None:
self._comparable_parts_value = (
self.major, self.minor, self.patch,
_PrereleaseParts(self.pre_release),
)
return self._comparable_parts_value
def __hash__(self):
return hash(self._comparable_parts)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
else:
return self._comparable_parts == other._comparable_parts
def __ne__(self, other):
return not self == other
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
else:
return self._comparable_parts < other._comparable_parts
def __le__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
else:
return self._comparable_parts <= other._comparable_parts
def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
else:
return self._comparable_parts > other._comparable_parts
def __ge__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
else:
return self._comparable_parts >= other._comparable_parts
def __str__(self):
s = "{0}.{1}.{2}".format(self.major, self.minor, self.patch)
if len(self.pre_release) > 0:
s += "-" + ".".join(str(v) for v in self.pre_release)
if len(self.build) > 0:
s += "+" + ".".join(str(v) for v in self.build)
return s
def __repr__(self):
return "SemanticVersion('{0}')".format(self)