diff --git a/MODULE.bazel b/MODULE.bazel index c649896344..b1c89b4f2d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -98,7 +98,7 @@ internal_dev_deps = use_extension( "internal_dev_deps", dev_dependency = True, ) -use_repo(internal_dev_deps, "buildkite_config", "wheel_for_testing") +use_repo(internal_dev_deps, "buildkite_config", "wheel_for_testing", "whl_metadata_parsing_tests") # Add gazelle plugin so that we can run the gazelle example as an e2e integration # test and include the distribution files. diff --git a/WORKSPACE b/WORKSPACE index 3ad83ca04b..a275bee32c 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -166,3 +166,10 @@ http_file( "https://files.pythonhosted.org/packages/50/67/3e966d99a07d60a21a21d7ec016e9e4c2642a86fea251ec68677daf71d4d/numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", ], ) + +load("//python/private/pypi:whl_metadata_repo.bzl", "whl_metadata_repo") # buildifier: disable=bzl-visibility + +whl_metadata_repo( + name = "whl_metadata_parsing_tests", + limit = 50, +) diff --git a/python/private/internal_dev_deps.bzl b/python/private/internal_dev_deps.bzl index 2a3b84e7df..d57654a6c6 100644 --- a/python/private/internal_dev_deps.bzl +++ b/python/private/internal_dev_deps.bzl @@ -15,6 +15,7 @@ load("@bazel_ci_rules//:rbe_repo.bzl", "rbe_preconfig") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") +load("//python/private/pypi:whl_metadata_repo.bzl", "whl_metadata_repo") def _internal_dev_deps_impl(mctx): _ = mctx # @unused @@ -38,6 +39,11 @@ def _internal_dev_deps_impl(mctx): toolchain = "ubuntu1804-bazel-java11", ) + whl_metadata_repo( + name = "whl_metadata_parsing_tests", + limit = 50, + ) + internal_dev_deps = module_extension( implementation = _internal_dev_deps_impl, doc = "This extension creates internal rules_python dev dependencies.", diff --git a/python/private/pypi/whl_installer/BUILD.bazel b/python/private/pypi/whl_installer/BUILD.bazel index 5fb617004d..92c46b8b06 100644 --- a/python/private/pypi/whl_installer/BUILD.bazel +++ b/python/private/pypi/whl_installer/BUILD.bazel @@ -8,6 +8,7 @@ py_library( "namespace_pkgs.py", "platform.py", "wheel.py", + "wheel_deps.py", "wheel_installer.py", ], visibility = [ diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py index 25003e6280..16a610db18 100644 --- a/python/private/pypi/whl_installer/wheel.py +++ b/python/private/pypi/whl_installer/wheel.py @@ -15,233 +15,14 @@ """Utility class to inspect an extracted wheel directory""" import email -import re -from collections import defaultdict -from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, Optional, Set, Tuple import installer -from packaging.requirements import Requirement from pip._vendor.packaging.utils import canonicalize_name -from python.private.pypi.whl_installer.platform import ( - Platform, - host_interpreter_version, -) - - -@dataclass(frozen=True) -class FrozenDeps: - deps: List[str] - deps_select: Dict[str, List[str]] - - -class Deps: - """Deps is a dependency builder that has a build() method to return FrozenDeps.""" - - def __init__( - self, - name: str, - requires_dist: List[str], - *, - extras: Optional[Set[str]] = None, - platforms: Optional[Set[Platform]] = None, - ): - """Create a new instance and parse the requires_dist - - Args: - name (str): The name of the whl distribution - requires_dist (list[Str]): The Requires-Dist from the METADATA of the whl - distribution. - extras (set[str], optional): The list of requested extras, defaults to None. - platforms (set[Platform], optional): The list of target platforms, defaults to - None. If the list of platforms has multiple `minor_version` values, it - will change the code to generate the select statements using - `@rules_python//python/config_settings:is_python_3.y` conditions. - """ - self.name: str = Deps._normalize(name) - self._platforms: Set[Platform] = platforms or set() - self._target_versions = { - (p.minor_version, p.micro_version) for p in platforms or {} - } - if platforms and len(self._target_versions) > 1: - # TODO @aignas 2024-06-23: enable this to be set via a CLI arg - # for being more explicit. - self._default_minor_version, _ = host_interpreter_version() - else: - self._default_minor_version = None - - if None in self._target_versions and len(self._target_versions) > 2: - raise ValueError( - f"all python versions need to be specified explicitly, got: {platforms}" - ) - - # Sort so that the dictionary order in the FrozenDeps is deterministic - # without the final sort because Python retains insertion order. That way - # the sorting by platform is limited within the Platform class itself and - # the unit-tests for the Deps can be simpler. - reqs = sorted( - (Requirement(wheel_req) for wheel_req in requires_dist), - key=lambda x: f"{x.name}:{sorted(x.extras)}", - ) - - want_extras = self._resolve_extras(reqs, extras) - - # Then add all of the requirements in order - self._deps: Set[str] = set() - self._select: Dict[Platform, Set[str]] = defaultdict(set) - - reqs_by_name = {} - for req in reqs: - reqs_by_name.setdefault(req.name, []).append(req) - - for req_name, reqs in reqs_by_name.items(): - self._add_req(req_name, reqs, want_extras) - - def _add(self, dep: str, platform: Optional[Platform]): - dep = Deps._normalize(dep) - - # Self-edges are processed in _resolve_extras - if dep == self.name: - return - - if not platform: - self._deps.add(dep) - - # If the dep is in the platform-specific list, remove it from the select. - pop_keys = [] - for p, deps in self._select.items(): - if dep not in deps: - continue - - deps.remove(dep) - if not deps: - pop_keys.append(p) - - for p in pop_keys: - self._select.pop(p) - return - - if dep in self._deps: - # If the dep is already in the main dependency list, no need to add it in the - # platform-specific dependency list. - return - - # Add the platform-specific dep - self._select[platform].add(dep) - - @staticmethod - def _normalize(name: str) -> str: - return re.sub(r"[-_.]+", "_", name).lower() - - def _resolve_extras( - self, reqs: List[Requirement], want_extras: Optional[Set[str]] - ) -> Set[str]: - """Resolve extras which are due to depending on self[some_other_extra]. - - Some packages may have cyclic dependencies resulting from extras being used, one example is - `etils`, where we have one set of extras as aliases for other extras - and we have an extra called 'all' that includes all other extras. - - Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. - - When the `requirements.txt` is generated by `pip-tools`, then it is likely that - this step is not needed, but for other `requirements.txt` files this may be useful. - - NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, - but in order for it to become platform dependent we would have to have - separate targets for each extra in extras. - """ - - # Resolve any extra extras due to self-edges, empty string means no - # extras The empty string in the set is just a way to make the handling - # of no extras and a single extra easier and having a set of {"", "foo"} - # is equivalent to having {"foo"}. - extras: Set[str] = want_extras or {""} - - self_reqs = [] - for req in reqs: - if Deps._normalize(req.name) != self.name: - continue - - if req.marker is None: - # I am pretty sure we cannot reach this code as it does not - # make sense to specify packages in this way, but since it is - # easy to handle, lets do it. - # - # TODO @aignas 2023-12-08: add a test - extras = extras | req.extras - else: - # process these in a separate loop - self_reqs.append(req) - - # A double loop is not strictly optimal, but always correct without recursion - for req in self_reqs: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - extras = extras | req.extras - else: - continue - - # Iterate through all packages to ensure that we include all of the extras from previously - # visited packages. - for req_ in self_reqs: - if any(req_.marker.evaluate({"extra": extra}) for extra in extras): - extras = extras | req_.extras - - return extras - - def _add_req(self, req_name, reqs: List[Requirement], extras: Set[str]) -> None: - platforms_to_add = set() - for req in reqs: - if req.marker is None: - self._add(req.name, None) - return - - if not self._platforms: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) - return - - for plat in self._platforms: - if plat in platforms_to_add: - # marker evaluation is more expensive than this check - continue - - added = False - for extra in extras: - if added: - break - - if req.marker.evaluate(plat.env_markers(extra)): - platforms_to_add.add(plat) - added = True - break - - if not self._platforms: - return - - if len(platforms_to_add) == len(self._platforms): - # the dep is in all target platforms, let's just add it to the regular - # list - self._add(req_name, None) - return - - for plat in platforms_to_add: - if self._default_minor_version is not None: - self._add(req_name, plat) - - if ( - self._default_minor_version is None - or plat.minor_version == self._default_minor_version - ): - self._add(req_name, Platform(os=plat.os, arch=plat.arch)) - - def build(self) -> FrozenDeps: - return FrozenDeps( - deps=sorted(self._deps), - deps_select={str(p): sorted(deps) for p, deps in self._select.items()}, - ) +from python.private.pypi.whl_installer.platform import Platform +from python.private.pypi.whl_installer.wheel_deps import Deps, FrozenDeps class Wheel: diff --git a/python/private/pypi/whl_installer/wheel_deps.py b/python/private/pypi/whl_installer/wheel_deps.py new file mode 100644 index 0000000000..7ede0feae4 --- /dev/null +++ b/python/private/pypi/whl_installer/wheel_deps.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 + +import re +from collections import defaultdict +from dataclasses import dataclass +from typing import Dict, List, Optional, Set + +from packaging.requirements import Requirement + +from python.private.pypi.whl_installer.platform import ( + Platform, + host_interpreter_version, +) + + +@dataclass(frozen=True) +class FrozenDeps: + deps: List[str] + deps_select: Dict[str, List[str]] + + +class Deps: + """Deps is a dependency builder that has a build() method to return FrozenDeps.""" + + def __init__( + self, + name: str, + requires_dist: List[str], + *, + extras: Optional[Set[str]] = None, + platforms: Optional[Set[Platform]] = None, + ): + """Create a new instance and parse the requires_dist + + Args: + name (str): The name of the whl distribution + requires_dist (list[Str]): The Requires-Dist from the METADATA of the whl + distribution. + extras (set[str], optional): The list of requested extras, defaults to None. + platforms (set[Platform], optional): The list of target platforms, defaults to + None. If the list of platforms has multiple `minor_version` values, it + will change the code to generate the select statements using + `@rules_python//python/config_settings:is_python_3.y` conditions. + """ + self.name: str = Deps._normalize(name) + self._platforms: Set[Platform] = platforms or set() + self._target_versions = { + (p.minor_version, p.micro_version) for p in platforms or {} + } + if platforms and len(self._target_versions) > 1: + # TODO @aignas 2024-06-23: enable this to be set via a CLI arg + # for being more explicit. + self._default_minor_version, _ = host_interpreter_version() + else: + self._default_minor_version = None + + if None in self._target_versions and len(self._target_versions) > 2: + raise ValueError( + f"all python versions need to be specified explicitly, got: {platforms}" + ) + + # Sort so that the dictionary order in the FrozenDeps is deterministic + # without the final sort because Python retains insertion order. That way + # the sorting by platform is limited within the Platform class itself and + # the unit-tests for the Deps can be simpler. + reqs = sorted( + (Requirement(wheel_req) for wheel_req in requires_dist), + key=lambda x: f"{x.name}:{sorted(x.extras)}", + ) + + want_extras = self._resolve_extras(reqs, extras) + + # Then add all of the requirements in order + self._deps: Set[str] = set() + self._select: Dict[Platform, Set[str]] = defaultdict(set) + + reqs_by_name = {} + for req in reqs: + reqs_by_name.setdefault(req.name, []).append(req) + + for req_name, reqs in reqs_by_name.items(): + self._add_req(req_name, reqs, want_extras) + + def _add(self, dep: str, platform: Optional[Platform]): + dep = Deps._normalize(dep) + + # Self-edges are processed in _resolve_extras + if dep == self.name: + return + + if not platform: + self._deps.add(dep) + + # If the dep is in the platform-specific list, remove it from the select. + pop_keys = [] + for p, deps in self._select.items(): + if dep not in deps: + continue + + deps.remove(dep) + if not deps: + pop_keys.append(p) + + for p in pop_keys: + self._select.pop(p) + return + + if dep in self._deps: + # If the dep is already in the main dependency list, no need to add it in the + # platform-specific dependency list. + return + + # Add the platform-specific dep + self._select[platform].add(dep) + + @staticmethod + def _normalize(name: str) -> str: + return re.sub(r"[-_.]+", "_", name).lower() + + def _resolve_extras( + self, reqs: List[Requirement], want_extras: Optional[Set[str]] + ) -> Set[str]: + """Resolve extras which are due to depending on self[some_other_extra]. + + Some packages may have cyclic dependencies resulting from extras being used, one example is + `etils`, where we have one set of extras as aliases for other extras + and we have an extra called 'all' that includes all other extras. + + Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. + + When the `requirements.txt` is generated by `pip-tools`, then it is likely that + this step is not needed, but for other `requirements.txt` files this may be useful. + + NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, + but in order for it to become platform dependent we would have to have + separate targets for each extra in extras. + """ + + # Resolve any extra extras due to self-edges, empty string means no + # extras The empty string in the set is just a way to make the handling + # of no extras and a single extra easier and having a set of {"", "foo"} + # is equivalent to having {"foo"}. + extras: Set[str] = want_extras or {""} + + self_reqs = [] + for req in reqs: + if Deps._normalize(req.name) != self.name: + continue + + if req.marker is None: + # I am pretty sure we cannot reach this code as it does not + # make sense to specify packages in this way, but since it is + # easy to handle, lets do it. + # + # TODO @aignas 2023-12-08: add a test + extras = extras | req.extras + else: + # process these in a separate loop + self_reqs.append(req) + + # A double loop is not strictly optimal, but always correct without recursion + for req in self_reqs: + if any(req.marker.evaluate({"extra": extra}) for extra in extras): + extras = extras | req.extras + else: + continue + + # Iterate through all packages to ensure that we include all of the extras from previously + # visited packages. + for req_ in self_reqs: + if any(req_.marker.evaluate({"extra": extra}) for extra in extras): + extras = extras | req_.extras + + return extras + + def _add_req(self, req_name, reqs: List[Requirement], extras: Set[str]) -> None: + platforms_to_add = set() + for req in reqs: + if req.marker is None: + self._add(req.name, None) + return + + if not self._platforms: + if any(req.marker.evaluate({"extra": extra}) for extra in extras): + self._add(req.name, None) + return + + for plat in self._platforms: + if plat in platforms_to_add: + # marker evaluation is more expensive than this check + continue + + added = False + for extra in extras: + if added: + break + + if req.marker.evaluate(plat.env_markers(extra)): + platforms_to_add.add(plat) + added = True + break + + if not self._platforms: + return + + if len(platforms_to_add) == len(self._platforms): + # the dep is in all target platforms, let's just add it to the regular + # list + self._add(req_name, None) + return + + for plat in platforms_to_add: + if self._default_minor_version is not None: + self._add(req_name, plat) + + if ( + self._default_minor_version is None + or plat.minor_version == self._default_minor_version + ): + self._add(req_name, Platform(os=plat.os, arch=plat.arch)) + + def build(self) -> FrozenDeps: + return FrozenDeps( + deps=sorted(self._deps), + deps_select={str(p): sorted(deps) for p, deps in self._select.items()}, + ) + + +if __name__ == "__main__": + import json + import sys + from pathlib import Path + + from packaging.metadata import Metadata + + current_version = ( + f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}" + ) + packages = Path(sys.argv[1]).read_text().split("\n") + output = { + "version": current_version, + "deps": {}, + } + for pkg in packages: + metadata_contents = Path(f"{pkg}.METADATA").read_text() + metadata = Metadata.from_email(metadata_contents, validate=False) + deps = Deps( + name=metadata.name, + requires_dist=[str(r) for r in metadata.requires_dist or []], + ).build() + output["deps"][metadata.name] = deps.deps + + with Path(sys.argv[1] + ".out").open(mode="w") as f: + json.dump(output, f, indent=" ", sort_keys=True) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 0c09f7960a..8236e510f4 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -521,6 +521,7 @@ attr makes `extra_pip_args` and `download_only` ignored.""", default = [ Label("//python/private/pypi/whl_installer:platform.py"), Label("//python/private/pypi/whl_installer:wheel.py"), + Label("//python/private/pypi/whl_installer:wheel_deps.py"), Label("//python/private/pypi/whl_installer:wheel_installer.py"), Label("//python/private/pypi/whl_installer:arguments.py"), Label("//python/private/pypi/whl_installer:namespace_pkgs.py"), diff --git a/python/private/pypi/whl_metadata_repo.bzl b/python/private/pypi/whl_metadata_repo.bzl new file mode 100644 index 0000000000..d60ea2c4eb --- /dev/null +++ b/python/private/pypi/whl_metadata_repo.bzl @@ -0,0 +1,159 @@ +""" +This is a repo rule for downloading lots of METADATA files and then comparing them. +""" + +load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS") +load("//python/private:repo_utils.bzl", "repo_utils") +load("//python/private:text_util.bzl", "render") +load(":deps.bzl", "record_files") +load(":parse_requirements.bzl", "host_platform") +load(":pypi_repo_utils.bzl", "pypi_repo_utils") +load(":simpleapi_download.bzl", "simpleapi_download") +load(":whl_metadata.bzl", "parse_whl_metadata") + +# Used as a default value in a rule to ensure we fetch the dependencies. +PY_SRCS = [ + # When the version, or any of the files in `packaging` package changes, + # this file will change as well. + record_files["pypi__packaging"], + Label("//python/private/pypi/whl_installer:platform.py"), + Label("//python/private/pypi/whl_installer:wheel_deps.py"), +] + +def _impl(rctx): + logger = repo_utils.logger(rctx) + result = rctx.download(url = [rctx.attr.stats_url], output = "stats.json") + if not result.success: + fail(result) + + stats = json.decode(rctx.read("stats.json")) + packages = [k["project"] for k in stats["rows"][:rctx.attr.limit]] + + data = simpleapi_download( + rctx, + attr = struct( + index_url = rctx.attr.index_url, + index_url_overrides = {}, + extra_index_urls = [], + sources = packages, + envsubst = [], + netrc = None, + auth_patterns = None, + ), + cache = {}, + ) + metadata_files = {} + for pkg, d in data.items(): + last_version = d.sha256_by_version.keys()[-1] + shas = d.sha256_by_version[last_version] + whls = [ + d.whls[sha] + for sha in shas + if sha in d.whls + ] + if not whls: + logger.warn("{} does not have any wheels, skipping".format(pkg)) + continue + + whl = whls[0] + metadata_files[pkg] = ( + whl.metadata_url, + whl.metadata_sha256, + ) + + downloads = { + pkg + ".METADATA": rctx.download( + url = [url], + output = pkg + ".METADATA", + sha256 = sha256, + block = False, + ) + for pkg, (url, sha256) in metadata_files.items() + } + + rctx.file("BUILD.bazel", "") + rctx.file("REPO.bazel", "") + + # TODO @aignas 2025-05-02: Change the algorithm to first: + # Run a single execution of Python for each version where in one go we parse all of the METADATA files + # + # Then in a second loop we do the same for starlark whilst passing the python versions that we got. + METADATA = {} + for fname, d in downloads.items(): + result = d.wait() + if not result.success: + fail(fname) + + contents = rctx.read(fname) + parsed = parse_whl_metadata(contents) + + METADATA[fname[:-len(".METADATA")]] = { + "provides_extra": parsed.provides_extra, + "requires_dist": parsed.requires_dist, + } + + rctx.file("packages.txt", "\n".join(METADATA.keys())) + + py_parsed_deps = {} + for py in rctx.attr.interpreters: + output = pypi_repo_utils.execute_checked( + rctx, + op = "ParseDeps({})".format(py), + python = pypi_repo_utils.resolve_python_interpreter( + rctx, + python_interpreter = None, + python_interpreter_target = py, + ), + arguments = [ + "-m", + "python.private.pypi.whl_installer.wheel_deps", + "packages.txt", + ], + srcs = PY_SRCS, + environment = { + "PYTHONPATH": [ + Label("@pypi__packaging//:BUILD.bazel"), + Label("//:BUILD.bazel"), + ], + }, + logger = logger, + ) + if output.return_code != 0: + # We have failed + fail(output) + + decoded = json.decode(rctx.read("packages.txt.out")) + python_version = decoded["version"] + + for name, deps in decoded["deps"].items(): + py_parsed_deps.setdefault(name, {})[python_version] = deps + + def _render_dict_of_dicts(outer): + return render.dict( + { + k: render.dict(inner) + for k, inner in outer.items() + }, + value_repr = str, + ) + + defs_bzl = [ + "{} = {}".format(k, v) + for k, v in { + "HOST_PLATFORM": repr(host_platform(rctx)), + "METADATA": _render_dict_of_dicts(METADATA), + "WANT": _render_dict_of_dicts(py_parsed_deps), + }.items() + ] + + rctx.file("defs.bzl", "\n\n".join(defs_bzl)) + +whl_metadata_repo = repository_rule( + implementation = _impl, + attrs = { + "index_url": attr.string(default = "https://pypi.org/simple"), + "interpreters": attr.label_list(default = INTERPRETER_LABELS.values()), + "limit": attr.int(default = 5000), + "stats_url": attr.string(default = "https://hugovk.github.io/top-pypi-packages/top-pypi-packages.min.json"), + }, +) diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl index a64b5d6243..cf85ae4e3a 100644 --- a/python/private/text_util.bzl +++ b/python/private/text_util.bzl @@ -76,13 +76,14 @@ def _render_select(selects, *, no_match_error = None, key_repr = repr, value_rep return "{}({})".format(name, args) -def _render_list(items, *, hanging_indent = ""): +def _render_list(items, *, hanging_indent = "", repr = repr): """Convert a list to formatted text. Args: items: list of items. hanging_indent: str, indent to apply to second and following lines of the formatted text. + repr: the repr function to use. Returns: The list pretty formatted as a string. diff --git a/tests/pypi/pep508/BUILD.bazel b/tests/pypi/pep508/BUILD.bazel index 7eab2e096a..519f4a4cce 100644 --- a/tests/pypi/pep508/BUILD.bazel +++ b/tests/pypi/pep508/BUILD.bazel @@ -1,3 +1,4 @@ +load(":deps_integration_tests.bzl", "deps_integration_test_suite") load(":deps_tests.bzl", "deps_test_suite") load(":evaluate_tests.bzl", "evaluate_test_suite") load(":requirement_tests.bzl", "requirement_test_suite") @@ -6,6 +7,10 @@ deps_test_suite( name = "deps_tests", ) +deps_integration_test_suite( + name = "deps_integration_tests", +) + evaluate_test_suite( name = "evaluate_tests", ) diff --git a/tests/pypi/pep508/deps_integration_tests.bzl b/tests/pypi/pep508/deps_integration_tests.bzl new file mode 100644 index 0000000000..38ab62e481 --- /dev/null +++ b/tests/pypi/pep508/deps_integration_tests.bzl @@ -0,0 +1,33 @@ +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@whl_metadata_parsing_tests//:defs.bzl", "HOST_PLATFORM", "METADATA", "WANT") +load("//python/private:normalize_name.bzl", "normalize_name") # buildifier: disable=bzl-visibility +load("//python/private/pypi:pep508_deps.bzl", "deps") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_compare_python_star_implementation(env): + target_platforms = [HOST_PLATFORM] + + for pkg, want_deps_by_version in WANT.items(): + name = normalize_name(pkg) + metadata = METADATA[name] + for python_version, want_deps in want_deps_by_version.items(): + got = deps( + name = name, + requires_dist = metadata["requires_dist"], + platforms = target_platforms, + excludes = [], + extras = [], + default_python_version = python_version, + ).deps + env.expect.that_collection(got).contains_exactly(want_deps) + +_tests.append(_test_compare_python_star_implementation) + +def deps_integration_test_suite(name): # buildifier: disable=function-docstring + test_suite( + name = name, + basic_tests = _tests, + ) diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py index 3599fd1868..aca4abc8b1 100644 --- a/tests/pypi/whl_installer/wheel_test.py +++ b/tests/pypi/whl_installer/wheel_test.py @@ -5,7 +5,7 @@ from python.private.pypi.whl_installer.platform import OS, Arch, Platform _HOST_INTERPRETER_FN = ( - "python.private.pypi.whl_installer.wheel.host_interpreter_version" + "python.private.pypi.whl_installer.wheel_deps.host_interpreter_version" )