1
1
"""Version handling by a semver compatible version class."""
2
2
3
+ from ast import operator
3
4
import collections
4
5
import re
5
6
from functools import wraps
14
15
cast ,
15
16
Callable ,
16
17
Collection ,
18
+ Match
17
19
Type ,
18
20
TypeVar ,
19
21
)
@@ -75,6 +77,10 @@ class Version:
75
77
#: The names of the different parts of a version
76
78
NAMES = tuple ([item [1 :] for item in __slots__ ])
77
79
80
+ #:
81
+ _RE_NUMBER = r"0|[1-9]\d*"
82
+
83
+
78
84
#: Regex for number in a prerelease
79
85
_LAST_NUMBER = re .compile (r"(?:[^\d]*(\d+)[^\d]*)+" )
80
86
#: Regex template for a semver version
@@ -110,6 +116,14 @@ class Version:
110
116
re .VERBOSE ,
111
117
)
112
118
119
+ #: The default prefix for the prerelease part.
120
+ #: Used in :meth:`Version.bump_prerelease`.
121
+ default_prerelease_prefix = "rc"
122
+
123
+ #: The default prefix for the build part
124
+ #: Used in :meth:`Version.bump_build`.
125
+ default_build_prefix = "build"
126
+
113
127
def __init__ (
114
128
self ,
115
129
major : SupportsInt ,
@@ -382,22 +396,21 @@ def compare(self, other: Comparable) -> int:
382
396
:return: The return value is negative if ver1 < ver2,
383
397
zero if ver1 == ver2 and strictly positive if ver1 > ver2
384
398
385
- >>> semver.compare("2.0.0")
399
+ >>> ver = semver.Version.parse("3.4.5")
400
+ >>> ver.compare("4.0.0")
386
401
-1
387
- >>> semver .compare("1 .0.0")
402
+ >>> ver .compare("3 .0.0")
388
403
1
389
- >>> semver.compare("2.0.0")
390
- 0
391
- >>> semver.compare(dict(major=2, minor=0, patch=0))
404
+ >>> ver.compare("3.4.5")
392
405
0
393
406
"""
394
407
cls = type (self )
395
408
if isinstance (other , String .__args__ ): # type: ignore
396
- other = cls .parse (other )
409
+ other = cls .parse (other ) # type: ignore
397
410
elif isinstance (other , dict ):
398
- other = cls (** other )
411
+ other = cls (** other ) # type: ignore
399
412
elif isinstance (other , (tuple , list )):
400
- other = cls (* other )
413
+ other = cls (* other ) # type: ignore
401
414
elif not isinstance (other , cls ):
402
415
raise TypeError (
403
416
f"Expected str, bytes, dict, tuple, list, or { cls .__name__ } instance, "
@@ -555,25 +568,19 @@ def finalize_version(self) -> "Version":
555
568
cls = type (self )
556
569
return cls (self .major , self .minor , self .patch )
557
570
558
- def match (self , match_expr : str ) -> bool :
571
+ def _match (self , match_expr : str ) -> bool :
559
572
"""
560
573
Compare self to match a match expression.
561
574
562
575
:param match_expr: optional operator and version; valid operators are
563
- ``<`` smaller than
576
+ ``<``` smaller than
564
577
``>`` greater than
565
578
``>=`` greator or equal than
566
579
``<=`` smaller or equal than
567
580
``==`` equal
568
581
``!=`` not equal
582
+ ``~=`` compatible release clause
569
583
:return: True if the expression matches the version, otherwise False
570
-
571
- >>> semver.Version.parse("2.0.0").match(">=1.0.0")
572
- True
573
- >>> semver.Version.parse("1.0.0").match(">1.0.0")
574
- False
575
- >>> semver.Version.parse("4.0.4").match("4.0.4")
576
- True
577
584
"""
578
585
prefix = match_expr [:2 ]
579
586
if prefix in (">=" , "<=" , "==" , "!=" ):
@@ -588,7 +595,7 @@ def match(self, match_expr: str) -> bool:
588
595
raise ValueError (
589
596
"match_expr parameter should be in format <op><ver>, "
590
597
"where <op> is one of "
591
- "['<', '>', '==', '<=', '>=', '!=']. "
598
+ "['<', '>', '==', '<=', '>=', '!=', '~=' ]. "
592
599
"You provided: %r" % match_expr
593
600
)
594
601
@@ -606,6 +613,119 @@ def match(self, match_expr: str) -> bool:
606
613
607
614
return cmp_res in possibilities
608
615
616
+ def match (self , match_expr : str ) -> bool :
617
+ """Compare self to match a match expression.
618
+
619
+ :param match_expr: optional operator and version; valid operators are
620
+ ``<``` smaller than
621
+ ``>`` greater than
622
+ ``>=`` greator or equal than
623
+ ``<=`` smaller or equal than
624
+ ``==`` equal
625
+ ``!=`` not equal
626
+ ``~=`` compatible release clause
627
+ :return: True if the expression matches the version, otherwise False
628
+ """
629
+ # TODO: The following function should be better
630
+ # integrated into a special Spec class
631
+ def compare_eq (index , other ) -> bool :
632
+ return self [:index ] == other [:index ]
633
+
634
+ def compare_ne (index , other ) -> bool :
635
+ return not compare_eq (index , other )
636
+
637
+ def compare_lt (index , other ) -> bool :
638
+ return self [:index ] < other [:index ]
639
+
640
+ def compare_gt (index , other ) -> bool :
641
+ return not compare_lt (index , other )
642
+
643
+ def compare_le (index , other ) -> bool :
644
+ return self [:index ] <= other [:index ]
645
+
646
+ def compare_ge (index , other ) -> bool :
647
+ return self [:index ] >= other [:index ]
648
+
649
+ def compare_compatible (index , other ) -> bool :
650
+ return compare_gt (index , other ) and compare_eq (index , other )
651
+
652
+ op_table : Dict [str , Callable [[int , Tuple ], bool ]] = {
653
+ '==' : compare_eq ,
654
+ '!=' : compare_ne ,
655
+ '<' : compare_lt ,
656
+ '>' : compare_gt ,
657
+ '<=' : compare_le ,
658
+ '>=' : compare_ge ,
659
+ '~=' : compare_compatible ,
660
+ }
661
+
662
+ regex = r"""(?P<operator>[<]|[>]|<=|>=|~=|==|!=)?
663
+ (?P<version>
664
+ (?P<major>0|[1-9]\d*)
665
+ (?:\.(?P<minor>\*|0|[1-9]\d*)
666
+ (?:\.(?P<patch>\*|0|[1-9]\d*))?
667
+ )?
668
+ )"""
669
+ match = re .match (regex , match_expr , re .VERBOSE )
670
+ if match is None :
671
+ raise ValueError (
672
+ "match_expr parameter should be in format <op><ver>, "
673
+ "where <op> is one of %s. "
674
+ "<ver> is a version string like '1.2.3' or '1.*' "
675
+ "You provided: %r" % (list (op_table .keys ()), match_expr )
676
+ )
677
+ match_version = match ["version" ]
678
+ operator = cast (Dict , match ).get ('operator' , '==' )
679
+
680
+ if "*" not in match_version :
681
+ # conventional compare
682
+ possibilities_dict = {
683
+ ">" : (1 ,),
684
+ "<" : (- 1 ,),
685
+ "==" : (0 ,),
686
+ "!=" : (- 1 , 1 ),
687
+ ">=" : (0 , 1 ),
688
+ "<=" : (- 1 , 0 ),
689
+ }
690
+
691
+ possibilities = possibilities_dict [operator ]
692
+ cmp_res = self .compare (match_version )
693
+
694
+ return cmp_res in possibilities
695
+
696
+ # Advanced compare with "*" like "<=1.2.*"
697
+ # Algorithm:
698
+ # TL;DR: Delegate the comparison to tuples
699
+ #
700
+ # 1. Create a tuple of the string with major, minor, and path
701
+ # unless one of them is None
702
+ # 2. Determine the position of the first "*" in the tuple from step 1
703
+ # 3. Extract the matched operators
704
+ # 4. Look up the function in the operator table
705
+ # 5. Call the found function and pass the index (step 2) and
706
+ # the tuple (step 1)
707
+ # 6. Compare the both tuples up to the position of index
708
+ # For example, if you have (1, 2, "*") and self is
709
+ # (1, 2, 3, None, None), you compare (1, 2) <OPERATOR> (1, 2)
710
+ # 7. Return the result of the comparison
711
+ match_version = tuple ([match [item ]
712
+ for item in ('major' , 'minor' , 'patch' )
713
+ if item is not None
714
+ ]
715
+ )
716
+
717
+ try :
718
+ index = match_version .index ("*" )
719
+ except ValueError :
720
+ index = None
721
+
722
+ if not index :
723
+ raise ValueError ("Major version cannot be set to '*'" )
724
+
725
+ # At this point, only valid operators should be available
726
+ func : Callable [[int , Tuple ], bool ] = op_table [operator ]
727
+ return func (index , match_version )
728
+
609
729
@classmethod
610
730
def parse (
611
731
cls : Type [T ], version : String , optional_minor_and_patch : bool = False
0 commit comments