diff options
Diffstat (limited to 'third_party/python/mozilla_version/mozilla_version/mobile.py')
-rw-r--r-- | third_party/python/mozilla_version/mozilla_version/mobile.py | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/third_party/python/mozilla_version/mozilla_version/mobile.py b/third_party/python/mozilla_version/mozilla_version/mobile.py new file mode 100644 index 0000000000..97e0f5b6aa --- /dev/null +++ b/third_party/python/mozilla_version/mozilla_version/mobile.py @@ -0,0 +1,250 @@ +"""Defines characteristics of a Mobile version at Mozilla.""" + +import attr +import re + +from mozilla_version.errors import PatternNotMatchedError, TooManyTypesError, NoVersionTypeError +from mozilla_version.gecko import GeckoVersion +from mozilla_version.version import BaseVersion, VersionType +from mozilla_version.parser import strictly_positive_int_or_none + + +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_beta: + ensure_version_type_is_not_already_defined(version_type, VersionType.BETA) + version_type = VersionType.BETA + if version.is_release_candidate: + ensure_version_type_is_not_already_defined(version_type, VersionType.RELEASE_CANDIDATE) + version_type = VersionType.RELEASE_CANDIDATE + 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, eq=False, hash=True) +class MobileVersion(BaseVersion): + """Validate and handle version numbers for mobile products. + + This covers applications such as Fenix and Focus for Android. + """ + + _VALID_ENOUGH_VERSION_PATTERN = re.compile(r""" + ^(?P<major_number>\d+) + \.(?P<minor_number>\d+) + (\.(?P<patch_number>\d+))? + ( + (?P<is_nightly>a1) + |(-beta\.|b)(?P<beta_number>\d+) + |-rc\.(?P<release_candidate_number>\d+) + )? + -?(build(?P<build_number>\d+))?$""", re.VERBOSE) + + _OPTIONAL_NUMBERS = ( + 'patch_number', 'beta_number', 'release_candidate_number', 'build_number' + ) + + _ALL_NUMBERS = BaseVersion._MANDATORY_NUMBERS + _OPTIONAL_NUMBERS + + # Focus-Android and Fenix were the first ones to be converted to the Gecko + # pattern (bug 1777255) + _FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN = 104 + # Android-Components later (bug 1800611) + _LAST_VERSION_TO_FOLLOW_MAVEN_PATTERN = 108 + + 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) + release_candidate_number = attr.ib( + type=int, converter=strictly_positive_int_or_none, default=None + ) + version_type = attr.ib(init=False, default=attr.Factory(_find_type, takes_self=True)) + + def __attrs_post_init__(self): + """Ensure attributes are sane all together.""" + error_messages = [] + + if self.is_gecko_pattern: + error_messages.extend([ + pattern_message + for condition, pattern_message in (( + self.beta_number is not None and self.patch_number is not None, + 'Beta number and patch number cannot be both defined', + ), ( + self.release_candidate_number is not None, + 'Release candidate number cannot be defined after Mobile v{}'.format( + self._FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN + ), + ), ( + self.major_number > self._LAST_VERSION_TO_FOLLOW_MAVEN_PATTERN and + self.minor_number == 0 and + self.patch_number == 0, + 'Minor number and patch number cannot be both equal to 0 past ' + 'Mobile v{}'.format( + self._LAST_VERSION_TO_FOLLOW_MAVEN_PATTERN + ), + ), ( + self.minor_number != 0 and self.patch_number is None, + 'Patch number cannot be undefined if minor number is greater than 0', + )) + if condition + ]) + else: + error_messages.extend([ + pattern_message + for condition, pattern_message in (( + self.patch_number is None, + 'Patch number must be defined before Mobile v{}'.format( + self._FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN + ), + ), ( + self.is_nightly, + 'Nightlies are not supported until Mobile v{}'.format( + self._FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN + ), + )) + if condition + ]) + + if error_messages: + raise PatternNotMatchedError(self, patterns=error_messages) + + @classmethod + def parse(cls, version_string): + """Construct an object representing a valid Firefox version number.""" + mobile_version = super().parse( + version_string, regex_groups=('is_nightly',) + ) + + # Betas are supported in both the old and the gecko pattern. Let's make sure + # the string we got follows the right rules + if mobile_version.is_beta: + if mobile_version.is_gecko_pattern and '-beta.' in version_string: + raise PatternNotMatchedError( + mobile_version, ['"-beta." can only be used before Mobile v{}'.format( + cls._FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN + )] + ) + if not mobile_version.is_gecko_pattern and re.search(r"\db\d", version_string): + raise PatternNotMatchedError( + mobile_version, [ + '"b" cannot be used before Mobile v{} to define a ' + 'beta version'.format( + cls._FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN + ) + ] + ) + + return mobile_version + + @property + def is_gecko_pattern(self): + """Return `True` if `MobileVersion` was built with against the Gecko scheme.""" + return self.major_number >= self._FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN + + @property + def is_beta(self): + """Return `True` if `MobileVersion` was built with a string matching a beta version.""" + return self.beta_number is not None + + @property + def is_release_candidate(self): + """Return `True` if `MobileVersion` was built with a string matching an RC version.""" + return self.release_candidate_number is not None + + @property + def is_release(self): + """Return `True` if `MobileVersion` was built with a string matching a release version.""" + return not any(( + self.is_nightly, self.is_beta, self.is_release_candidate, + )) + + def __str__(self): + """Implement string representation. + + Computes a new string based on the given attributes. + """ + if self.is_gecko_pattern: + string = str(GeckoVersion( + major_number=self.major_number, + minor_number=self.minor_number, + patch_number=self.patch_number, + build_number=self.build_number, + beta_number=self.beta_number, + is_nightly=self.is_nightly, + )) + else: + string = super().__str__() + if self.is_beta: + string = f'{string}-beta.{self.beta_number}' + elif self.is_release_candidate: + string = f'{string}-rc.{self.release_candidate_number}' + + return string + + def _compare(self, other): + if isinstance(other, str): + other = MobileVersion.parse(other) + elif not isinstance(other, MobileVersion): + raise ValueError(f'Cannot compare "{other}", type not supported!') + + difference = super()._compare(other) + 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 + + if self.is_release_candidate and other.is_release_candidate: + rc_difference = self.release_candidate_number - other.release_candidate_number + if rc_difference != 0: + return rc_difference + + return 0 + + def _compare_version_type(self, other): + return self.version_type.compare(other.version_type) + + def _create_bump_kwargs(self, field): + bump_kwargs = super()._create_bump_kwargs(field) + + if field != 'build_number' and bump_kwargs.get('build_number') == 0: + del bump_kwargs['build_number'] + if bump_kwargs.get('beta_number') == 0: + if self.is_beta: + bump_kwargs['beta_number'] = 1 + else: + del bump_kwargs['beta_number'] + + if field != 'release_candidate_number': + del bump_kwargs['release_candidate_number'] + + if ( + field == 'major_number' + and bump_kwargs.get('major_number') == self._FIRST_VERSION_TO_FOLLOW_GECKO_PATTERN + ): + del bump_kwargs['patch_number'] + + bump_kwargs['is_nightly'] = self.is_nightly + + return bump_kwargs |