Skip to content

Commit 3f20b4b

Browse files
authored
refactor(internal): add a semver parsing utility function (#2218)
This `semver` function may turn out to be useful in validating the input for the `python.*override` tag classes to be added in a followup PR. Because this is a refactor of an existing code and adding tests, I decided to split it out. For a POC see #2151, work towards #2081.
1 parent 451e62c commit 3f20b4b

File tree

7 files changed

+205
-17
lines changed

7 files changed

+205
-17
lines changed

examples/bzlmod/MODULE.bazel.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/private/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ bzl_library(
295295
srcs = ["repo_utils.bzl"],
296296
)
297297

298+
bzl_library(
299+
name = "semver_bzl",
300+
srcs = ["semver.bzl"],
301+
)
302+
298303
bzl_library(
299304
name = "sentinel_bzl",
300305
srcs = ["sentinel.bzl"],

python/private/pypi/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ bzl_library(
5959
srcs = ["extension.bzl"],
6060
deps = [
6161
":attrs_bzl",
62+
"//python/private:semver_bzl",
6263
":hub_repository_bzl",
6364
":parse_requirements_bzl",
6465
":evaluate_markers_bzl",

python/private/pypi/extension.bzl

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ load("@pythons_hub//:interpreters.bzl", "DEFAULT_PYTHON_VERSION", "INTERPRETER_L
1919
load("//python/private:auth.bzl", "AUTH_ATTRS")
2020
load("//python/private:normalize_name.bzl", "normalize_name")
2121
load("//python/private:repo_utils.bzl", "repo_utils")
22+
load("//python/private:semver.bzl", "semver")
2223
load("//python/private:version_label.bzl", "version_label")
2324
load(":attrs.bzl", "use_isolated")
2425
load(":evaluate_markers.bzl", "evaluate_markers", EVALUATE_MARKERS_SRCS = "SRCS")
@@ -32,22 +33,8 @@ load(":simpleapi_download.bzl", "simpleapi_download")
3233
load(":whl_library.bzl", "whl_library")
3334
load(":whl_repo_name.bzl", "whl_repo_name")
3435

35-
def _parse_version(version):
36-
major, _, version = version.partition(".")
37-
minor, _, version = version.partition(".")
38-
patch, _, version = version.partition(".")
39-
build, _, version = version.partition(".")
40-
41-
return struct(
42-
# use semver vocabulary here
43-
major = major,
44-
minor = minor,
45-
patch = patch, # this is called `micro` in the Python interpreter versioning scheme
46-
build = build,
47-
)
48-
4936
def _major_minor_version(version):
50-
version = _parse_version(version)
37+
version = semver(version)
5138
return "{}.{}".format(version.major, version.minor)
5239

5340
def _whl_mods_impl(mctx):

python/private/semver.bzl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"A semver version parser"
16+
17+
def _key(version):
18+
return (
19+
version.major,
20+
version.minor,
21+
version.patch,
22+
# non pre-release versions are higher
23+
version.pre_release == "",
24+
# then we compare each element of the pre_release tag separately
25+
tuple([
26+
(
27+
i if not i.isdigit() else "",
28+
# digit values take precedence
29+
int(i) if i.isdigit() else 0,
30+
)
31+
for i in version.pre_release.split(".")
32+
]) if version.pre_release else None,
33+
# And build info is just alphabetic
34+
version.build,
35+
)
36+
37+
def semver(version):
38+
"""Parse the semver version and return the values as a struct.
39+
40+
Args:
41+
version: {type}`str` the version string
42+
43+
Returns:
44+
A {type}`struct` with `major`, `minor`, `patch` and `build` attributes.
45+
"""
46+
47+
# Implement the https://semver.org/ spec
48+
major, _, tail = version.partition(".")
49+
minor, _, tail = tail.partition(".")
50+
patch, _, build = tail.partition("+")
51+
patch, _, pre_release = patch.partition("-")
52+
53+
public = struct(
54+
major = int(major),
55+
minor = int(minor or "0"),
56+
# NOTE: this is called `micro` in the Python interpreter versioning scheme
57+
patch = int(patch or "0"),
58+
pre_release = pre_release,
59+
build = build,
60+
# buildifier: disable=uninitialized
61+
key = lambda: _key(self.actual),
62+
str = lambda: version,
63+
)
64+
self = struct(actual = public)
65+
return public

tests/semver/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
load(":semver_test.bzl", "semver_test_suite")
16+
17+
semver_test_suite(name = "semver_tests")

tests/semver/semver_test.bzl

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
""
16+
17+
load("@rules_testing//lib:test_suite.bzl", "test_suite")
18+
load("//python/private:semver.bzl", "semver") # buildifier: disable=bzl-visibility
19+
20+
_tests = []
21+
22+
def _test_semver_from_major(env):
23+
actual = semver("3")
24+
env.expect.that_int(actual.major).equals(3)
25+
env.expect.that_int(actual.minor).equals(0)
26+
env.expect.that_int(actual.patch).equals(0)
27+
env.expect.that_str(actual.build).equals("")
28+
29+
_tests.append(_test_semver_from_major)
30+
31+
def _test_semver_from_major_minor_version(env):
32+
actual = semver("4.9")
33+
env.expect.that_int(actual.major).equals(4)
34+
env.expect.that_int(actual.minor).equals(9)
35+
env.expect.that_int(actual.patch).equals(0)
36+
env.expect.that_str(actual.build).equals("")
37+
38+
_tests.append(_test_semver_from_major_minor_version)
39+
40+
def _test_semver_with_build_info(env):
41+
actual = semver("1.2.3+mybuild")
42+
env.expect.that_int(actual.major).equals(1)
43+
env.expect.that_int(actual.minor).equals(2)
44+
env.expect.that_int(actual.patch).equals(3)
45+
env.expect.that_str(actual.build).equals("mybuild")
46+
47+
_tests.append(_test_semver_with_build_info)
48+
49+
def _test_semver_with_build_info_multiple_pluses(env):
50+
actual = semver("1.2.3-rc0+build+info")
51+
env.expect.that_int(actual.major).equals(1)
52+
env.expect.that_int(actual.minor).equals(2)
53+
env.expect.that_int(actual.patch).equals(3)
54+
env.expect.that_str(actual.pre_release).equals("rc0")
55+
env.expect.that_str(actual.build).equals("build+info")
56+
57+
_tests.append(_test_semver_with_build_info_multiple_pluses)
58+
59+
def _test_semver_alpha_beta(env):
60+
actual = semver("1.2.3-alpha.beta")
61+
env.expect.that_int(actual.major).equals(1)
62+
env.expect.that_int(actual.minor).equals(2)
63+
env.expect.that_int(actual.patch).equals(3)
64+
env.expect.that_str(actual.pre_release).equals("alpha.beta")
65+
66+
_tests.append(_test_semver_alpha_beta)
67+
68+
def _test_semver_sort(env):
69+
want = [
70+
semver(item)
71+
for item in [
72+
# The items are sorted from lowest to highest version
73+
"0.0.1",
74+
"0.1.0-rc",
75+
"0.1.0",
76+
"0.9.11",
77+
"0.9.12",
78+
"1.0.0-alpha",
79+
"1.0.0-alpha.1",
80+
"1.0.0-alpha.beta",
81+
"1.0.0-beta",
82+
"1.0.0-beta.2",
83+
"1.0.0-beta.11",
84+
"1.0.0-rc.1",
85+
"1.0.0-rc.2",
86+
"1.0.0",
87+
# Also handle missing minor and patch version strings
88+
"2.0",
89+
"3",
90+
# Alphabetic comparison for different builds
91+
"3.0.0+build0",
92+
"3.0.0+build1",
93+
]
94+
]
95+
actual = sorted(want, key = lambda x: x.key())
96+
env.expect.that_collection(actual).contains_exactly(want).in_order()
97+
for i, greater in enumerate(want[1:]):
98+
smaller = actual[i]
99+
if greater.key() <= smaller.key():
100+
env.fail("Expected '{}' to be smaller than '{}', but got otherwise".format(
101+
smaller.str(),
102+
greater.str(),
103+
))
104+
105+
_tests.append(_test_semver_sort)
106+
107+
def semver_test_suite(name):
108+
"""Create the test suite.
109+
110+
Args:
111+
name: the name of the test suite
112+
"""
113+
test_suite(name = name, basic_tests = _tests)

0 commit comments

Comments
 (0)