2929Comparator = Callable [["Version" , Comparable ], bool ]
3030
3131
32- def cmp (a , b ): # TODO: type hints
33- """Return negative if a<b, zero if a==b, positive if a>b."""
34- return (a > b ) - (a < b )
35-
36-
37- def ensure_str (s : String , encoding = "utf-8" , errors = "strict" ) -> str :
38- # Taken from six project
39- """
40- Coerce *s* to `str`.
41-
42- * `str` -> `str`
43- * `bytes` -> decoded to `str`
44-
45- :param s: the string to convert
46- :param encoding: the encoding to apply, defaults to "utf-8"
47- :param errors: set a different error handling scheme,
48- defaults to "strict".
49- Other possible values are `ignore`, `replace`, and
50- `xmlcharrefreplace` as well as any other name
51- registered with :func:`codecs.register_error`.
52- :raises TypeError: if ``s`` is not str or bytes type
53- :return: the converted string
54- """
55- if isinstance (s , bytes ):
56- s = s .decode (encoding , errors )
57- elif not isinstance (s , String .__args__ ): # type: ignore
58- raise TypeError ("not expecting type '%s'" % type (s ))
59- return s
60-
61-
62- def comparator (operator : Comparator ) -> Comparator :
32+ def _comparator (operator : Comparator ) -> Comparator :
6333 """Wrap a Version binary op method in a type-check."""
6434
6535 @wraps (operator )
@@ -78,31 +48,9 @@ def wrapper(self: "Version", other: Comparable) -> bool:
7848 return wrapper
7949
8050
81- def _nat_cmp (a , b ): # TODO: type hints
82- def convert (text ):
83- return int (text ) if re .match ("^[0-9]+$" , text ) else text
84-
85- def split_key (key ):
86- return [convert (c ) for c in key .split ("." )]
87-
88- def cmp_prerelease_tag (a , b ):
89- if isinstance (a , int ) and isinstance (b , int ):
90- return cmp (a , b )
91- elif isinstance (a , int ):
92- return - 1
93- elif isinstance (b , int ):
94- return 1
95- else :
96- return cmp (a , b )
97-
98- a , b = a or "" , b or ""
99- a_parts , b_parts = split_key (a ), split_key (b )
100- for sub_a , sub_b in zip (a_parts , b_parts ):
101- cmp_result = cmp_prerelease_tag (sub_a , sub_b )
102- if cmp_result != 0 :
103- return cmp_result
104- else :
105- return cmp (len (a ), len (b ))
51+ def _cmd (a , b ): # TODO: type hints
52+ """Return negative if a<b, zero if a==b, positive if a>b."""
53+ return (a > b ) - (a < b )
10654
10755
10856class Version :
@@ -165,6 +113,29 @@ def __init__(
165113 self ._prerelease = None if prerelease is None else str (prerelease )
166114 self ._build = None if build is None else str (build )
167115
116+ @classmethod
117+ def _nat_cmp (cls , a , b ): # TODO: type hints
118+ def cmp_prerelease_tag (a , b ):
119+ if isinstance (a , int ) and isinstance (b , int ):
120+ return _cmd (a , b )
121+ elif isinstance (a , int ):
122+ return - 1
123+ elif isinstance (b , int ):
124+ return 1
125+ else :
126+ return _cmd (a , b )
127+
128+ a , b = a or "" , b or ""
129+ a_parts , b_parts = a .split ("." ), b .split ("." )
130+ a_parts = [int (x ) if re .match (r"^\d+$" , x ) else x for x in a_parts ]
131+ b_parts = [int (x ) if re .match (r"^\d+$" , x ) else x for x in b_parts ]
132+ for sub_a , sub_b in zip (a_parts , b_parts ):
133+ cmp_result = cmp_prerelease_tag (sub_a , sub_b )
134+ if cmp_result != 0 :
135+ return cmp_result
136+ else :
137+ return _cmd (len (a ), len (b ))
138+
168139 @property
169140 def major (self ) -> int :
170141 """The major part of a version (read-only)."""
@@ -381,12 +352,12 @@ def compare(self, other: Comparable) -> int:
381352
382353 v1 = self .to_tuple ()[:3 ]
383354 v2 = other .to_tuple ()[:3 ]
384- x = cmp (v1 , v2 )
355+ x = _cmd (v1 , v2 )
385356 if x :
386357 return x
387358
388359 rc1 , rc2 = self .prerelease , other .prerelease
389- rccmp = _nat_cmp (rc1 , rc2 )
360+ rccmp = self . _nat_cmp (rc1 , rc2 )
390361
391362 if not rccmp :
392363 return 0
@@ -444,27 +415,27 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
444415 version = version .bump_patch ()
445416 return version .bump_prerelease (prerelease_token )
446417
447- @comparator
418+ @_comparator
448419 def __eq__ (self , other : Comparable ) -> bool : # type: ignore
449420 return self .compare (other ) == 0
450421
451- @comparator
422+ @_comparator
452423 def __ne__ (self , other : Comparable ) -> bool : # type: ignore
453424 return self .compare (other ) != 0
454425
455- @comparator
426+ @_comparator
456427 def __lt__ (self , other : Comparable ) -> bool :
457428 return self .compare (other ) < 0
458429
459- @comparator
430+ @_comparator
460431 def __le__ (self , other : Comparable ) -> bool :
461432 return self .compare (other ) <= 0
462433
463- @comparator
434+ @_comparator
464435 def __gt__ (self , other : Comparable ) -> bool :
465436 return self .compare (other ) > 0
466437
467- @comparator
438+ @_comparator
468439 def __ge__ (self , other : Comparable ) -> bool :
469440 return self .compare (other ) >= 0
470441
@@ -593,15 +564,20 @@ def parse(cls, version: String) -> "Version":
593564 :param version: version string
594565 :return: a new :class:`Version` instance
595566 :raises ValueError: if version is invalid
567+ :raises TypeError: if version contains the wrong type
596568
597569 >>> semver.Version.parse('3.4.5-pre.2+build.4')
598570 Version(major=3, minor=4, patch=5, \
599571 prerelease='pre.2', build='build.4')
600572 """
601- version_str = ensure_str (version )
602- match = cls ._REGEX .match (version_str )
573+ if isinstance (version , bytes ):
574+ version = version .decode ("UTF-8" )
575+ elif not isinstance (version , String .__args__ ): # type: ignore
576+ raise TypeError ("not expecting type '%s'" % type (version ))
577+
578+ match = cls ._REGEX .match (version )
603579 if match is None :
604- raise ValueError (f"{ version_str } is not valid SemVer string" )
580+ raise ValueError (f"{ version } is not valid SemVer string" )
605581
606582 matched_version_parts : Dict [str , Any ] = match .groupdict ()
607583
0 commit comments