@@ -544,6 +544,7 @@ def _parse(version_str, strict = True):
544544 parts [key ] = parser .context ()["norm" ][start :]
545545 parts ["norm" ] = parser .context ()["norm" ]
546546
547+ # TODO @aignas 2025-05-09: move the `is_prefix` handling to `accept_release`
547548 if is_prefix and (parts ["local" ] or parts ["post" ] or parts ["dev" ] or parts ["pre" ]):
548549 if strict :
549550 fail ("local version part has been obtained, but only public segments can have prefix matches" )
@@ -582,65 +583,75 @@ def version(version_str, strict = False):
582583
583584 return _new_version (** parts )
584585
585- def _new_version (
586- * ,
587- norm ,
588- epoch = 0 ,
589- release ,
590- pre = "" ,
591- post = "" ,
592- dev = "" ,
593- local = "" ,
594- is_prefix = False ):
595- epoch = epoch or 0
596- _release = tuple ([int (d ) for d in release .split ("." )])
586+ def _parse_epoch (value ):
587+ if not value :
588+ return 0
597589
598- if pre :
599- if pre .startswith ("rc" ):
600- prefix = "rc"
601- else :
602- prefix = pre [0 ]
590+ return int (value )
603591
604- pre = (prefix , int (pre [len (prefix ):]))
605- else :
606- pre = None
592+ def _parse_release (value ):
593+ return tuple ([int (d ) for d in value .split ("." )])
607594
608- if post :
609- if not post .startswith (".post" ):
610- fail ("post release identifier must start with '.post', got: {}" .format (post ))
611- post = int (post [len (".post" ):])
595+ def _parse_local (value ):
596+ if not value :
597+ return None
612598
613- # We choose `~` since almost all of the ASCII characters will be before
614- # it. Use `ord` and `chr` functions to find a good value.
615- post = ("~" , post )
616- else :
617- post = None
599+ local = value .lstrip ("+" )
618600
619- if dev :
620- if not dev .startswith (".dev" ):
621- fail ("dev release identifier must start with '.dev', got: {}" .format (dev ))
622- dev = int (dev [len (".dev" ):])
601+ # If the part is numerical, handle it as a number
602+ return tuple ([int (part ) if part .isdigit () else part for part in local .split ("." )])
623603
624- # Empty string goes first when comparing
625- dev = ("" , dev )
626- else :
627- dev = None
604+ def _parse_dev (value ):
605+ if not value :
606+ return None
628607
629- if local :
630- local = local .lstrip ("+" )
608+ if not value .startswith (".dev" ):
609+ fail ("dev release identifier must start with '.dev', got: {}" .format (value ))
610+ dev = int (value [len (".dev" ):])
631611
632- # If the part is numerical, handle it as a number
633- local = tuple ([int (part ) if part .isdigit () else part for part in local .split ("." )])
612+ # Empty string goes first when comparing
613+ return ("" , dev )
614+
615+ def _parse_pre (value ):
616+ if not value :
617+ return None
618+
619+ if value .startswith ("rc" ):
620+ prefix = "rc"
634621 else :
635- local = None
622+ prefix = value [0 ]
623+
624+ return (prefix , int (value [len (prefix ):]))
636625
626+ def _parse_post (value ):
627+ if not value :
628+ return None
629+
630+ if not value .startswith (".post" ):
631+ fail ("post release identifier must start with '.post', got: {}" .format (value ))
632+ post = int (value [len (".post" ):])
633+
634+ # We choose `~` since almost all of the ASCII characters will be before
635+ # it. Use `ord` and `chr` functions to find a good value.
636+ return ("~" , post )
637+
638+ def _new_version (
639+ * ,
640+ norm ,
641+ epoch = 0 ,
642+ release ,
643+ pre = "" ,
644+ post = "" ,
645+ dev = "" ,
646+ local = "" ,
647+ is_prefix = False ):
637648 self = struct (
638- epoch = epoch ,
639- release = _release ,
640- pre = pre ,
641- post = post ,
642- dev = dev ,
643- local = local ,
649+ epoch = _parse_epoch ( epoch ) ,
650+ release = _parse_release ( release ) ,
651+ pre = _parse_pre ( pre ) ,
652+ post = _parse_post ( post ) ,
653+ dev = _parse_dev ( dev ) ,
654+ local = _parse_local ( local ) ,
644655 is_prefix = is_prefix ,
645656 norm = norm ,
646657 eq = lambda x : _version_eq (self , x ), # buildifier: disable=uninitialized
@@ -652,7 +663,7 @@ def _new_version(
652663 ne = lambda x : _version_ne (self , x ), # buildifier: disable=uninitialized
653664 compatible = lambda x : _version_compatible (self , x ), # buildifier: disable=uninitialized
654665 str = lambda : norm ,
655- key = lambda * , local = True : _key (self , local = local ), # buildifier: disable=uninitialized
666+ key = lambda * , local = True : _version_key (self , local = local ), # buildifier: disable=uninitialized
656667 )
657668
658669 return self
@@ -665,15 +676,25 @@ def _pad_zeros(release, n):
665676 release = list (release ) + [0 ] * padding
666677 return tuple (release )
667678
679+ def _prefix_err (left , op , right ):
680+ if left .is_prefix or right .is_prefix :
681+ fail ("PEP440: only '==' and '!=' operators can use prefix matching: {} {} {}" .format (
682+ left .norm ,
683+ op ,
684+ right .norm ,
685+ ))
686+
668687def _version_eqq (left , right ):
688+ """=== operator"""
669689 if left .is_prefix or right .is_prefix :
670- fail (_prefix_err (left , "< " , right ))
690+ fail (_prefix_err (left , "=== " , right ))
671691
672692 # https://peps.python.org/pep-0440/#arbitrary-equality
673693 # > simple string equality operations
674694 return left .norm == right .norm
675695
676696def _version_eq (left , right ):
697+ """== operator"""
677698 if left .is_prefix and right .is_prefix :
678699 fail ("Invalid comparison: both versions cannot be prefix matching" )
679700 if left .is_prefix :
@@ -699,18 +720,24 @@ def _version_eq(left, right):
699720 ##and left.local == right.local
700721 )
701722
723+ def _version_compatible (left , right ):
724+ """~= operator"""
725+ if left .is_prefix or right .is_prefix :
726+ fail (_prefix_err (left , "~=" , right ))
727+
728+ # https://peps.python.org/pep-0440/#compatible-release
729+ # Note, the ~= operator can be also expressed as:
730+ # >= V.N, == V.*
731+ head , _ , _ = right .norm .partition ("." )
732+ right_star = version ("{}.*" .format (head ))
733+ return _version_ge (left , right ) and _version_eq (left , right_star )
734+
702735def _version_ne (left , right ):
736+ """!= operator"""
703737 return not _version_eq (left , right )
704738
705- def _prefix_err (left , op , right ):
706- if left .is_prefix or right .is_prefix :
707- fail ("PEP440: only '==' and '!=' operators can use prefix matching: {} {} {}" .format (
708- left .str (),
709- op ,
710- right .str (),
711- ))
712-
713739def _version_lt (left , right ):
740+ """< operator"""
714741 if left .is_prefix or right .is_prefix :
715742 fail (_prefix_err (left , "<" , right ))
716743
@@ -738,6 +765,7 @@ def _version_lt(left, right):
738765 return False
739766
740767def _version_gt (left , right ):
768+ """> operator"""
741769 if left .is_prefix or right .is_prefix :
742770 fail (_prefix_err (left , ">" , right ))
743771
@@ -770,38 +798,29 @@ def _version_gt(left, right):
770798 # False anyway.
771799 return False
772800
773- def _version_ge (left , right ):
774- if left .is_prefix or right .is_prefix :
775- fail (_prefix_err (left , ">=" , right ))
776-
777- # PEP440: simple order check
778- # https://peps.python.org/pep-0440/#inclusive-ordered-comparison
779- _left = left .key (local = False )
780- _right = right .key (local = False )
781- return _left > _right or _version_eq (left , right )
782-
783801def _version_le (left , right ):
802+ """<= operator"""
784803 if left .is_prefix or right .is_prefix :
785804 fail (_prefix_err (left , "<=" , right ))
786805
787806 # PEP440: simple order check
788807 # https://peps.python.org/pep-0440/#inclusive-ordered-comparison
789- _left = left . key ( local = False )
790- _right = right . key ( local = False )
808+ _left = _version_key ( left , local = False )
809+ _right = _version_key ( right , local = False )
791810 return _left < _right or _version_eq (left , right )
792811
793- def _version_compatible (left , right ):
812+ def _version_ge (left , right ):
813+ """>= operator"""
794814 if left .is_prefix or right .is_prefix :
795- fail (_prefix_err (left , "~ =" , right ))
815+ fail (_prefix_err (left , "> =" , right ))
796816
797- # https://peps.python.org/pep-0440/#compatible-release
798- # Note, the ~= operator can be also expressed as:
799- # >= V.N, == V.*
800- head , _ , _ = right .norm .partition ("." )
801- right_star = version ("{}.*" .format (head ))
802- return left .ge (right ) and left .eq (right_star )
817+ # PEP440: simple order check
818+ # https://peps.python.org/pep-0440/#inclusive-ordered-comparison
819+ _left = _version_key (left , local = False )
820+ _right = _version_key (right , local = False )
821+ return _left > _right or _version_eq (left , right )
803822
804- def _key (self , * , local , release_key = ("z" ,)):
823+ def _version_key (self , * , local , release_key = ("z" ,)):
805824 """This function returns a tuple that can be used in 'sorted' calls.
806825
807826 This implements the PEP440 version sorting.
0 commit comments