@@ -553,11 +553,10 @@ def _version_eq(left, right):
553553 ##and left.local == right.local
554554 )
555555
556- # TODO @aignas 2025-05-04: add tests for the comparison
557556def _version_lt (left , right ):
558557 if left .epoch > right .epoch :
559558 return False
560- if left .epoch < right .epoch :
559+ elif left .epoch < right .epoch :
561560 return True
562561
563562 release_len = max (len (left .release ), len (right .release ))
@@ -569,14 +568,22 @@ def _version_lt(left, right):
569568 elif left_release < right_release :
570569 return True
571570
572- return (
573- left .pre < right .pre and
574- left .post < right .post and
575- left .dev < right .dev and
576- left .local < right .local
577- )
571+ # the release is equal, check for pre version
572+ if right .pre :
573+ if left .pre != None :
574+ # PEP440: The exclusive ordered comparison <V MUST NOT allow a pre-release of
575+ # the specified version unless the specified version is itself a pre-release.
576+ return False
577+
578+ if left .pre > right .pre :
579+ return False
580+ elif left .pre < right .pre :
581+ return True
582+ elif left .pre :
583+ return True
584+
585+ return False
578586
579- # TODO @aignas 2025-05-04: add tests for the comparison
580587def _version_gt (left , right ):
581588 if left .epoch > right .epoch :
582589 return True
@@ -603,19 +610,39 @@ def _version_gt(left, right):
603610 elif left .post < right .post :
604611 return False
605612
613+ if right .pre :
614+ return True
615+
606616 return False
607617
608618def _new_version (* , epoch = 0 , release , pre = "" , post = "" , dev = "" , local = "" , is_prefix = False , norm ):
609619 epoch = epoch or 0
610620 _release = tuple ([int (d ) for d in release .split ("." )])
611- pre = pre or ""
621+
622+ if pre :
623+ if pre .startswith ("rc" ):
624+ prefix = "rc"
625+ else :
626+ prefix = pre [0 ]
627+
628+ pre = (prefix , int (pre [len (prefix ):]))
629+ else :
630+ pre = None
631+
612632 if post :
613633 if not post .startswith (".post" ):
614634 fail ("post release identifier must start with '.post', got: {}" .format (post ))
615635 post = int (post [len (".post" ):])
616636 else :
617637 post = None
618- dev = dev or ""
638+
639+ if dev :
640+ if not dev .startswith (".dev" ):
641+ fail ("dev release identifier must start with '.dev', got: {}" .format (dev ))
642+ dev = int (dev [len (".dev" ):])
643+ else :
644+ dev = None
645+
619646 local = local or ""
620647
621648 self = struct (
@@ -634,11 +661,20 @@ def _new_version(*, epoch = 0, release, pre = "", post = "", dev = "", local = "
634661 gt = lambda x : _version_gt (self , x ), # buildifier: disable=uninitialized
635662 le = lambda x : not _version_gt (self , x ), # buildifier: disable=uninitialized
636663 ge = lambda x : not _version_lt (self , x ), # buildifier: disable=uninitialized
664+ str = lambda : norm ,
665+ key = lambda : (
666+ epoch ,
667+ release ,
668+ pre or ("release" ,),
669+ post if post != None else - 1 ,
670+ dev if dev != None else - 1 ,
671+ local ,
672+ ),
637673 )
638674
639675 return self
640676
641- def parse_version (version ):
677+ def parse_version (version , strict = False ):
642678 """Parse a PEP4408 compliant version
643679
644680 TODO: finish
@@ -648,13 +684,14 @@ def parse_version(version):
648684
649685 Args:
650686 version: version string to be normalized according to PEP 440.
687+ strict: fail if the version is invalid.
651688
652689 Returns:
653690 string containing the normalized version.
654691 """
655692
656- parser = _new (version .strip (" .*" )) # PEP 440: Leading and Trailing Whitespace and .*
657- parser_2 = _new (version .strip (" .*" )) # PEP 440: Leading and Trailing Whitespace and .*
693+ parser = _new (version .strip (" " if strict else " .*" )) # PEP 440: Leading and Trailing Whitespace and .*
694+ parser_2 = _new (version .strip (" " if strict else " .*" )) # PEP 440: Leading and Trailing Whitespace and .*
658695 accept (parser , _is ("v" ), "" ) # PEP 440: Preceding v character
659696 accept (parser_2 , _is ("v" ), "" ) # PEP 440: Preceding v character
660697
@@ -677,12 +714,19 @@ def parse_version(version):
677714 is_prefix = version .endswith (".*" )
678715 parts ["is_prefix" ] = is_prefix
679716 if is_prefix and (parts ["local" ] or parts ["post" ] or parts ["dev" ] or parts ["pre" ]):
680- # local version part has been obtained, but only public segments can have prefix
681- # matches. Just return None.
717+ if strict :
718+ fail ("local version part has been obtained, but only public segments can have prefix matches" )
719+
682720 # https://peps.python.org/pep-0440/#public-version-identifiers
683721 return None
684722
685- if parser .input [parser .context ()["start" ]:]:
723+ if parser_2 .input [parser .context ()["start" ]:]:
724+ if strict :
725+ fail (
726+ "Failed to parse PEP 440 version identifier '%s'." % parser .input ,
727+ "Parse error at '%s'" % parser .input [parser .context ()["start" ]:],
728+ )
729+
686730 # If we fail to parse the version return None
687731 return None
688732
0 commit comments