Skip to content

Commit 118590c

Browse files
committed
wip: finish PEP508/PEP440 impl for version matching
1 parent 2c12496 commit 118590c

File tree

5 files changed

+180
-65
lines changed

5 files changed

+180
-65
lines changed

python/private/py_wheel_normalize_pep440.bzl

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,3 +517,128 @@ def normalize_pep440(version):
517517
"Parse error at '%s'" % parser.input[parser.context()["start"]:],
518518
)
519519
return parser.context()["norm"]
520+
521+
def _upper(*, epoch = 0, release, pre = "", post = "", dev = "", local = ""):
522+
epoch = epoch
523+
_release = list(release[:-1])
524+
pre = pre
525+
post = post
526+
dev = dev
527+
local = local
528+
529+
if pre or dev or local:
530+
return _new_version(
531+
epoch = epoch,
532+
release = release,
533+
)
534+
535+
_release[-1] = _release[-1] + 1
536+
release = ".".join([str(d) for d in _release])
537+
538+
return _new_version(
539+
epoch = epoch,
540+
release = release,
541+
)
542+
543+
def _version_eq(left, right):
544+
if left.epoch != right.epoch:
545+
return False
546+
547+
# Check at most 3 terms and check the same number of terms
548+
check_len = min(min(len(left.release), len(right.release)), 3)
549+
550+
return left.release[:check_len] == right.release[:check_len]
551+
552+
def _version_lt(left, right):
553+
if left.epoch < right.epoch:
554+
return True
555+
elif left.epoch > right.epoch:
556+
return False
557+
558+
return left.release < right.release
559+
560+
def _version_gt(left, right):
561+
if left.epoch > right.epoch:
562+
return True
563+
elif left.epoch < right.epoch:
564+
return False
565+
566+
return left.release > right.release
567+
568+
def _new_version(*, epoch = 0, release, pre = "", post = "", dev = "", local = ""):
569+
epoch = epoch or 0
570+
_release = tuple([int(d) for d in release.split(".")])
571+
pre = pre or ""
572+
post = post or ""
573+
dev = dev or ""
574+
local = local or ""
575+
576+
self = struct(
577+
epoch = epoch,
578+
release = _release,
579+
pre = pre,
580+
post = post,
581+
dev = dev,
582+
local = local,
583+
upper = lambda: _upper(
584+
epoch = epoch,
585+
release = _release,
586+
pre = pre,
587+
post = post,
588+
dev = dev,
589+
local = local,
590+
),
591+
key = lambda: (
592+
epoch,
593+
_release,
594+
pre,
595+
post,
596+
dev,
597+
local,
598+
),
599+
eq = lambda x: _version_eq(self, x), # buildifier: disable=uninitialized
600+
ne = lambda x: not _version_eq(self, x), # buildifier: disable=uninitialized
601+
lt = lambda x: _version_lt(self, x), # buildifier: disable=uninitialized
602+
gt = lambda x: _version_gt(self, x), # buildifier: disable=uninitialized
603+
le = lambda x: not _version_gt(self, x), # buildifier: disable=uninitialized
604+
ge = lambda x: not _version_lt(self, x), # buildifier: disable=uninitialized
605+
eqq = lambda x: _version_eqq(self, x), # buildifier: disable=uninitialized
606+
)
607+
608+
return self
609+
610+
def parse_version(version):
611+
"""Escape the version component of a filename.
612+
613+
See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode
614+
and https://peps.python.org/pep-0440/
615+
616+
Args:
617+
version: version string to be normalized according to PEP 440.
618+
619+
Returns:
620+
string containing the normalized version.
621+
"""
622+
parser = _new(version.strip()) # PEP 440: Leading and Trailing Whitespace
623+
accept(parser, _is("v"), "") # PEP 440: Preceding v character
624+
625+
parts = {}
626+
fns = [
627+
("epoch", accept_epoch),
628+
("release", accept_release),
629+
("pre", accept_prerelease),
630+
("post", accept_postrelease),
631+
("dev", accept_devrelease),
632+
("local", accept_local),
633+
]
634+
635+
for p, fn in fns:
636+
fn(parser)
637+
parts[p] = parser.context()["norm"]
638+
parser.context()["norm"] = ""
639+
640+
if parser.input[parser.context()["start"]:]:
641+
# If we fail to parse the version return None
642+
return None
643+
644+
return _new_version(**parts)

python/private/pypi/pep508_evaluate.bzl

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"""
1717

1818
load("//python/private:enum.bzl", "enum")
19-
load("//python/private:semver.bzl", "semver")
19+
load("//python/private:py_wheel_normalize_pep440.bzl", "parse_version")
20+
2021

2122
# The expression parsing and resolution for the PEP508 is below
2223
#
@@ -353,36 +354,37 @@ def _env_expr(left, op, right):
353354
elif op == ">=":
354355
return left >= right
355356
else:
356-
return fail("TODO: op unsupported: '{}'".format(op))
357+
return fail("unsupported op: '{}' {} '{}'".format(left, op, right))
357358

358359
def _version_expr(left, op, right):
359360
"""Evaluate a version comparison expression"""
360-
left = semver(left)
361-
right = semver(right)
362-
_left = left.key()
363-
_right = right.key()
361+
if op == "===":
362+
# https://peps.python.org/pep-0440/#arbitrary-equality
363+
# > simple string equality operations
364+
return _env_expr(left, "==", right)
365+
366+
_left = parse_version(left)
367+
_right = parse_version(right)
368+
if _left == None or _right == None:
369+
# Sometimes `platform_version` is not a true PEP440 version,
370+
# so then we fallback to a simple string expression evaluation
371+
return _env_expr(left, op, right)
372+
364373
if op == "<":
365-
return _left < _right
374+
return _left.lt(_right)
366375
elif op == ">":
367-
return _left > _right
376+
return _left.gt(_right)
368377
elif op == "<=":
369-
return _left <= _right
378+
return _left.le(_right)
370379
elif op == ">=":
371-
return _left >= _right
380+
return _left.ge(_right)
372381
elif op == "!=":
373-
return _left != _right
382+
return _left.ne(_right)
374383
elif op == "==":
375384
# Matching of major, minor, patch only
376-
return _left[:3] == _right[:3]
385+
return _left.eq(_right)
377386
elif op == "~=":
378-
right_plus = right.upper()
379-
_right_plus = right_plus.key()
380-
return _left >= _right and _left < _right_plus
381-
elif op == "===":
382-
# Strict matching
383-
return _left == _right
384-
elif op in _VERSION_CMP:
385-
fail("TODO: op unsupported: '{}'".format(op))
387+
return _left.ge(_right) and _left.lt(_right.upper())
386388
else:
387389
return False # Let's just ignore the invalid ops
388390

python/private/semver.bzl

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -43,32 +43,6 @@ def _to_dict(self):
4343
"pre_release": self.pre_release,
4444
}
4545

46-
def _upper(self):
47-
major = self.major
48-
minor = self.minor
49-
patch = self.patch
50-
build = ""
51-
pre_release = ""
52-
version = self.str()
53-
54-
if patch != None:
55-
minor = minor + 1
56-
patch = 0
57-
elif minor != None:
58-
major = major + 1
59-
minor = 0
60-
elif minor == None:
61-
major = major + 1
62-
63-
return _new(
64-
major = major,
65-
minor = minor,
66-
patch = patch,
67-
build = build,
68-
pre_release = pre_release,
69-
version = "~" + version,
70-
)
71-
7246
def _new(*, major, minor, patch, pre_release, build, version = None):
7347
# buildifier: disable=uninitialized
7448
self = struct(
@@ -82,7 +56,6 @@ def _new(*, major, minor, patch, pre_release, build, version = None):
8256
key = lambda: _key(self),
8357
str = lambda: version,
8458
to_dict = lambda: _to_dict(self),
85-
upper = lambda: _upper(self),
8659
)
8760
return self
8861

tests/pypi/pep508/evaluate_tests.bzl

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def _evaluate_version_env_tests(env):
123123
"{} <= '3.7.10'".format(var_name): True,
124124
"{} <= '3.7.8'".format(var_name): False,
125125
"{} == '3.7.9'".format(var_name): True,
126+
"{} == '3.7.*'".format(var_name): True,
126127
"{} != '3.7.9'".format(var_name): False,
127128
"{} ~= '3.7.1'".format(var_name): True,
128129
"{} ~= '3.7.10'".format(var_name): False,
@@ -148,6 +149,38 @@ def _evaluate_version_env_tests(env):
148149

149150
_tests.append(_evaluate_version_env_tests)
150151

152+
def _evaluate_platform_version_is_special(env):
153+
# Given
154+
marker_env = {"platform_version": "FooBar Linux v1.2.3"}
155+
156+
# When the platform version is not
157+
input = "platform_version == '0'"
158+
got = evaluate(
159+
input,
160+
env = marker_env,
161+
)
162+
env.expect.that_collection((input, got)).contains_exactly((input, False))
163+
164+
# And when I compare it as string
165+
input = "'FooBar' in platform_version"
166+
got = evaluate(
167+
input,
168+
env = marker_env,
169+
)
170+
env.expect.that_collection((input, got)).contains_exactly((input, True))
171+
172+
173+
# Check that the non-strict eval gives us back the input when no
174+
# env is supplied.
175+
got = evaluate(
176+
input,
177+
env = {},
178+
strict = False,
179+
)
180+
env.expect.that_bool(got).equals(input.replace("'", '"'))
181+
182+
_tests.append(_evaluate_platform_version_is_special)
183+
151184
def _logical_expression_tests(env):
152185
for input, want in {
153186
# Basic

tests/semver/semver_test.bzl

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,24 +104,6 @@ def _test_semver_sort(env):
104104

105105
_tests.append(_test_semver_sort)
106106

107-
def _test_upper(env):
108-
for input, want in {
109-
# Depending on how many version numbers are specified we will increase
110-
# the upper bound differently. See https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release for docs
111-
"0.0.1": "0.1.0",
112-
"0.1": "1.0",
113-
"0.1.0": "0.2.0",
114-
"1": "2",
115-
"1.0.0-pre": "1.1.0", # pre-release info is dropped
116-
"1.2.0": "1.3.0",
117-
"2.0.0+build0": "2.1.0", # build info is dropped
118-
}.items():
119-
actual = semver(input).upper().key()
120-
want = semver(want).key()
121-
env.expect.that_collection(actual).contains_exactly(want).in_order()
122-
123-
_tests.append(_test_upper)
124-
125107
def semver_test_suite(name):
126108
"""Create the test suite.
127109

0 commit comments

Comments
 (0)