27
27
)
28
28
29
29
# These types are required here because of circular imports
30
- Comparable = Union ["Version" , Dict [str , VersionPart ], Collection [VersionPart ], str ]
30
+ Comparable = Union ["Version" , Dict [str , VersionPart
10000
], Collection [VersionPart ], String ]
31
31
Comparator = Callable [["Version" , Comparable ], bool ]
32
32
33
33
@@ -63,7 +63,7 @@ class Version:
63
63
64
64
* a maximum length of 5 items that comprehend the major,
65
65
minor, patch, prerelease, or build parts.
66
- * a str or bytes string that contains a valid semver
66
+ * a str or bytes string at first position that contains a valid semver
67
67
version string.
68
68
:param major: version when you make incompatible API changes.
69
69
:param minor: version when you add functionality in
@@ -83,6 +83,21 @@ class Version:
83
83
Version(major=2, minor=3, patch=4, prerelease=None, build="build.2")
84
84
"""
85
85
86
+ #: The name of the version parts
87
+ VERSIONPARTS : Tuple [str , str , str , str , str ] = (
88
+ "major" , "minor" , "patch" , "prerelease" , "build"
89
+ )
90
+ #: The default values for each part (position match with ``VERSIONPARTS``):
91
+ VERSIONPARTDEFAULTS : VersionTuple = (0 , 0 , 0 , None , None )
92
+ #: The allowed types for each part (position match with ``VERSIONPARTS``):
93
+ ALLOWED_TYPES = (
94
+ (int , str , bytes ), # major
95
+ (int , str , bytes ), # minor
96
+ (int , str , bytes ), # patch
97
+ (int , str , bytes , type (None )), # prerelease
98
+ (int , str , bytes , type (None )), # build
99
+ )
100
+
86
101
__slots__ = ("_major" , "_minor" , "_patch" , "_prerelease" , "_build" )
87
102
#: Regex for number in a prerelease
88
103
_LAST_NUMBER = re .compile (r"(?:[^\d]*(\d+)[^\d]*)+" )
@@ -108,6 +123,45 @@ class Version:
108
123
re .VERBOSE ,
109
124
)
110
125
126
+ def _check_types (self , * args : Tuple ) -> List [bool ]:
127
+ """
128
+ Check if the given arguments conform to the types in ``ALLOWED_TYPES``.
129
+
130
+ :return: bool for each position
131
+ """
132
+ cls
10000
= self .__class__
133
+ return [
134
+ isinstance (item , expected_type )
135
+ for item , expected_type in zip (args , cls .ALLOWED_TYPES )
136
+ ]
137
+
138
+ def _raise_if_args_are_invalid (self , * args ):
139
+ """
140
+ Checks conditions for positional arguments. For example:
141
+
142
+ * No more than 5 arguments.
143
+ * If first argument is a string, contains a dot, and there
144
+ are more arguments.
145
+ * Arguments have invalid types.
146
+
147
+ :raises ValueError: if more arguments than 5 or if first argument
148
+ is a string, contains a dot, and there are more arguments.
149
+ :raises TypeError: if there are invalid types.
150
+ """
151
+ if args and len (args ) > 5 :
152
+ raise ValueError ("You cannot pass more than 5 arguments to Version" )
153
+ elif len (args ) > 1 and "." in str (args [0 ]):
154
+ raise ValueError (
155
+ "You cannot pass a string and additional positional arguments"
156
+ )
157
+ types_in_args = self ._check_types (* args )
158
+ if not all (types_in_args ):
159
+ pos = types_in_args .index (False )
160
+ raise TypeError (
161
+ "not expecting type in argument position "
162
+ f"{ pos } (type: { type (args [pos ])} )"
163
+ )
164
+
111
165
def __init__ (
112
166
self ,
113
167
* args : Tuple [
@@ -117,76 +171,82 @@ def __init__(
117
171
Optional [StringOrInt ], # prerelease
118
172
Optional [StringOrInt ], # build
119
173
],
120
- major : SupportsInt = 0 ,
121
- minor : SupportsInt = 0 ,
122
- patch : SupportsInt = 0 ,
174
+ # *,
175
+ major : SupportsInt = None ,
176
+ minor : SupportsInt = None ,
177
+ patch : SupportsInt = None ,
123
178
prerelease : StringOrInt = None ,
124
179
build : StringOrInt = None ,
125
180
):
126
- def _check_types (* args ):
127
- if args and len (args ) > 5 :
128
- raise ValueError ("You cannot pass more than 5 arguments to Version" )
129
- elif len (args ) > 1 and "." in str (args [0 ]):
130
- raise ValueError (
131
- "You cannot pass a string and additional positional arguments"
132
- )
133
- allowed_types_in_args = (
134
- (int , str , bytes ), # major
135
- (int , str , bytes ), # minor
136
- (int , str , bytes ), # patch
137
- (int , str , bytes , type (None )), # prerelease
138
- (int , str , bytes , type (None )), # build
139
- )
140
- return [
141
- isinstance (item , expected_type )
142
- for item , expected_type in zip (args , allowed_types_in_args )
143
- ]
181
+ #
182
+ # The algorithm to support different Version calls is this:
183
+ #
184
+ # 1. Check first, if there are invalid calls. For example
185
+ # more than 5 items in args or a unsupported combination
186
+ # of args and version part arguments (major, minor, etc.)
187
+ # If yes, raise an exception.
188
+ #
189
+ # 2. Create a dictargs dict:
190
+ # a. If the first argument is a version string which contains
191
+ # a dot it's likely it's a semver string. Try to convert
192
+ # them into a dict and save it to dictargs.
193
+ # b. If the first argument is not a version string, try to
194
+ # create the dictargs from the args argument.
195
+ #
196
+ # 3. Create a versiondict from the version part arguments.
197
+ # This contains only items if the argument is not None.
198
+ #
199
+ # 4. Merge the two dicts, versiondict overwrites dictargs.
200
+ # In other words, if the user specifies Version(1, major=2)
201
+ # the major=2 has precedence over the 1.
202
<
10000
td data-grid-cell-id="diff-d6e8c013b9e7be774edf21bed5411e65f2a2bf6b43110b67e6c4dccdcab23791-143-202-2" data-line-anchor="diff-d6e8c013b9e7be774edf21bed5411e65f2a2bf6b43110b67e6c4dccdcab23791R202" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionLine-bgColor, var(--diffBlob-addition-bgColor-line));padding-right:24px" tabindex="-1" valign="top" class="focusable-grid-cell diff-text-cell right-side-diff-cell left-side">+ #
203
+ # 5. Set all version components from versiondict. If the key
204
+ # doesn't exist, set a default value.
144
205
145
206
cls = self .__class__
146
- verlist : List [Optional [StringOrInt ]] = [None , None , None , None , None ]
207
+ # (1) check combinations and types
208
+ self ._raise_if_args_are_invalid (* args )
147
209
148
- types_in_args = _check_types (* args )
149
- if not all (types_in_args ):
150
- pos = types_in_args .index (False )
151
- raise TypeError (
152
- "not expecting type in argument position "
153
- f"{ pos } (type: { type (args [pos ])} )"
154
- )
155
- elif args and "." in str (args [0 ]):
156
- # we have a version string as first argument
157
- v = cls ._parse (args [0 ]) # type: ignore
158
- for idx , key in enumerate (
159
- ("major" , "minor" , "patch" , "prerelease" , "build" )
160
- ):
161
- verlist [idx ] = v [key ]
210
+ # (2) First argument was a string
211
+ if args and args [0 ] and "." in cls ._enforce_str (args [0 ]): # type: ignore
212
+ dictargs = cls ._parse (cast (String , args [0 ]))
162
213
else :
163
- for index , item in enumerate (args ):
164
- verlist [index ] = args [index ] # type: ignore
214
+ dictargs = dict (zip (cls .VERSIONPARTS , args ))
165
215
166
- # Build a dictionary of the arguments except prerelease and build
167
- try :
168
- version_parts = {
169
- # Prefer major, minor, and patch arguments over args
170
- "major" : int (major or verlist [0 ] or 0 ),
171
- "minor" : int (minor or verlist [1 ] or 0 ),
172
- "patch" : int (patch or verlist [2 ] or 0 ),
173
- }
174
- except ValueError :
175
- raise ValueError (
176
- "Expected integer or integer string for major, minor, or patch"
216
+ # (3) Only include part in versiondict if value is not None
217
+ versiondict = {
218
+ part : value
219
+ for part , value in zip (
220
+ cls .VERSIONPARTS , (major , minor , patch , prerelease , build )
177
221
)
222
+ if value is not None
223
+ }
178
224
179
- for name , value in version_parts .items ():
180
- if value < 0 :
181
- raise ValueError (
182
- "{!r} is negative. A version can only be positive." .format (name )
183
- )
225
+ # (4) Order here is important: versiondict overwrites dictargs
226
+ versiondict = {** dictargs , ** versiondict } # type: ignore
184
227
185
- self ._major = version_parts ["major" ]
186
- self ._minor = version_parts ["minor" ]
187
- self ._patch = version_parts ["patch" ]
188
- self ._prerelease = cls ._enforce_str (prerelease or verlist [3 ])
189
- self ._build = cls ._enforce_str (build or verlist [4 ])
228
+ # (5) Set all version components:
229
+ self ._major = cls ._ensure_int (
230
+ cast (StringOrInt , versiondict .get ("major" , cls .VERSIONPARTDEFAULTS [0 ]))
231
+ )
232
+ self ._minor = cls ._ensure_int (
233
+ cast (StringOrInt , versiondict .get ("minor" , cls .VERSIONPARTDEFAULTS [1 ]))
234
+ )
235
+ self ._patch = cls ._ensure_int (
236
+ cast (StringOrInt , versiondict .get ("patch" , cls .VERSIONPARTDEFAULTS [2 ]))
237
+ )
238
+ self ._prerelease = cls ._enforce_str (
239
+ cast (
240
+ Optional [StringOrInt ],
241
+ versiondict .get ("prerelease" , cls .VERSIONPARTDEFAULTS [3 ]),
242
+ )
243
+ )
244
+ self ._build = cls ._enforce_str (
245
+ cast (
246
+ Optional [StringOrInt ],
247
+ versiondict .get ("build" , cls .VERSIONPARTDEFAULTS [4 ]),
248
+ )
249
+ )
190
250
191
251
@classmethod
192
252
def _nat_cmp (cls , a , b ): # TODO: type hints
@@ -211,6 +271,31 @@ def cmp_prerelease_tag(a, b):
211
271
else :
212
272
return _cmp (len (a ), len (b ))
213
273
274
+ @classmethod
275
+ def _ensure_int (cls , value : StringOrInt ) -> int :
276
+ """
277
+ Ensures integer value type regardless if argument type is str or bytes.
278
+ Otherwise raise ValueError.
279
+
280
+ :param value:
281
+ :raises ValueError: Two conditions:
282
+ * If value is not an integer or cannot be converted.
283
+ * If value is negative.
284
+ :return: the converted value as integer
285
+ """
286
+ try :
287
+ value = int (value )
288
+ except ValueError :
289
+ raise ValueError (
290
+ "Expected integer or integer string for major, minor, or patch"
291
+ )
292
+
293
+ if value < 0 :
294
+ raise ValueError (
295
+ f"Argument { value } is negative. A version can only be positive."
296
+ )
297
+ return value
298
+
214
299
@classmethod
215
300
def _enforce_str (cls , s : Optional [StringOrInt ]) -> Optional [str ]:
216
301
"""
@@ -462,8 +547,12 @@ def compare(self, other: Comparable) -> int:
462
547
0
463
548
"""
464
549
cls = type (self )
550
+
551
+ # See https://github.com/python/mypy/issues/4019
465
552
if isinstance (other , String .__args__ ): # type: ignore
466
- other = cls .parse (other )
553
+ if "." not in cast (str , cls ._ensure_str (other )):
554
+ raise ValueError ("Expected semver version string." )
555
+ other = cls (other )
467
556
elif isinstance (other , dict ):
468
557
other = cls (** other )
469
558
elif isinstance (other , (tuple , list )):
0 commit comments