diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index e72a8fcaa7..0b50ccf0b7 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -139,7 +139,7 @@ bzl_library( name = "config_settings_bzl", srcs = ["config_settings.bzl"], deps = [ - ":semver_bzl", + ":version_bzl", "@bazel_skylib//lib:selects", "@bazel_skylib//rules:common_settings", ], @@ -249,9 +249,9 @@ bzl_library( ":python_register_toolchains_bzl", ":pythons_hub_bzl", ":repo_utils_bzl", - ":semver_bzl", ":toolchains_repo_bzl", ":util_bzl", + ":version_bzl", "@bazel_features//:features", ], ) @@ -610,11 +610,6 @@ bzl_library( ], ) -bzl_library( - name = "semver_bzl", - srcs = ["semver.bzl"], -) - bzl_library( name = "sentinel_bzl", srcs = ["sentinel.bzl"], diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 5eb858e2e4..aff5d016fb 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -18,7 +18,7 @@ load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//python/private:text_util.bzl", "render") -load(":semver.bzl", "semver") +load(":version.bzl", "version") _PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version") _PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor") @@ -181,8 +181,8 @@ _python_version_flag = rule( def _python_version_major_minor_flag_impl(ctx): input = _flag_value(ctx.attr._python_version_flag) if input: - version = semver(input) - value = "{}.{}".format(version.major, version.minor) + ver = version.parse(input) + value = "{}.{}".format(ver.release[0], ver.release[1]) else: value = "" diff --git a/python/private/hermetic_runtime_repo_setup.bzl b/python/private/hermetic_runtime_repo_setup.bzl index 64d721ecad..f944b0b914 100644 --- a/python/private/hermetic_runtime_repo_setup.bzl +++ b/python/private/hermetic_runtime_repo_setup.bzl @@ -20,7 +20,7 @@ load("//python:py_runtime_pair.bzl", "py_runtime_pair") load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") load(":glob_excludes.bzl", "glob_excludes") load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain") -load(":semver.bzl", "semver") +load(":version.bzl", "version") _IS_FREETHREADED = Label("//python/config_settings:is_py_freethreaded") @@ -53,8 +53,11 @@ def define_hermetic_runtime_toolchain_impl( use. """ _ = name # @unused - version_info = semver(python_version) - version_dict = version_info.to_dict() + version_info = version.parse(python_version) + version_dict = { + "major": version_info.release[0], + "minor": version_info.release[1], + } native.filegroup( name = "files", srcs = native.glob( @@ -198,9 +201,9 @@ def define_hermetic_runtime_toolchain_impl( files = [":files"], interpreter = python_bin, interpreter_version_info = { - "major": str(version_info.major), - "micro": str(version_info.patch), - "minor": str(version_info.minor), + "major": str(version_info.release[0]), + "micro": str(version_info.release[2]), + "minor": str(version_info.release[1]), }, coverage_tool = select({ # Convert empty string to None diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 06ca3a8e34..84e0535289 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -116,7 +116,7 @@ bzl_library( ":whl_target_platforms_bzl", "//python/private:full_version_bzl", "//python/private:normalize_name_bzl", - "//python/private:semver_bzl", + "//python/private:version_bzl", "//python/private:version_label_bzl", "@bazel_features//:features", "@pythons_hub//:interpreters_bzl", @@ -256,7 +256,7 @@ bzl_library( srcs = ["pep508_evaluate.bzl"], deps = [ "//python/private:enum_bzl", - "//python/private:semver_bzl", + "//python/private:version_bzl", ], ) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 84caa0aee7..3896f2940a 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -22,7 +22,7 @@ load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") -load("//python/private:semver.bzl", "semver") +load("//python/private:version.bzl", "version") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") @@ -36,9 +36,9 @@ load(":whl_config_setting.bzl", "whl_config_setting") load(":whl_library.bzl", "whl_library") load(":whl_repo_name.bzl", "pypi_repo_name", "whl_repo_name") -def _major_minor_version(version): - version = semver(version) - return "{}.{}".format(version.major, version.minor) +def _major_minor_version(version_str): + ver = version.parse(version_str) + return "{}.{}".format(ver.release[0], ver.release[1]) def _whl_mods_impl(whl_mods_dict): """Implementation of the pip.whl_mods tag class. diff --git a/python/private/python.bzl b/python/private/python.bzl index 53cd5e9cd2..c187904322 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -21,9 +21,9 @@ load(":full_version.bzl", "full_version") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") -load(":semver.bzl", "semver") load(":toolchains_repo.bzl", "multi_toolchain_aliases") load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") +load(":version.bzl", "version") def parse_modules(*, module_ctx, _fail = fail): """Parse the modules and return a struct for registrations. @@ -458,16 +458,20 @@ def _fail_multiple_default_toolchains(first, second): second = second, )) -def _validate_version(*, version, _fail = fail): - parsed = semver(version) - if parsed.patch == None or parsed.build or parsed.pre_release: - _fail("The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '{}'".format(version)) +def _validate_version(version_str, *, _fail = fail): + v = version.parse(version_str, strict = True, _fail = _fail) + if v == None: + # Only reachable in tests + return False + + if len(v.release) < 3: + _fail("The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '{}'".format(v.string)) return False return True def _process_single_version_overrides(*, tag, _fail = fail, default): - if not _validate_version(version = tag.python_version, _fail = _fail): + if not _validate_version(tag.python_version, _fail = _fail): return available_versions = default["tool_versions"] @@ -517,7 +521,7 @@ def _process_single_version_overrides(*, tag, _fail = fail, default): kwargs.setdefault(tag.python_version, {})["distutils"] = tag.distutils def _process_single_version_platform_overrides(*, tag, _fail = fail, default): - if not _validate_version(version = tag.python_version, _fail = _fail): + if not _validate_version(tag.python_version, _fail = _fail): return available_versions = default["tool_versions"] @@ -558,12 +562,12 @@ def _process_global_overrides(*, tag, default, _fail = fail): if tag.minor_mapping: for minor_version, full_version in tag.minor_mapping.items(): - parsed = semver(minor_version) - if parsed.patch != None or parsed.build or parsed.pre_release: - fail("Expected the key to be of `X.Y` format but got `{}`".format(minor_version)) - parsed = semver(full_version) - if parsed.patch == None: - fail("Expected the value to at least be of `X.Y.Z` format but got `{}`".format(minor_version)) + parsed = version.parse(minor_version, strict = True, _fail = _fail) + if len(parsed.release) > 2 or parsed.pre or parsed.post or parsed.dev or parsed.local: + fail("Expected the key to be of `X.Y` format but got `{}`".format(parsed.string)) + + # Ensure that the version is valid + version.parse(full_version, strict = True, _fail = _fail) default["minor_mapping"] = tag.minor_mapping @@ -651,8 +655,11 @@ def _get_toolchain_config(*, modules, _fail = fail): versions = {} for version_string in available_versions: - v = semver(version_string) - versions.setdefault("{}.{}".format(v.major, v.minor), []).append((int(v.patch), version_string)) + v = version.parse(version_string, strict = True) + versions.setdefault( + "{}.{}".format(v.release[0], v.release[1]), + [], + ).append((version.key(v), v.string)) minor_mapping = { major_minor: max(subset)[1] diff --git a/python/private/semver.bzl b/python/private/semver.bzl deleted file mode 100644 index 0cbd172348..0000000000 --- a/python/private/semver.bzl +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2024 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"A semver version parser" - -def _key(version): - return ( - version.major, - version.minor or 0, - version.patch or 0, - # non pre-release versions are higher - version.pre_release == "", - # then we compare each element of the pre_release tag separately - tuple([ - ( - i if not i.isdigit() else "", - # digit values take precedence - int(i) if i.isdigit() else 0, - ) - for i in version.pre_release.split(".") - ]) if version.pre_release else None, - # And build info is just alphabetic - version.build, - ) - -def _to_dict(self): - return { - "build": self.build, - "major": self.major, - "minor": self.minor, - "patch": self.patch, - "pre_release": self.pre_release, - } - -def _new(*, major, minor, patch, pre_release, build, version = None): - # buildifier: disable=uninitialized - self = struct( - major = int(major), - minor = None if minor == None else int(minor), - # NOTE: this is called `micro` in the Python interpreter versioning scheme - patch = None if patch == None else int(patch), - pre_release = pre_release, - build = build, - # buildifier: disable=uninitialized - key = lambda: _key(self), - str = lambda: version, - to_dict = lambda: _to_dict(self), - ) - return self - -def semver(version): - """Parse the semver version and return the values as a struct. - - Args: - version: {type}`str` the version string. - - Returns: - A {type}`struct` with `major`, `minor`, `patch` and `build` attributes. - """ - - # Implement the https://semver.org/ spec - major, _, tail = version.partition(".") - minor, _, tail = tail.partition(".") - patch, _, build = tail.partition("+") - patch, _, pre_release = patch.partition("-") - - return _new( - major = int(major), - minor = int(minor) if minor.isdigit() else None, - patch = int(patch) if patch.isdigit() else None, - build = build, - pre_release = pre_release, - version = version, - ) diff --git a/python/private/version.bzl b/python/private/version.bzl index 4425cc7661..f98165d391 100644 --- a/python/private/version.bzl +++ b/python/private/version.bzl @@ -510,7 +510,7 @@ def normalize_pep440(version): """ return _parse(version, strict = True)["norm"] -def _parse(version_str, strict = True): +def _parse(version_str, strict = True, _fail = fail): """Escape the version component of a filename. See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode @@ -519,6 +519,7 @@ def _parse(version_str, strict = True): Args: version_str: version string to be normalized according to PEP 440. strict: fail if the version is invalid, defaults to True. + _fail: Used for tests Returns: string containing the normalized version. @@ -544,7 +545,7 @@ def _parse(version_str, strict = True): parser_ctx = parser.context() if parser.input[parser_ctx["start"]:]: if strict: - fail( + _fail( "Failed to parse PEP 440 version identifier '%s'." % parser.input, "Parse error at '%s'" % parser.input[parser_ctx["start"]:], ) @@ -554,7 +555,7 @@ def _parse(version_str, strict = True): parser_ctx["is_prefix"] = is_prefix return parser_ctx -def parse(version_str, strict = False): +def parse(version_str, strict = False, _fail = fail): """Parse a PEP4408 compliant version. This is similar to `normalize_pep440`, but it parses individual components to @@ -563,6 +564,7 @@ def parse(version_str, strict = False): Args: version_str: version string to be normalized according to PEP 440. strict: fail if the version is invalid. + _fail: used for tests Returns: a struct with individual components of a version: @@ -580,29 +582,29 @@ def parse(version_str, strict = False): * `string` {type}`str` normalized value of the input. """ - parts = _parse(version_str, strict = strict) + parts = _parse(version_str, strict = strict, _fail = _fail) if not parts: return None if parts["is_prefix"] and (parts["local"] or parts["post"] or parts["dev"] or parts["pre"]): if strict: - fail("local version part has been obtained, but only public segments can have prefix matches") + _fail("local version part has been obtained, but only public segments can have prefix matches") # https://peps.python.org/pep-0440/#public-version-identifiers return None return struct( - epoch = _parse_epoch(parts["epoch"]), + epoch = _parse_epoch(parts["epoch"], _fail), release = _parse_release(parts["release"]), pre = _parse_pre(parts["pre"]), - post = _parse_post(parts["post"]), - dev = _parse_dev(parts["dev"]), - local = _parse_local(parts["local"]), + post = _parse_post(parts["post"], _fail), + dev = _parse_dev(parts["dev"], _fail), + local = _parse_local(parts["local"], _fail), string = parts["norm"], is_prefix = parts["is_prefix"], ) -def _parse_epoch(value): +def _parse_epoch(value, fail): if not value: return 0 @@ -614,7 +616,7 @@ def _parse_epoch(value): def _parse_release(value): return tuple([int(d) for d in value.split(".")]) -def _parse_local(value): +def _parse_local(value, fail): if not value: return None @@ -624,7 +626,7 @@ def _parse_local(value): # If the part is numerical, handle it as a number return tuple([int(part) if part.isdigit() else part for part in value[1:].split(".")]) -def _parse_dev(value): +def _parse_dev(value, fail): if not value: return None @@ -646,7 +648,7 @@ def _parse_pre(value): return (prefix, int(value[len(prefix):])) -def _parse_post(value): +def _parse_post(value, fail): if not value: return None diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 97c47b57db..443174c966 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -746,12 +746,6 @@ def _test_single_version_override_errors(env): ], want_error = "Only a single 'python.single_version_override' can be present for '3.12.4'", ), - struct( - overrides = [ - _single_version_override(python_version = "3.12.4+3", distutils_content = "foo"), - ], - want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.4+3'", - ), ]: errors = [] parse_modules( @@ -781,13 +775,13 @@ def _test_single_version_platform_override_errors(env): overrides = [ _single_version_platform_override(python_version = "3.12", platform = "foo"), ], - want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12'", + want_error = "The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '3.12'", ), struct( overrides = [ - _single_version_platform_override(python_version = "3.12.1+my_build", platform = "foo"), + _single_version_platform_override(python_version = "foo", platform = "foo"), ], - want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.1+my_build'", + want_error = "Failed to parse PEP 440 version identifier 'foo'. Parse error at 'foo'", ), ]: errors = [] @@ -799,7 +793,7 @@ def _test_single_version_platform_override_errors(env): single_version_platform_override = test.overrides, ), ), - _fail = errors.append, + _fail = lambda *a: errors.append(" ".join(a)), ) env.expect.that_collection(errors).contains_exactly([test.want_error]) diff --git a/tests/semver/BUILD.bazel b/tests/semver/BUILD.bazel deleted file mode 100644 index e12b1e5300..0000000000 --- a/tests/semver/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2024 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -load(":semver_test.bzl", "semver_test_suite") - -semver_test_suite(name = "semver_tests") diff --git a/tests/semver/semver_test.bzl b/tests/semver/semver_test.bzl deleted file mode 100644 index 9d13402c92..0000000000 --- a/tests/semver/semver_test.bzl +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright 2023 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"" - -load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private:semver.bzl", "semver") # buildifier: disable=bzl-visibility - -_tests = [] - -def _test_semver_from_major(env): - actual = semver("3") - env.expect.that_int(actual.major).equals(3) - env.expect.that_int(actual.minor).equals(None) - env.expect.that_int(actual.patch).equals(None) - env.expect.that_str(actual.build).equals("") - -_tests.append(_test_semver_from_major) - -def _test_semver_from_major_minor_version(env): - actual = semver("4.9") - env.expect.that_int(actual.major).equals(4) - env.expect.that_int(actual.minor).equals(9) - env.expect.that_int(actual.patch).equals(None) - env.expect.that_str(actual.build).equals("") - -_tests.append(_test_semver_from_major_minor_version) - -def _test_semver_with_build_info(env): - actual = semver("1.2.3+mybuild") - env.expect.that_int(actual.major).equals(1) - env.expect.that_int(actual.minor).equals(2) - env.expect.that_int(actual.patch).equals(3) - env.expect.that_str(actual.build).equals("mybuild") - -_tests.append(_test_semver_with_build_info) - -def _test_semver_with_build_info_multiple_pluses(env): - actual = semver("1.2.3-rc0+build+info") - env.expect.that_int(actual.major).equals(1) - env.expect.that_int(actual.minor).equals(2) - env.expect.that_int(actual.patch).equals(3) - env.expect.that_str(actual.pre_release).equals("rc0") - env.expect.that_str(actual.build).equals("build+info") - -_tests.append(_test_semver_with_build_info_multiple_pluses) - -def _test_semver_alpha_beta(env): - actual = semver("1.2.3-alpha.beta") - env.expect.that_int(actual.major).equals(1) - env.expect.that_int(actual.minor).equals(2) - env.expect.that_int(actual.patch).equals(3) - env.expect.that_str(actual.pre_release).equals("alpha.beta") - -_tests.append(_test_semver_alpha_beta) - -def _test_semver_sort(env): - want = [ - semver(item) - for item in [ - # The items are sorted from lowest to highest version - "0.0.1", - "0.1.0-rc", - "0.1.0", - "0.9.11", - "0.9.12", - "1.0.0-alpha", - "1.0.0-alpha.1", - "1.0.0-alpha.beta", - "1.0.0-beta", - "1.0.0-beta.2", - "1.0.0-beta.11", - "1.0.0-rc.1", - "1.0.0-rc.2", - "1.0.0", - # Also handle missing minor and patch version strings - "2.0", - "3", - # Alphabetic comparison for different builds - "3.0.0+build0", - "3.0.0+build1", - ] - ] - actual = sorted(want, key = lambda x: x.key()) - env.expect.that_collection(actual).contains_exactly(want).in_order() - for i, greater in enumerate(want[1:]): - smaller = actual[i] - if greater.key() <= smaller.key(): - env.fail("Expected '{}' to be smaller than '{}', but got otherwise".format( - smaller.str(), - greater.str(), - )) - -_tests.append(_test_semver_sort) - -def semver_test_suite(name): - """Create the test suite. - - Args: - name: the name of the test suite - """ - test_suite(name = name, basic_tests = _tests)