import attr
import re
from mozilla_version.errors import (
PatternNotMatchedError, MissingFieldError, TooManyTypesError, NoVersionTypeError
)
from mozilla_version.parser import get_value_matched_by_regex
from mozilla_version.version import VersionType
def _positive_int(val):
if isinstance(val, float):
raise ValueError('"{}" must not be a float'.format(val))
val = int(val)
if val >= 0:
return val
raise ValueError('"{}" must be positive'.format(val))
def _positive_int_or_none(val):
if val is None:
return val
return _positive_int(val)
def _strictly_positive_int_or_none(val):
val = _positive_int_or_none(val)
if val is None or val > 0:
return val
raise ValueError('"{}" must be strictly positive'.format(val))
def _does_regex_have_group(regex_matches, group_name):
try:
return regex_matches.group(group_name) is not None
except IndexError:
return False
def _find_type(version):
version_type = None
def ensure_version_type_is_not_already_defined(previous_type, candidate_type):
if previous_type is not None:
raise TooManyTypesError(
str(version), previous_type, candidate_type
)
if version.is_nightly:
version_type = VersionType.NIGHTLY
if version.is_aurora_or_devedition:
ensure_version_type_is_not_already_defined(
version_type, VersionType.AURORA_OR_DEVEDITION
)
version_type = VersionType.AURORA_OR_DEVEDITION
if version.is_beta:
ensure_version_type_is_not_already_defined(version_type, VersionType.BETA)
version_type = VersionType.BETA
if version.is_esr:
ensure_version_type_is_not_already_defined(version_type, VersionType.ESR)
version_type = VersionType.ESR
if version.is_release:
ensure_version_type_is_not_already_defined(version_type, VersionType.RELEASE)
version_type = VersionType.RELEASE
if version_type is None:
raise NoVersionTypeError(str(version))
return version_type
@attr.s(frozen=True, cmp=False)
class GeckoVersion(object):
_VALID_ENOUGH_VERSION_PATTERN = re.compile(r"""
^(?P<major_number>\d+)
\.(?P<minor_number>\d+)
(\.(?P<patch_number>\d+))?
(
(?P<is_nightly>a1)
|(?P<is_aurora_or_devedition>a2)
|b(?P<beta_number>\d+)
|(?P<is_esr>esr)
)?
-?(build(?P<build_number>\d+))?$""", re.VERBOSE)
_ALL_VERSION_NUMBERS_TYPES = (
'major_number', 'minor_number', 'patch_number', 'beta_number',
)
major_number = attr.ib(type=int, converter=_positive_int)
minor_number = attr.ib(type=int, converter=_positive_int)
patch_number = attr.ib(type=int, converter=_positive_int_or_none, default=None)
build_number = attr.ib(type=int, converter=_strictly_positive_int_or_none, default=None)
beta_number = attr.ib(type=int, converter=_strictly_positive_int_or_none, default=None)
is_nightly = attr.ib(type=bool, default=False)
is_aurora_or_devedition = attr.ib(type=bool, default=False)
is_esr = attr.ib(type=bool, default=False)
version_type = attr.ib(init=False, default=attr.Factory(_find_type, takes_self=True))
def __attrs_post_init__(self):
if (
(self.minor_number == 0 and self.patch_number == 0) or
(self.minor_number != 0 and self.patch_number is None) or
(self.beta_number is not None and self.patch_number is not None) or
(self.patch_number is not None and self.is_nightly) or
(self.patch_number is not None and self.is_aurora_or_devedition)
):
raise PatternNotMatchedError(self, pattern='hard coded checks')
@classmethod
def parse(cls, version_string):
regex_matches = cls._VALID_ENOUGH_VERSION_PATTERN.match(version_string)
if regex_matches is None:
raise PatternNotMatchedError(version_string, cls._VALID_ENOUGH_VERSION_PATTERN)
args = {}
for field in ('major_number', 'minor_number'):
args[field] = get_value_matched_by_regex(field, regex_matches, version_string)
for field in ('patch_number', 'beta_number', 'build_number'):
try:
args[field] = get_value_matched_by_regex(field, regex_matches, version_string)
except MissingFieldError:
pass
return cls(
is_nightly=_does_regex_have_group(regex_matches, 'is_nightly'),
is_aurora_or_devedition=_does_regex_have_group(
regex_matches, 'is_aurora_or_devedition'
),
is_esr=_does_regex_have_group(regex_matches, 'is_esr'),
**args
)
@property
def is_beta(self):
return self.beta_number is not None
@property
def is_release(self):
return not (self.is_nightly or self.is_aurora_or_devedition or self.is_beta or self.is_esr)
def __str__(self):
semvers = [str(self.major_number), str(self.minor_number)]
if self.patch_number is not None:
semvers.append(str(self.patch_number))
string = '.'.join(semvers)
if self.is_nightly:
string = '{}a1'.format(string)
elif self.is_aurora_or_devedition:
string = '{}a2'.format(string)
elif self.is_beta:
string = '{}b{}'.format(string, self.beta_number)
elif self.is_esr:
string = '{}esr'.format(string)
if self.build_number is not None:
string = '{}build{}'.format(string, self.build_number)
return string
def __eq__(self, other):
return self._compare(other) == 0
def __ne__(self, other):
return self._compare(other) != 0
def __lt__(self, other):
return self._compare(other) < 0
def __le__(self, other):
return self._compare(other) <= 0
def __gt__(self, other):
return self._compare(other) > 0
def __ge__(self, other):
return self._compare(other) >= 0
def _compare(self, other):
if isinstance(other, str):
other = GeckoVersion.parse(other)
elif not isinstance(other, GeckoVersion):
raise ValueError('Cannot compare "{}", type not supported!'.format(other))
for field in ('major_number', 'minor_number', 'patch_number'):
this_number = getattr(self, field)
this_number = 0 if this_number is None else this_number
other_number = getattr(other, field)
other_number = 0 if other_number is None else other_number
difference = this_number - other_number
if difference != 0:
return difference
channel_difference = self._compare_version_type(other)
if channel_difference != 0:
return channel_difference
if self.is_beta and other.is_beta:
beta_difference = self.beta_number - other.beta_number
if beta_difference != 0:
return beta_difference
try:
return self.build_number - other.build_number
except TypeError:
pass
return 0
def _compare_version_type(self, other):
return self.version_type.compare(other.version_type)
class _VersionWithEdgeCases(GeckoVersion):
def __attrs_post_init__(self):
for edge_case in self._RELEASED_EDGE_CASES:
if all(
getattr(self, number_type) == edge_case.get(number_type, None)
for number_type in self._ALL_VERSION_NUMBERS_TYPES
):
if self.build_number is None:
return
elif self.build_number == edge_case.get('build_number', None):
return
super(_VersionWithEdgeCases, self).__attrs_post_init__()
class FirefoxVersion(_VersionWithEdgeCases):
_RELEASED_EDGE_CASES = ({
'major_number': 33,
'minor_number': 1,
'build_number': 1,
}, {
'major_number': 33,
'minor_number': 1,
'build_number': 2,
}, {
'major_number': 33,
'minor_number': 1,
'build_number': 3,
}, {
'major_number': 38,
'minor_number': 0,
'patch_number': 5,
'beta_number': 1,
'build_number': 1,
}, {
'major_number': 38,
'minor_number': 0,
'patch_number': 5,
'beta_number': 1,
'build_number': 2,
}, {
'major_number': 38,
'minor_number': 0,
'patch_number': 5,
'beta_number': 2,
'build_number': 1,
}, {
'major_number': 38,
'minor_number': 0,
'patch_number': 5,
'beta_number': 3,
'build_number': 1,
})
class DeveditionVersion(GeckoVersion):
def __attrs_post_init__(self):
if (
(not self.is_beta) or
(self.major_number < 54) or
(self.major_number == 54 and self.beta_number < 11)
):
raise PatternNotMatchedError(
self, pattern='Devedition as a product must be a beta >= 54.0b11'
)
class FennecVersion(_VersionWithEdgeCases):
_RELEASED_EDGE_CASES = ({
'major_number': 33,
'minor_number': 1,
'build_number': 1,
}, {
'major_number': 33,
'minor_number': 1,
'build_number': 2,
}, {
'major_number': 38,
'minor_number': 0,
'patch_number': 5,
'beta_number': 4,
'build_number': 1,
})
class ThunderbirdVersion(_VersionWithEdgeCases):
_RELEASED_EDGE_CASES = ({
'major_number': 45,
'minor_number': 1,
'beta_number': 1,
'build_number': 1,
}, {
'major_number': 45,
'minor_number': 2,
'build_number': 1,
}, {
'major_number': 45,
'minor_number': 2,
'build_number': 2,
}, {
'major_number': 45,
'minor_number': 2,
'beta_number': 1,
'build_number': 2,
})
class GeckoSnapVersion(GeckoVersion):
_VALID_ENOUGH_VERSION_PATTERN = re.compile(r"""
^(?P<major_number>\d+)
\.(?P<minor_number>\d+)
(\.(?P<patch_number>\d+))?
(
(?P<is_nightly>a1)
|b(?P<beta_number>\d+)
|(?P<is_esr>esr)
)?
-(?P<build_number>\d+)$""", re.VERBOSE)
def __str__(self):
string = super(GeckoSnapVersion, self).__str__()
return string.replace('build', '-')