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
)
18
20
19
21
from ._types import (
@@ -66,6 +68,10 @@ class Version:
66
68
"""
67
69
68
70
__slots__ = ("_major" , "_minor" , "_patch" , "_prerelease" , "_build" )
71
+ #:
72
+ _RE_NUMBER = r"0|[1-9]\d*"
73
+
74
+
69
75
#: Regex for number in a prerelease
70
76
_LAST_NUMBER = re .compile (r"(?:[^\d]*(\d+)[^\d]*)+" )
71
77
#: Regex template for a semver version
@@ -102,6 +108,14 @@ class Version:
102
108
re .VERBOSE ,
103
109
)
104
110
111
+ #: The default prefix for the prerelease part.
112
+ #: Used in :meth:`Version.bump_prerelease`.
113
+ default_prerelease_prefix = "rc"
114
+
115
+ #: The default prefix for the build part
116
+ #: Used in :meth:`Version.bump_build`.
117
+ default_build_prefix = "build"
118
+
105
119
def __init__ (
106
120
self ,
107
121
major : SupportsInt ,
@@ -340,22 +354,21 @@ def compare(self, other: Comparable) -> int:
340
354
:return: The return value is negative if ver1 < ver2,
341
355
zero if ver1 == ver2 and strictly positive if ver1 > ver2
342
356
343
- >>> semver.compare("2.0.0")
357
+ >>> ver = semver.Version.parse("3.4.5")
358
+ >>> ver.compare("4.0.0")
344
359
-1
345
- >>> semver .compare("1 .0.0")
360
+ >>> ver .compare("3 .0.0")
346
361
1
347
- >>> semver.compare("2.0.0")
348
- 0
349
- >>> semver.compare(dict(major=2, minor=0, patch=0))
362
+ >>> ver.compare("3.4.5")
350
363
0
351
364
"""
352
365
cls = type (self )
353
366
if isinstance (other , String .__args__ ): # type: ignore
354
- other = cls .parse (other )
367
+ other = cls .parse (other ) # type: ignore
355
368
elif isinstance (other , dict ):
356
- other = cls (** other )
369
+ other = cls (** other ) # type: ignore
357
370
elif isinstance (other , (tuple , list )):
358
- other = cls (* other )
371
+ other = cls (* other ) # type: ignore
359
372
elif not isinstance (other , cls ):
360
373
raise TypeError (
361
374
f"Expected str, bytes, dict, tuple, list, or { cls .__name__ } instance, "
@@ -518,25 +531,19 @@ def finalize_version(self) -> "Version":
518
531
cls = type (self )
519
532
return cls (self .major , self .minor , self .patch )
520
533
521
- def match (self , match_expr : str ) -> bool :
534
+ def _match (self , match_expr : str ) -> bool :
522
535
"""
523
536
Compare self to match a match expression.
524
537
525
538
:param match_expr: optional operator and version; valid operators are
526
- ``<``` smaller than
539
+ ``<``` smaller than
527
540
``>`` greater than
528
541
``>=`` greator or equal than
529
542
``<=`` smaller or equal than
530
543
``==`` equal
531
544
``!=`` not equal
545
+ ``~=`` compatible release clause
532
546
:return: True if the expression matches the version, otherwise False
533
-
534
- >>> semver.Version.parse("2.0.0").match(">=1.0.0")
535
- True
536
- >>> semver.Version.parse("1.0.0").match(">1.0.0")
537
- False
538
- >>> semver.Version.parse("4.0.4").match("4.0.4")
539
- True
540
547
"""
541
548
prefix = match_expr [:2 ]
542
549
if prefix in (">=" , "<=" , "==" , "!=" ):
@@ -551,7 +558,7 @@ def match(self, match_expr: str) -> bool:
551
558
raise ValueError (
552
559
"match_expr parameter should be in format <op><ver>, "
553
560
"where <op> is one of "
554
- "['<', '>', '==', '<=', '>=', '!=']. "
561
+ "['<', '>', '==', '<=', '>=', '!=', '~=' ]. "
555
562
"You provided: %r" % match_expr
556
563
)
557
564
@@ -569,6 +576,119 @@ def match(self, match_expr: str) -> bool:
569
576
570
577
return cmp_res in possibilities
571
578
579
+ def match (self , match_expr : str ) -> bool :
580
+ """Compare self to match a match expression.
581
+
582
+ :param match_expr: optional operator and version; valid operators are
583
+ ``<``` smaller than
584
+ ``>`` greater than
585
+ ``>=`` greator or equal than
586
+ ``<=`` smaller or equal than
587
+ ``==`` equal
588
+ ``!=`` not equal
589
+ ``~=`` compatible release clause
590
+ :return: True if the expression matches the version, otherwise False
591
+ """
592
+ # TODO: The following function should be better
593
+ # integrated into a special Spec class
594
+ def compare_eq (index , other ) -> bool :
595
+ return self [:index ] == other [:index ]
596
+
597
+ def compare_ne (index , other ) -> bool :
598
+ return not compare_eq (index , other )
599
+
600
+ def compare_lt (index , other ) -> bool :
601
+ return self [:index ] < other [:index ]
602
+
603
+ def compare_gt (index , other ) -> bool :
604
+ return not compare_lt (index , other )
605
+
606
+ def compare_le (index , other ) -> bool :
607
+ return self [:index ] <= other [:index ]
608
+
609
+ def compare_ge (index , other ) -> bool :
610
+ return self [:index ] >= other [:index ]
611
+
612
+ def compare_compatible (index , other ) -> bool :
613
+ return compare_gt (index , other ) and compare_eq (index , other )
614
+
615
+ op_table : Dict [str , Callable [[int , Tuple ], bool ]] = {
616
+ '==' : compare_eq ,
617
+ '!=' : compare_ne ,
618
+ '<' : <
10670
span class=pl-s1>compare_lt,
619
+ '>' : compare_gt ,
620
+ '<=' : compare_le ,
621
+ '>=' : compare_ge ,
622
+ '~=' : compare_compatible ,
623
+ }
624
+
625
+ regex = r"""(?P<operator>[<]|[>]|<=|>=|~=|==|!=)?
626
+ (?P<version>
627
+ (?P<major>0|[1-9]\d*)
628
+ (?:\.(?P<minor>\*|0|[1-9]\d*)
629
+ (?:\.(?P<patch>\*|0|[1-9]\d*))?
630
+ )?
631
+ )"""
632
+ match = re .match (regex , match_expr , re .VERBOSE )
633
+ if match is None :
634
+ raise ValueError (
635
+ "match_expr parameter should be in format <op><ver>, "
636
+ "where <op> is one of %s. "
637
+ "<ver> is a version string like '1.2.3' or '1.*' "
638
+ "You provided: %r" % (list (op_table .keys ()), match_expr )
639
+ )
640
+ match_version = match ["version" ]
641
+ operator = cast (Dict , match ).get ('operator' , '==' )
642
+
643
+ if "*" not in match_version :
644
+ # conventional compare
645
+ possibilities_dict = {
646
+ ">" : (1 ,),
647
+ "<" : (- 1 ,),
648
+ "==" : (0 ,),
649
+ "!=" : (- 1 , 1 ),
650
+ ">=" : (0 , 1 ),
651
+ "<=" : (- 1 , 0 ),
652
+ }
653
+
654
+ possibilities = possibilities_dict [operator ]
655
+ cmp_res = self .compare (match_version )
656
+
657
+ return cmp_res in possibilities
658
+
659
+ # Advanced compare with "*" like "<=1.2.*"
660
+ # Algorithm:
661
+ # TL;DR: Delegate the comparison to tuples
662
+ #
663
+ # 1. Create a tuple of the string with major, minor, and path
664
+ # unless one of them is None
665
+ # 2. Determine the position of the first "*" in the tuple from step 1
666
+ # 3. Extract the matched operators
667
+ # 4. Look up the function in the operator table
668
+ # 5. Call the found function and pass the index (step 2) and
669
+ # the tuple (step 1)
670
+ # 6. Compare the both tuples up to the position of index
671
+ # For example, if you have (1, 2, "*") and self is
672
+ # (1, 2, 3, None, None), you compare (1, 2) <OPERATOR> (1, 2)
673
+ # 7. Return the result of the comparison
674
+ match_version = tuple ([match [item ]
675
+ for item in ('major' , 'minor' , 'patch' )
676
+ if item is not None
677
+ ]
678
+ )
679
+
680
+ try :
681
+ index = match_version .index ("*" )
682
+ except ValueError :
683
+ index = None
684
+
685
+ if not index :
686
+ raise ValueError ("Major version cannot be set to '*'" )
687
+
688
+ # At this point, only valid operators should be available
689
+ func : Callable [[int , Tuple ], bool ] = op_table [operator ]
690
+ return func (index , match_version )
691
+
572
692
@classmethod
573
693
def parse (
574
694
cls ,
0 commit comments