1010from __future__ import annotations
1111
1212import re
13- from typing import Any , Callable , NamedTuple , SupportsInt , Tuple , Union
13+ import sys
14+ from typing import Any , Callable , SupportsInt , Tuple , Union
1415
1516from ._structures import Infinity , InfinityType , NegativeInfinity , NegativeInfinityType
1617
3435VersionComparisonMethod = Callable [[CmpKey , CmpKey ], bool ]
3536
3637
37- class _Version (NamedTuple ):
38- epoch : int
39- release : tuple [int , ...]
40- dev : tuple [str , int ] | None
41- pre : tuple [str , int ] | None
42- post : tuple [str , int ] | None
43- local : LocalType | None
44-
45-
4638def parse (version : str ) -> Version :
4739 """Parse the given version string.
4840
@@ -66,7 +58,9 @@ class InvalidVersion(ValueError):
6658
6759
6860class _BaseVersion :
69- _key : tuple [Any , ...]
61+ @property
62+ def _key (self ) -> tuple [Any , ...]:
63+ raise NotImplementedError # pragma: no cover
7064
7165 def __hash__ (self ) -> int :
7266 return hash (self ._key )
@@ -113,17 +107,19 @@ def __ne__(self, other: object) -> bool:
113107
114108# Deliberately not anchored to the start and end of the string, to make it
115109# easier for 3rd party code to reuse
110+
111+ # Note that ++ doesn't behave identically on CPython and PyPy, so not using it here
116112_VERSION_PATTERN = r"""
117- v? # optional leading v
113+ v?+ # optional leading v
118114 (?:
119- (?:(?P<epoch>[0-9]+)!)? # epoch
120- (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
115+ (?:(?P<epoch>[0-9]+)!)?+ # epoch
116+ (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment
121117 (?P<pre> # pre-release
122- [._-]?
118+ [._-]?+
123119 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
124- [._-]?
120+ [._-]?+
125121 (?P<pre_n>[0-9]+)?
126- )?
122+ )?+
127123 (?P<post> # post release
128124 (?:-(?P<post_n1>[0-9]+))
129125 |
@@ -133,23 +129,27 @@ def __ne__(self, other: object) -> bool:
133129 [._-]?
134130 (?P<post_n2>[0-9]+)?
135131 )
136- )?
132+ )?+
137133 (?P<dev> # dev release
138- [._-]?
134+ [._-]?+
139135 (?P<dev_l>dev)
140- [._-]?
136+ [._-]?+
141137 (?P<dev_n>[0-9]+)?
142- )?
138+ )?+
143139 )
144140 (?:\+
145141 (?P<local> # local version
146142 [a-z0-9]+
147- (?:[._-][a-z0-9]+)*
143+ (?:[._-][a-z0-9]+)*+
148144 )
149- )?
145+ )?+
150146"""
151147
152- VERSION_PATTERN = _VERSION_PATTERN
148+ _VERSION_PATTERN_OLD = _VERSION_PATTERN .replace ("*+" , "*" ).replace ("?+" , "?" )
149+
150+ VERSION_PATTERN = (
151+ _VERSION_PATTERN_OLD if sys .version_info < (3 , 11 ) else _VERSION_PATTERN
152+ )
153153"""
154154A string containing the regular expression used to match a valid version.
155155
@@ -186,9 +186,16 @@ class Version(_BaseVersion):
186186 True
187187 """
188188
189+ _epoch : int
190+ _release : tuple [int , ...]
191+ _dev : tuple [str , int ] | None
192+ _pre : tuple [str , int ] | None
193+ _post : tuple [str , int ] | None
194+ _local : LocalType | None
195+
196+ _key_cache : CmpKey | None
197+
189198 _regex = re .compile (r"\s*" + VERSION_PATTERN + r"\s*" , re .VERBOSE | re .IGNORECASE )
190- _version : _Version
191- _key : CmpKey
192199
193200 def __init__ (self , version : str ) -> None :
194201 """Initialize a Version object.
@@ -200,33 +207,34 @@ def __init__(self, version: str) -> None:
200207 If the ``version`` does not conform to PEP 440 in any way then this
201208 exception will be raised.
202209 """
203-
204210 # Validate the version and parse it into pieces
205211 match = self ._regex .fullmatch (version )
206212 if not match :
207213 raise InvalidVersion (f"Invalid version: { version !r} " )
208-
209- # Store the parsed out pieces of the version
210- self ._version = _Version (
211- epoch = int (match .group ("epoch" )) if match .group ("epoch" ) else 0 ,
212- release = tuple (int (i ) for i in match .group ("release" ).split ("." )),
213- pre = _parse_letter_version (match .group ("pre_l" ), match .group ("pre_n" )),
214- post = _parse_letter_version (
215- match .group ("post_l" ), match .group ("post_n1" ) or match .group ("post_n2" )
216- ),
217- dev = _parse_letter_version (match .group ("dev_l" ), match .group ("dev_n" )),
218- local = _parse_local_version (match .group ("local" )),
214+ self ._epoch = int (match .group ("epoch" )) if match .group ("epoch" ) else 0
215+ self ._release = tuple (map (int , match .group ("release" ).split ("." )))
216+ self ._pre = _parse_letter_version (match .group ("pre_l" ), match .group ("pre_n" ))
217+ self ._post = _parse_letter_version (
218+ match .group ("post_l" ), match .group ("post_n1" ) or match .group ("post_n2" )
219219 )
220+ self ._dev = _parse_letter_version (match .group ("dev_l" ), match .group ("dev_n" ))
221+ self ._local = _parse_local_version (match .group ("local" ))
220222
221- # Generate a key which will be used for sorting
222- self ._key = _cmpkey (
223- self ._version .epoch ,
224- self ._version .release ,
225- self ._version .pre ,
226- self ._version .post ,
227- self ._version .dev ,
228- self ._version .local ,
229- )
223+ # Key which will be used for sorting
224+ self ._key_cache = None
225+
226+ @property
227+ def _key (self ) -> CmpKey :
228+ if self ._key_cache is None :
229+ self ._key_cache = _cmpkey (
230+ self ._epoch ,
231+ self ._release ,
232+ self ._pre ,
233+ self ._post ,
234+ self ._dev ,
235+ self ._local ,
236+ )
237+ return self ._key_cache
230238
231239 def __repr__ (self ) -> str :
232240 """A representation of the Version that shows all internal state.
@@ -246,7 +254,7 @@ def __str__(self) -> str:
246254
247255 # Pre-release
248256 if self .pre is not None :
249- parts .append ("" .join (str ( x ) for x in self .pre ))
257+ parts .append ("" .join (map ( str , self .pre ) ))
250258
251259 # Post-release
252260 if self .post is not None :
@@ -271,7 +279,7 @@ def epoch(self) -> int:
271279 >>> Version("1!2.0.0").epoch
272280 1
273281 """
274- return self ._version . epoch
282+ return self ._epoch
275283
276284 @property
277285 def release (self ) -> tuple [int , ...]:
@@ -287,7 +295,7 @@ def release(self) -> tuple[int, ...]:
287295 Includes trailing zeroes but not the epoch or any pre-release / development /
288296 post-release suffixes.
289297 """
290- return self ._version . release
298+ return self ._release
291299
292300 @property
293301 def pre (self ) -> tuple [str , int ] | None :
@@ -302,7 +310,7 @@ def pre(self) -> tuple[str, int] | None:
302310 >>> Version("1.2.3rc1").pre
303311 ('rc', 1)
304312 """
305- return self ._version . pre
313+ return self ._pre
306314
307315 @property
308316 def post (self ) -> int | None :
@@ -313,7 +321,7 @@ def post(self) -> int | None:
313321 >>> Version("1.2.3.post1").post
314322 1
315323 """
316- return self ._version . post [1 ] if self ._version . post else None
324+ return self ._post [1 ] if self ._post else None
317325
318326 @property
319327 def dev (self ) -> int | None :
@@ -324,7 +332,7 @@ def dev(self) -> int | None:
324332 >>> Version("1.2.3.dev1").dev
325333 1
326334 """
327- return self ._version . dev [1 ] if self ._version . dev else None
335+ return self ._dev [1 ] if self ._dev else None
328336
329337 @property
330338 def local (self ) -> str | None :
@@ -335,8 +343,8 @@ def local(self) -> str | None:
335343 >>> Version("1.2.3+abc").local
336344 'abc'
337345 """
338- if self ._version . local :
339- return "." .join (str (x ) for x in self ._version . local )
346+ if self ._local :
347+ return "." .join (str (x ) for x in self ._local )
340348 else :
341349 return None
342350
@@ -442,6 +450,18 @@ def micro(self) -> int:
442450
443451
444452class _TrimmedRelease (Version ):
453+ def __init__ (self , version : str | Version ) -> None :
454+ if isinstance (version , Version ):
455+ self ._epoch = version ._epoch
456+ self ._release = version ._release
457+ self ._dev = version ._dev
458+ self ._pre = version ._pre
459+ self ._post = version ._post
460+ self ._local = version ._local
461+ self ._key_cache = version ._key_cache
462+ return
463+ super ().__init__ (version ) # pragma: no cover
464+
445465 @property
446466 def release (self ) -> tuple [int , ...]:
447467 """
@@ -452,6 +472,7 @@ def release(self) -> tuple[int, ...]:
452472 >>> _TrimmedRelease('0.0').release
453473 (0,)
454474 """
475+ # Unlike _strip_trailing_zeros, this leaves one 0.
455476 rel = super ().release
456477 i = len (rel )
457478 while i > 1 and rel [i - 1 ] == 0 :
0 commit comments