feat(pypi): implement a new whl selection algorithm (#3111)
This PR only implements the selection algorithm where instead of
selecting all wheels that are compatible with the set of target
platforms, we select a single wheel that is most specialized for a
particular single target platform.
What is more, compared to the existing algorithm it does not assume
a particular list of supported platforms and just fully implements the
spec.
Work towards #2747
Work towards #2759
Work towards #2849
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index 3a66170..4b56b73 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -366,6 +366,16 @@
)
bzl_library(
+ name = "select_whl_bzl",
+ srcs = ["select_whl.bzl"],
+ deps = [
+ ":parse_whl_name_bzl",
+ ":python_tag_bzl",
+ "//python/private:version_bzl",
+ ],
+)
+
+bzl_library(
name = "simpleapi_download_bzl",
srcs = ["simpleapi_download.bzl"],
deps = [
diff --git a/python/private/pypi/select_whl.bzl b/python/private/pypi/select_whl.bzl
new file mode 100644
index 0000000..e9db188
--- /dev/null
+++ b/python/private/pypi/select_whl.bzl
@@ -0,0 +1,237 @@
+"Select a single wheel that fits the parameters of a target platform."
+
+load("//python/private:version.bzl", "version")
+load(":parse_whl_name.bzl", "parse_whl_name")
+load(":python_tag.bzl", "PY_TAG_GENERIC", "python_tag")
+
+_ANDROID = "android"
+_IOS = "ios"
+_MANYLINUX = "manylinux"
+_MACOSX = "macosx"
+_MUSLLINUX = "musllinux"
+
+def _value_priority(*, tag, values):
+ keys = []
+ for priority, wp in enumerate(values):
+ if tag == wp:
+ keys.append(priority)
+
+ return max(keys) if keys else None
+
+def _platform_tag_priority(*, tag, values):
+ # Implements matching platform tag
+ # https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/
+
+ if not (
+ tag.startswith(_ANDROID) or
+ tag.startswith(_IOS) or
+ tag.startswith(_MACOSX) or
+ tag.startswith(_MANYLINUX) or
+ tag.startswith(_MUSLLINUX)
+ ):
+ res = _value_priority(tag = tag, values = values)
+ if res == None:
+ return res
+
+ return (res, (0, 0))
+
+ # Only android, ios, macosx, manylinux or musllinux platforms should be considered
+
+ os, _, tail = tag.partition("_")
+ major, _, tail = tail.partition("_")
+ if not os.startswith(_ANDROID):
+ minor, _, arch = tail.partition("_")
+ else:
+ minor = "0"
+ arch = tail
+ version = (int(major), int(minor))
+
+ keys = []
+ for priority, wp in enumerate(values):
+ want_os, sep, tail = wp.partition("_")
+ if not sep:
+ # if there is no `_` separator, then it means that we have something like `win32` or
+ # similar wheels that we are considering, this means that it should be discarded because
+ # we are dealing only with platforms that have `_`.
+ continue
+
+ if want_os != os:
+ # os should always match exactly for us to match and assign a priority
+ continue
+
+ want_major, _, tail = tail.partition("_")
+ if want_major == "*":
+ # the expected match is any version
+ want_major = ""
+ want_minor = ""
+ want_arch = tail
+ elif os.startswith(_ANDROID):
+ # we set it to `0` above, so setting the `want_minor` her to `0` will make things
+ # consistent.
+ want_minor = "0"
+ want_arch = tail
+ else:
+ # here we parse the values from the given platform
+ want_minor, _, want_arch = tail.partition("_")
+
+ if want_arch != arch:
+ # the arch should match exactly
+ continue
+
+ # if want_major is defined, then we know that we don't have a `*` in the matcher.
+ want_version = (int(want_major), int(want_minor)) if want_major else None
+ if not want_version or version <= want_version:
+ keys.append((priority, version))
+
+ return max(keys) if keys else None
+
+def _python_tag_priority(*, tag, implementation, py_version):
+ if tag.startswith(PY_TAG_GENERIC):
+ ver_str = tag[len(PY_TAG_GENERIC):]
+ elif tag.startswith(implementation):
+ ver_str = tag[len(implementation):]
+ else:
+ return None
+
+ # Add a 0 at the end in case it is a single digit
+ ver_str = "{}.{}".format(ver_str[0], ver_str[1:] or "0")
+
+ ver = version.parse(ver_str)
+ if not version.is_compatible(py_version, ver):
+ return None
+
+ return (
+ tag.startswith(implementation),
+ version.key(ver),
+ )
+
+def _candidates_by_priority(
+ *,
+ whls,
+ implementation_name,
+ python_version,
+ whl_abi_tags,
+ whl_platform_tags,
+ logger):
+ """Calculate the priority of each wheel
+
+ Returns:
+ A dictionary where keys are priority tuples which allows us to sort and pick the
+ last item.
+ """
+ py_version = version.parse(python_version, strict = True)
+ implementation = python_tag(implementation_name)
+
+ ret = {}
+ for whl in whls:
+ parsed = parse_whl_name(whl.filename)
+ priority = None
+
+ # See https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#compressed-tag-sets
+ for platform in parsed.platform_tag.split("."):
+ platform = _platform_tag_priority(tag = platform, values = whl_platform_tags)
+ if platform == None:
+ logger.debug(lambda: "The platform_tag in '{}' does not match given list: {}".format(
+ whl.filename,
+ whl_platform_tags,
+ ))
+ continue
+
+ for py in parsed.python_tag.split("."):
+ py = _python_tag_priority(
+ tag = py,
+ implementation = implementation,
+ py_version = py_version,
+ )
+ if py == None:
+ logger.debug(lambda: "The python_tag in '{}' does not match implementation or version: {} {}".format(
+ whl.filename,
+ implementation,
+ py_version.string,
+ ))
+ continue
+
+ for abi in parsed.abi_tag.split("."):
+ abi = _value_priority(
+ tag = abi,
+ values = whl_abi_tags,
+ )
+ if abi == None:
+ logger.debug(lambda: "The abi_tag in '{}' does not match given list: {}".format(
+ whl.filename,
+ whl_abi_tags,
+ ))
+ continue
+
+ # 1. Prefer platform wheels
+ # 2. Then prefer implementation/python version
+ # 3. Then prefer more specific ABI wheels
+ candidate = (platform, py, abi)
+ priority = priority or candidate
+ if candidate > priority:
+ priority = candidate
+
+ if priority == None:
+ logger.debug(lambda: "The whl '{}' is incompatible".format(
+ whl.filename,
+ ))
+ continue
+
+ ret[priority] = whl
+
+ return ret
+
+def select_whl(
+ *,
+ whls,
+ python_version,
+ whl_platform_tags,
+ whl_abi_tags,
+ implementation_name = "cpython",
+ limit = 1,
+ logger):
+ """Select a whl that is the most suitable for the given platform.
+
+ Args:
+ whls: {type}`list[struct]` a list of candidates which have a `filename`
+ attribute containing the `whl` filename.
+ python_version: {type}`str` the target python version.
+ implementation_name: {type}`str` the `implementation_name` from the target_platform env.
+ whl_abi_tags: {type}`list[str]` The whl abi tags to select from. The preference is
+ for wheels that have ABI values appearing later in the `whl_abi_tags` list.
+ whl_platform_tags: {type}`list[str]` The whl platform tags to select from.
+ The platform tag may contain `*` and this means that if the platform tag is
+ versioned (e.g. `manylinux`), then we will select the highest available
+ platform version, e.g. if `manylinux_2_17` and `manylinux_2_5` wheels are both
+ compatible, we will select `manylinux_2_17`. Otherwise for versioned platform
+ tags we select the highest *compatible* version, e.g. if `manylinux_2_6`
+ support is requested, then we would select `manylinux_2_5` in the previous
+ example. This allows us to pass the same filtering parameters when selecting
+ all of the whl dependencies irrespective of what actual platform tags they
+ contain.
+ limit: {type}`int` number of wheels to return. Defaults to 1.
+ logger: {type}`struct` the logger instance.
+
+ Returns:
+ {type}`list[struct] | struct | None`, a single struct from the `whls` input
+ argument or `None` if a match is not found. If the `limit` is greater than
+ one, then we will return a list.
+ """
+ candidates = _candidates_by_priority(
+ whls = whls,
+ implementation_name = implementation_name,
+ python_version = python_version,
+ whl_abi_tags = whl_abi_tags,
+ whl_platform_tags = whl_platform_tags,
+ logger = logger,
+ )
+
+ if not candidates:
+ return None
+
+ res = [i[1] for i in sorted(candidates.items())]
+ logger.debug(lambda: "Sorted candidates:\n{}".format(
+ "\n".join([c.filename for c in res]),
+ ))
+
+ return res[-1] if limit == 1 else res[-limit:]
diff --git a/tests/pypi/select_whl/BUILD.bazel b/tests/pypi/select_whl/BUILD.bazel
new file mode 100644
index 0000000..0ad8cba
--- /dev/null
+++ b/tests/pypi/select_whl/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":select_whl_tests.bzl", "select_whl_test_suite")
+
+select_whl_test_suite(name = "select_whl_tests")
diff --git a/tests/pypi/select_whl/select_whl_tests.bzl b/tests/pypi/select_whl/select_whl_tests.bzl
new file mode 100644
index 0000000..28e17ba
--- /dev/null
+++ b/tests/pypi/select_whl/select_whl_tests.bzl
@@ -0,0 +1,463 @@
+""
+
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "REPO_VERBOSITY_ENV_VAR", "repo_utils") # buildifier: disable=bzl-visibility
+load("//python/private/pypi:select_whl.bzl", "select_whl") # buildifier: disable=bzl-visibility
+
+WHL_LIST = [
+ "pkg-0.0.1-cp311-cp311-macosx_10_9_universal2.whl",
+ "pkg-0.0.1-cp311-cp311-macosx_10_9_x86_64.whl",
+ "pkg-0.0.1-cp311-cp311-macosx_11_0_arm64.whl",
+ "pkg-0.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+ "pkg-0.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
+ "pkg-0.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",
+ "pkg-0.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ "pkg-0.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",
+ "pkg-0.0.1-cp313-cp313t-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp313-cp313-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp313-abi3-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp313-none-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp311-cp311-musllinux_1_1_aarch64.whl",
+ "pkg-0.0.1-cp311-cp311-musllinux_1_1_i686.whl",
+ "pkg-0.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl",
+ "pkg-0.0.1-cp311-cp311-musllinux_1_1_s390x.whl",
+ "pkg-0.0.1-cp311-cp311-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp311-cp311-win32.whl",
+ "pkg-0.0.1-cp311-cp311-win_amd64.whl",
+ "pkg-0.0.1-cp37-cp37m-macosx_10_9_x86_64.whl",
+ "pkg-0.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+ "pkg-0.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
+ "pkg-0.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl",
+ "pkg-0.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ "pkg-0.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",
+ "pkg-0.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl",
+ "pkg-0.0.1-cp37-cp37m-musllinux_1_1_i686.whl",
+ "pkg-0.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl",
+ "pkg-0.0.1-cp37-cp37m-musllinux_1_1_s390x.whl",
+ "pkg-0.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp37-cp37m-win32.whl",
+ "pkg-0.0.1-cp37-cp37m-win_amd64.whl",
+ "pkg-0.0.1-cp39-cp39-macosx_10_9_universal2.whl",
+ "pkg-0.0.1-cp39-cp39-macosx_10_9_x86_64.whl",
+ "pkg-0.0.1-cp39-cp39-macosx_11_0_arm64.whl",
+ "pkg-0.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+ "pkg-0.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
+ "pkg-0.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",
+ "pkg-0.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ "pkg-0.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",
+ "pkg-0.0.1-cp39-cp39-musllinux_1_1_aarch64.whl",
+ "pkg-0.0.1-cp39-cp39-musllinux_1_1_i686.whl",
+ "pkg-0.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl",
+ "pkg-0.0.1-cp39-cp39-musllinux_1_1_s390x.whl",
+ "pkg-0.0.1-cp39-cp39-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp39-cp39-win32.whl",
+ "pkg-0.0.1-cp39-cp39-win_amd64.whl",
+ "pkg-0.0.1-cp39-abi3-any.whl",
+ "pkg-0.0.1-py310-abi3-any.whl",
+ "pkg-0.0.1-py3-abi3-any.whl",
+ "pkg-0.0.1-py3-none-any.whl",
+ # Extra examples that should be discarded
+ "pkg-0.0.1-py27-cp27mu-win_amd64.whl",
+ "pkg-0.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+]
+
+def _match(env, got, *want_filenames):
+ if not want_filenames:
+ env.expect.that_collection(got).has_size(len(want_filenames))
+ return
+
+ got = [g for g in got if g]
+ got_filenames = [g.filename for g in got]
+ env.expect.that_collection(got_filenames).contains_exactly(want_filenames).in_order()
+
+ if got:
+ # Check that we pass the original structs
+ env.expect.that_str(got[0].other).equals("dummy")
+
+def _select_whl(whls, debug = False, **kwargs):
+ return select_whl(
+ whls = [
+ struct(
+ filename = f,
+ other = "dummy",
+ )
+ for f in whls
+ ],
+ logger = repo_utils.logger(struct(
+ os = struct(
+ environ = {
+ REPO_DEBUG_ENV_VAR: "1",
+ REPO_VERBOSITY_ENV_VAR: "TRACE" if debug else "INFO",
+ },
+ ),
+ ), "unit-test"),
+ **kwargs
+ )
+
+_tests = []
+
+def _test_not_select_py2(env):
+ # Check we prefer platform specific wheels
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py2-none-any.whl",
+ "pkg-0.0.1-py3-none-any.whl",
+ "pkg-0.0.1-py312-none-any.whl",
+ ],
+ whl_platform_tags = ["any"],
+ whl_abi_tags = ["none"],
+ python_version = "3.13",
+ limit = 2,
+ )
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-py3-none-any.whl",
+ "pkg-0.0.1-py312-none-any.whl",
+ )
+
+_tests.append(_test_not_select_py2)
+
+def _test_not_select_abi3(env):
+ # Check we prefer platform specific wheels
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py3-none-any.whl",
+ # the following should be ignored
+ "pkg-0.0.1-py3-abi3-any.whl",
+ "pkg-0.0.1-py3-abi3-p1.p2.p2.whl",
+ ],
+ whl_platform_tags = ["any", "p1"],
+ whl_abi_tags = ["none"],
+ python_version = "3.13",
+ limit = 2,
+ debug = True,
+ )
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-py3-none-any.whl",
+ )
+
+_tests.append(_test_not_select_abi3)
+
+def _test_select_cp312(env):
+ # Check we prefer platform specific wheels
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py2-none-any.whl",
+ "pkg-0.0.1-py3-none-any.whl",
+ "pkg-0.0.1-py312-none-any.whl",
+ "pkg-0.0.1-cp39-none-any.whl",
+ "pkg-0.0.1-cp312-none-any.whl",
+ "pkg-0.0.1-cp314-none-any.whl",
+ ],
+ whl_platform_tags = ["any"],
+ whl_abi_tags = ["none"],
+ python_version = "3.13",
+ limit = 5,
+ )
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-py3-none-any.whl",
+ "pkg-0.0.1-py312-none-any.whl",
+ "pkg-0.0.1-cp39-none-any.whl",
+ "pkg-0.0.1-cp312-none-any.whl",
+ )
+
+_tests.append(_test_select_cp312)
+
+def _test_simplest(env):
+ whls = [
+ "pkg-0.0.1-py2.py3-abi3-any.whl",
+ "pkg-0.0.1-py3-abi3-any.whl",
+ "pkg-0.0.1-py3-none-any.whl",
+ ]
+
+ got = _select_whl(
+ whls = whls,
+ whl_platform_tags = ["any"],
+ whl_abi_tags = ["abi3"],
+ python_version = "3.0",
+ )
+ _match(
+ env,
+ [got],
+ "pkg-0.0.1-py3-abi3-any.whl",
+ )
+
+_tests.append(_test_simplest)
+
+def _test_select_by_supported_py_version(env):
+ whls = [
+ "pkg-0.0.1-py2.py3-abi3-any.whl",
+ "pkg-0.0.1-py3-abi3-any.whl",
+ "pkg-0.0.1-py311-abi3-any.whl",
+ ]
+
+ for minor_version, match in {
+ 8: "pkg-0.0.1-py3-abi3-any.whl",
+ 11: "pkg-0.0.1-py311-abi3-any.whl",
+ }.items():
+ got = _select_whl(
+ whls = whls,
+ whl_platform_tags = ["any"],
+ whl_abi_tags = ["abi3"],
+ python_version = "3.{}".format(minor_version),
+ )
+ _match(env, [got], match)
+
+_tests.append(_test_select_by_supported_py_version)
+
+def _test_select_by_supported_cp_version(env):
+ whls = [
+ "pkg-0.0.1-py2.py3-abi3-any.whl",
+ "pkg-0.0.1-py3-abi3-any.whl",
+ "pkg-0.0.1-py311-abi3-any.whl",
+ "pkg-0.0.1-cp311-abi3-any.whl",
+ ]
+
+ for minor_version, match in {
+ 11: "pkg-0.0.1-cp311-abi3-any.whl",
+ 8: "pkg-0.0.1-py3-abi3-any.whl",
+ }.items():
+ got = _select_whl(
+ whls = whls,
+ whl_platform_tags = ["any"],
+ whl_abi_tags = ["abi3"],
+ python_version = "3.{}".format(minor_version),
+ )
+ _match(env, [got], match)
+
+_tests.append(_test_select_by_supported_cp_version)
+
+def _test_supported_cp_version_manylinux(env):
+ whls = [
+ "pkg-0.0.1-py2.py3-none-manylinux_1_1_x86_64.whl",
+ "pkg-0.0.1-py3-none-manylinux_1_1_x86_64.whl",
+ "pkg-0.0.1-py311-none-manylinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp311-none-manylinux_1_1_x86_64.whl",
+ ]
+
+ for minor_version, match in {
+ 8: "pkg-0.0.1-py3-none-manylinux_1_1_x86_64.whl",
+ 11: "pkg-0.0.1-cp311-none-manylinux_1_1_x86_64.whl",
+ }.items():
+ got = _select_whl(
+ whls = whls,
+ whl_platform_tags = ["manylinux_1_1_x86_64"],
+ whl_abi_tags = ["none"],
+ python_version = "3.{}".format(minor_version),
+ )
+ _match(env, [got], match)
+
+_tests.append(_test_supported_cp_version_manylinux)
+
+def _test_ignore_unsupported(env):
+ whls = ["pkg-0.0.1-xx3-abi3-any.whl"]
+ got = _select_whl(
+ whls = whls,
+ whl_platform_tags = ["any"],
+ whl_abi_tags = ["none"],
+ python_version = "3.0",
+ )
+ if got:
+ _match(env, [got], None)
+
+_tests.append(_test_ignore_unsupported)
+
+def _test_match_abi_and_not_py_version(env):
+ # Check we match the ABI and not the py version
+ whls = WHL_LIST
+ whl_platform_tags = [
+ "musllinux_*_x86_64",
+ "manylinux_*_x86_64",
+ ]
+ got = _select_whl(
+ whls = whls,
+ whl_platform_tags = whl_platform_tags,
+ whl_abi_tags = ["abi3", "cp37m"],
+ python_version = "3.7",
+ )
+ _match(
+ env,
+ [got],
+ "pkg-0.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ )
+
+ got = _select_whl(
+ whls = whls,
+ whl_platform_tags = whl_platform_tags[::-1],
+ whl_abi_tags = ["abi3", "cp37m"],
+ python_version = "3.7",
+ )
+ _match(
+ env,
+ [got],
+ "pkg-0.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl",
+ )
+
+_tests.append(_test_match_abi_and_not_py_version)
+
+def _test_select_filename_with_many_tags(env):
+ # Check we can select a filename with many platform tags
+ got = _select_whl(
+ whls = WHL_LIST,
+ whl_platform_tags = [
+ "any",
+ "musllinux_*_i686",
+ "manylinux_*_i686",
+ ],
+ whl_abi_tags = ["none", "abi3", "cp39"],
+ python_version = "3.9",
+ limit = 5,
+ )
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-py3-none-any.whl",
+ "pkg-0.0.1-py3-abi3-any.whl",
+ "pkg-0.0.1-cp39-abi3-any.whl",
+ "pkg-0.0.1-cp39-cp39-musllinux_1_1_i686.whl",
+ "pkg-0.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",
+ )
+
+_tests.append(_test_select_filename_with_many_tags)
+
+def _test_freethreaded_wheels(env):
+ # Check we prefer platform specific wheels
+ got = _select_whl(
+ whls = WHL_LIST,
+ whl_platform_tags = [
+ "any",
+ "musllinux_*_x86_64",
+ ],
+ whl_abi_tags = ["none", "abi3", "cp313", "cp313t"],
+ python_version = "3.13",
+ limit = 8,
+ )
+ _match(
+ env,
+ got,
+ # The last item has the most priority
+ "pkg-0.0.1-py3-none-any.whl",
+ "pkg-0.0.1-py3-abi3-any.whl",
+ "pkg-0.0.1-py310-abi3-any.whl",
+ "pkg-0.0.1-cp39-abi3-any.whl",
+ "pkg-0.0.1-cp313-none-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp313-abi3-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp313-cp313-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp313-cp313t-musllinux_1_1_x86_64.whl",
+ )
+
+_tests.append(_test_freethreaded_wheels)
+
+def _test_pytags_all_possible(env):
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313-none-win_amd64.whl",
+ ],
+ whl_platform_tags = ["win_amd64"],
+ whl_abi_tags = ["none"],
+ python_version = "3.12",
+ )
+ _match(
+ env,
+ [got],
+ "pkg-0.0.1-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313-none-win_amd64.whl",
+ )
+
+_tests.append(_test_pytags_all_possible)
+
+def _test_manylinx_musllinux_pref(env):
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py3-none-manylinux_2_31_x86_64.musllinux_1_1_x86_64.whl",
+ ],
+ whl_platform_tags = [
+ "manylinux_*_x86_64",
+ "musllinux_*_x86_64",
+ ],
+ whl_abi_tags = ["none"],
+ python_version = "3.12",
+ limit = 2,
+ )
+ _match(
+ env,
+ got,
+ # there is only one wheel, just select that
+ "pkg-0.0.1-py3-none-manylinux_2_31_x86_64.musllinux_1_1_x86_64.whl",
+ )
+
+_tests.append(_test_manylinx_musllinux_pref)
+
+def _test_multiple_musllinux(env):
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
+ "pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
+ ],
+ whl_platform_tags = ["musllinux_*_x86_64"],
+ whl_abi_tags = ["none"],
+ python_version = "3.12",
+ limit = 2,
+ )
+ _match(
+ env,
+ got,
+ # select the one with the highest version that is matching
+ "pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
+ )
+
+_tests.append(_test_multiple_musllinux)
+
+def _test_multiple_musllinux_exact_params(env):
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
+ "pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
+ ],
+ whl_platform_tags = ["musllinux_1_2_x86_64", "musllinux_1_1_x86_64"],
+ whl_abi_tags = ["none"],
+ python_version = "3.12",
+ limit = 2,
+ )
+ _match(
+ env,
+ got,
+ # select the one with the lowest version, because of the input to the function
+ "pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
+ "pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
+ )
+
+_tests.append(_test_multiple_musllinux_exact_params)
+
+def _test_android(env):
+ got = _select_whl(
+ whls = [
+ "pkg-0.0.1-py3-none-android_4_x86_64.whl",
+ "pkg-0.0.1-py3-none-android_8_x86_64.whl",
+ ],
+ whl_platform_tags = ["android_5_x86_64"],
+ whl_abi_tags = ["none"],
+ python_version = "3.12",
+ limit = 2,
+ )
+ _match(
+ env,
+ got,
+ # select the one with the highest version that is matching
+ "pkg-0.0.1-py3-none-android_4_x86_64.whl",
+ )
+
+_tests.append(_test_android)
+
+def select_whl_test_suite(name):
+ """Create the test suite.
+
+ Args:
+ name: the name of the test suite
+ """
+ test_suite(name = name, basic_tests = _tests)