Skip to content

Commit fe0ca65

Browse files
committed
Add more implementation for the version comparisons
1 parent db949b2 commit fe0ca65

File tree

3 files changed

+128
-19
lines changed

3 files changed

+128
-19
lines changed

python/private/py_wheel_normalize_pep440.bzl

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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
557556
def _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
580587
def _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

608618
def _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

python/private/pypi/pep508_evaluate.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
load("//python/private:enum.bzl", "enum")
1919
load("//python/private:py_wheel_normalize_pep440.bzl", "parse_version")
2020

21+
# TODO @aignas 2025-05-06: this is exposed for tests only
22+
version = parse_version
23+
2124
# The expression parsing and resolution for the PEP508 is below
2225
#
2326

tests/pypi/pep508/evaluate_tests.bzl

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
load("@rules_testing//lib:test_suite.bzl", "test_suite")
1717
load("//python/private/pypi:pep508_env.bzl", pep508_env = "env") # buildifier: disable=bzl-visibility
18-
load("//python/private/pypi:pep508_evaluate.bzl", "evaluate", "tokenize") # buildifier: disable=bzl-visibility
18+
load("//python/private/pypi:pep508_evaluate.bzl", "evaluate", "tokenize", "version") # buildifier: disable=bzl-visibility
1919

2020
_tests = []
2121

@@ -283,7 +283,15 @@ _MISC_EXPRESSIONS = [
283283
_expr_case('python_version > "1.7.post2"', True, {"python_version": "1.7.post3"}),
284284
_expr_case('python_version > "1.7.post2"', False, {"python_version": "1.7.0"}),
285285
_expr_case('python_version > "1.7.1+local"', False, {"python_version": "1.7.1"}),
286-
# TODO @aignas 2025-05-05: add tests for pre-releases
286+
_expr_case('python_version > "1.7.1+local"', True, {"python_version": "1.7.2"}),
287+
_expr_case('python_version < "1.7.1"', False, {"python_version": "1.7.2"}),
288+
_expr_case('python_version < "1.7.3"', True, {"python_version": "1.7.2"}),
289+
_expr_case('python_version < "1.7.1"', True, {"python_version": "1.7"}),
290+
_expr_case('python_version < "1.7.1-rc2"', True, {"python_version": "1.7"}),
291+
_expr_case('python_version < "1.7-rc2"', False, {"python_version": "1.7"}),
292+
_expr_case('python_version < "1.7-rc2"', True, {"python_version": "1.7-rc1"}),
293+
_expr_case('python_version < "1.7-rc2"', False, {"python_version": "1.7-rc3"}),
294+
_expr_case('python_version < "1.7-rc12"', True, {"python_version": "1.7-rc3"}),
287295
]
288296

289297
def _misc_expressions(env):
@@ -292,6 +300,60 @@ def _misc_expressions(env):
292300

293301
_tests.append(_misc_expressions)
294302

303+
def _test_ordering(env):
304+
want = [
305+
# The items are sorted from lowest to highest version
306+
"0.0.1",
307+
"0.1.0-rc",
308+
"0.1.0",
309+
"0.9.11",
310+
"0.9.12.dev",
311+
"0.9.12",
312+
"1.0.0-alpha",
313+
"1.0.0-alpha1",
314+
"1.0.0-beta",
315+
"1.0.0-beta.dev",
316+
"1.0.0-beta2",
317+
"1.0.0-beta11",
318+
"1.0.0-rc1",
319+
"1.0.0-rc2.dev",
320+
"1.0.0-rc2",
321+
"1.0.0",
322+
# Also handle missing minor and patch version strings
323+
"2.0",
324+
"3",
325+
]
326+
327+
for lower, higher in zip(want[:-1], want[1:]):
328+
lower = version(lower, strict = True)
329+
higher = version(higher, strict = True)
330+
331+
if not lower.key() < higher.key():
332+
env.fail("Expected '{}'.key() to be smaller than '{}'.key(), but got otherwise".format(
333+
lower.str(),
334+
higher.str(),
335+
))
336+
337+
if not lower.lt(higher):
338+
env.fail("Expected '{}' to be smaller than '{}', but got otherwise".format(
339+
lower.str(),
340+
higher.str(),
341+
))
342+
343+
if not (higher.key() > lower.key()):
344+
env.fail("Expected '{}'.key() to be greater than '{}'.key(), but got otherwise".format(
345+
higher.str(),
346+
lower.str(),
347+
))
348+
349+
if not (higher.gt(lower)):
350+
env.fail("Expected '{}' to be greater than '{}', but got otherwise".format(
351+
higher.str(),
352+
lower.str(),
353+
))
354+
355+
_tests.append(_test_ordering)
356+
295357
def evaluate_test_suite(name): # buildifier: disable=function-docstring
296358
test_suite(
297359
name = name,

0 commit comments

Comments
 (0)