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