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))
|