@@ -227,6 +227,9 @@ def accept_alnum(parser):
227227 for i in range (start , len (parser .input ) + 1 ):
228228 if not accept (parser , _isalnum , _lower ) and not accept_placeholder (parser ):
229229 if i - start >= 1 :
230+ if ctx ["norm" ].isdigit ():
231+ # PEP 440: Integer Normalization
232+ ctx ["norm" ] = str (int (ctx ["norm" ]))
230233 return parser .accept ()
231234 break
232235
@@ -266,44 +269,9 @@ def accept_dot_number_sequence(parser):
266269 break
267270 return i - start >= 1
268271
269- def accept_separator_alnum (parser ):
270- """Accept a separator followed by an alphanumeric string.
271-
272- Args:
273- parser: The normalizer.
274-
275- Returns:
276- whether a separator and an alphanumeric string were accepted.
277- """
278- parser .open_context ()
279-
280- # PEP 440: Local version segments
281- if (
282- accept (parser , _in (["." , "-" , "_" ]), "." ) and
283- (accept_digits (parser ) or accept_alnum (parser ))
284- ):
285- return parser .accept ()
286-
287- return parser .discard ()
288-
289- def accept_separator_alnum_sequence (parser ):
290- """Accept a sequence of separator+alphanumeric.
291-
292- Args:
293- parser: The normalizer.
294-
295- Returns:
296- whether a sequence of separator+alphanumerics was accepted.
297- """
298- ctx = parser .context ()
299- start = ctx ["start" ]
300- i = start
301-
302- for i in range (start , len (parser .input ) + 1 ):
303- if not accept_separator_alnum (parser ):
304- break
305-
306- return i - start >= 1
272+ def accept_local_version_segment (parser ):
273+ """Accept a local version segment (alphanumeric or numeric)."""
274+ return accept_alnum (parser )
307275
308276def accept_epoch (parser ):
309277 """PEP 440: Version epochs.
@@ -418,15 +386,16 @@ def accept_explicit_postrelease(parser):
418386 ctx = parser .open_context ()
419387
420388 # PEP 440: Post release separators
421- if not accept (parser , _in (["-" , "_" , "." ]), "." ):
422- ctx ["norm" ] += "."
389+ accept (parser , _in (["-" , "_" , "." ]), "" )
423390
424391 # PEP 440: Post release spelling
425392 if (
426393 accept_string (parser , "post" , "post" ) or
427394 accept_string (parser , "rev" , "post" ) or
428395 accept_string (parser , "r" , "post" )
429396 ):
397+ ctx ["norm" ] = ".post"
398+
430399 accept (parser , _in (["-" , "_" , "." ]), "" )
431400
432401 if not accept_digits (parser ):
@@ -465,10 +434,11 @@ def accept_devrelease(parser):
465434 ctx = parser .open_context ()
466435
467436 # PEP 440: Development release separators
468- if not accept (parser , _in (["-" , "_" , "." ]), "." ):
469- ctx ["norm" ] += "."
437+ accept (parser , _in (["-" , "_" , "." ]), "" )
470438
471439 if accept_string (parser , "dev" , "dev" ):
440+ ctx ["norm" ] = ".dev"
441+
472442 accept (parser , _in (["-" , "_" , "." ]), "" )
473443
474444 if not accept_digits (parser ):
@@ -490,8 +460,10 @@ def accept_local(parser):
490460 """
491461 parser .open_context ()
492462
493- if accept (parser , _is ("+" ), "+" ) and accept_alnum (parser ):
494- accept_separator_alnum_sequence (parser )
463+ if accept (parser , _is ("+" ), "+" ) and accept_local_version_segment (parser ):
464+ for _ in range (len (parser .input ) - parser .context ()["start" ]):
465+ if not (accept (parser , _in (["." , "-" , "_" ]), "." ) and accept_local_version_segment (parser )):
466+ break
495467 return parser .accept ("local" )
496468
497469 return parser .discard ("local" )
@@ -624,7 +596,27 @@ def _parse_local(value, fail):
624596 fail ("local release identifier must start with '+', got: {}" .format (value ))
625597
626598 # If the part is numerical, handle it as a number
627- return tuple ([int (part ) if part .isdigit () else part for part in value [1 :].split ("." )])
599+ # PEP 440: Local version identifiers
600+ # Local version identifiers are not compared, but are used for ordering.
601+ # They consist of a series of dot separated segments, where each segment can be
602+ # either an alphanumeric string or a number.
603+ # When comparing, alphanumeric segments are compared lexicographically, and
604+ # numeric segments are compared numerically.
605+ # The comparison is case-insensitive.
606+ # The comparison is done segment by segment.
607+ # If a segment is alphanumeric, it is compared to other alphanumeric segments
608+ # lexicographically.
609+ # If a segment is numeric, it is compared to other numeric segments numerically.
610+ # If a segment is alphanumeric and the other is numeric, the alphanumeric
611+ # segment is considered to be greater than the numeric segment.
612+ # This means that `1.0+abc.1` is greater than `1.0+123.1`.
613+ # This is implemented by returning a tuple of (is_int, value) for each segment.
614+ # is_int is True if the segment is an int, False otherwise.
615+ # This ensures that (False, "abc") > (True, 123).
616+ return tuple ([
617+ (part .isdigit (), int (part ) if part .isdigit () else part .lower ())
618+ for part in value [1 :].replace ("-" , "." ).replace ("_" , "." ).split ("." )
619+ ])
628620
629621def _parse_dev (value , fail ):
630622 if not value :
0 commit comments