Skip to content

Commit

Permalink
Merge 0e551f8 into 228c788
Browse files Browse the repository at this point in the history
  • Loading branch information
JohanLorenzo committed Jul 25, 2019
2 parents 228c788 + 0e551f8 commit f8e93c3
Show file tree
Hide file tree
Showing 11 changed files with 629 additions and 392 deletions.
8 changes: 4 additions & 4 deletions mozilla_version/balrog.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
def _supported_product(string):
product = string.lower()
if product not in _SUPPORTED_PRODUCTS:
raise PatternNotMatchedError(string, pattern='unknown product')
raise PatternNotMatchedError(string, patterns=('unknown product',))
return product


Expand Down Expand Up @@ -83,20 +83,20 @@ class BalrogReleaseName(object):
def __attrs_post_init__(self):
"""Ensure attributes are sane all together."""
if self.version.build_number is None:
raise PatternNotMatchedError(self, pattern='build_number must exist')
raise PatternNotMatchedError(self, patterns=('build_number must exist',))

@classmethod
def parse(cls, release_string):
"""Construct an object representing a valid Firefox version number."""
regex_matches = _VALID_ENOUGH_BALROG_RELEASE_PATTERN.match(release_string)
if regex_matches is None:
raise PatternNotMatchedError(release_string, _VALID_ENOUGH_BALROG_RELEASE_PATTERN)
raise PatternNotMatchedError(release_string, (_VALID_ENOUGH_BALROG_RELEASE_PATTERN,))

product = get_value_matched_by_regex('product', regex_matches, release_string)
try:
VersionClass = _SUPPORTED_PRODUCTS[product.lower()]
except KeyError:
raise PatternNotMatchedError(release_string, pattern='unknown product')
raise PatternNotMatchedError(release_string, patterns=('unknown product',))

version_string = get_value_matched_by_regex('version', regex_matches, release_string)
version = VersionClass.parse(version_string)
Expand Down
19 changes: 14 additions & 5 deletions mozilla_version/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ class PatternNotMatchedError(ValueError):
Args:
string (str): The string it was unable to match.
pattern (str): The pattern used it tried to match.
patterns (sequence): The patterns it tried to match.
"""

def __init__(self, string, pattern):
def __init__(self, string, patterns):
"""Constructor."""
super(PatternNotMatchedError, self).__init__(
'"{}" does not match the pattern: {}'.format(string, pattern)
)
number_of_patterns = len(patterns)
if number_of_patterns == 0:
raise ValueError('At least one pattern must be provided')
elif number_of_patterns == 1:
message = '"{}" does not match the pattern: {}'.format(string, patterns[0])
else:
message = '"{}" does not match the patterns:\n - {}'.format(
string,
'\n - '.join(patterns)
)

super(PatternNotMatchedError, self).__init__(message)


class NoVersionTypeError(ValueError):
Expand Down
150 changes: 117 additions & 33 deletions mozilla_version/gecko.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ def ensure_version_type_is_not_already_defined(previous_type, candidate_type):
if version.is_esr:
ensure_version_type_is_not_already_defined(version_type, VersionType.ESR)
version_type = VersionType.ESR
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
Expand Down Expand Up @@ -112,53 +115,96 @@ class GeckoVersion(BaseVersion):
^(?P<major_number>\d+)
\.(?P<minor_number>\d+)
(\.(?P<patch_number>\d+))?
(\.(?P<old_fourth_number>\d+))?
(
(?P<is_nightly>a1)
|(?P<is_aurora_or_devedition>a2)
|rc(?P<release_candidate_number>\d+)
|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',
_OPTIONAL_NUMBERS = BaseVersion._OPTIONAL_NUMBERS + (
'old_fourth_number', 'release_candidate_number', 'beta_number', 'build_number'
)

_OPTIONAL_NUMBERS = BaseVersion._OPTIONAL_NUMBERS + ('beta_number', 'build_number')
_ALL_NUMBERS = BaseVersion._ALL_NUMBERS + _OPTIONAL_NUMBERS

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)
old_fourth_number = attr.ib(type=int, converter=strictly_positive_int_or_none, default=None)
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."""
if self.minor_number == 0 and self.patch_number == 0:
raise PatternNotMatchedError(
self, pattern='Minor number and patch number cannot be both equal to 0'
)

if self.minor_number != 0 and self.patch_number is None:
raise PatternNotMatchedError(
self, pattern='Patch number cannot be undefined if minor number is greater than 0'
)

if self.beta_number is not None and self.patch_number is not None:
raise PatternNotMatchedError(
self, pattern='Beta number and patch number cannot be both defined'
)

if self.patch_number is not None and self.is_nightly:
raise PatternNotMatchedError(
self, pattern='Patch number cannot be defined on a nightly version'
)

if self.patch_number is not None and self.is_aurora_or_devedition:
raise PatternNotMatchedError(
self, pattern='Patch number cannot be defined on an aurora version'
)
# General checks
error_messages = [
pattern_message
for condition, pattern_message in ((
self.old_fourth_number is not None and self.major_number >= 3,
'The old fourth number cannot be defined starting Gecko 3',
), (
self.beta_number is not None and self.patch_number is not None,
'Beta number and patch number cannot be both defined',
))
if condition
]

# Firefox 5 is the first version to implement the rapid release model, which defines
# the scheme used so far.
if self.major_number >= 5:
error_messages.extend([
pattern_message
for condition, pattern_message in ((
self.release_candidate_number is not None,
'Release candidate number cannot be defined starting Gecko 5',
), (
self.minor_number == 0 and self.patch_number == 0,
'Minor number and patch number cannot be both equal to 0',
), (
self.minor_number != 0 and self.patch_number is None,
'Patch number cannot be undefined if minor number is greater than 0',
), (
self.patch_number is not None and self.is_nightly,
'Patch number cannot be defined on a nightly version',
), (
self.patch_number is not None and self.is_aurora_or_devedition,
'Patch number cannot be defined on an aurora version',
))
if condition
])
else:
if self.release_candidate_number is not None:
error_messages.extend([
pattern_message
for condition, pattern_message in ((
self.patch_number is not None,
'Release candidate and patch number cannot be both defined',
), (
self.old_fourth_number is not None,
'Release candidate and the old fourth number cannot be both defined',
), (
self.beta_number is not None,
'Release candidate and beta number cannot be both defined',
))
if condition
])

if self.old_fourth_number is not None and self.patch_number != 0:
error_messages.append(
'The old fourth number cannot be defined if the patch number is not 0 '
'(we have never shipped a release that did so)'
)

if error_messages:
raise PatternNotMatchedError(self, patterns=error_messages)

@classmethod
def parse(cls, version_string):
Expand All @@ -169,13 +215,21 @@ def parse(cls, version_string):

@property
def is_beta(self):
"""Return `True` if `FirefoxVersion` was built with a string matching a beta version."""
"""Return `True` if `GeckoVersion` 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 `GeckoVersion` 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 `FirefoxVersion` was built with a string matching a release version."""
return not (self.is_nightly or self.is_aurora_or_devedition or self.is_beta or self.is_esr)
"""Return `True` if `GeckoVersion` was built with a string matching a release version."""
return not any((
self.is_nightly, self.is_aurora_or_devedition, self.is_beta,
self.is_release_candidate, self.is_esr
))

def __str__(self):
"""Implement string representation.
Expand All @@ -184,12 +238,17 @@ def __str__(self):
"""
string = super(GeckoVersion, self).__str__()

if self.old_fourth_number is not None:
string = '{}.{}'.format(string, self.old_fourth_number)

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_release_candidate:
string = '{}rc{}'.format(string, self.release_candidate_number)
elif self.is_esr:
string = '{}esr'.format(string)

Expand Down Expand Up @@ -243,6 +302,10 @@ def _compare(self, other):
if difference != 0:
return difference

difference = self._substract_other_number_from_this_number(other, 'old_fourth_number')
if difference != 0:
return difference

channel_difference = self._compare_version_type(other)
if channel_difference != 0:
return channel_difference
Expand All @@ -252,6 +315,11 @@ def _compare(self, other):
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

# Build numbers are a special case. We might compare a regular version number
# (like "32.0b8") versus a release build (as in "32.0b8build1"). As a consequence,
# we only compare build_numbers when we both have them.
Expand All @@ -271,7 +339,8 @@ 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
for number_type in self._ALL_NUMBERS
if number_type != 'build_number'
):
if self.build_number is None:
return
Expand Down Expand Up @@ -336,7 +405,7 @@ def __attrs_post_init__(self):
(self.major_number == 54 and self.beta_number < 11)
):
raise PatternNotMatchedError(
self, pattern='Devedition as a product must be a beta >= 54.0b11'
self, patterns=('Devedition as a product must be a beta >= 54.0b11',)
)


Expand Down Expand Up @@ -371,7 +440,7 @@ def __attrs_post_init__(self):
return

if self.major_number >= 69:
raise PatternNotMatchedError(self, pattern='Last Fennec version is 68')
raise PatternNotMatchedError(self, patterns=('Last Fennec version is 68',))

super(FennecVersion, self).__attrs_post_init__()

Expand All @@ -380,6 +449,21 @@ class ThunderbirdVersion(_VersionWithEdgeCases):
"""Class that validates and handles Thunderbird version numbers."""

_RELEASED_EDGE_CASES = ({
'major_number': 1,
'minor_number': 5,
'beta_number': 1,
}, {
'major_number': 1,
'minor_number': 5,
'beta_number': 2,
}, {
'major_number': 3,
'minor_number': 1,
'beta_number': 1,
}, {
'major_number': 3,
'minor_number': 1,
}, {
'major_number': 45,
'minor_number': 1,
'beta_number': 1,
Expand Down
28 changes: 28 additions & 0 deletions mozilla_version/test/test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest

from mozilla_version.errors import PatternNotMatchedError


@pytest.mark.parametrize('string, patterns, expected_message', ((
'some string',
['one single pattern'],
'"some string" does not match the pattern: one single pattern',
), (
'some string',
['one pattern', 'two patterns'],
'''"some string" does not match the patterns:
- one pattern
- two patterns''',
)))
def test_pattern_not_matched_error_changes_its_error_message(string, patterns, expected_message):
with pytest.raises(PatternNotMatchedError) as exc_info:
raise PatternNotMatchedError(string, patterns)

assert exc_info.value.args == (expected_message,)


def test_pattern_not_matched_error_raises_if_badly_initialized():
with pytest.raises(ValueError) as exc_info:
raise PatternNotMatchedError('some string', patterns=())

assert exc_info.value.args == ('At least one pattern must be provided',)

0 comments on commit f8e93c3

Please sign in to comment.