refactor/fix: store dists in parse_requirements output (#1917)
This moves some of the code out of the `pip.bzl` extension and changes
the layout of the code to prepare for multi-platform whl support.
Summary:
* parse_requirements: add whls and sdists attribute, so that we can use
a function to populate the lists. Not sure if there is a better way to
do this.
* parse_requirements: add an extra code to ensure that we are handling
the target platform filtering correctly.
* select_whl: split the `select_whl` into `select_whls`, which filters
the whls (this can be used later in multi-platform selects) and
select_whl , which just is used get the most appropriate whl for the
host platform.
* Additionally fix the logic in `select_whl`, which would result in
Python 3.12 wheels being selected on Python 3.11 interpreters because
we were not taking into account the interpreter tag when doing the
filtering.
Fixes #1930
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e15be3b..3fdd039 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,8 @@
"panic: runtime error: invalid memory address or nil pointer dereference"
* (bzlmod) remove `pip.parse(annotations)` attribute as it is unused and has been
replaced by whl_modifications.
+* (pip) Correctly select wheels when the python tag includes minor versions.
+ See ([#1930](https://github.com/bazelbuild/rules_python/issues/1930))
### Added
* (rules) Precompiling Python source at build time is available. but is
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index 17d8083..d6c8d91 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -584,7 +584,17 @@
),
"quiet": attr.bool(
default = True,
- doc = "If True, suppress printing stdout and stderr output to the terminal.",
+ doc = """\
+If True, suppress printing stdout and stderr output to the terminal.
+
+If you would like to get more diagnostic output, please use:
+
+ RULES_PYTHON_REPO_DEBUG=1
+
+or
+
+ RULES_PYTHON_REPO_DEBUG_VERBOSITY=<INFO|DEBUG|TRACE>
+""",
),
"repo_prefix": attr.string(
doc = """
diff --git a/python/private/bzlmod/pip.bzl b/python/private/bzlmod/pip.bzl
index aa70810..9e29877 100644
--- a/python/private/bzlmod/pip.bzl
+++ b/python/private/bzlmod/pip.bzl
@@ -28,6 +28,7 @@
load("//python/private:parse_whl_name.bzl", "parse_whl_name")
load("//python/private:pypi_index.bzl", "simpleapi_download")
load("//python/private:render_pkg_aliases.bzl", "whl_alias")
+load("//python/private:repo_utils.bzl", "repo_utils")
load("//python/private:version_label.bzl", "version_label")
load("//python/private:whl_target_platforms.bzl", "select_whl")
load(":pip_repository.bzl", "pip_repository")
@@ -100,6 +101,7 @@
)
def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, simpleapi_cache):
+ logger = repo_utils.logger(module_ctx)
python_interpreter_target = pip_attr.python_interpreter_target
# if we do not have the python_interpreter set in the attributes
@@ -160,32 +162,18 @@
# Create a new wheel library for each of the different whls
- requirements_by_platform = parse_requirements(
- module_ctx,
- requirements_by_platform = pip_attr.requirements_by_platform,
- requirements_linux = pip_attr.requirements_linux,
- requirements_lock = pip_attr.requirements_lock,
- requirements_osx = pip_attr.requirements_darwin,
- requirements_windows = pip_attr.requirements_windows,
- extra_pip_args = pip_attr.extra_pip_args,
- )
-
- index_urls = {}
+ get_index_urls = None
if pip_attr.experimental_index_url:
if pip_attr.download_only:
fail("Currently unsupported to use `download_only` and `experimental_index_url`")
- index_urls = simpleapi_download(
- module_ctx,
+ get_index_urls = lambda ctx, distributions: simpleapi_download(
+ ctx,
attr = struct(
index_url = pip_attr.experimental_index_url,
extra_index_urls = pip_attr.experimental_extra_index_urls or [],
index_url_overrides = pip_attr.experimental_index_url_overrides or {},
- sources = list({
- req.distribution: None
- for reqs in requirements_by_platform.values()
- for req in reqs
- }),
+ sources = distributions,
envsubst = pip_attr.envsubst,
# Auth related info
netrc = pip_attr.netrc,
@@ -195,6 +183,19 @@
parallel_download = pip_attr.parallel_download,
)
+ requirements_by_platform = parse_requirements(
+ module_ctx,
+ requirements_by_platform = pip_attr.requirements_by_platform,
+ requirements_linux = pip_attr.requirements_linux,
+ requirements_lock = pip_attr.requirements_lock,
+ requirements_osx = pip_attr.requirements_darwin,
+ requirements_windows = pip_attr.requirements_windows,
+ extra_pip_args = pip_attr.extra_pip_args,
+ get_index_urls = get_index_urls,
+ python_version = major_minor,
+ logger = logger,
+ )
+
repository_platform = host_platform(module_ctx.os)
for whl_name, requirements in requirements_by_platform.items():
requirement = select_requirement(
@@ -255,37 +256,22 @@
)
whl_library_args.update({k: v for k, (v, default) in maybe_args_with_default.items() if v == default})
- if index_urls:
- whls = []
- sdist = None
- for sha256 in requirement.srcs.shas:
- # For now if the artifact is marked as yanked we just ignore it.
- #
- # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api
-
- maybe_whl = index_urls[whl_name].whls.get(sha256)
- if maybe_whl and not maybe_whl.yanked:
- whls.append(maybe_whl)
- continue
-
- maybe_sdist = index_urls[whl_name].sdists.get(sha256)
- if maybe_sdist and not maybe_sdist.yanked:
- sdist = maybe_sdist
- continue
-
- print("WARNING: Could not find a whl or an sdist with sha256={}".format(sha256)) # buildifier: disable=print
-
+ if requirement.whls or requirement.sdist:
+ logger.debug(lambda: "Selecting a compatible dist for {} from dists:\n{}".format(
+ repository_platform,
+ json.encode(
+ struct(
+ whls = requirement.whls,
+ sdist = requirement.sdist,
+ ),
+ ),
+ ))
distribution = select_whl(
- whls = whls,
- want_abis = [
- "none",
- "abi3",
- "cp" + major_minor.replace(".", ""),
- # Older python versions have wheels for the `*m` ABI.
- "cp" + major_minor.replace(".", "") + "m",
- ],
+ whls = requirement.whls,
want_platform = repository_platform,
- ) or sdist
+ ) or requirement.sdist
+
+ logger.debug(lambda: "Selected: {}".format(distribution))
if distribution:
whl_library_args["requirement"] = requirement.srcs.requirement
@@ -303,7 +289,7 @@
# This is no-op because pip is not used to download the wheel.
whl_library_args.pop("download_only", None)
else:
- print("WARNING: falling back to pip for installing the right file for {}".format(requirement.requirement_line)) # buildifier: disable=print
+ logger.warn("falling back to pip for installing the right file for {}".format(requirement.requirement_line))
# We sort so that the lock-file remains the same no matter the order of how the
# args are manipulated in the code going before.
diff --git a/python/private/parse_requirements.bzl b/python/private/parse_requirements.bzl
index f9d7a05..c6a4985 100644
--- a/python/private/parse_requirements.bzl
+++ b/python/private/parse_requirements.bzl
@@ -29,7 +29,7 @@
load("//python/pip_install:requirements_parser.bzl", "parse")
load(":normalize_name.bzl", "normalize_name")
load(":pypi_index_sources.bzl", "get_simpleapi_sources")
-load(":whl_target_platforms.bzl", "whl_target_platforms")
+load(":whl_target_platforms.bzl", "select_whls", "whl_target_platforms")
# This includes the vendored _translate_cpu and _translate_os from
# @platforms//host:extension.bzl at version 0.0.9 so that we don't
@@ -84,6 +84,11 @@
if not filter:
fail("Must specific a filter string, got: {}".format(filter))
+ if filter.startswith("cp3"):
+ # TODO @aignas 2024-05-23: properly handle python versions in the filter.
+ # For now we are just dropping it to ensure that we don't fail.
+ _, _, filter = filter.partition("_")
+
sanitized = filter.replace("*", "").replace("_", "")
if sanitized and not sanitized.isalnum():
fail("The platform filter can only contain '*', '_' and alphanumerics")
@@ -142,6 +147,9 @@
requirements_lock = None,
requirements_windows = None,
extra_pip_args = [],
+ get_index_urls = None,
+ python_version = None,
+ logger = None,
fail_fn = fail):
"""Get the requirements with platforms that the requirements apply to.
@@ -156,6 +164,12 @@
requirements_windows (label): The requirements file for windows OS.
extra_pip_args (string list): Extra pip arguments to perform extra validations and to
be joined with args fined in files.
+ get_index_urls: Callable[[ctx, list[str]], dict], a callable to get all
+ of the distribution URLs from a PyPI index. Accepts ctx and
+ distribution names to query.
+ python_version: str or None. This is needed when the get_index_urls is
+ specified. It should be of the form "3.x.x",
+ logger: repo_utils.logger or None, a simple struct to log diagnostic messages.
fail_fn (Callable[[str], None]): A failure function used in testing failure cases.
Returns:
@@ -312,20 +326,46 @@
)
for_req.target_platforms.append(target_platform)
- return {
- whl_name: [
- struct(
- distribution = r.distribution,
- srcs = r.srcs,
- requirement_line = r.requirement_line,
- target_platforms = sorted(r.target_platforms),
- extra_pip_args = r.extra_pip_args,
- download = r.download,
+ index_urls = {}
+ if get_index_urls:
+ if not python_version:
+ fail_fn("'python_version' must be provided")
+ return None
+
+ index_urls = get_index_urls(
+ ctx,
+ # Use list({}) as a way to have a set
+ list({
+ req.distribution: None
+ for reqs in requirements_by_platform.values()
+ for req in reqs.values()
+ }),
+ )
+
+ ret = {}
+ for whl_name, reqs in requirements_by_platform.items():
+ for r in sorted(reqs.values(), key = lambda r: r.requirement_line):
+ whls, sdist = _add_dists(
+ r,
+ index_urls.get(whl_name),
+ python_version = python_version,
+ logger = logger,
)
- for r in sorted(reqs.values(), key = lambda r: r.requirement_line)
- ]
- for whl_name, reqs in requirements_by_platform.items()
- }
+
+ ret.setdefault(whl_name, []).append(
+ struct(
+ distribution = r.distribution,
+ srcs = r.srcs,
+ requirement_line = r.requirement_line,
+ target_platforms = sorted(r.target_platforms),
+ extra_pip_args = r.extra_pip_args,
+ download = r.download,
+ whls = whls,
+ sdist = sdist,
+ ),
+ )
+
+ return ret
def select_requirement(requirements, *, platform):
"""A simple function to get a requirement for a particular platform.
@@ -372,3 +412,58 @@
_translate_os(repository_os.name.lower()),
_translate_cpu(repository_os.arch.lower()),
)
+
+def _add_dists(requirement, index_urls, python_version, logger = None):
+ """Populate dists based on the information from the PyPI index.
+
+ This function will modify the given requirements_by_platform data structure.
+
+ Args:
+ requirement: The result of parse_requirements function.
+ index_urls: The result of simpleapi_download.
+ python_version: The version of the python interpreter.
+ logger: A logger for printing diagnostic info.
+ """
+ if not index_urls:
+ return [], None
+
+ whls = []
+ sdist = None
+
+ # TODO @aignas 2024-05-22: it is in theory possible to add all
+ # requirements by version instead of by sha256. This may be useful
+ # for some projects.
+ for sha256 in requirement.srcs.shas:
+ # For now if the artifact is marked as yanked we just ignore it.
+ #
+ # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api
+
+ maybe_whl = index_urls.whls.get(sha256)
+ if maybe_whl and not maybe_whl.yanked:
+ whls.append(maybe_whl)
+ continue
+
+ maybe_sdist = index_urls.sdists.get(sha256)
+ if maybe_sdist and not maybe_sdist.yanked:
+ sdist = maybe_sdist
+ continue
+
+ if logger:
+ logger.warn("Could not find a whl or an sdist with sha256={}".format(sha256))
+
+ # Filter out the wheels that are incompatible with the target_platforms.
+ whls = select_whls(
+ whls = whls,
+ want_abis = [
+ "none",
+ "abi3",
+ "cp" + python_version.replace(".", ""),
+ # Older python versions have wheels for the `*m` ABI.
+ "cp" + python_version.replace(".", "") + "m",
+ ],
+ want_platforms = requirement.target_platforms,
+ want_python_version = python_version,
+ logger = logger,
+ )
+
+ return whls, sdist
diff --git a/python/private/parse_whl_name.bzl b/python/private/parse_whl_name.bzl
index 9c7866e..063ac84 100644
--- a/python/private/parse_whl_name.bzl
+++ b/python/private/parse_whl_name.bzl
@@ -16,6 +16,30 @@
A starlark implementation of a Wheel filename parsing.
"""
+# Taken from https://peps.python.org/pep-0600/
+_LEGACY_ALIASES = {
+ "manylinux1_i686": "manylinux_2_5_i686",
+ "manylinux1_x86_64": "manylinux_2_5_x86_64",
+ "manylinux2010_i686": "manylinux_2_12_i686",
+ "manylinux2010_x86_64": "manylinux_2_12_x86_64",
+ "manylinux2014_aarch64": "manylinux_2_17_aarch64",
+ "manylinux2014_armv7l": "manylinux_2_17_armv7l",
+ "manylinux2014_i686": "manylinux_2_17_i686",
+ "manylinux2014_ppc64": "manylinux_2_17_ppc64",
+ "manylinux2014_ppc64le": "manylinux_2_17_ppc64le",
+ "manylinux2014_s390x": "manylinux_2_17_s390x",
+ "manylinux2014_x86_64": "manylinux_2_17_x86_64",
+}
+
+def normalize_platform_tag(tag):
+ """Resolve legacy aliases to modern equivalents for easier parsing elsewhere."""
+ return ".".join(list({
+ # The `list({})` usage here is to use it as a string set, where we will
+ # deduplicate, but otherwise retain the order of the tags.
+ _LEGACY_ALIASES.get(p, p): None
+ for p in tag.split(".")
+ }))
+
def parse_whl_name(file):
"""Parse whl file name into a struct of constituents.
@@ -68,5 +92,5 @@
build_tag = build_tag,
python_tag = python_tag,
abi_tag = abi_tag,
- platform_tag = platform_tag,
+ platform_tag = normalize_platform_tag(platform_tag),
)
diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl
index 7a59217..5267758 100644
--- a/python/private/repo_utils.bzl
+++ b/python/private/repo_utils.bzl
@@ -18,6 +18,7 @@
"""
REPO_DEBUG_ENV_VAR = "RULES_PYTHON_REPO_DEBUG"
+REPO_VERBOSITY_ENV_VAR = "RULES_PYTHON_REPO_DEBUG_VERBOSITY"
def _is_repo_debug_enabled(rctx):
"""Tells if debbugging output is requested during repo operatiosn.
@@ -41,6 +42,42 @@
if _is_repo_debug_enabled(rctx):
print(message_cb()) # buildifier: disable=print
+def _logger(rctx):
+ """Creates a logger instance for printing messages.
+
+ Args:
+ rctx: repository_ctx object.
+
+ Returns:
+ A struct with attributes logging: trace, debug, info, warn, fail.
+ """
+ if _is_repo_debug_enabled(rctx):
+ verbosity_level = "DEBUG"
+ else:
+ verbosity_level = "WARN"
+
+ env_var_verbosity = rctx.os.environ.get(REPO_VERBOSITY_ENV_VAR)
+ verbosity_level = env_var_verbosity or verbosity_level
+
+ verbosity = {
+ "DEBUG": 2,
+ "INFO": 1,
+ "TRACE": 3,
+ }.get(verbosity_level, 0)
+
+ def _log(enabled_on_verbosity, level, message_cb):
+ if verbosity < enabled_on_verbosity:
+ return
+
+ print("\nrules_python: {}: ".format(level.upper()), message_cb()) # buildifier: disable=print
+
+ return struct(
+ trace = lambda message_cb: _log(3, "TRACE", message_cb),
+ debug = lambda message_cb: _log(2, "DEBUG", message_cb),
+ info = lambda message_cb: _log(1, "INFO", message_cb),
+ warn = lambda message_cb: _log(0, "WARNING", message_cb),
+ )
+
def _execute_internal(
rctx,
*,
@@ -232,4 +269,5 @@
is_repo_debug_enabled = _is_repo_debug_enabled,
debug_print = _debug_print,
which_checked = _which_checked,
+ logger = _logger,
)
diff --git a/python/private/whl_target_platforms.bzl b/python/private/whl_target_platforms.bzl
index 14e178a..678c841 100644
--- a/python/private/whl_target_platforms.bzl
+++ b/python/private/whl_target_platforms.bzl
@@ -18,21 +18,6 @@
load(":parse_whl_name.bzl", "parse_whl_name")
-# Taken from https://peps.python.org/pep-0600/
-_LEGACY_ALIASES = {
- "manylinux1_i686": "manylinux_2_5_i686",
- "manylinux1_x86_64": "manylinux_2_5_x86_64",
- "manylinux2010_i686": "manylinux_2_12_i686",
- "manylinux2010_x86_64": "manylinux_2_12_x86_64",
- "manylinux2014_aarch64": "manylinux_2_17_aarch64",
- "manylinux2014_armv7l": "manylinux_2_17_armv7l",
- "manylinux2014_i686": "manylinux_2_17_i686",
- "manylinux2014_ppc64": "manylinux_2_17_ppc64",
- "manylinux2014_ppc64le": "manylinux_2_17_ppc64le",
- "manylinux2014_s390x": "manylinux_2_17_s390x",
- "manylinux2014_x86_64": "manylinux_2_17_x86_64",
-}
-
# The order of the dictionaries is to keep definitions with their aliases next to each
# other
_CPU_ALIASES = {
@@ -48,6 +33,7 @@
"ppc64": "ppc",
"ppc64le": "ppc",
"s390x": "s390x",
+ "arm": "arm",
"armv6l": "arm",
"armv7l": "arm",
} # buildifier: disable=unsorted-dict-items
@@ -90,7 +76,6 @@
value, _, _ = value.partition(".")
if "any" == value:
- # This is just a big value that should be larger than any other value returned by this function
return (True, False, 0, 0)
if "linux" in value:
@@ -118,12 +103,111 @@
# Windows does not have multiple wheels for the same target platform
return (False, False, 0, 0)
-def select_whl(*, whls, want_abis, want_platform):
+def select_whls(*, whls, want_python_version = "3.0", want_abis = [], want_platforms = [], logger = None):
+ """Select a subset of wheels suitable for target platforms from a list.
+
+ Args:
+ whls(list[struct]): A list of candidates which have a `filename`
+ attribute containing the `whl` filename.
+ want_python_version(str): An optional parameter to filter whls by python version. Defaults to '3.0'.
+ want_abis(list[str]): A list of ABIs that are supported.
+ want_platforms(str): The platforms
+ logger: A logger for printing diagnostic messages.
+
+ Returns:
+ A filtered list of items from the `whls` arg where `filename` matches
+ the selected criteria. If no match is found, an empty list is returned.
+ """
+ if not whls:
+ return []
+
+ version_limit = -1
+ if want_python_version:
+ version_limit = int(want_python_version.split(".")[1])
+
+ candidates = {}
+ for whl in whls:
+ parsed = parse_whl_name(whl.filename)
+
+ if logger:
+ logger.trace(lambda: "Deciding whether to use '{}'".format(whl.filename))
+
+ supported_implementations = {}
+ whl_version_min = 0
+ for tag in parsed.python_tag.split("."):
+ supported_implementations[tag[:2]] = None
+
+ if tag.startswith("cp3") or tag.startswith("py3"):
+ version = int(tag[len("..3"):] or 0)
+ else:
+ # In this case it should be eithor "cp2" or "py2" and we will default
+ # to `whl_version_min` = 0
+ continue
+
+ if whl_version_min == 0 or version < whl_version_min:
+ whl_version_min = version
+
+ if not ("cp" in supported_implementations or "py" in supported_implementations):
+ if logger:
+ logger.trace(lambda: "Discarding the whl because the whl does not support CPython, whl supported implementations are: {}".format(supported_implementations))
+ continue
+
+ if want_abis and parsed.abi_tag not in want_abis:
+ # Filter out incompatible ABIs
+ if logger:
+ logger.trace(lambda: "Discarding the whl because the whl abi did not match")
+ continue
+
+ if version_limit != -1 and whl_version_min > version_limit:
+ if logger:
+ logger.trace(lambda: "Discarding the whl because the whl supported python version is too high")
+ continue
+
+ compatible = False
+ if parsed.platform_tag == "any":
+ compatible = True
+ else:
+ for p in whl_target_platforms(parsed.platform_tag):
+ if p.target_platform in want_platforms:
+ compatible = True
+ break
+
+ if not compatible:
+ if logger:
+ logger.trace(lambda: "Discarding the whl because the whl does not support the desired platforms: {}".format(want_platforms))
+ continue
+
+ for implementation in supported_implementations:
+ candidates.setdefault(
+ (
+ parsed.abi_tag,
+ parsed.platform_tag,
+ ),
+ {},
+ ).setdefault(
+ (
+ # prefer cp implementation
+ implementation == "cp",
+ # prefer higher versions
+ whl_version_min,
+ # prefer abi3 over none
+ parsed.abi_tag != "none",
+ # prefer cpx abi over abi3
+ parsed.abi_tag != "abi3",
+ ),
+ [],
+ ).append(whl)
+
+ return [
+ candidates[key][sorted(v)[-1]][-1]
+ for key, v in candidates.items()
+ ]
+
+def select_whl(*, whls, want_platform):
"""Select a suitable wheel from a list.
Args:
whls(list[struct]): A list of candidates.
- want_abis(list[str]): A list of ABIs that are supported.
want_platform(str): The target platform.
Returns:
@@ -133,53 +217,31 @@
if not whls:
return None
- candidates = {}
- for whl in whls:
- parsed = parse_whl_name(whl.filename)
- if parsed.abi_tag not in want_abis:
- # Filter out incompatible ABIs
- continue
+ # TODO @aignas 2024-05-23: once we do the selection in the hub repo using
+ # an actual select, then this function will be the one that is used within
+ # the repository context instead of `select_whl`.
+ whls = select_whls(
+ whls = whls,
+ want_python_version = "",
+ want_platforms = [want_platform],
+ )
- platform_tags = list({_LEGACY_ALIASES.get(p, p): True for p in parsed.platform_tag.split(".")})
+ candidates = {
+ parse_whl_name(w.filename).platform_tag: w
+ for w in whls
+ # TODO @aignas 2024-06-01: to be addressed in #1837, where we add the necessary
+ # config settings.
+ if "musllinux_" not in w.filename
+ }
- for tag in platform_tags:
- candidates[tag] = whl
+ target_whl_platform = sorted(
+ candidates.keys(),
+ key = _whl_priority,
+ )
+ if not target_whl_platform:
+ return None
- # For most packages - if they supply 'any' wheel and there are no other
- # compatible wheels with the selected abis, we can just return the value.
- if len(candidates) == 1 and "any" in candidates:
- return struct(
- url = candidates["any"].url,
- sha256 = candidates["any"].sha256,
- filename = candidates["any"].filename,
- )
-
- target_plats = {}
- has_any = "any" in candidates
- for platform_tag, whl in candidates.items():
- if platform_tag == "any":
- continue
-
- if "musl" in platform_tag:
- # Ignore musl wheels for now
- continue
-
- platform_tag = ".".join({_LEGACY_ALIASES.get(p, p): True for p in platform_tag.split(".")})
- platforms = whl_target_platforms(platform_tag)
- for p in platforms:
- target_plats.setdefault("{}_{}".format(p.os, p.cpu), []).append(platform_tag)
-
- for p, platform_tags in target_plats.items():
- if has_any:
- platform_tags.append("any")
-
- target_plats[p] = sorted(platform_tags, key = _whl_priority)
-
- want = target_plats.get(want_platform)
- if not want:
- return want
-
- return candidates[want[0]]
+ return candidates[target_whl_platform[0]]
def whl_target_platforms(platform_tag, abi_tag = ""):
"""Parse the wheel abi and platform tags and return (os, cpu) tuples.
diff --git a/tests/private/parse_requirements/parse_requirements_tests.bzl b/tests/private/parse_requirements/parse_requirements_tests.bzl
index 0d6cd4e..81cf523 100644
--- a/tests/private/parse_requirements/parse_requirements_tests.bzl
+++ b/tests/private/parse_requirements/parse_requirements_tests.bzl
@@ -96,6 +96,8 @@
"osx_x86_64",
"windows_x86_64",
],
+ whls = [],
+ sdist = None,
),
],
})
@@ -109,6 +111,47 @@
_tests.append(_test_simple)
+def _test_platform_markers_with_python_version(env):
+ got = parse_requirements(
+ ctx = _mock_ctx(),
+ requirements_by_platform = {
+ "requirements_lock": "cp39_linux_*",
+ },
+ )
+ got_alternative = parse_requirements(
+ ctx = _mock_ctx(),
+ requirements_by_platform = {
+ "requirements_lock": "linux_*",
+ },
+ )
+ env.expect.that_dict(got).contains_exactly({
+ "foo": [
+ struct(
+ distribution = "foo",
+ download = False,
+ extra_pip_args = [],
+ requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef",
+ srcs = struct(
+ requirement = "foo[extra]==0.0.1",
+ shas = ["deadbeef"],
+ version = "0.0.1",
+ ),
+ target_platforms = [
+ "linux_aarch64",
+ "linux_arm",
+ "linux_ppc",
+ "linux_s390x",
+ "linux_x86_64",
+ ],
+ whls = [],
+ sdist = None,
+ ),
+ ],
+ })
+ env.expect.that_dict(got).contains_exactly(got_alternative)
+
+_tests.append(_test_platform_markers_with_python_version)
+
def _test_dupe_requirements(env):
got = parse_requirements(
ctx = _mock_ctx(),
@@ -136,6 +179,8 @@
"osx_x86_64",
"windows_x86_64",
],
+ whls = [],
+ sdist = None,
),
],
})
@@ -173,6 +218,8 @@
version = "0.0.1",
),
target_platforms = ["windows_x86_64"],
+ whls = [],
+ sdist = None,
),
],
"foo": [
@@ -195,6 +242,8 @@
"osx_aarch64",
"osx_x86_64",
],
+ whls = [],
+ sdist = None,
),
struct(
distribution = "foo",
@@ -207,6 +256,8 @@
version = "0.0.2",
),
target_platforms = ["windows_x86_64"],
+ whls = [],
+ sdist = None,
),
],
})
@@ -264,6 +315,8 @@
version = "0.0.3",
),
target_platforms = ["linux_x86_64"],
+ whls = [],
+ sdist = None,
),
],
})
@@ -316,6 +369,8 @@
version = "0.0.3",
),
target_platforms = ["linux_aarch64", "linux_x86_64"],
+ whls = [],
+ sdist = None,
),
struct(
distribution = "foo",
@@ -328,6 +383,8 @@
version = "",
),
target_platforms = ["linux_super_exotic"],
+ whls = [],
+ sdist = None,
),
struct(
distribution = "foo",
@@ -347,6 +404,8 @@
"osx_x86_64",
"windows_x86_64",
],
+ whls = [],
+ sdist = None,
),
],
})
@@ -365,6 +424,18 @@
_tests.append(_test_os_arch_requirements_with_default)
+def _test_fail_no_python_version(env):
+ errors = []
+ parse_requirements(
+ ctx = _mock_ctx(),
+ requirements_lock = "requirements_lock",
+ get_index_urls = lambda _, __: {},
+ fail_fn = errors.append,
+ )
+ env.expect.that_str(errors[0]).equals("'python_version' must be provided")
+
+_tests.append(_test_fail_no_python_version)
+
def parse_requirements_test_suite(name):
"""Create the test suite.
diff --git a/tests/private/whl_target_platforms/select_whl_tests.bzl b/tests/private/whl_target_platforms/select_whl_tests.bzl
index bed6d66..ebd2b26 100644
--- a/tests/private/whl_target_platforms/select_whl_tests.bzl
+++ b/tests/private/whl_target_platforms/select_whl_tests.bzl
@@ -15,108 +15,281 @@
""
load("@rules_testing//lib:test_suite.bzl", "test_suite")
-load("//python/private:whl_target_platforms.bzl", "select_whl") # buildifier: disable=bzl-visibility
+load("//python/private:whl_target_platforms.bzl", "select_whl", "select_whls") # buildifier: disable=bzl-visibility
WHL_LIST = [
- struct(
- filename = f,
- url = "https://" + f,
- sha256 = "sha256://" + f,
- )
- for f in [
- "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-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-py3-abi3-any.whl",
- "pkg-0.0.1-py3-none-any.whl",
- ]
+ "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-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",
]
-def _match(env, got, want_filename):
- if want_filename:
- env.expect.that_str(got.filename).equals(want_filename)
- env.expect.that_str(got.sha256).equals("sha256://" + want_filename)
- env.expect.that_str(got.url).equals("https://" + want_filename)
- else:
- env.expect.that_int(got).equals(None)
+def _match(env, got, *want_filenames):
+ if not want_filenames:
+ env.expect.that_collection(got).has_size(len(want_filenames))
+ return
+
+ got_filenames = [g.filename for g in got]
+ env.expect.that_collection(got_filenames).contains_exactly(want_filenames)
+
+ if got:
+ # Check that we pass the original structs
+ env.expect.that_str(got[0].other).equals("dummy")
+
+def _select_whl(**kwargs):
+ """A small wrapper to make the tests more DRY."""
+ got_single = select_whl(**kwargs)
+ return [got_single] if got_single else []
+
+def _select_whls(whls, **kwargs):
+ return select_whls(
+ whls = [
+ struct(
+ filename = f,
+ other = "dummy",
+ )
+ for f in whls
+ ],
+ **kwargs
+ )
_tests = []
-def _test_selecting(env):
- got = select_whl(whls = WHL_LIST, want_abis = ["none"], want_platform = "ignored")
- _match(env, got, "pkg-0.0.1-py3-none-any.whl")
+def _test_simplest(env):
+ got = _select_whls(
+ 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",
+ ],
+ want_abis = ["none"],
+ want_platforms = ["ignored"],
+ )
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-py3-none-any.whl",
+ )
- got = select_whl(whls = WHL_LIST, want_abis = ["abi3"], want_platform = "ignored")
- _match(env, got, "pkg-0.0.1-py3-abi3-any.whl")
+_tests.append(_test_simplest)
- # Check the selection failure
- got = select_whl(whls = WHL_LIST, want_abis = ["cp39"], want_platform = "fancy_exotic")
- _match(env, got, None)
+def _test_select_abi3(env):
+ got = _select_whls(
+ 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",
+ ],
+ want_abis = ["abi3"],
+ want_platforms = ["ignored"],
+ )
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-py3-abi3-any.whl",
+ )
+_tests.append(_test_select_abi3)
+
+def _test_select_by_supported_py_version(env):
+ for want_python_version, match in {
+ "3.11": "pkg-0.0.1-py311-abi3-any.whl",
+ "3.8": "pkg-0.0.1-py3-abi3-any.whl",
+ }.items():
+ got = _select_whls(
+ 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",
+ ],
+ want_abis = ["abi3"],
+ want_platforms = ["ignored"],
+ want_python_version = want_python_version,
+ )
+ _match(env, got, match)
+
+_tests.append(_test_select_by_supported_py_version)
+
+def _test_select_by_supported_cp_version(env):
+ for want_python_version, match in {
+ "3.11": "pkg-0.0.1-cp311-abi3-any.whl",
+ "3.8": "pkg-0.0.1-py3-abi3-any.whl",
+ }.items():
+ got = _select_whls(
+ 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",
+ ],
+ want_abis = ["abi3"],
+ want_platforms = ["ignored"],
+ want_python_version = want_python_version,
+ )
+ _match(env, got, match)
+
+_tests.append(_test_select_by_supported_cp_version)
+
+def _test_supported_cp_version_manylinux(env):
+ for want_python_version, match in {
+ "3.11": "pkg-0.0.1-cp311-none-manylinux_x86_64.whl",
+ "3.8": "pkg-0.0.1-py3-none-manylinux_x86_64.whl",
+ }.items():
+ got = _select_whls(
+ whls = [
+ "pkg-0.0.1-py2.py3-none-manylinux_x86_64.whl",
+ "pkg-0.0.1-py3-none-manylinux_x86_64.whl",
+ "pkg-0.0.1-py311-none-manylinux_x86_64.whl",
+ "pkg-0.0.1-cp311-none-manylinux_x86_64.whl",
+ ],
+ want_abis = ["none"],
+ want_platforms = ["linux_x86_64"],
+ want_python_version = want_python_version,
+ )
+ _match(env, got, match)
+
+_tests.append(_test_supported_cp_version_manylinux)
+
+def _test_ignore_unsupported(env):
+ got = _select_whls(
+ whls = [
+ "pkg-0.0.1-xx3-abi3-any.whl",
+ ],
+ want_abis = ["abi3"],
+ want_platforms = ["ignored"],
+ )
+ _match(env, got)
+
+_tests.append(_test_ignore_unsupported)
+
+def _test_match_abi_and_not_py_version(env):
# Check we match the ABI and not the py version
- got = select_whl(whls = WHL_LIST, want_abis = ["cp37m"], want_platform = "linux_x86_64")
+ got = _select_whls(whls = WHL_LIST, want_abis = ["cp37m"], want_platforms = ["linux_x86_64"], want_python_version = "3.7")
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ "pkg-0.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl",
+ )
+ got = _select_whl(whls = got, want_platform = "linux_x86_64")
_match(env, got, "pkg-0.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_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, want_abis = ["cp39"], want_platform = "linux_x86_32")
+ got = _select_whls(whls = WHL_LIST, want_abis = ["cp39"], want_platforms = ["linux_x86_32"], want_python_version = "3.9")
+ _match(
+ env,
+ got,
+ "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_i686.whl",
+ )
+ got = _select_whl(whls = got, want_platform = "linux_x86_32")
_match(env, got, "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_osx_prefer_arch_specific(env):
# Check that we prefer the specific wheel
- got = select_whl(whls = WHL_LIST, want_abis = ["cp311"], want_platform = "osx_x86_64")
+ got = _select_whls(
+ whls = WHL_LIST,
+ want_abis = ["cp311"],
+ want_platforms = ["osx_x86_64", "osx_x86_32"],
+ want_python_version = "3.11",
+ )
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-cp311-cp311-macosx_10_9_universal2.whl",
+ "pkg-0.0.1-cp311-cp311-macosx_10_9_x86_64.whl",
+ )
+ got = _select_whl(whls = got, want_platform = "osx_x86_64")
_match(env, got, "pkg-0.0.1-cp311-cp311-macosx_10_9_x86_64.whl")
- got = select_whl(whls = WHL_LIST, want_abis = ["cp311"], want_platform = "osx_aarch64")
+ got = _select_whls(whls = WHL_LIST, want_abis = ["cp311"], want_platforms = ["osx_aarch64"], want_python_version = "3.11")
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-cp311-cp311-macosx_10_9_universal2.whl",
+ "pkg-0.0.1-cp311-cp311-macosx_11_0_arm64.whl",
+ )
+ got = _select_whl(whls = got, want_platform = "osx_aarch64")
_match(env, got, "pkg-0.0.1-cp311-cp311-macosx_11_0_arm64.whl")
+_tests.append(_test_osx_prefer_arch_specific)
+
+def _test_osx_fallback_to_universal2(env):
# Check that we can use the universal2 if the arm wheel is not available
- got = select_whl(whls = [w for w in WHL_LIST if "arm64" not in w.filename], want_abis = ["cp311"], want_platform = "osx_aarch64")
+ got = _select_whls(whls = [w for w in WHL_LIST if "arm64" not in w], want_abis = ["cp311"], want_platforms = ["osx_aarch64"], want_python_version = "3.11")
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-cp311-cp311-macosx_10_9_universal2.whl",
+ )
+ got = _select_whl(whls = got, want_platform = "osx_aarch64")
_match(env, got, "pkg-0.0.1-cp311-cp311-macosx_10_9_universal2.whl")
+_tests.append(_test_osx_fallback_to_universal2)
+
+def _test_prefer_manylinux_wheels(env):
# Check we prefer platform specific wheels
- got = select_whl(whls = WHL_LIST, want_abis = ["none", "abi3", "cp39"], want_platform = "linux_x86_64")
+ got = _select_whls(whls = WHL_LIST, want_abis = ["none", "abi3", "cp39"], want_platforms = ["linux_x86_64"], want_python_version = "3.9")
+ _match(
+ env,
+ got,
+ "pkg-0.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ "pkg-0.0.1-cp39-cp39-musllinux_1_1_x86_64.whl",
+ "pkg-0.0.1-cp39-abi3-any.whl",
+ "pkg-0.0.1-py3-none-any.whl",
+ )
+ got = _select_whl(whls = got, want_platform = "linux_x86_64")
_match(env, got, "pkg-0.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
-_tests.append(_test_selecting)
+_tests.append(_test_prefer_manylinux_wheels)
def select_whl_test_suite(name):
"""Create the test suite.