aboutsummaryrefslogtreecommitdiffstats
path: root/build/plugins/lib/nots/semver/semver.py
blob: 1398da8586ff2d6ee509cda8dbb5b4988de17c59 (plain) (blame)
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import re


class Version:
    """
    This class is intended to provide utility methods to work with semver ranges.
    Right now it is limited to the simplest case: a ">=" operator followed by an exact version with no prerelease or build specification.
    Example: ">= 1.2.3"
    """

    @classmethod
    def from_str(cls, input):
        """
        :param str input: save exact formatted version e.g. 1.2.3
        :rtype: Version
        :raises: ValueError
        """
        parts = input.strip().split(".", 2)
        major = int(parts[0])
        minor = int(parts[1])
        patch = int(parts[2])

        return cls(major, minor, patch)

    STABLE_VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$')

    @classmethod
    def is_stable(cls, v):
        """
        Verifies that the version is in a supported format.

        :param v:string with the version
        :return: bool
        """
        return cls.STABLE_VERSION_RE.match(v) is not None

    @classmethod
    def cmp(cls, a, b):
        """
        Compare two versions. Should be used with "cmp_to_key" wrapper in sorted(), min(), max()...

        For example:
                sorted(["1.2.3", "2.4.2", "1.2.7"], key=cmp_to_key(Version.cmp))

        :param a:string with version or Version instance
        :param b:string with version or Version instance
        :return: int
        :raises: ValueError
        """
        a_version = a if isinstance(a, cls) else cls.from_str(a)
        b_version = b if isinstance(b, cls) else cls.from_str(b)

        if a_version > b_version:
            return 1
        elif a_version < b_version:
            return -1
        else:
            return 0

    __slots__ = "_values"

    def __init__(self, major, minor, patch):
        """
        :param int major
        :param int minor
        :param int patch
        :raises ValueError
        """
        version_parts = {
            "major": major,
            "minor": minor,
            "patch": patch,
        }

        for name, value in version_parts.items():
            value = int(value)
            version_parts[name] = value
            if value < 0:
                raise ValueError("{!r} is negative. A version can only be positive.".format(name))

        self._values = (version_parts["major"], version_parts["minor"], version_parts["patch"])

    def __str__(self):
        return "{}.{}.{}".format(self._values[0], self._values[1], self._values[2])

    def __repr__(self):
        return '<Version({})>'.format(self)

    def __eq__(self, other):
        """
        :param Version|str other
        :rtype: bool
        """
        if isinstance(other, str):
            if self.is_stable(other):
                other = self.from_str(other)
            else:
                return False

        return self.as_tuple() == other.as_tuple()

    def __ne__(self, other):
        return not self == other

    def __gt__(self, other):
        """
        :param Version other
        :rtype: bool
        """
        return self.as_tuple() > other.as_tuple()

    def __ge__(self, other):
        """
        :param Version other
        :rtype: bool
        """
        return self.as_tuple() >= other.as_tuple()

    def __lt__(self, other):
        """
        :param Version other
        :rtype: bool
        """
        return self.as_tuple() < other.as_tuple()

    def __le__(self, other):
        """
        :param Version other
        :rtype: bool
        """
        return self.as_tuple() <= other.as_tuple()

    @property
    def major(self):
        """The major part of the version (read-only)."""
        return self._values[0]

    @major.setter
    def major(self, value):
        raise AttributeError("Attribute 'major' is readonly")

    @property
    def minor(self):
        """The minor part of the version (read-only)."""
        return self._values[1]

    @minor.setter
    def minor(self, value):
        raise AttributeError("Attribute 'minor' is readonly")

    @property
    def patch(self):
        """The patch part of the version (read-only)."""
        return self._values[2]

    @patch.setter
    def patch(self, value):
        raise AttributeError("Attribute 'patch' is readonly")

    def as_tuple(self):
        """
        :rtype: tuple
        """
        return self._values


class Operator:
    EQ = "="
    GT = ">"
    GE = ">="
    LT = "<"
    LE = "<="


class VersionRange:
    @classmethod
    def operator_is_ok(self, operator):
        return [Operator.GE, Operator.EQ, None].count(operator)

    @classmethod
    def from_str(cls, input):
        """
        :param str input
        :rtype: VersionRange
        :raises: ValueError
        """
        m = re.match(r"^\s*([<>=]+)?\s*(\d+\.\d+\.\d+)\s*$", input)
        res = m.groups() if m else None
        if not res or not cls.operator_is_ok(res[0]):
            raise ValueError(
                "Unsupported version range: '{}'. Currently we only support ranges with stable versions and GE / EQ: '>= 1.2.3' / '= 1.2.3' / '1.2.3'".format(
                    input
                )
            )

        version = Version.from_str(res[1])

        return cls(res[0], version)

    __slots__ = ("_operator", "_version")

    def __init__(self, operator, version):
        """
        :param str operator
        :raises: ValueError
        """
        if not self.operator_is_ok(operator):
            raise ValueError("Unsupported range operator '{}'".format(operator))

        # None defaults to Operator.EQ
        self._operator = operator or Operator.EQ
        self._version = version

    @property
    def operator(self):
        """The comparison operator to be used (read-only)."""
        return self._operator

    @operator.setter
    def operator(self, value):
        raise AttributeError("Attribute 'operator' is readonly")

    @property
    def version(self):
        """Version to be used with the operator (read-only)."""
        return self._version

    @version.setter
    def version(self, value):
        raise AttributeError("Attribute 'version' is readonly")

    def is_satisfied_by(self, version):
        """
        :param Version version
        :rtype: bool
        :raises: ValueError
        """
        if self._operator == Operator.GE:
            return version >= self._version

        if self._operator == Operator.EQ:
            return version == self._version

        raise ValueError("Unsupported operator '{}'".format(self._operator))