summaryrefslogtreecommitdiffstats
path: root/third_party/python/mozilla-version/mozilla_version
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/python/mozilla-version/mozilla_version
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/mozilla-version/mozilla_version')
-rw-r--r--third_party/python/mozilla-version/mozilla_version/__init__.py1
-rw-r--r--third_party/python/mozilla-version/mozilla_version/balrog.py142
-rw-r--r--third_party/python/mozilla-version/mozilla_version/errors.py64
-rw-r--r--third_party/python/mozilla-version/mozilla_version/gecko.py435
-rw-r--r--third_party/python/mozilla-version/mozilla_version/maven.py56
-rw-r--r--third_party/python/mozilla-version/mozilla_version/parser.py48
-rw-r--r--third_party/python/mozilla-version/mozilla_version/test/__init__.py5
-rw-r--r--third_party/python/mozilla-version/mozilla_version/test/test_balrog.py172
-rw-r--r--third_party/python/mozilla-version/mozilla_version/test/test_gecko.py411
-rw-r--r--third_party/python/mozilla-version/mozilla_version/test/test_maven.py87
-rw-r--r--third_party/python/mozilla-version/mozilla_version/test/test_version.py171
-rw-r--r--third_party/python/mozilla-version/mozilla_version/version.py177
12 files changed, 1769 insertions, 0 deletions
diff --git a/third_party/python/mozilla-version/mozilla_version/__init__.py b/third_party/python/mozilla-version/mozilla_version/__init__.py
new file mode 100644
index 0000000000..ba46ee264d
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/__init__.py
@@ -0,0 +1 @@
+"""Defines characteristics of Mozilla's version numbers."""
diff --git a/third_party/python/mozilla-version/mozilla_version/balrog.py b/third_party/python/mozilla-version/mozilla_version/balrog.py
new file mode 100644
index 0000000000..0860d5698a
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/balrog.py
@@ -0,0 +1,142 @@
+"""Defines characteristics of a Balrog release name.
+
+Balrog is the server that delivers Firefox and Thunderbird updates. Release names follow
+the pattern "{product}-{version}-build{build_number}"
+
+Examples:
+ .. code-block:: python
+
+ from mozilla_version.balrog import BalrogReleaseName
+
+ balrog_release = BalrogReleaseName.parse('firefox-60.0.1-build1')
+
+ balrog_release.product # firefox
+ balrog_release.version.major_number # 60
+ str(balrog_release) # 'firefox-60.0.1-build1'
+
+ previous_release = BalrogReleaseName.parse('firefox-60.0-build2')
+ previous_release < balrog_release # True
+
+ invalid = BalrogReleaseName.parse('60.0.1') # raises PatternNotMatchedError
+ invalid = BalrogReleaseName.parse('firefox-60.0.1') # raises PatternNotMatchedError
+
+ # Releases can be built thanks to version classes like FirefoxVersion
+ BalrogReleaseName('firefox', FirefoxVersion(60, 0, 1, 1)) # 'firefox-60.0.1-build1'
+
+"""
+
+import attr
+import re
+
+from mozilla_version.errors import PatternNotMatchedError
+from mozilla_version.parser import get_value_matched_by_regex
+from mozilla_version.gecko import (
+ GeckoVersion, FirefoxVersion, DeveditionVersion, FennecVersion, ThunderbirdVersion
+)
+
+
+_VALID_ENOUGH_BALROG_RELEASE_PATTERN = re.compile(
+ r"^(?P<product>[a-z]+)-(?P<version>.+)$", re.IGNORECASE
+)
+
+
+_SUPPORTED_PRODUCTS = {
+ 'firefox': FirefoxVersion,
+ 'devedition': DeveditionVersion,
+ 'fennec': FennecVersion,
+ 'thunderbird': ThunderbirdVersion,
+}
+
+
+def _supported_product(string):
+ product = string.lower()
+ if product not in _SUPPORTED_PRODUCTS:
+ raise PatternNotMatchedError(string, pattern='unknown product')
+ return product
+
+
+def _products_must_be_identical(method):
+ def checker(this, other):
+ if this.product != other.product:
+ raise ValueError('Cannot compare "{}" and "{}"'.format(this.product, other.product))
+ return method(this, other)
+ return checker
+
+
+@attr.s(frozen=True, cmp=False, hash=True)
+class BalrogReleaseName(object):
+ """Class that validates and handles Balrog release names.
+
+ Raises:
+ PatternNotMatchedError: if a parsed string doesn't match the pattern of a valid release
+ MissingFieldError: if a mandatory field is missing in the string. Mandatory fields are
+ `product`, `major_number`, `minor_number`, and `build_number`
+ ValueError: if an integer can't be cast or is not (strictly) positive
+ TooManyTypesError: if the string matches more than 1 `VersionType`
+ NoVersionTypeError: if the string matches none.
+
+ """
+
+ product = attr.ib(type=str, converter=_supported_product)
+ version = attr.ib(type=GeckoVersion)
+
+ 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')
+
+ @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)
+
+ 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')
+
+ version_string = get_value_matched_by_regex('version', regex_matches, release_string)
+ version = VersionClass.parse(version_string)
+
+ return cls(product, version)
+
+ def __str__(self):
+ """Implement string representation.
+
+ Computes a new string based on the given attributes.
+ """
+ version_string = str(self.version).replace('build', '-build')
+ return '{}-{}'.format(self.product, version_string)
+
+ @_products_must_be_identical
+ def __eq__(self, other):
+ """Implement `==` operator."""
+ return self.version == other.version
+
+ @_products_must_be_identical
+ def __ne__(self, other):
+ """Implement `!=` operator."""
+ return self.version != other.version
+
+ @_products_must_be_identical
+ def __lt__(self, other):
+ """Implement `<` operator."""
+ return self.version < other.version
+
+ @_products_must_be_identical
+ def __le__(self, other):
+ """Implement `<=` operator."""
+ return self.version <= other.version
+
+ @_products_must_be_identical
+ def __gt__(self, other):
+ """Implement `>` operator."""
+ return self.version > other.version
+
+ @_products_must_be_identical
+ def __ge__(self, other):
+ """Implement `>=` operator."""
+ return self.version >= other.version
diff --git a/third_party/python/mozilla-version/mozilla_version/errors.py b/third_party/python/mozilla-version/mozilla_version/errors.py
new file mode 100644
index 0000000000..84cd2169f5
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/errors.py
@@ -0,0 +1,64 @@
+"""Defines all errors reported by mozilla-version."""
+
+
+class PatternNotMatchedError(ValueError):
+ """Error when a string doesn't match an expected pattern.
+
+ Args:
+ string (str): The string it was unable to match.
+ pattern (str): The pattern used it tried to match.
+ """
+
+ def __init__(self, string, pattern):
+ """Constructor."""
+ super(PatternNotMatchedError, self).__init__(
+ '"{}" does not match the pattern: {}'.format(string, pattern)
+ )
+
+
+class NoVersionTypeError(ValueError):
+ """Error when `version_string` matched the pattern, but was unable to find its type.
+
+ Args:
+ version_string (str): The string it was unable to guess the type.
+ """
+
+ def __init__(self, version_string):
+ """Constructor."""
+ super(NoVersionTypeError, self).__init__(
+ 'Version "{}" matched the pattern of a valid version, but it is unable to find what type it is. \
+This is likely a bug in mozilla-version'.format(version_string)
+ )
+
+
+class MissingFieldError(ValueError):
+ """Error when `version_string` lacks an expected field.
+
+ Args:
+ version_string (str): The string it was unable to extract a given field.
+ field_name (str): The name of the missing field.
+ """
+
+ def __init__(self, version_string, field_name):
+ """Constructor."""
+ super(MissingFieldError, self).__init__(
+ 'Release "{}" does not contain a valid {}'.format(version_string, field_name)
+ )
+
+
+class TooManyTypesError(ValueError):
+ """Error when `version_string` has too many types."""
+
+ def __init__(self, version_string, first_matched_type, second_matched_type):
+ """Constructor.
+
+ Args:
+ version_string (str): The string that gave too many types.
+ first_matched_type (str): The name of the first detected type.
+ second_matched_type (str): The name of the second detected type
+ """
+ super(TooManyTypesError, self).__init__(
+ 'Release "{}" cannot match types "{}" and "{}"'.format(
+ version_string, first_matched_type, second_matched_type
+ )
+ )
diff --git a/third_party/python/mozilla-version/mozilla_version/gecko.py b/third_party/python/mozilla-version/mozilla_version/gecko.py
new file mode 100644
index 0000000000..dd14dda6b0
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/gecko.py
@@ -0,0 +1,435 @@
+"""Defines characteristics of a Gecko version number, including Firefox.
+
+Examples:
+ .. code-block:: python
+
+ from mozilla_version.gecko import FirefoxVersion
+
+ version = FirefoxVersion.parse('60.0.1')
+
+ version.major_number # 60
+ version.minor_number # 0
+ version.patch_number # 1
+
+ version.is_release # True
+ version.is_beta # False
+ version.is_nightly # False
+
+ str(version) # '60.0.1'
+
+ previous_version = FirefoxVersion.parse('60.0b14')
+ previous_version < version # True
+
+ previous_version.beta_number # 14
+ previous_version.major_number # 60
+ previous_version.minor_number # 0
+ previous_version.patch_number # raises AttributeError
+
+ previous_version.is_beta # True
+ previous_version.is_release # False
+ previous_version.is_nightly # False
+
+ invalid_version = FirefoxVersion.parse('60.1') # raises PatternNotMatchedError
+ invalid_version = FirefoxVersion.parse('60.0.0') # raises PatternNotMatchedError
+ version = FirefoxVersion.parse('60.0') # valid
+
+ # Versions can be built by raw values
+ FirefoxVersion(60, 0)) # '60.0'
+ FirefoxVersion(60, 0, 1)) # '60.0.1'
+ FirefoxVersion(60, 1, 0)) # '60.1.0'
+ FirefoxVersion(60, 0, 1, 1)) # '60.0.1build1'
+ FirefoxVersion(60, 0, beta_number=1)) # '60.0b1'
+ FirefoxVersion(60, 0, is_nightly=True)) # '60.0a1'
+ FirefoxVersion(60, 0, is_aurora_or_devedition=True)) # '60.0a2'
+ FirefoxVersion(60, 0, is_esr=True)) # '60.0esr'
+ FirefoxVersion(60, 0, 1, is_esr=True)) # '60.0.1esr'
+
+"""
+
+import attr
+import re
+
+from mozilla_version.errors import (
+ PatternNotMatchedError, TooManyTypesError, NoVersionTypeError
+)
+from mozilla_version.parser import strictly_positive_int_or_none
+from mozilla_version.version import BaseVersion, VersionType
+
+
+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, hash=True)
+class GeckoVersion(BaseVersion):
+ """Class that validates and handles version numbers for Gecko-based products.
+
+ You may want to use specific classes like FirefoxVersion. These classes define edge cases
+ that were shipped.
+
+ Raises:
+ PatternNotMatchedError: if the string doesn't match the pattern of a valid version number
+ MissingFieldError: if a mandatory field is missing in the string. Mandatory fields are
+ `major_number` and `minor_number`
+ ValueError: if an integer can't be cast or is not (strictly) positive
+ TooManyTypesError: if the string matches more than 1 `VersionType`
+ NoVersionTypeError: if the string matches none.
+
+ """
+
+ # XXX This pattern doesn't catch all subtleties of a Firefox version (like 32.5 isn't valid).
+ # This regex is intended to assign numbers. Then checks are done by attrs and
+ # __attrs_post_init__()
+ _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',
+ )
+
+ _OPTIONAL_NUMBERS = BaseVersion._OPTIONAL_NUMBERS + ('beta_number', 'build_number')
+
+ 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):
+ """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'
+ )
+
+ @classmethod
+ def parse(cls, version_string):
+ """Construct an object representing a valid Firefox version number."""
+ return super(GeckoVersion, cls).parse(
+ version_string, regex_groups=('is_nightly', 'is_aurora_or_devedition', 'is_esr')
+ )
+
+ @property
+ def is_beta(self):
+ """Return `True` if `FirefoxVersion` was built with a string matching a beta version."""
+ return self.beta_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)
+
+ def __str__(self):
+ """Implement string representation.
+
+ Computes a new string based on the given attributes.
+ """
+ string = super(GeckoVersion, self).__str__()
+
+ 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):
+ """Implement `==` operator.
+
+ A version is considered equal to another if all numbers match and if they are of the same
+ `VersionType`. Like said in `VersionType`, release and ESR are considered equal (if they
+ share the same numbers). If a version contains a build number but not the other, the build
+ number won't be considered in the comparison.
+
+ Examples:
+ .. code-block:: python
+
+ assert GeckoVersion.parse('60.0') == GeckoVersion.parse('60.0')
+ assert GeckoVersion.parse('60.0') == GeckoVersion.parse('60.0esr')
+ assert GeckoVersion.parse('60.0') == GeckoVersion.parse('60.0build1')
+ assert GeckoVersion.parse('60.0build1') == GeckoVersion.parse('60.0build1')
+
+ assert GeckoVersion.parse('60.0') != GeckoVersion.parse('61.0')
+ assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.1.0')
+ assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0.1')
+ assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0a1')
+ assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0a2')
+ assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0b1')
+ assert GeckoVersion.parse('60.0build1') != GeckoVersion.parse('60.0build2')
+
+ """
+ return super(GeckoVersion, self).__eq__(other)
+
+ def _compare(self, other):
+ """Compare this release with another.
+
+ Returns:
+ 0 if equal
+ < 0 is this precedes the other
+ > 0 if the other precedes this
+
+ """
+ if isinstance(other, str):
+ other = GeckoVersion.parse(other)
+ elif not isinstance(other, GeckoVersion):
+ raise ValueError('Cannot compare "{}", type not supported!'.format(other))
+
+ difference = super(GeckoVersion, self)._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
+
+ # 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.
+ 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):
+ """Class that validates and handles Firefox version numbers."""
+
+ _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):
+ """Class that validates and handles Devedition after it became an equivalent to beta."""
+
+ # No edge case were shipped
+
+ def __attrs_post_init__(self):
+ """Ensure attributes are sane all together."""
+ 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):
+ """Class that validates and handles Fennec (Firefox for Android) version numbers."""
+
+ _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,
+ })
+
+ def __attrs_post_init__(self):
+ """Ensure attributes are sane all together."""
+ # Versions matching 68.Xa1, 68.XbN, or simply 68.X are expected since bug 1523402. The
+ # latter is needed because of the version.txt of beta
+ if (
+ self.major_number == 68 and
+ self.minor_number > 0 and
+ self.patch_number is None
+ ):
+ return
+
+ if self.major_number >= 69:
+ raise PatternNotMatchedError(self, pattern='Last Fennec version is 68')
+
+ super(FennecVersion, self).__attrs_post_init__()
+
+
+class ThunderbirdVersion(_VersionWithEdgeCases):
+ """Class that validates and handles Thunderbird version numbers."""
+
+ _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):
+ """Class that validates and handles Gecko's Snap version numbers.
+
+ Snap is a Linux packaging format developped by Canonical. Valid numbers are like "63.0b7-1",
+ "1" stands for "build1". Release Engineering set this scheme at the beginning of Snap and now
+ we can't rename published snap to the regular pattern like "63.0b7-build1".
+ """
+
+ # Our Snaps are recent enough to not list any edge case, yet.
+
+ # Differences between this regex and the one in GeckoVersion:
+ # * no a2
+ # * no "build"
+ # * but mandatory dash and build number.
+ # Example: 63.0b7-1
+ _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):
+ """Implement string representation.
+
+ Returns format like "63.0b7-1"
+ """
+ string = super(GeckoSnapVersion, self).__str__()
+ return string.replace('build', '-')
diff --git a/third_party/python/mozilla-version/mozilla_version/maven.py b/third_party/python/mozilla-version/mozilla_version/maven.py
new file mode 100644
index 0000000000..91cecb3431
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/maven.py
@@ -0,0 +1,56 @@
+"""Defines characteristics of a Maven version at Mozilla."""
+
+import attr
+import re
+
+from mozilla_version.version import BaseVersion
+
+
+@attr.s(frozen=True, cmp=False, hash=True)
+class MavenVersion(BaseVersion):
+ """Class that validates and handles Maven version numbers.
+
+ At Mozilla, Maven packages are used in projects like "GeckoView" or "Android-Components".
+ """
+
+ is_snapshot = attr.ib(type=bool, default=False)
+
+ _VALID_ENOUGH_VERSION_PATTERN = re.compile(r"""
+ ^(?P<major_number>\d+)
+ \.(?P<minor_number>\d+)
+ (\.(?P<patch_number>\d+))?
+ (?P<is_snapshot>-SNAPSHOT)?$""", re.VERBOSE)
+
+ @classmethod
+ def parse(cls, version_string):
+ """Construct an object representing a valid Maven version number."""
+ return super(MavenVersion, cls).parse(version_string, regex_groups=('is_snapshot', ))
+
+ def __str__(self):
+ """Implement string representation.
+
+ Computes a new string based on the given attributes.
+ """
+ string = super(MavenVersion, self).__str__()
+
+ if self.is_snapshot:
+ string = '{}-SNAPSHOT'.format(string)
+
+ return string
+
+ def _compare(self, other):
+ if isinstance(other, str):
+ other = MavenVersion.parse(other)
+ elif not isinstance(other, MavenVersion):
+ raise ValueError('Cannot compare "{}", type not supported!'.format(other))
+
+ difference = super(MavenVersion, self)._compare(other)
+ if difference != 0:
+ return difference
+
+ if not self.is_snapshot and other.is_snapshot:
+ return 1
+ elif self.is_snapshot and not other.is_snapshot:
+ return -1
+ else:
+ return 0
diff --git a/third_party/python/mozilla-version/mozilla_version/parser.py b/third_party/python/mozilla-version/mozilla_version/parser.py
new file mode 100644
index 0000000000..d3a6ef6bb6
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/parser.py
@@ -0,0 +1,48 @@
+"""Defines parser helpers."""
+
+from mozilla_version.errors import MissingFieldError
+
+
+def get_value_matched_by_regex(field_name, regex_matches, string):
+ """Ensure value stored in regex group exists."""
+ try:
+ value = regex_matches.group(field_name)
+ if value is not None:
+ return value
+ except IndexError:
+ pass
+
+ raise MissingFieldError(string, field_name)
+
+
+def does_regex_have_group(regex_matches, group_name):
+ """Return a boolean depending on whether a regex group is matched."""
+ try:
+ return regex_matches.group(group_name) is not None
+ except IndexError:
+ return False
+
+
+def positive_int(val):
+ """Parse `val` into a positive integer."""
+ 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):
+ """Parse `val` into either `None` or a positive integer."""
+ if val is None:
+ return val
+ return positive_int(val)
+
+
+def strictly_positive_int_or_none(val):
+ """Parse `val` into either `None` or a strictly positive integer."""
+ val = positive_int_or_none(val)
+ if val is None or val > 0:
+ return val
+ raise ValueError('"{}" must be strictly positive'.format(val))
diff --git a/third_party/python/mozilla-version/mozilla_version/test/__init__.py b/third_party/python/mozilla-version/mozilla_version/test/__init__.py
new file mode 100644
index 0000000000..f094a83248
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/test/__init__.py
@@ -0,0 +1,5 @@
+from contextlib import contextmanager
+
+@contextmanager
+def does_not_raise():
+ yield
diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_balrog.py b/third_party/python/mozilla-version/mozilla_version/test/test_balrog.py
new file mode 100644
index 0000000000..dd9dc24d3c
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/test/test_balrog.py
@@ -0,0 +1,172 @@
+import pytest
+
+from mozilla_version.balrog import BalrogReleaseName
+from mozilla_version.errors import PatternNotMatchedError
+from mozilla_version.gecko import FirefoxVersion
+
+
+@pytest.mark.parametrize(
+ 'product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly, \
+is_aurora_or_devedition, is_esr, expected_output_string', ((
+ 'firefox', 32, 0, None, None, 1, False, False, False, 'firefox-32.0-build1'
+), (
+ 'firefox', 32, 0, 1, None, 2, False, False, False, 'firefox-32.0.1-build2'
+), (
+ 'firefox', 32, 0, None, 3, 4, False, False, False, 'firefox-32.0b3-build4'
+), (
+ 'firefox', 32, 0, None, None, 5, True, False, False, 'firefox-32.0a1-build5'
+), (
+ 'firefox', 32, 0, None, None, 6, False, True, False, 'firefox-32.0a2-build6'
+), (
+ 'firefox', 32, 0, None, None, 7, False, False, True, 'firefox-32.0esr-build7'
+), (
+ 'firefox', 32, 0, 1, None, 8, False, False, True, 'firefox-32.0.1esr-build8'
+), (
+ 'devedition', 54, 0, None, 12, 1, False, False, False, 'devedition-54.0b12-build1'
+), (
+ 'fennec', 32, 0, None, None, 1, False, False, False, 'fennec-32.0-build1'
+), (
+ 'thunderbird', 32, 0, None, None, 1, False, False, False, 'thunderbird-32.0-build1'
+)))
+def test_balrog_release_name_constructor_and_str(
+ product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly,
+ is_aurora_or_devedition, is_esr, expected_output_string
+):
+ assert str(BalrogReleaseName(product, FirefoxVersion(
+ major_number=major_number,
+ minor_number=minor_number,
+ patch_number=patch_number,
+ build_number=build_number,
+ beta_number=beta_number,
+ is_nightly=is_nightly,
+ is_aurora_or_devedition=is_aurora_or_devedition,
+ is_esr=is_esr
+ ))) == expected_output_string
+
+
+@pytest.mark.parametrize('product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType', ((
+ ('nonexistingproduct', 32, 0, None, None, 1, False, False, False, PatternNotMatchedError),
+ ('firefox', 32, 0, None, None, None, False, False, False, PatternNotMatchedError),
+)))
+def test_fail_balrog_release_constructor(product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType):
+ with pytest.raises(ExpectedErrorType):
+ BalrogReleaseName(product, FirefoxVersion(
+ major_number=major_number,
+ minor_number=minor_number,
+ patch_number=patch_number,
+ beta_number=beta_number,
+ build_number=build_number,
+ is_nightly=is_nightly,
+ is_aurora_or_devedition=is_aurora_or_devedition,
+ is_esr=is_esr
+ ))
+
+
+@pytest.mark.parametrize('string, expected_string', ((
+ ('firefox-32.0-build1', 'firefox-32.0-build1'),
+ ('firefox-32.0.1-build2', 'firefox-32.0.1-build2'),
+ ('firefox-32.0b3-build4', 'firefox-32.0b3-build4'),
+ ('firefox-32.0a1-build5', 'firefox-32.0a1-build5'),
+ ('firefox-32.0a2-build6', 'firefox-32.0a2-build6'),
+ ('firefox-32.0esr-build7', 'firefox-32.0esr-build7'),
+ ('firefox-32.0.1esr-build8', 'firefox-32.0.1esr-build8'),
+
+ ('firefox-32.0build1', 'firefox-32.0-build1'),
+)))
+def test_balrog_release_name_parse(string, expected_string):
+ assert str(BalrogReleaseName.parse(string)) == expected_string
+
+
+@pytest.mark.parametrize('string, ExpectedErrorType', (
+ ('firefox-32.0', PatternNotMatchedError),
+
+ ('firefox32.0-build1', PatternNotMatchedError),
+ ('firefox32.0build1', PatternNotMatchedError),
+ ('firefox-32.0--build1', PatternNotMatchedError),
+ ('firefox-build1', PatternNotMatchedError),
+ ('nonexistingproduct-32.0-build1', PatternNotMatchedError),
+
+ ('firefox-32-build1', PatternNotMatchedError),
+ ('firefox-32.b2-build1', PatternNotMatchedError),
+ ('firefox-.1-build1', PatternNotMatchedError),
+ ('firefox-32.0.0-build1', PatternNotMatchedError),
+ ('firefox-32.2-build1', PatternNotMatchedError),
+ ('firefox-32.02-build1', PatternNotMatchedError),
+ ('firefox-32.0a0-build1', ValueError),
+ ('firefox-32.0b0-build1', ValueError),
+ ('firefox-32.0.1a1-build1', PatternNotMatchedError),
+ ('firefox-32.0.1a2-build1', PatternNotMatchedError),
+ ('firefox-32.0.1b2-build1', PatternNotMatchedError),
+ ('firefox-32.0-build0', ValueError),
+ ('firefox-32.0a1a2-build1', PatternNotMatchedError),
+ ('firefox-32.0a1b2-build1', PatternNotMatchedError),
+ ('firefox-32.0b2esr-build1', PatternNotMatchedError),
+ ('firefox-32.0esrb2-build1', PatternNotMatchedError),
+))
+def test_firefox_version_raises_when_invalid_version_is_given(string, ExpectedErrorType):
+ with pytest.raises(ExpectedErrorType):
+ BalrogReleaseName.parse(string)
+
+
+@pytest.mark.parametrize('previous, next', (
+ ('firefox-32.0-build1', 'firefox-33.0-build1'),
+ ('firefox-32.0-build1', 'firefox-32.1.0-build1'),
+ ('firefox-32.0-build1', 'firefox-32.0.1-build1'),
+ ('firefox-32.0-build1', 'firefox-32.0-build2'),
+
+ ('firefox-32.0a1-build1', 'firefox-32.0-build1'),
+ ('firefox-32.0a2-build1', 'firefox-32.0-build1'),
+ ('firefox-32.0b1-build1', 'firefox-32.0-build1'),
+
+ ('firefox-32.0.1-build1', 'firefox-33.0-build1'),
+ ('firefox-32.0.1-build1', 'firefox-32.1.0-build1'),
+ ('firefox-32.0.1-build1', 'firefox-32.0.2-build1'),
+ ('firefox-32.0.1-build1', 'firefox-32.0.1-build2'),
+
+ ('firefox-32.1.0-build1', 'firefox-33.0-build1'),
+ ('firefox-32.1.0-build1', 'firefox-32.2.0-build1'),
+ ('firefox-32.1.0-build1', 'firefox-32.1.1-build1'),
+ ('firefox-32.1.0-build1', 'firefox-32.1.0-build2'),
+
+ ('firefox-32.0b1-build1', 'firefox-33.0b1-build1'),
+ ('firefox-32.0b1-build1', 'firefox-32.0b2-build1'),
+ ('firefox-32.0b1-build1', 'firefox-32.0b1-build2'),
+
+ ('firefox-2.0-build1', 'firefox-10.0-build1'),
+ ('firefox-10.2.0-build1', 'firefox-10.10.0-build1'),
+ ('firefox-10.0.2-build1', 'firefox-10.0.10-build1'),
+ ('firefox-10.10.1-build1', 'firefox-10.10.10-build1'),
+ ('firefox-10.0-build2', 'firefox-10.0-build10'),
+ ('firefox-10.0b2-build1', 'firefox-10.0b10-build1'),
+))
+def test_balrog_release_implements_lt_operator(previous, next):
+ assert BalrogReleaseName.parse(previous) < BalrogReleaseName.parse(next)
+
+
+def test_fail_balrog_release_lt_operator():
+ with pytest.raises(ValueError):
+ assert BalrogReleaseName.parse('thunderbird-32.0-build1') < BalrogReleaseName.parse('Firefox-32.0-build2')
+
+
+def test_balrog_release_implements_remaining_comparision_operators():
+ assert BalrogReleaseName.parse('firefox-32.0-build1') == BalrogReleaseName.parse('firefox-32.0-build1')
+ assert BalrogReleaseName.parse('firefox-32.0-build1') != BalrogReleaseName.parse('firefox-33.0-build1')
+
+ assert BalrogReleaseName.parse('firefox-32.0-build1') <= BalrogReleaseName.parse('firefox-32.0-build1')
+ assert BalrogReleaseName.parse('firefox-32.0-build1') <= BalrogReleaseName.parse('firefox-33.0-build1')
+
+ assert BalrogReleaseName.parse('firefox-33.0-build1') >= BalrogReleaseName.parse('firefox-32.0-build1')
+ assert BalrogReleaseName.parse('firefox-33.0-build1') >= BalrogReleaseName.parse('firefox-33.0-build1')
+
+ assert BalrogReleaseName.parse('firefox-33.0-build1') > BalrogReleaseName.parse('firefox-32.0-build1')
+ assert not BalrogReleaseName.parse('firefox-33.0-build1') > BalrogReleaseName.parse('firefox-33.0-build1')
+
+ assert not BalrogReleaseName.parse('firefox-32.0-build1') < BalrogReleaseName.parse('firefox-32.0-build1')
+
+ assert BalrogReleaseName.parse('firefox-33.0-build1') != BalrogReleaseName.parse('firefox-32.0-build1')
+
+def test_balrog_release_hashable():
+ """
+ It is possible to hash `BalrogReleaseNmae`.
+ """
+ hash(BalrogReleaseName.parse('firefox-63.0-build1'))
diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_gecko.py b/third_party/python/mozilla-version/mozilla_version/test/test_gecko.py
new file mode 100644
index 0000000000..020e959e81
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/test/test_gecko.py
@@ -0,0 +1,411 @@
+import pytest
+import re
+
+from distutils.version import StrictVersion, LooseVersion
+
+import mozilla_version.gecko
+
+from mozilla_version.errors import PatternNotMatchedError, TooManyTypesError, NoVersionTypeError
+from mozilla_version.gecko import (
+ GeckoVersion, FirefoxVersion, DeveditionVersion,
+ ThunderbirdVersion, FennecVersion, GeckoSnapVersion,
+)
+from mozilla_version.test import does_not_raise
+
+
+VALID_VERSIONS = {
+ '32.0a1': 'nightly',
+ '32.0a2': 'aurora_or_devedition',
+ '32.0b2': 'beta',
+ '32.0b10': 'beta',
+ '32.0': 'release',
+ '32.0.1': 'release',
+ '32.0esr': 'esr',
+ '32.0.1esr': 'esr',
+}
+
+
+@pytest.mark.parametrize('major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, expected_output_string', ((
+ 32, 0, None, None, None, False, False, False, '32.0'
+), (
+ 32, 0, 1, None, None, False, False, False, '32.0.1'
+), (
+ 32, 0, None, 3, None, False, False, False, '32.0b3'
+), (
+ 32, 0, None, None, 10, False, False, False, '32.0build10'
+), (
+ 32, 0, None, None, None, True, False, False, '32.0a1'
+), (
+ 32, 0, None, None, None, False, True, False, '32.0a2'
+), (
+ 32, 0, None, None, None, False, False, True, '32.0esr'
+), (
+ 32, 0, 1, None, None, False, False, True, '32.0.1esr'
+)))
+def test_firefox_version_constructor_and_str(major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, expected_output_string):
+ assert str(FirefoxVersion(
+ major_number=major_number,
+ minor_number=minor_number,
+ patch_number=patch_number,
+ beta_number=beta_number,
+ build_number=build_number,
+ is_nightly=is_nightly,
+ is_aurora_or_devedition=is_aurora_or_devedition,
+ is_esr=is_esr
+ )) == expected_output_string
+
+
+@pytest.mark.parametrize('major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType', ((
+ 32, 0, None, 1, None, True, False, False, TooManyTypesError
+), (
+ 32, 0, None, 1, None, False, True, False, TooManyTypesError
+), (
+ 32, 0, None, 1, None, False, False, True, TooManyTypesError
+), (
+ 32, 0, None, None, None, True, True, False, TooManyTypesError
+), (
+ 32, 0, None, None, None, True, False, True, TooManyTypesError
+), (
+ 32, 0, None, None, None, False, True, True, TooManyTypesError
+), (
+ 32, 0, None, None, None, True, True, True, TooManyTypesError
+), (
+ 32, 0, 0, None, None, False, False, False, PatternNotMatchedError
+), (
+ 32, 0, None, 0, None, False, False, False, ValueError
+), (
+ 32, 0, None, None, 0, False, False, False, ValueError
+), (
+ 32, 0, 1, 1, None, False, False, False, PatternNotMatchedError
+), (
+ 32, 0, 1, None, None, True, False, False, PatternNotMatchedError
+), (
+ 32, 0, 1, None, None, False, True, False, PatternNotMatchedError
+), (
+ -1, 0, None, None, None, False, False, False, ValueError
+), (
+ 32, -1, None, None, None, False, False, False, ValueError
+), (
+ 32, 0, -1, None, None, False, False, False, ValueError
+), (
+ 2.2, 0, 0, None, None, False, False, False, ValueError
+), (
+ 'some string', 0, 0, None, None, False, False, False, ValueError
+)))
+def test_fail_firefox_version_constructor(major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType):
+ with pytest.raises(ExpectedErrorType):
+ FirefoxVersion(
+ major_number=major_number,
+ minor_number=minor_number,
+ patch_number=patch_number,
+ beta_number=beta_number,
+ build_number=build_number,
+ is_nightly=is_nightly,
+ is_aurora_or_devedition=is_aurora_or_devedition,
+ is_esr=is_esr
+ )
+
+
+def test_firefox_version_constructor_minimum_kwargs():
+ assert str(FirefoxVersion(32, 0)) == '32.0'
+ assert str(FirefoxVersion(32, 0, 1)) == '32.0.1'
+ assert str(FirefoxVersion(32, 1, 0)) == '32.1.0'
+ assert str(FirefoxVersion(32, 0, 1, 1)) == '32.0.1build1'
+ assert str(FirefoxVersion(32, 0, beta_number=1)) == '32.0b1'
+ assert str(FirefoxVersion(32, 0, is_nightly=True)) == '32.0a1'
+ assert str(FirefoxVersion(32, 0, is_aurora_or_devedition=True)) == '32.0a2'
+ assert str(FirefoxVersion(32, 0, is_esr=True)) == '32.0esr'
+ assert str(FirefoxVersion(32, 0, 1, is_esr=True)) == '32.0.1esr'
+
+
+@pytest.mark.parametrize('version_string, ExpectedErrorType', (
+ ('32', PatternNotMatchedError),
+ ('32.b2', PatternNotMatchedError),
+ ('.1', PatternNotMatchedError),
+ ('32.0.0', PatternNotMatchedError),
+ ('32.2', PatternNotMatchedError),
+ ('32.02', PatternNotMatchedError),
+ ('32.0a0', ValueError),
+ ('32.0b0', ValueError),
+ ('32.0.1a1', PatternNotMatchedError),
+ ('32.0.1a2', PatternNotMatchedError),
+ ('32.0.1b2', PatternNotMatchedError),
+ ('32.0build0', ValueError),
+ ('32.0a1a2', PatternNotMatchedError),
+ ('32.0a1b2', PatternNotMatchedError),
+ ('32.0b2esr', PatternNotMatchedError),
+ ('32.0esrb2', PatternNotMatchedError),
+))
+def test_firefox_version_raises_when_invalid_version_is_given(version_string, ExpectedErrorType):
+ with pytest.raises(ExpectedErrorType):
+ FirefoxVersion.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string, expected_type', VALID_VERSIONS.items())
+def test_firefox_version_is_of_a_defined_type(version_string, expected_type):
+ release = FirefoxVersion.parse(version_string)
+ assert getattr(release, 'is_{}'.format(expected_type))
+
+
+@pytest.mark.parametrize('previous, next', (
+ ('32.0', '33.0'),
+ ('32.0', '32.1.0'),
+ ('32.0', '32.0.1'),
+ ('32.0build1', '32.0build2'),
+
+ ('32.0.1', '33.0'),
+ ('32.0.1', '32.1.0'),
+ ('32.0.1', '32.0.2'),
+ ('32.0.1build1', '32.0.1build2'),
+
+ ('32.1.0', '33.0'),
+ ('32.1.0', '32.2.0'),
+ ('32.1.0', '32.1.1'),
+ ('32.1.0build1', '32.1.0build2'),
+
+ ('32.0b1', '33.0b1'),
+ ('32.0b1', '32.0b2'),
+ ('32.0b1build1', '32.0b1build2'),
+
+ ('32.0a1', '32.0a2'),
+ ('32.0a1', '32.0b1'),
+ ('32.0a1', '32.0'),
+ ('32.0a1', '32.0esr'),
+
+ ('32.0a2', '32.0b1'),
+ ('32.0a2', '32.0'),
+ ('32.0a2', '32.0esr'),
+
+ ('32.0b1', '32.0'),
+ ('32.0b1', '32.0esr'),
+
+ ('32.0', '32.0esr'),
+
+ ('2.0', '10.0'),
+ ('10.2.0', '10.10.0'),
+ ('10.0.2', '10.0.10'),
+ ('10.10.1', '10.10.10'),
+ ('10.0build2', '10.0build10'),
+ ('10.0b2', '10.0b10'),
+))
+def test_firefox_version_implements_lt_operator(previous, next):
+ assert FirefoxVersion.parse(previous) < FirefoxVersion.parse(next)
+
+
+@pytest.mark.parametrize('equivalent_version_string', (
+ '32.0', '032.0', '32.0build1', '32.0build01', '32.0-build1', '32.0build2',
+))
+def test_firefox_version_implements_eq_operator(equivalent_version_string):
+ assert FirefoxVersion.parse('32.0') == FirefoxVersion.parse(equivalent_version_string)
+ # raw strings are also converted
+ assert FirefoxVersion.parse('32.0') == equivalent_version_string
+
+
+@pytest.mark.parametrize('wrong_type', (
+ 32,
+ 32.0,
+ ('32', '0', '1'),
+ ['32', '0', '1'],
+ LooseVersion('32.0'),
+ StrictVersion('32.0'),
+))
+def test_firefox_version_raises_eq_operator(wrong_type):
+ with pytest.raises(ValueError):
+ assert FirefoxVersion.parse('32.0') == wrong_type
+ # AttributeError is raised by LooseVersion and StrictVersion
+ with pytest.raises((ValueError, AttributeError)):
+ assert wrong_type == FirefoxVersion.parse('32.0')
+
+
+def test_firefox_version_implements_remaining_comparision_operators():
+ assert FirefoxVersion.parse('32.0') <= FirefoxVersion.parse('32.0')
+ assert FirefoxVersion.parse('32.0') <= FirefoxVersion.parse('33.0')
+
+ assert FirefoxVersion.parse('33.0') >= FirefoxVersion.parse('32.0')
+ assert FirefoxVersion.parse('33.0') >= FirefoxVersion.parse('33.0')
+
+ assert FirefoxVersion.parse('33.0') > FirefoxVersion.parse('32.0')
+ assert not FirefoxVersion.parse('33.0') > FirefoxVersion.parse('33.0')
+
+ assert not FirefoxVersion.parse('32.0') < FirefoxVersion.parse('32.0')
+
+ assert FirefoxVersion.parse('33.0') != FirefoxVersion.parse('32.0')
+
+
+@pytest.mark.parametrize('version_string, expected_output', (
+ ('32.0', '32.0'),
+ ('032.0', '32.0'),
+ ('32.0build1', '32.0build1'),
+ ('32.0build01', '32.0build1'),
+ ('32.0.1', '32.0.1'),
+ ('32.0a1', '32.0a1'),
+ ('32.0a2', '32.0a2'),
+ ('32.0b1', '32.0b1'),
+ ('32.0b01', '32.0b1'),
+ ('32.0esr', '32.0esr'),
+ ('32.0.1esr', '32.0.1esr'),
+))
+def test_firefox_version_implements_str_operator(version_string, expected_output):
+ assert str(FirefoxVersion.parse(version_string)) == expected_output
+
+
+_SUPER_PERMISSIVE_PATTERN = re.compile(r"""
+(?P<major_number>\d+)\.(?P<minor_number>\d+)(\.(\d+))*
+(?P<is_nightly>a1)?(?P<is_aurora_or_devedition>a2)?(b(?P<beta_number>\d+))?
+(?P<is_esr>esr)?
+""", re.VERBOSE)
+
+
+@pytest.mark.parametrize('version_string', (
+ '32.0a1a2', '32.0a1b2', '32.0b2esr'
+))
+def test_firefox_version_ensures_it_does_not_have_multiple_type(monkeypatch, version_string):
+ # Let's make sure the sanity checks detect a broken regular expression
+ original_pattern = FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN
+ FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = _SUPER_PERMISSIVE_PATTERN
+
+ with pytest.raises(TooManyTypesError):
+ FirefoxVersion.parse(version_string)
+
+ FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = original_pattern
+
+
+def test_firefox_version_ensures_a_new_added_release_type_is_caught(monkeypatch):
+ # Let's make sure the sanity checks detect a broken regular expression
+ original_pattern = FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN
+ FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = _SUPER_PERMISSIVE_PATTERN
+
+ # And a broken type detection
+ original_is_release = FirefoxVersion.is_release
+ FirefoxVersion.is_release = False
+
+ with pytest.raises(NoVersionTypeError):
+ mozilla_version.gecko.FirefoxVersion.parse('32.0.0.0')
+
+ FirefoxVersion.is_release = original_is_release
+ FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = original_pattern
+
+
+@pytest.mark.parametrize('version_string', (
+ '33.1', '33.1build1', '33.1build2', '33.1build3',
+ '38.0.5b1', '38.0.5b1build1', '38.0.5b1build2',
+ '38.0.5b2', '38.0.5b2build1',
+ '38.0.5b3', '38.0.5b3build1',
+))
+def test_firefox_version_supports_released_edge_cases(version_string):
+ assert str(FirefoxVersion.parse(version_string)) == version_string
+ for Class in (DeveditionVersion, FennecVersion, ThunderbirdVersion):
+ if Class == FennecVersion and version_string in ('33.1', '33.1build1', '33.1build2'):
+ # These edge cases also exist in Fennec
+ continue
+ with pytest.raises(PatternNotMatchedError):
+ Class.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string', (
+ '54.0b11', '54.0b12', '55.0b1'
+))
+def test_devedition_version(version_string):
+ DeveditionVersion.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string', (
+ '53.0a1', '53.0b1', '54.0b10', '55.0', '55.0a1', '60.0esr'
+))
+def test_devedition_version_bails_on_wrong_version(version_string):
+ with pytest.raises(PatternNotMatchedError):
+ DeveditionVersion.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string', (
+ '33.1', '33.1build1', '33.1build2',
+ '38.0.5b4', '38.0.5b4build1'
+))
+def test_fennec_version_supports_released_edge_cases(version_string):
+ assert str(FennecVersion.parse(version_string)) == version_string
+ for Class in (FirefoxVersion, DeveditionVersion, ThunderbirdVersion):
+ if Class == FirefoxVersion and version_string in ('33.1', '33.1build1', '33.1build2'):
+ # These edge cases also exist in Firefox
+ continue
+ with pytest.raises(PatternNotMatchedError):
+ Class.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string, expectation', (
+ ('68.0a1', does_not_raise()),
+ ('68.0b3', does_not_raise()),
+ ('68.0b17', does_not_raise()),
+ ('68.0', does_not_raise()),
+ ('68.0.1', does_not_raise()),
+ ('68.1a1', does_not_raise()),
+ ('68.1b2', does_not_raise()),
+ ('68.1.0', does_not_raise()),
+ ('68.1', does_not_raise()),
+ ('68.1b3', does_not_raise()),
+ ('68.1.1', does_not_raise()),
+ ('68.2a1', does_not_raise()),
+ ('68.2b1', does_not_raise()),
+ ('68.2', does_not_raise()),
+
+ ('67.1', pytest.raises(PatternNotMatchedError)),
+ ('68.0.1a1', pytest.raises(PatternNotMatchedError)),
+ ('68.1a1b1', pytest.raises(PatternNotMatchedError)),
+ ('68.0.1b1', pytest.raises(PatternNotMatchedError)),
+ ('68.1.0a1', pytest.raises(PatternNotMatchedError)),
+ ('68.1.0b1', pytest.raises(PatternNotMatchedError)),
+ ('68.1.1a1', pytest.raises(PatternNotMatchedError)),
+ ('68.1.1b2', pytest.raises(PatternNotMatchedError)),
+
+ ('69.0a1', pytest.raises(PatternNotMatchedError)),
+ ('69.0b3', pytest.raises(PatternNotMatchedError)),
+ ('69.0', pytest.raises(PatternNotMatchedError)),
+ ('69.0.1', pytest.raises(PatternNotMatchedError)),
+ ('69.1', pytest.raises(PatternNotMatchedError)),
+
+ ('70.0', pytest.raises(PatternNotMatchedError)),
+))
+def test_fennec_version_ends_at_68(version_string, expectation):
+ with expectation:
+ FennecVersion.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string', (
+ '45.1b1', '45.1b1build1',
+ '45.2', '45.2build1', '45.2build2',
+ '45.2b1', '45.2b1build2',
+))
+def test_thunderbird_version_supports_released_edge_cases(version_string):
+ assert str(ThunderbirdVersion.parse(version_string)) == version_string
+ for Class in (FirefoxVersion, DeveditionVersion, FennecVersion):
+ with pytest.raises(PatternNotMatchedError):
+ Class.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string', (
+ '63.0b7-1', '63.0b7-2',
+ '62.0-1', '62.0-2',
+ '60.2.1esr-1', '60.2.0esr-2',
+ '60.0esr-1', '60.0esr-13',
+ # TODO Bug 1451694: Figure out what nightlies version numbers looks like
+))
+def test_gecko_snap_version(version_string):
+ GeckoSnapVersion.parse(version_string)
+
+
+@pytest.mark.parametrize('version_string', (
+ '32.0a2', '32.0esr1', '32.0-build1',
+))
+def test_gecko_snap_version_bails_on_wrong_version(version_string):
+ with pytest.raises(PatternNotMatchedError):
+ GeckoSnapVersion.parse(version_string)
+
+
+def test_gecko_snap_version_implements_its_own_string():
+ assert str(GeckoSnapVersion.parse('63.0b7-1')) == '63.0b7-1'
+
+
+def test_gecko_version_hashable():
+ """
+ It is possible to hash `GeckoVersion`.
+ """
+ hash(GeckoVersion.parse('63.0'))
diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_maven.py b/third_party/python/mozilla-version/mozilla_version/test/test_maven.py
new file mode 100644
index 0000000000..f8d539872d
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/test/test_maven.py
@@ -0,0 +1,87 @@
+import pytest
+
+from distutils.version import LooseVersion, StrictVersion
+
+from mozilla_version.errors import PatternNotMatchedError
+from mozilla_version.maven import MavenVersion
+
+@pytest.mark.parametrize('major_number, minor_number, patch_number, is_snapshot, expected_output_string', ((
+ 32, 0, None, False, '32.0'
+), (
+ 32, 0, 1, False, '32.0.1'
+), (
+ 32, 0, None, True, '32.0-SNAPSHOT'
+), (
+ 32, 0, 1, True, '32.0.1-SNAPSHOT'
+)))
+def test_maven_version_constructor_and_str(major_number, minor_number, patch_number, is_snapshot, expected_output_string):
+ assert str(MavenVersion(
+ major_number=major_number,
+ minor_number=minor_number,
+ patch_number=patch_number,
+ is_snapshot=is_snapshot,
+ )) == expected_output_string
+
+
+def test_maven_version_constructor_minimum_kwargs():
+ assert str(MavenVersion(32, 0)) == '32.0'
+ assert str(MavenVersion(32, 0, 1)) == '32.0.1'
+ assert str(MavenVersion(32, 1, 0)) == '32.1.0'
+ assert str(MavenVersion(32, 1, 0, False)) == '32.1.0'
+ assert str(MavenVersion(32, 1, 0, True)) == '32.1.0-SNAPSHOT'
+
+
+@pytest.mark.parametrize('version_string, ExpectedErrorType', (
+ ('32.0SNAPSHOT', PatternNotMatchedError),
+ ('32.1.0SNAPSHOT', PatternNotMatchedError),
+))
+def test_maven_version_raises_when_invalid_version_is_given(version_string, ExpectedErrorType):
+ with pytest.raises(ExpectedErrorType):
+ MavenVersion.parse(version_string)
+
+
+@pytest.mark.parametrize('previous, next', (
+ ('32.0-SNAPSHOT', '32.0'),
+ ('31.0', '32.0-SNAPSHOT'),
+ ('32.0', '32.0.1-SNAPSHOT'),
+ ('32.0.1-SNAPSHOT', '32.1.0'),
+ ('32.0.1-SNAPSHOT', '33.0'),
+))
+def test_maven_version_implements_lt_operator(previous, next):
+ assert MavenVersion.parse(previous) < MavenVersion.parse(next)
+
+
+@pytest.mark.parametrize('previous, next', (
+ ('32.0', '32.0-SNAPSHOT'),
+ ('32.0-SNAPSHOT', '31.0'),
+ ('32.0.1-SNAPSHOT', '32.0'),
+ ('32.1.0', '32.0.1-SNAPSHOT'),
+))
+def test_maven_version_implements_gt_operator(previous, next):
+ assert MavenVersion.parse(previous) > MavenVersion.parse(next)
+
+
+@pytest.mark.parametrize('wrong_type', (
+ 32,
+ 32.0,
+ ('32', '0', '1'),
+ ['32', '0', '1'],
+ LooseVersion('32.0'),
+ StrictVersion('32.0'),
+))
+def test_base_version_raises_eq_operator(wrong_type):
+ with pytest.raises(ValueError):
+ assert MavenVersion.parse('32.0') == wrong_type
+ # AttributeError is raised by LooseVersion and StrictVersion
+ with pytest.raises((ValueError, AttributeError)):
+ assert wrong_type == MavenVersion.parse('32.0')
+
+
+def test_maven_version_implements_eq_operator():
+ assert MavenVersion.parse('32.0-SNAPSHOT') == MavenVersion.parse('32.0-SNAPSHOT')
+ # raw strings are also converted
+ assert MavenVersion.parse('32.0-SNAPSHOT') == '32.0-SNAPSHOT'
+
+
+def test_maven_version_hashable():
+ hash(MavenVersion.parse('32.0.1'))
diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_version.py b/third_party/python/mozilla-version/mozilla_version/test/test_version.py
new file mode 100644
index 0000000000..c74bdb735a
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/test/test_version.py
@@ -0,0 +1,171 @@
+import pytest
+
+from distutils.version import LooseVersion, StrictVersion
+
+from mozilla_version.errors import PatternNotMatchedError
+from mozilla_version.version import BaseVersion, VersionType
+
+
+@pytest.mark.parametrize('major_number, minor_number, patch_number, expected_output_string', ((
+ 32, 0, None, '32.0'
+), (
+ 32, 0, 1, '32.0.1'
+)))
+def test_base_version_constructor_and_str(major_number, minor_number, patch_number, expected_output_string):
+ assert str(BaseVersion(
+ major_number=major_number,
+ minor_number=minor_number,
+ patch_number=patch_number,
+ )) == expected_output_string
+
+
+@pytest.mark.parametrize('major_number, minor_number, patch_number, ExpectedErrorType', ((
+ -1, 0, None, ValueError
+), (
+ 32, -1, None, ValueError
+), (
+ 32, 0, -1, ValueError
+), (
+ 2.2, 0, 0, ValueError
+), (
+ 'some string', 0, 0, ValueError
+)))
+def test_fail_base_version_constructor(major_number, minor_number, patch_number, ExpectedErrorType):
+ with pytest.raises(ExpectedErrorType):
+ BaseVersion(
+ major_number=major_number,
+ minor_number=minor_number,
+ patch_number=patch_number,
+ )
+
+
+def test_base_version_constructor_minimum_kwargs():
+ assert str(BaseVersion(32, 0)) == '32.0'
+ assert str(BaseVersion(32, 0, 1)) == '32.0.1'
+ assert str(BaseVersion(32, 1, 0)) == '32.1.0'
+
+
+@pytest.mark.parametrize('version_string, ExpectedErrorType', (
+ ('32', PatternNotMatchedError),
+ ('.1', PatternNotMatchedError),
+))
+def test_base_version_raises_when_invalid_version_is_given(version_string, ExpectedErrorType):
+ with pytest.raises(ExpectedErrorType):
+ BaseVersion.parse(version_string)
+
+
+@pytest.mark.parametrize('previous, next', (
+ ('32.0', '33.0'),
+ ('32.0', '32.1.0'),
+ ('32.0', '32.0.1'),
+
+ ('32.0.1', '33.0'),
+ ('32.0.1', '32.1.0'),
+ ('32.0.1', '32.0.2'),
+
+ ('32.1.0', '33.0'),
+ ('32.1.0', '32.2.0'),
+ ('32.1.0', '32.1.1'),
+
+ ('2.0', '10.0'),
+ ('10.2.0', '10.10.0'),
+ ('10.0.2', '10.0.10'),
+ ('10.10.1', '10.10.10'),
+))
+def test_base_version_implements_lt_operator(previous, next):
+ assert BaseVersion.parse(previous) < BaseVersion.parse(next)
+
+
+@pytest.mark.parametrize('equivalent_version_string', (
+ '32.0', '032.0', '32.00'
+))
+def test_base_version_implements_eq_operator(equivalent_version_string):
+ assert BaseVersion.parse('32.0') == BaseVersion.parse(equivalent_version_string)
+ # raw strings are also converted
+ assert BaseVersion.parse('32.0') == equivalent_version_string
+
+
+@pytest.mark.parametrize('wrong_type', (
+ 32,
+ 32.0,
+ ('32', '0', '1'),
+ ['32', '0', '1'],
+ LooseVersion('32.0'),
+ StrictVersion('32.0'),
+))
+def test_base_version_raises_eq_operator(wrong_type):
+ with pytest.raises(ValueError):
+ assert BaseVersion.parse('32.0') == wrong_type
+ # AttributeError is raised by LooseVersion and StrictVersion
+ with pytest.raises((ValueError, AttributeError)):
+ assert wrong_type == BaseVersion.parse('32.0')
+
+
+def test_base_version_implements_remaining_comparision_operators():
+ assert BaseVersion.parse('32.0') <= BaseVersion.parse('32.0')
+ assert BaseVersion.parse('32.0') <= BaseVersion.parse('33.0')
+
+ assert BaseVersion.parse('33.0') >= BaseVersion.parse('32.0')
+ assert BaseVersion.parse('33.0') >= BaseVersion.parse('33.0')
+
+ assert BaseVersion.parse('33.0') > BaseVersion.parse('32.0')
+ assert not BaseVersion.parse('33.0') > BaseVersion.parse('33.0')
+
+ assert not BaseVersion.parse('32.0') < BaseVersion.parse('32.0')
+
+ assert BaseVersion.parse('33.0') != BaseVersion.parse('32.0')
+
+
+def test_base_version_hashable():
+ hash(BaseVersion.parse('63.0'))
+
+
+@pytest.mark.parametrize('previous, next', (
+ (VersionType.NIGHTLY, VersionType.AURORA_OR_DEVEDITION),
+ (VersionType.NIGHTLY, VersionType.BETA),
+ (VersionType.NIGHTLY, VersionType.RELEASE),
+ (VersionType.NIGHTLY, VersionType.ESR),
+
+ (VersionType.AURORA_OR_DEVEDITION, VersionType.BETA),
+ (VersionType.AURORA_OR_DEVEDITION, VersionType.RELEASE),
+ (VersionType.AURORA_OR_DEVEDITION, VersionType.ESR),
+
+ (VersionType.BETA, VersionType.RELEASE),
+ (VersionType.BETA, VersionType.ESR),
+
+ (VersionType.RELEASE, VersionType.ESR),
+))
+def test_version_type_implements_lt_operator(previous, next):
+ assert previous < next
+
+
+@pytest.mark.parametrize('first, second', (
+ (VersionType.NIGHTLY, VersionType.NIGHTLY),
+ (VersionType.AURORA_OR_DEVEDITION, VersionType.AURORA_OR_DEVEDITION),
+ (VersionType.BETA, VersionType.BETA),
+ (VersionType.RELEASE, VersionType.RELEASE),
+ (VersionType.ESR, VersionType.ESR),
+))
+def test_version_type_implements_eq_operator(first, second):
+ assert first == second
+
+
+def test_version_type_implements_remaining_comparision_operators():
+ assert VersionType.NIGHTLY <= VersionType.NIGHTLY
+ assert VersionType.NIGHTLY <= VersionType.BETA
+
+ assert VersionType.NIGHTLY >= VersionType.NIGHTLY
+ assert VersionType.BETA >= VersionType.NIGHTLY
+
+ assert not VersionType.NIGHTLY > VersionType.NIGHTLY
+ assert VersionType.BETA > VersionType.NIGHTLY
+
+ assert not VersionType.BETA < VersionType.NIGHTLY
+
+ assert VersionType.NIGHTLY != VersionType.BETA
+
+
+def test_version_type_compare():
+ assert VersionType.NIGHTLY.compare(VersionType.NIGHTLY) == 0
+ assert VersionType.NIGHTLY.compare(VersionType.BETA) < 0
+ assert VersionType.BETA.compare(VersionType.NIGHTLY) > 0
diff --git a/third_party/python/mozilla-version/mozilla_version/version.py b/third_party/python/mozilla-version/mozilla_version/version.py
new file mode 100644
index 0000000000..1f78bec590
--- /dev/null
+++ b/third_party/python/mozilla-version/mozilla_version/version.py
@@ -0,0 +1,177 @@
+"""Defines common characteristics of a version at Mozilla."""
+
+import attr
+import re
+
+from enum import Enum
+
+from mozilla_version.errors import MissingFieldError, PatternNotMatchedError
+from mozilla_version.parser import (
+ get_value_matched_by_regex,
+ does_regex_have_group,
+ positive_int,
+ positive_int_or_none
+)
+
+
+@attr.s(frozen=True, cmp=False, hash=True)
+class BaseVersion(object):
+ """Class that validates and handles general version numbers."""
+
+ 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)
+
+ _MANDATORY_NUMBERS = ('major_number', 'minor_number')
+ _OPTIONAL_NUMBERS = ('patch_number', )
+ _ALL_NUMBERS = _MANDATORY_NUMBERS + _OPTIONAL_NUMBERS
+
+ _VALID_ENOUGH_VERSION_PATTERN = re.compile(r"""
+ ^(?P<major_number>\d+)
+ \.(?P<minor_number>\d+)
+ (\.(?P<patch_number>\d+))?$""", re.VERBOSE)
+
+ @classmethod
+ def parse(cls, version_string, regex_groups=()):
+ """Construct an object representing a valid version number."""
+ regex_matches = cls._VALID_ENOUGH_VERSION_PATTERN.match(version_string)
+
+ if regex_matches is None:
+ raise PatternNotMatchedError(version_string, cls._VALID_ENOUGH_VERSION_PATTERN)
+
+ kwargs = {}
+
+ for field in cls._MANDATORY_NUMBERS:
+ kwargs[field] = get_value_matched_by_regex(field, regex_matches, version_string)
+ for field in cls._OPTIONAL_NUMBERS:
+ try:
+ kwargs[field] = get_value_matched_by_regex(field, regex_matches, version_string)
+ except MissingFieldError:
+ pass
+
+ for regex_group in regex_groups:
+ kwargs[regex_group] = does_regex_have_group(regex_matches, regex_group)
+
+ return cls(**kwargs)
+
+ def __str__(self):
+ """Implement string representation.
+
+ Computes a new string based on the given attributes.
+ """
+ semvers = [str(self.major_number), str(self.minor_number)]
+ if self.patch_number is not None:
+ semvers.append(str(self.patch_number))
+
+ return '.'.join(semvers)
+
+ def __eq__(self, other):
+ """Implement `==` operator."""
+ return self._compare(other) == 0
+
+ def __ne__(self, other):
+ """Implement `!=` operator."""
+ return self._compare(other) != 0
+
+ def __lt__(self, other):
+ """Implement `<` operator."""
+ return self._compare(other) < 0
+
+ def __le__(self, other):
+ """Implement `<=` operator."""
+ return self._compare(other) <= 0
+
+ def __gt__(self, other):
+ """Implement `>` operator."""
+ return self._compare(other) > 0
+
+ def __ge__(self, other):
+ """Implement `>=` operator."""
+ return self._compare(other) >= 0
+
+ def _compare(self, other):
+ """Compare this release with another.
+
+ Returns:
+ 0 if equal
+ < 0 is this precedes the other
+ > 0 if the other precedes this
+
+ """
+ if isinstance(other, str):
+ other = BaseVersion.parse(other)
+ elif not isinstance(other, BaseVersion):
+ 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
+
+ return 0
+
+
+class VersionType(Enum):
+ """Enum that sorts types of versions (e.g.: nightly, beta, release, esr).
+
+ Supports comparison. `ESR` is considered higher than `RELEASE` (even if they technically have
+ the same codebase). For instance: 60.0.1 < 60.0.1esr but 61.0 > 60.0.1esr.
+ This choice has a practical use case: if you have a list of Release and ESR version, you can
+ easily extract one kind or the other thanks to the VersionType.
+
+ Examples:
+ .. code-block:: python
+
+ assert VersionType.NIGHTLY == VersionType.NIGHTLY
+ assert VersionType.ESR > VersionType.RELEASE
+
+ """
+
+ NIGHTLY = 1
+ AURORA_OR_DEVEDITION = 2
+ BETA = 3
+ RELEASE = 4
+ ESR = 5
+
+ def __eq__(self, other):
+ """Implement `==` operator."""
+ return self.compare(other) == 0
+
+ def __ne__(self, other):
+ """Implement `!=` operator."""
+ return self.compare(other) != 0
+
+ def __lt__(self, other):
+ """Implement `<` operator."""
+ return self.compare(other) < 0
+
+ def __le__(self, other):
+ """Implement `<=` operator."""
+ return self.compare(other) <= 0
+
+ def __gt__(self, other):
+ """Implement `>` operator."""
+ return self.compare(other) > 0
+
+ def __ge__(self, other):
+ """Implement `>=` operator."""
+ return self.compare(other) >= 0
+
+ def compare(self, other):
+ """Compare this `VersionType` with anotherself.
+
+ Returns:
+ 0 if equal
+ < 0 is this precedes the other
+ > 0 if the other precedes this
+
+ """
+ return self.value - other.value
+
+ __hash__ = Enum.__hash__