summaryrefslogtreecommitdiffstats
path: root/third_party/python/mozilla_version
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/mozilla_version')
-rw-r--r--third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/LICENSE363
-rw-r--r--third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/METADATA12
-rw-r--r--third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/RECORD22
-rw-r--r--third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/WHEEL5
-rw-r--r--third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/top_level.txt1
-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.py75
-rw-r--r--third_party/python/mozilla_version/mozilla_version/fenix.py3
-rw-r--r--third_party/python/mozilla_version/mozilla_version/gecko.py672
-rw-r--r--third_party/python/mozilla_version/mozilla_version/maven.py65
-rw-r--r--third_party/python/mozilla_version/mozilla_version/mobile.py250
-rw-r--r--third_party/python/mozilla_version/mozilla_version/parser.py48
-rw-r--r--third_party/python/mozilla_version/mozilla_version/version.py236
14 files changed, 1895 insertions, 0 deletions
diff --git a/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/LICENSE b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/LICENSE
new file mode 100644
index 0000000000..e87a115e46
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/LICENSE
@@ -0,0 +1,363 @@
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
+
diff --git a/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/METADATA b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/METADATA
new file mode 100644
index 0000000000..3edee7bd3d
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/METADATA
@@ -0,0 +1,12 @@
+Metadata-Version: 2.1
+Name: mozilla-version
+Version: 2.0.0
+Summary: Process Firefox versions numbers. Tells whether they are valid or not, whether they are nightlies or regular releases, whether this version precedes that other.
+Home-page: https://github.com/mozilla-releng/mozilla-version
+Author: Mozilla Release Engineering
+Author-email: release+python@mozilla.com
+License: MPL2
+Classifier: Programming Language :: Python :: 3
+License-File: LICENSE
+Requires-Dist: attrs (>=19.2)
+
diff --git a/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/RECORD b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/RECORD
new file mode 100644
index 0000000000..8da74e9bde
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/RECORD
@@ -0,0 +1,22 @@
+mozilla_version/__init__.py,sha256=ro9IDUmjUco6GHJhqbgynResbswRnh6HL5Iv1ttDuWU,60
+mozilla_version/balrog.py,sha256=p75Ln9W5IiEzO8C-HIDmKsgdpN4hc9zbvTEOMZodNhQ,4961
+mozilla_version/errors.py,sha256=DvBsNaJdhpaT3wb4E3Rnl7KAuxnqXlElBllYOcijwbQ,2468
+mozilla_version/fenix.py,sha256=zruk3WsTMCeaRaaNi5ezxaSAb8t_8CATXpLhbVryOPM,199
+mozilla_version/gecko.py,sha256=t4JcuF7mehXqFhKIxvFM3hrEx-qZpCmF9YcwWTUuCHM,24783
+mozilla_version/maven.py,sha256=jH0F-Rq3tJJ_N3KbNE1KBi0i_BlXGZCYyjZ7K_CRxoM,1988
+mozilla_version/mobile.py,sha256=3VJgbC90NpQMUTfy75zWyK4kMKjb3E7MnE_cfdHZriM,9520
+mozilla_version/parser.py,sha256=kwaw3UeAbWgUFtCmCheY9grKwabmq9tc64JyTlPrHS8,1335
+mozilla_version/version.py,sha256=MNTbIWmRWlN4jofkt2wKmyq3z3MWGWFDqrJYN1nKxj0,7929
+mozilla_version/test/__init__.py,sha256=r9z_NrSZeN6vCBiocNFI00XPm2bveSogzO-jsLa7Q-I,87
+mozilla_version/test/test_balrog.py,sha256=olr3NBdF1wtsz2Rfnb1aT3-cD7YgWQlDMfszmgz-ZgM,7839
+mozilla_version/test/test_errors.py,sha256=oR6PZorSCYDWDRrye560gz6MCXD2E4J-eyfIVCVoenw,933
+mozilla_version/test/test_fenix.py,sha256=qs8sD39N_cM9rNEZxyCaLuxx53hIIeHZIrJe_EBpYoQ,193
+mozilla_version/test/test_gecko.py,sha256=TbIoRzfvCqtbrdIOw8aeNi-eieuZBSCE9c7nNghsOps,24494
+mozilla_version/test/test_maven.py,sha256=_KaMDq47nQNctmPfA8zbTSq35vUFtaHyLkjdP9HL0zk,3526
+mozilla_version/test/test_mobile.py,sha256=uMNZhPE1Go4vJ7hxzIs23T9qBVbNYQVs6gjN32NTP4U,11948
+mozilla_version/test/test_version.py,sha256=AeWRvkgW739mEbq3JBd1hlY9hQqHro4h9gaUuLAChqU,7441
+mozilla_version-2.0.0.dist-info/LICENSE,sha256=YCIsKMGn9qksffmOXF9EWeYk5uKF4Lm5RGevX2qzND0,15922
+mozilla_version-2.0.0.dist-info/METADATA,sha256=3ZeZKRMprBj6yz8xBbHQTvK5h3vk18joltdq29yj2gY,482
+mozilla_version-2.0.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
+mozilla_version-2.0.0.dist-info/top_level.txt,sha256=K1r8SXa4ny0i7OTfimG0Ct33oHkXtLjuU1E5_aHBe94,16
+mozilla_version-2.0.0.dist-info/RECORD,,
diff --git a/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/WHEEL b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/WHEEL
new file mode 100644
index 0000000000..57e3d840d5
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.38.4)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/top_level.txt b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/top_level.txt
new file mode 100644
index 0000000000..f5c7efa40b
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version-2.0.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+mozilla_version
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..ed2808314c
--- /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, patterns=('unknown product',))
+ return product
+
+
+def _products_must_be_identical(method):
+ def checker(this, other):
+ if this.product != other.product:
+ raise ValueError(f'Cannot compare "{this.product}" and "{other.product}"')
+ return method(this, other)
+ return checker
+
+
+@attr.s(frozen=True, eq=False, hash=True)
+class BalrogReleaseName:
+ """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, 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,))
+
+ product = get_value_matched_by_regex('product', regex_matches, release_string)
+ try:
+ VersionClass = _SUPPORTED_PRODUCTS[product.lower()]
+ except KeyError:
+ raise PatternNotMatchedError(release_string, patterns=('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 f'{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..356fe16cc3
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version/errors.py
@@ -0,0 +1,75 @@
+"""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.
+ patterns (sequence): The patterns it tried to match.
+ """
+
+ def __init__(self, string, patterns):
+ """Initialize error."""
+ 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 = f'"{string}" does not match the pattern: {patterns[0]}'
+ else:
+ message = '"{}" does not match the patterns:\n - {}'.format(
+ string,
+ '\n - '.join(patterns)
+ )
+
+ super().__init__(message)
+
+
+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):
+ """Initialize error."""
+ super().__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):
+ """Initialize error."""
+ super().__init__(
+ f'Release "{version_string}" does not contain a valid {field_name}'
+ )
+
+
+class TooManyTypesError(ValueError):
+ """Error when `version_string` has too many types.
+
+ 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
+ """
+
+ def __init__(self, version_string, first_matched_type, second_matched_type):
+ """Initialize error."""
+ super().__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/fenix.py b/third_party/python/mozilla_version/mozilla_version/fenix.py
new file mode 100644
index 0000000000..038745aeff
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version/fenix.py
@@ -0,0 +1,3 @@
+"""Deprecated module for backwards compatibility."""
+# TODO remove in a future release - deprecated in favor of MobileVersion
+from mozilla_version.mobile import MobileVersion as FenixVersion # noqa
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..ab63b2c780
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version/gecko.py
@@ -0,0 +1,672 @@
+"""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_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 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<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)
+
+ _OPTIONAL_NUMBERS = BaseVersion._OPTIONAL_NUMBERS + (
+ 'old_fourth_number', 'release_candidate_number', 'beta_number', 'build_number'
+ )
+
+ _ALL_NUMBERS = BaseVersion._ALL_NUMBERS + _OPTIONAL_NUMBERS
+
+ _KNOWN_ESR_MAJOR_NUMBERS = (10, 17, 24, 31, 38, 45, 52, 60, 68, 78, 91, 102, 115)
+
+ _LAST_AURORA_DEVEDITION_AS_VERSION_TYPE = 54
+
+ 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."""
+ # General checks
+ error_messages = [
+ pattern_message
+ for condition, pattern_message in ((
+ not self.is_four_digit_scheme and self.old_fourth_number is not None,
+ 'The old fourth number can only be defined on Gecko 1.5.x.y or 2.0.x.y',
+ ), (
+ 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.is_rapid_release_scheme:
+ 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',
+ ), (
+ self.major_number > self._LAST_AURORA_DEVEDITION_AS_VERSION_TYPE and
+ self.is_aurora_or_devedition,
+ 'Last aurora/devedition version was 54.0a2. Please use the DeveditionVersion '
+ 'class, past this version.',
+ ), (
+ self.major_number not in self._KNOWN_ESR_MAJOR_NUMBERS and self.is_esr,
+ '"{}" is not a valid ESR major number. Valid ones are: {}'.format(
+ self.major_number, self._KNOWN_ESR_MAJOR_NUMBERS
+ )
+ ))
+ 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):
+ """Construct an object representing a valid Firefox version number."""
+ return super().parse(
+ version_string, regex_groups=('is_nightly', 'is_aurora_or_devedition', 'is_esr')
+ )
+
+ @property
+ def is_beta(self):
+ """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_rapid_release_scheme(self):
+ """Return `True` if `GeckoVersion` was built with against the rapid release scheme."""
+ return self.major_number >= 5
+
+ @property
+ def is_four_digit_scheme(self):
+ """Return `True` if `GeckoVersion` was built with the 4 digits schemes.
+
+ Only Firefox 1.5.x.y and 2.0.x.y were.
+ """
+ return (
+ all((self.major_number == 1, self.minor_number == 5)) or
+ all((self.major_number == 2, self.minor_number == 0))
+ )
+
+ @property
+ def is_release(self):
+ """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.
+
+ Computes a new string based on the given attributes.
+ """
+ string = super().__str__()
+
+ if self.old_fourth_number is not None:
+ string = f'{string}.{self.old_fourth_number}'
+
+ if self.is_nightly:
+ string = f'{string}a1'
+ elif self.is_aurora_or_devedition:
+ string = f'{string}a2'
+ elif self.is_beta:
+ string = f'{string}b{self.beta_number}'
+ elif self.is_release_candidate:
+ string = f'{string}rc{self.release_candidate_number}'
+ elif self.is_esr:
+ string = f'{string}esr'
+
+ if self.build_number is not None:
+ string = f'{string}build{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().__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(f'Cannot compare "{other}", type not supported!')
+
+ difference = super()._compare(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
+
+ 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
+
+ # 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)
+
+ def _create_bump_kwargs(self, field):
+ if field == 'build_number' and self.build_number is None:
+ raise ValueError('Cannot bump the build number if it is not already set')
+
+ bump_kwargs = super()._create_bump_kwargs(field)
+
+ if field == 'major_number' and self.is_esr:
+ current_esr_index = self._KNOWN_ESR_MAJOR_NUMBERS.index(self.major_number)
+ try:
+ next_major_esr_number = self._KNOWN_ESR_MAJOR_NUMBERS[current_esr_index + 1]
+ except IndexError:
+ raise ValueError(
+ "Cannot bump the major number past last known major ESR. We don't know it yet."
+ )
+ bump_kwargs['major_number'] = next_major_esr_number
+
+ 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 != 'old_fourth_number' and not self.is_four_digit_scheme:
+ del bump_kwargs['old_fourth_number']
+ if bump_kwargs.get('minor_number') == 0 and bump_kwargs.get('patch_number') == 0:
+ del bump_kwargs['patch_number']
+
+ if self.is_four_digit_scheme:
+ if (
+ bump_kwargs.get('patch_number') == 0 and
+ bump_kwargs.get('old_fourth_number') in (0, None)
+ ):
+ del bump_kwargs['patch_number']
+ del bump_kwargs['old_fourth_number']
+ elif (
+ bump_kwargs.get('patch_number') is None and
+ bump_kwargs.get('old_fourth_number') is not None and
+ bump_kwargs.get('old_fourth_number') > 0
+ ):
+ bump_kwargs['patch_number'] = 0
+
+ if field != 'release_candidate_number' and self.is_rapid_release_scheme:
+ del bump_kwargs['release_candidate_number']
+
+ bump_kwargs['is_nightly'] = self.is_nightly
+ bump_kwargs['is_aurora_or_devedition'] = self.is_aurora_or_devedition
+ bump_kwargs['is_esr'] = self.is_esr
+
+ return bump_kwargs
+
+ def bump_version_type(self):
+ """Bump version type to the next one.
+
+ Returns:
+ A new GeckoVersion with the version type set to the next one. Builds numbers are reset,
+ if originally set.
+
+ For instance:
+ * 32.0a1 is bumped to 32.0b1
+ * 32.0bX is bumped to 32.0
+ * 32.0 is bumped to 32.0esr
+ * 31.0build1 is bumped to 31.0esrbuild1
+ * 31.0build2 is bumped to 31.0esrbuild1
+
+ """
+ try:
+ return self.__class__(**self._create_bump_version_type_kwargs())
+ except (ValueError, PatternNotMatchedError) as e:
+ raise ValueError(
+ 'Cannot bump version type for version "{}". New version number is not valid. '
+ 'Cause: {}'.format(self, e)
+ ) from e
+
+ def _create_bump_version_type_kwargs(self):
+ bump_version_type_kwargs = {
+ 'major_number': self.major_number,
+ 'minor_number': self.minor_number,
+ 'patch_number': self.patch_number,
+ }
+
+ if self.is_nightly and self.major_number <= self._LAST_AURORA_DEVEDITION_AS_VERSION_TYPE:
+ bump_version_type_kwargs['is_aurora_or_devedition'] = True
+ elif (
+ self.is_nightly and self.major_number > self._LAST_AURORA_DEVEDITION_AS_VERSION_TYPE or
+ self.is_aurora_or_devedition
+ ):
+ bump_version_type_kwargs['beta_number'] = 1
+ elif self.is_beta and not self.is_rapid_release_scheme:
+ bump_version_type_kwargs['release_candidate_number'] = 1
+ elif self.is_release:
+ bump_version_type_kwargs['is_esr'] = True
+ elif self.is_esr:
+ raise ValueError('There is no higher version type than ESR.')
+
+ if self.build_number is not None:
+ bump_version_type_kwargs['build_number'] = 1
+
+ return bump_version_type_kwargs
+
+
+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_NUMBERS
+ if number_type != 'build_number'
+ ):
+ if self.build_number is None:
+ return
+ elif self.build_number == edge_case.get('build_number', None):
+ return
+
+ super().__attrs_post_init__()
+
+
+class FirefoxVersion(_VersionWithEdgeCases):
+ """Class that validates and handles Firefox version numbers."""
+
+ _RELEASED_EDGE_CASES = ({
+ 'major_number': 1,
+ 'minor_number': 5,
+ 'patch_number': 0,
+ 'old_fourth_number': 1,
+ 'release_candidate_number': 1,
+ }, {
+ '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, patterns=('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,
+ })
+
+ _LAST_FENNEC_VERSION = 68
+
+ 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 == self._LAST_FENNEC_VERSION and
+ self.minor_number > 0 and
+ self.patch_number is None
+ ):
+ return
+
+ if self.major_number > self._LAST_FENNEC_VERSION:
+ raise PatternNotMatchedError(
+ self, patterns=(f'Last Fennec version is {self._LAST_FENNEC_VERSION}',)
+ )
+
+ super().__attrs_post_init__()
+
+ def _create_bump_kwargs(self, field):
+ kwargs = super()._create_bump_kwargs(field)
+
+ if (
+ field != 'patch_number' and
+ kwargs['major_number'] == self._LAST_FENNEC_VERSION and
+ (kwargs['is_nightly'] or kwargs.get('beta_number'))
+ ):
+ del kwargs['patch_number']
+
+ return kwargs
+
+
+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,
+ '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().__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..19bc4f74c6
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version/maven.py
@@ -0,0 +1,65 @@
+"""Defines characteristics of a Maven version at Mozilla."""
+
+import attr
+import re
+
+from mozilla_version.version import BaseVersion
+
+
+@attr.s(frozen=True, eq=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)
+ is_beta = attr.ib(type=bool, default=False, init=False)
+ is_release_candidate = attr.ib(type=bool, default=False, init=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().parse(version_string, regex_groups=('is_snapshot', ))
+
+ def __str__(self):
+ """Implement string representation.
+
+ Computes a new string based on the given attributes.
+ """
+ string = super().__str__()
+
+ if self.is_snapshot:
+ string = f'{string}-SNAPSHOT'
+
+ return string
+
+ def _compare(self, other):
+ if isinstance(other, str):
+ other = MavenVersion.parse(other)
+ elif not isinstance(other, MavenVersion):
+ raise ValueError(f'Cannot compare "{other}", type not supported!')
+
+ difference = super()._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
+
+ @property
+ def is_release(self):
+ """Return `True` if the others are both False."""
+ return not any((
+ self.is_beta, self.is_release_candidate, self.is_snapshot
+ ))
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
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..1b96090c5a
--- /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(f'"{val}" must not be a float')
+ val = int(val)
+ if val >= 0:
+ return val
+ raise ValueError(f'"{val}" must be positive')
+
+
+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(f'"{val}" must be strictly positive')
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..6f7a603a5c
--- /dev/null
+++ b/third_party/python/mozilla_version/mozilla_version/version.py
@@ -0,0 +1,236 @@
+"""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, eq=False, hash=True)
+class BaseVersion:
+ """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(f'Cannot compare "{other}", type not supported!')
+
+ for field in ('major_number', 'minor_number', 'patch_number'):
+ difference = self._substract_other_number_from_this_number(other, field)
+ if difference != 0:
+ return difference
+
+ return 0
+
+ def _substract_other_number_from_this_number(self, other, field):
+ # BaseVersion sets unmatched numbers to None. E.g.: "32.0" sets the patch_number to None.
+ # Because of this behavior, `getattr(self, 'patch_number')` returns None too. That's why
+ # we can't call `getattr(self, field, 0)` directly, it will return None for all unmatched
+ # numbers
+ this_number = getattr(self, field, None)
+ this_number = 0 if this_number is None else this_number
+ other_number = getattr(other, field, None)
+ other_number = 0 if other_number is None else other_number
+
+ return this_number - other_number
+
+ def bump(self, field):
+ """Bump the number defined `field`.
+
+ Returns:
+ A new BaseVersion with the right field bumped and the following ones set to 0,
+ if they exist or if they need to be set.
+
+ For instance:
+ * 32.0 is bumped to 33.0, because the patch number does not exist
+ * 32.0.1 is bumped to 33.0.0, because the patch number exists
+ * 32.0 is bumped to 32.1.0, because patch number must be defined if the minor number
+ is not 0.
+
+ """
+ try:
+ return self.__class__(**self._create_bump_kwargs(field))
+ except (ValueError, PatternNotMatchedError) as e:
+ raise ValueError(
+ f'Cannot bump "{field}". New version number is not valid. Cause: {e}'
+ ) from e
+
+ def _create_bump_kwargs(self, field):
+ if field not in self._ALL_NUMBERS:
+ raise ValueError(f'Unknown field "{field}"')
+
+ kwargs = {}
+ has_requested_field_been_met = False
+ should_set_optional_numbers = False
+ for current_field in self._ALL_NUMBERS:
+ current_number = getattr(self, current_field, None)
+ if current_field == field:
+ has_requested_field_been_met = True
+ new_number = 1 if current_number is None else current_number + 1
+ if new_number == 1 and current_field == 'minor_number':
+ should_set_optional_numbers = True
+ kwargs[current_field] = new_number
+ else:
+ if (
+ has_requested_field_been_met and
+ (
+ current_field not in self._OPTIONAL_NUMBERS or
+ should_set_optional_numbers or
+ current_number is not None
+ )
+ ):
+ new_number = 0
+ else:
+ new_number = current_number
+ kwargs[current_field] = new_number
+
+ return kwargs
+
+
+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_CANDIDATE = 4
+ RELEASE = 5
+ ESR = 6
+
+ 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__