summaryrefslogtreecommitdiffstats
path: root/python/mozrelease/mozrelease/versions.py
blob: 348b77bd7545e72a4c61874ad4be0f747ff9db13 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# 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/.

import re

from looseversion import LooseVersion
from packaging.version import InvalidVersion


class StrictVersion:
    def __init__(self, vstring):
        self.parse(vstring)

    def __repr__(self):
        return "%s ('%s')" % (self.__class__.__name__, str(self))

    def __eq__(self, other):
        return self._cmp(other) == 0

    def __lt__(self, other):
        return self._cmp(other) < 0

    def parse(self, vstring):
        match = self.version_re.match(vstring)
        if not match:
            raise InvalidVersion("invalid version number '%s'" % vstring)

        major, minor, patch, pre, pre_num = match.group(1, 2, 4, 5, 6)
        self.version = int(major), int(minor), int(patch or 0)
        self.pre = (pre[0], int(pre_num)) if pre else ()

    def __str__(self):
        return ".".join(map(str, self.version)) + (
            "".join(map(str, self.pre)) if self.pre else ""
        )

    def _cmp(self, other):
        if isinstance(other, str):
            other = StrictVersion(other)
        elif not isinstance(other, StrictVersion):
            raise NotImplementedError

        if self.version < other.version:
            return -1
        elif self.version == other.version:
            if self.pre == other.pre:
                return 0
            elif not self.pre:
                return 1
            elif not other.pre:
                return -1
            elif self.pre < other.pre:
                return -1
            else:
                return 1
        else:
            return 1


class MozillaVersionCompareMixin:
    def __cmp__(self, other):
        # We expect this function to never be called.
        raise AssertionError()

    def _cmp(self, other):
        has_esr = set()
        if isinstance(other, LooseModernMozillaVersion) and str(other).endswith("esr"):
            # If other version ends with esr, coerce through MozillaVersion ending up with
            # a StrictVersion if possible
            has_esr.add("other")
            other = MozillaVersion(str(other)[:-3])  # strip ESR from end of string
        if isinstance(self, LooseModernMozillaVersion) and str(self).endswith("esr"):
            # If our version ends with esr, coerce through MozillaVersion ending up with
            # a StrictVersion if possible
            has_esr.add("self")
            self = MozillaVersion(str(self)[:-3])  # strip ESR from end of string
        if isinstance(other, LooseModernMozillaVersion) or isinstance(
            self, LooseModernMozillaVersion
        ):
            # If we're still LooseVersion for self or other, run LooseVersion compare
            # Being sure to pass through Loose Version type first
            val = LooseVersion._cmp(
                LooseModernMozillaVersion(str(self)),
                LooseModernMozillaVersion(str(other)),
            )
        else:
            # No versions are loose, therefore we can use StrictVersion
            val = StrictVersion._cmp(self, other)
        if has_esr.isdisjoint(set(["other", "self"])) or has_esr.issuperset(
            set(["other", "self"])
        ):
            #  If both had esr string or neither, then _cmp() was accurate
            return val
        elif val != 0:
            # cmp is accurate here even if esr is present in only 1 compare, since
            # versions are not equal
            return val
        elif "other" in has_esr:
            return -1  # esr is not greater than non esr
        return 1  # non esr is greater than esr


class ModernMozillaVersion(MozillaVersionCompareMixin, StrictVersion):
    """A version class that is slightly less restrictive than StrictVersion.
    Instead of just allowing "a" or "b" as prerelease tags, it allows any
    alpha. This allows us to support the once-shipped "3.6.3plugin1" and
    similar versions."""

    version_re = re.compile(
        r"""^(\d+) \. (\d+) (\. (\d+))?
                                ([a-zA-Z]+(\d+))?$""",
        re.VERBOSE,
    )


class AncientMozillaVersion(MozillaVersionCompareMixin, StrictVersion):
    """A version class that is slightly less restrictive than StrictVersion.
    Instead of just allowing "a" or "b" as prerelease tags, it allows any
    alpha. This allows us to support the once-shipped "3.6.3plugin1" and
    similar versions.
    It also supports versions w.x.y.z by transmuting to w.x.z, which
    is useful for versions like 1.5.0.x and 2.0.0.y"""

    version_re = re.compile(
        r"""^(\d+) \. (\d+) \. \d (\. (\d+))
                                ([a-zA-Z]+(\d+))?$""",
        re.VERBOSE,
    )


class LooseModernMozillaVersion(MozillaVersionCompareMixin, LooseVersion):
    """A version class that is more restrictive than LooseVersion.
    This class reduces the valid strings to "esr", "a", "b" and "rc" in order
    to support esr. StrictVersion requires a trailing number after all strings."""

    component_re = re.compile(r"(\d+ | a | b | rc | esr | \.)", re.VERBOSE)

    def __repr__(self):
        return "LooseModernMozillaVersion ('%s')" % str(self)


def MozillaVersion(version):
    try:
        return ModernMozillaVersion(version)
    except InvalidVersion:
        pass
    try:
        if version.count(".") == 3:
            return AncientMozillaVersion(version)
    except InvalidVersion:
        pass
    try:
        return LooseModernMozillaVersion(version)
    except ValueError:
        pass
    raise ValueError("Version number %s is invalid." % version)


def getPrettyVersion(version):
    version = re.sub(r"a([0-9]+)$", r" Alpha \1", version)
    version = re.sub(r"b([0-9]+)$", r" Beta \1", version)
    version = re.sub(r"rc([0-9]+)$", r" RC \1", version)
    return version