refactor(pypi): use a macro to define whl_library targets (#2347)

Summary:
- refactor: Start implementing whl_library_targets
- refactor: start using whl_library_targets macro
- refactor: generate config settings in the new macro
- refactor: copy_files in the new macro
- refactor: move entry_point generation to the macro
- refactor: move the py_library and whl generation to the new macro

This makes the code more maintainable by reducing the amount of tests
that are comparing BUILD.bazel outputs.
diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock
index 286e8c0..c41380c 100644
--- a/examples/bzlmod/MODULE.bazel.lock
+++ b/examples/bzlmod/MODULE.bazel.lock
@@ -1392,7 +1392,7 @@
     },
     "@@rules_python~//python/extensions:pip.bzl%pip": {
       "general": {
-        "bzlTransitiveDigest": "KZzbwT5y7SPbM+MgbQWr309EUGjGXvXvQ4/FMn+fEGE=",
+        "bzlTransitiveDigest": "g9NnJTZcM2BjPelxHHLy0ZyhFd+8XAb86u9OvNIOhFo=",
         "usagesDigest": "MChlcSw99EuW3K7OOoMcXQIdcJnEh6YmfyjJm+9mxIg=",
         "recordedFileInputs": {
           "@@other_module~//requirements_lock_3_11.txt": "a7d0061366569043d5efcf80e34a32c732679367cb3c831c4cdc606adc36d314",
@@ -6299,7 +6299,7 @@
     },
     "@@rules_python~//python/private/pypi:pip.bzl%pip_internal": {
       "general": {
-        "bzlTransitiveDigest": "mzsyVW4M380vwEPTn/pDXFMh5gtTHsv0sbqZCE7a1SY=",
+        "bzlTransitiveDigest": "ctc7nzMsQfNG16wSXLqbix2k99rf614qJRwcd/2RxGI=",
         "usagesDigest": "LYtSAPzhPjmfD9vF39mCED1UQSvHEo2Hv+aK5Z4ZWWc=",
         "recordedFileInputs": {
           "@@rules_python~//tools/publish/requirements_linux.txt": "8175b4c8df50ae2f22d1706961884beeb54e7da27bd2447018314a175981997d",
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index e76f9d3..9be355c 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -110,8 +110,7 @@
     name = "generate_whl_library_build_bazel_bzl",
     srcs = ["generate_whl_library_build_bazel.bzl"],
     deps = [
-        ":labels_bzl",
-        "//python/private:normalize_name_bzl",
+        "//python/private:text_util_bzl",
     ],
 )
 
diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl
index 934fa00..8050cd2 100644
--- a/python/private/pypi/generate_whl_library_build_bazel.bzl
+++ b/python/private/pypi/generate_whl_library_build_bazel.bzl
@@ -14,406 +14,69 @@
 
 """Generate the BUILD.bazel contents for a repo defined by a whl_library."""
 
-load("//python/private:normalize_name.bzl", "normalize_name")
 load("//python/private:text_util.bzl", "render")
-load(
-    ":labels.bzl",
-    "DATA_LABEL",
-    "DIST_INFO_LABEL",
-    "PY_LIBRARY_IMPL_LABEL",
-    "PY_LIBRARY_PUBLIC_LABEL",
-    "WHEEL_ENTRY_POINT_PREFIX",
-    "WHEEL_FILE_IMPL_LABEL",
-    "WHEEL_FILE_PUBLIC_LABEL",
-)
 
-_COPY_FILE_TEMPLATE = """\
-copy_file(
-    name = "{dest}.copy",
-    src = "{src}",
-    out = "{dest}",
-    is_executable = {is_executable},
-)
-"""
+_RENDER = {
+    "copy_executables": render.dict,
+    "copy_files": render.dict,
+    "data": render.list,
+    "data_exclude": render.list,
+    "dependencies": render.list,
+    "dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list),
+    "entry_points": render.dict,
+    "group_deps": render.list,
+    "srcs_exclude": render.list,
+    "tags": render.list,
+}
 
-_ENTRY_POINT_RULE_TEMPLATE = """\
-py_binary(
-    name = "{name}",
-    srcs = ["{src}"],
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["."],
-    deps = ["{pkg}"],
-)
-"""
-
-_BUILD_TEMPLATE = """\
-{loads}
+# NOTE @aignas 2024-10-25: We have to keep this so that files in
+# this repository can be publicly visible without the need for
+# export_files
+_TEMPLATE = """\
+load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets")
 
 package(default_visibility = ["//visibility:public"])
 
-filegroup(
-    name = "{dist_info_label}",
-    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "{data_label}",
-    srcs = glob(["data/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "{whl_file_label}",
-    srcs = ["{whl_name}"],
-    data = {whl_file_deps},
-    visibility = {impl_vis},
-)
-
-py_library(
-    name = "{py_library_label}",
-    srcs = glob(
-        ["site-packages/**/*.py"],
-        exclude={srcs_exclude},
-        # Empty sources are allowed to support wheels that don't have any
-        # pure-Python code, e.g. pymssql, which is written in Cython.
-        allow_empty = True,
-    ),
-    data = {data} + glob(
-        ["site-packages/**/*"],
-        exclude={data_exclude},
-    ),
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["site-packages"],
-    deps = {dependencies},
-    tags = {tags},
-    visibility = {impl_vis},
+whl_library_targets(
+{kwargs}
 )
 """
 
-def _plat_label(plat):
-    if plat.endswith("default"):
-        return plat
-    if plat.startswith("@//"):
-        return "@@" + str(Label("//:BUILD.bazel")).partition("//")[0].strip("@") + plat.strip("@")
-    elif plat.startswith("@"):
-        return str(Label(plat))
-    else:
-        return ":is_" + plat.replace("cp3", "python_3.")
-
-def _render_list_and_select(deps, deps_by_platform, tmpl):
-    deps = render.list([tmpl.format(d) for d in sorted(deps)])
-
-    if not deps_by_platform:
-        return deps
-
-    deps_by_platform = {
-        _plat_label(p): [
-            tmpl.format(d)
-            for d in sorted(deps)
-        ]
-        for p, deps in sorted(deps_by_platform.items())
-    }
-
-    # Add the default, which means that we will be just using the dependencies in
-    # `deps` for platforms that are not handled in a special way by the packages
-    deps_by_platform.setdefault("//conditions:default", [])
-    deps_by_platform = render.select(deps_by_platform, value_repr = render.list)
-
-    if deps == "[]":
-        return deps_by_platform
-    else:
-        return "{} + {}".format(deps, deps_by_platform)
-
-def _render_config_settings(dependencies_by_platform):
-    loads = []
-    additional_content = []
-    for p in dependencies_by_platform:
-        # p can be one of the following formats:
-        # * //conditions:default
-        # * @platforms//os:{value}
-        # * @platforms//cpu:{value}
-        # * @//python/config_settings:is_python_3.{minor_version}
-        # * {os}_{cpu}
-        # * cp3{minor_version}_{os}_{cpu}
-        if p.startswith("@") or p.endswith("default"):
-            continue
-
-        abi, _, tail = p.partition("_")
-        if not abi.startswith("cp"):
-            tail = p
-            abi = ""
-
-        os, _, arch = tail.partition("_")
-        os = "" if os == "anyos" else os
-        arch = "" if arch == "anyarch" else arch
-
-        constraint_values = []
-        if arch:
-            constraint_values.append("@platforms//cpu:{}".format(arch))
-        if os:
-            constraint_values.append("@platforms//os:{}".format(os))
-
-        constraint_values_str = render.indent(render.list(constraint_values)).lstrip()
-
-        if abi:
-            additional_content.append(
-                """\
-config_setting(
-    name = "is_{name}",
-    flag_values = {{
-        "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}",
-    }},
-    constraint_values = {constraint_values},
-    visibility = ["//visibility:private"],
-)""".format(
-                    name = p.replace("cp3", "python_3."),
-                    minor_version = abi[len("cp3"):],
-                    constraint_values = constraint_values_str,
-                ),
-            )
-        else:
-            additional_content.append(
-                """\
-config_setting(
-    name = "is_{name}",
-    constraint_values = {constraint_values},
-    visibility = ["//visibility:private"],
-)""".format(
-                    name = p.replace("cp3", "python_3."),
-                    constraint_values = constraint_values_str,
-                ),
-            )
-
-    return loads, "\n\n".join(additional_content)
-
 def generate_whl_library_build_bazel(
         *,
-        dep_template,
-        whl_name,
-        dependencies,
-        dependencies_by_platform,
-        data_exclude,
-        tags,
-        entry_points,
         annotation = None,
-        group_name = None,
-        group_deps = []):
+        **kwargs):
     """Generate a BUILD file for an unzipped Wheel
 
     Args:
-        dep_template: the dependency template that should be used for dependency lists.
-        whl_name: the whl_name that this is generated for.
-        dependencies: a list of PyPI packages that are dependencies to the py_library.
-        dependencies_by_platform: a dict[str, list] of PyPI packages that may vary by platform.
-        data_exclude: more patterns to exclude from the data attribute of generated py_library rules.
-        tags: list of tags to apply to generated py_library rules.
-        entry_points: A dict of entry points to add py_binary rules for.
         annotation: The annotation for the build file.
-        group_name: Optional[str]; name of the dependency group (if any) which contains this library.
-          If set, this library will behave as a shim to group implementation rules which will provide
-          simultaneously installed dependencies which would otherwise form a cycle.
-        group_deps: List[str]; names of fellow members of the group (if any). These will be excluded
-          from generated deps lists so as to avoid direct cycles. These dependencies will be provided
-          at runtime by the group rules which wrap this library and its fellows together.
+        **kwargs: Extra args serialized to be passed to the
+            {obj}`whl_library_targets`.
 
     Returns:
         A complete BUILD file as a string
     """
 
     additional_content = []
-    data = []
-    srcs_exclude = []
-    data_exclude = [] + data_exclude
-    dependencies = sorted([normalize_name(d) for d in dependencies])
-    dependencies_by_platform = {
-        platform: sorted([normalize_name(d) for d in deps])
-        for platform, deps in dependencies_by_platform.items()
-    }
-    tags = sorted(tags)
-
-    for entry_point, entry_point_script_name in entry_points.items():
-        additional_content.append(
-            _generate_entry_point_rule(
-                name = "{}_{}".format(WHEEL_ENTRY_POINT_PREFIX, entry_point),
-                script = entry_point_script_name,
-                pkg = ":" + PY_LIBRARY_PUBLIC_LABEL,
-            ),
-        )
-
     if annotation:
-        for src, dest in annotation.copy_files.items():
-            data.append(dest)
-            additional_content.append(_generate_copy_commands(src, dest))
-        for src, dest in annotation.copy_executables.items():
-            data.append(dest)
-            additional_content.append(
-                _generate_copy_commands(src, dest, is_executable = True),
-            )
-        data.extend(annotation.data)
-        data_exclude.extend(annotation.data_exclude_glob)
-        srcs_exclude.extend(annotation.srcs_exclude_glob)
+        kwargs["data"] = annotation.data
+        kwargs["copy_files"] = annotation.copy_files
+        kwargs["copy_executables"] = annotation.copy_executables
+        kwargs["data_exclude"] = kwargs.get("data_exclude", []) + annotation.data_exclude_glob
+        kwargs["srcs_exclude"] = annotation.srcs_exclude_glob
         if annotation.additive_build_content:
             additional_content.append(annotation.additive_build_content)
 
-    _data_exclude = [
-        "**/* *",
-        "**/*.py",
-        "**/*.pyc",
-        "**/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNNN are created
-        # RECORD is known to contain sha256 checksums of files which might include the checksums
-        # of generated files produced when wheels are installed. The file is ignored to avoid
-        # Bazel caching issues.
-        "**/*.dist-info/RECORD",
-    ]
-    for item in data_exclude:
-        if item not in _data_exclude:
-            _data_exclude.append(item)
-
-    # Ensure this list is normalized
-    # Note: mapping used as set
-    group_deps = {
-        normalize_name(d): True
-        for d in group_deps
-    }
-
-    dependencies = [
-        d
-        for d in dependencies
-        if d not in group_deps
-    ]
-    dependencies_by_platform = {
-        p: deps
-        for p, deps in dependencies_by_platform.items()
-        for deps in [[d for d in deps if d not in group_deps]]
-        if deps
-    }
-
-    loads = [
-        """load("@rules_python//python:defs.bzl", "py_library", "py_binary")""",
-        """load("@bazel_skylib//rules:copy_file.bzl", "copy_file")""",
-    ]
-
-    loads_, config_settings_content = _render_config_settings(dependencies_by_platform)
-    if config_settings_content:
-        for line in loads_:
-            if line not in loads:
-                loads.append(line)
-        additional_content.append(config_settings_content)
-
-    lib_dependencies = _render_list_and_select(
-        deps = dependencies,
-        deps_by_platform = dependencies_by_platform,
-        tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL),
-    )
-
-    whl_file_deps = _render_list_and_select(
-        deps = dependencies,
-        deps_by_platform = dependencies_by_platform,
-        tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL),
-    )
-
-    # If this library is a member of a group, its public label aliases need to
-    # point to the group implementation rule not the implementation rules. We
-    # also need to mark the implementation rules as visible to the group
-    # implementation.
-    if group_name and "//:" in dep_template:
-        # This is the legacy behaviour where the group library is outside the hub repo
-        label_tmpl = dep_template.format(
-            name = "_groups",
-            target = normalize_name(group_name) + "_{}",
-        )
-        impl_vis = [dep_template.format(
-            name = "_groups",
-            target = "__pkg__",
-        )]
-        additional_content.extend([
-            "",
-            render.alias(
-                name = PY_LIBRARY_PUBLIC_LABEL,
-                actual = repr(label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL)),
-            ),
-            "",
-            render.alias(
-                name = WHEEL_FILE_PUBLIC_LABEL,
-                actual = repr(label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL)),
-            ),
-        ])
-        py_library_label = PY_LIBRARY_IMPL_LABEL
-        whl_file_label = WHEEL_FILE_IMPL_LABEL
-
-    elif group_name:
-        py_library_label = PY_LIBRARY_PUBLIC_LABEL
-        whl_file_label = WHEEL_FILE_PUBLIC_LABEL
-        impl_vis = [dep_template.format(name = "", target = "__subpackages__")]
-
-    else:
-        py_library_label = PY_LIBRARY_PUBLIC_LABEL
-        whl_file_label = WHEEL_FILE_PUBLIC_LABEL
-        impl_vis = ["//visibility:public"]
-
     contents = "\n".join(
         [
-            _BUILD_TEMPLATE.format(
-                loads = "\n".join(sorted(loads)),
-                py_library_label = py_library_label,
-                dependencies = render.indent(lib_dependencies, " " * 4).lstrip(),
-                whl_file_deps = render.indent(whl_file_deps, " " * 4).lstrip(),
-                data_exclude = repr(_data_exclude),
-                whl_name = whl_name,
-                whl_file_label = whl_file_label,
-                tags = repr(tags),
-                data_label = DATA_LABEL,
-                dist_info_label = DIST_INFO_LABEL,
-                entry_point_prefix = WHEEL_ENTRY_POINT_PREFIX,
-                srcs_exclude = repr(srcs_exclude),
-                data = repr(data),
-                impl_vis = repr(impl_vis),
+            _TEMPLATE.format(
+                kwargs = render.indent("\n".join([
+                    "{} = {},".format(k, _RENDER.get(k, repr)(v))
+                    for k, v in sorted(kwargs.items())
+                ])),
             ),
         ] + additional_content,
     )
 
     # NOTE: Ensure that we terminate with a new line
     return contents.rstrip() + "\n"
-
-def _generate_copy_commands(src, dest, is_executable = False):
-    """Generate a [@bazel_skylib//rules:copy_file.bzl%copy_file][cf] target
-
-    [cf]: https://github.com/bazelbuild/bazel-skylib/blob/1.1.1/docs/copy_file_doc.md
-
-    Args:
-        src (str): The label for the `src` attribute of [copy_file][cf]
-        dest (str): The label for the `out` attribute of [copy_file][cf]
-        is_executable (bool, optional): Whether or not the file being copied is executable.
-            sets `is_executable` for [copy_file][cf]
-
-    Returns:
-        str: A `copy_file` instantiation.
-    """
-    return _COPY_FILE_TEMPLATE.format(
-        src = src,
-        dest = dest,
-        is_executable = is_executable,
-    )
-
-def _generate_entry_point_rule(*, name, script, pkg):
-    """Generate a Bazel `py_binary` rule for an entry point script.
-
-    Note that the script is used to determine the name of the target. The name of
-    entry point targets should be uniuqe to avoid conflicts with existing sources or
-    directories within a wheel.
-
-    Args:
-        name (str): The name of the generated py_binary.
-        script (str): The path to the entry point's python file.
-        pkg (str): The package owning the entry point. This is expected to
-            match up with the `py_library` defined for each repository.
-
-    Returns:
-        str: A `py_binary` instantiation.
-    """
-    return _ENTRY_POINT_RULE_TEMPLATE.format(
-        name = name,
-        src = script.replace("\\", "/"),
-        pkg = pkg,
-    )
diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl
index 82fe072..62c0c6d 100644
--- a/python/private/pypi/whl_library.bzl
+++ b/python/private/pypi/whl_library.bzl
@@ -332,8 +332,8 @@
         entry_points[entry_point_without_py] = entry_point_script_name
 
     build_file_contents = generate_whl_library_build_bazel(
+        name = whl_path.basename,
         dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix),
-        whl_name = whl_path.basename,
         dependencies = metadata["deps"],
         dependencies_by_platform = metadata["deps_by_platform"],
         group_name = rctx.attr.group_name,
diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl
new file mode 100644
index 0000000..1798b9d
--- /dev/null
+++ b/python/private/pypi/whl_library_targets.bzl
@@ -0,0 +1,343 @@
+# 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.
+
+"""Macro to generate all of the targets present in a {obj}`whl_library`."""
+
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+load("//python:py_binary.bzl", "py_binary")
+load("//python:py_library.bzl", "py_library")
+load("//python/private:normalize_name.bzl", "normalize_name")
+load(
+    ":labels.bzl",
+    "DATA_LABEL",
+    "DIST_INFO_LABEL",
+    "PY_LIBRARY_IMPL_LABEL",
+    "PY_LIBRARY_PUBLIC_LABEL",
+    "WHEEL_ENTRY_POINT_PREFIX",
+    "WHEEL_FILE_IMPL_LABEL",
+    "WHEEL_FILE_PUBLIC_LABEL",
+)
+
+def whl_library_targets(
+        *,
+        name,
+        dep_template,
+        data_exclude = [],
+        srcs_exclude = [],
+        tags = [],
+        filegroups = {
+            DIST_INFO_LABEL: ["site-packages/*.dist-info/**"],
+            DATA_LABEL: ["data/**"],
+        },
+        dependencies = [],
+        dependencies_by_platform = {},
+        group_deps = [],
+        group_name = "",
+        data = [],
+        copy_files = {},
+        copy_executables = {},
+        entry_points = {},
+        native = native,
+        rules = struct(
+            copy_file = copy_file,
+            py_binary = py_binary,
+            py_library = py_library,
+        )):
+    """Create all of the whl_library targets.
+
+    Args:
+        name: {type}`str` The file to match for including it into the `whl`
+            filegroup. This may be also parsed to generate extra metadata.
+        dep_template: {type}`str` The dep_template to use for dependency
+            interpolation.
+        tags: {type}`list[str]` The tags set on the `py_library`.
+        dependencies: {type}`list[str]` A list of dependencies.
+        dependencies_by_platform: {type}`dict[str, list[str]]` A list of
+            dependencies by platform key.
+        filegroups: {type}`dict[str, list[str]]` A dictionary of the target
+            names and the glob matches.
+        group_name: {type}`str` name of the dependency group (if any) which
+            contains this library. If set, this library will behave as a shim
+            to group implementation rules which will provide simultaneously
+            installed dependencies which would otherwise form a cycle.
+        group_deps: {type}`list[str]` names of fellow members of the group (if
+            any). These will be excluded from generated deps lists so as to avoid
+            direct cycles. These dependencies will be provided at runtime by the
+            group rules which wrap this library and its fellows together.
+        copy_executables: {type}`dict[str, str]` The mapping between src and
+            dest locations for the targets.
+        copy_files: {type}`dict[str, str]` The mapping between src and
+            dest locations for the targets.
+        data_exclude: {type}`list[str]` The globs for data attribute exclusion
+            in `py_library`.
+        srcs_exclude: {type}`list[str]` The globs for srcs attribute exclusion
+            in `py_library`.
+        data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`.
+        entry_points: {type}`dict[str, str]` The mapping between the script
+            name and the python file to use. DEPRECATED.
+        native: {type}`native` The native struct for overriding in tests.
+        rules: {type}`struct` A struct with references to rules for creating targets.
+    """
+    _ = name  # buildifier: @unused
+
+    dependencies = sorted([normalize_name(d) for d in dependencies])
+    dependencies_by_platform = {
+        platform: sorted([normalize_name(d) for d in deps])
+        for platform, deps in dependencies_by_platform.items()
+    }
+    tags = sorted(tags)
+    data = [] + data
+
+    for filegroup_name, glob in filegroups.items():
+        native.filegroup(
+            name = filegroup_name,
+            srcs = native.glob(glob, allow_empty = True),
+            visibility = ["//visibility:public"],
+        )
+
+    for src, dest in copy_files.items():
+        rules.copy_file(
+            name = dest + ".copy",
+            src = src,
+            out = dest,
+            visibility = ["//visibility:public"],
+        )
+        data.append(dest)
+    for src, dest in copy_executables.items():
+        rules.copy_file(
+            name = dest + ".copy",
+            src = src,
+            out = dest,
+            is_executable = True,
+            visibility = ["//visibility:public"],
+        )
+        data.append(dest)
+
+    _config_settings(
+        dependencies_by_platform.keys(),
+        native = native,
+        visibility = ["//visibility:private"],
+    )
+
+    # TODO @aignas 2024-10-25: remove the entry_point generation once
+    # `py_console_script_binary` is the only way to use entry points.
+    for entry_point, entry_point_script_name in entry_points.items():
+        rules.py_binary(
+            name = "{}_{}".format(WHEEL_ENTRY_POINT_PREFIX, entry_point),
+            # Ensure that this works on Windows as well - script may have Windows path separators.
+            srcs = [entry_point_script_name.replace("\\", "/")],
+            # This makes this directory a top-level in the python import
+            # search path for anything that depends on this.
+            imports = ["."],
+            deps = [":" + PY_LIBRARY_PUBLIC_LABEL],
+            visibility = ["//visibility:public"],
+        )
+
+    # Ensure this list is normalized
+    # Note: mapping used as set
+    group_deps = {
+        normalize_name(d): True
+        for d in group_deps
+    }
+
+    dependencies = [
+        d
+        for d in dependencies
+        if d not in group_deps
+    ]
+    dependencies_by_platform = {
+        p: deps
+        for p, deps in dependencies_by_platform.items()
+        for deps in [[d for d in deps if d not in group_deps]]
+        if deps
+    }
+
+    # If this library is a member of a group, its public label aliases need to
+    # point to the group implementation rule not the implementation rules. We
+    # also need to mark the implementation rules as visible to the group
+    # implementation.
+    if group_name and "//:" in dep_template:
+        # This is the legacy behaviour where the group library is outside the hub repo
+        label_tmpl = dep_template.format(
+            name = "_groups",
+            target = normalize_name(group_name) + "_{}",
+        )
+        impl_vis = [dep_template.format(
+            name = "_groups",
+            target = "__pkg__",
+        )]
+
+        native.alias(
+            name = PY_LIBRARY_PUBLIC_LABEL,
+            actual = label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL),
+            visibility = ["//visibility:public"],
+        )
+        native.alias(
+            name = WHEEL_FILE_PUBLIC_LABEL,
+            actual = label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL),
+            visibility = ["//visibility:public"],
+        )
+        py_library_label = PY_LIBRARY_IMPL_LABEL
+        whl_file_label = WHEEL_FILE_IMPL_LABEL
+
+    elif group_name:
+        py_library_label = PY_LIBRARY_PUBLIC_LABEL
+        whl_file_label = WHEEL_FILE_PUBLIC_LABEL
+        impl_vis = [dep_template.format(name = "", target = "__subpackages__")]
+
+    else:
+        py_library_label = PY_LIBRARY_PUBLIC_LABEL
+        whl_file_label = WHEEL_FILE_PUBLIC_LABEL
+        impl_vis = ["//visibility:public"]
+
+    if hasattr(native, "filegroup"):
+        native.filegroup(
+            name = whl_file_label,
+            srcs = [name],
+            data = _deps(
+                deps = dependencies,
+                deps_by_platform = dependencies_by_platform,
+                tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL),
+                # NOTE @aignas 2024-10-28: Actually, `select` is not part of
+                # `native`, but in order to support bazel 6.4 in unit tests, I
+                # have to somehow pass the `select` implementation in the unit
+                # tests and I chose this to be routed through the `native`
+                # struct. So, tests` will be successful in `getattr` and the
+                # real code will use the fallback provided here.
+                select = getattr(native, "select", select),
+            ),
+            visibility = impl_vis,
+        )
+
+    if hasattr(rules, "py_library"):
+        _data_exclude = [
+            "**/* *",
+            "**/*.py",
+            "**/*.pyc",
+            "**/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNNN are created
+            # RECORD is known to contain sha256 checksums of files which might include the checksums
+            # of generated files produced when wheels are installed. The file is ignored to avoid
+            # Bazel caching issues.
+            "**/*.dist-info/RECORD",
+        ]
+        for item in data_exclude:
+            if item not in _data_exclude:
+                _data_exclude.append(item)
+
+        rules.py_library(
+            name = py_library_label,
+            srcs = native.glob(
+                ["site-packages/**/*.py"],
+                exclude = srcs_exclude,
+                # Empty sources are allowed to support wheels that don't have any
+                # pure-Python code, e.g. pymssql, which is written in Cython.
+                allow_empty = True,
+            ),
+            data = data + native.glob(
+                ["site-packages/**/*"],
+                exclude = _data_exclude,
+            ),
+            # This makes this directory a top-level in the python import
+            # search path for anything that depends on this.
+            imports = ["site-packages"],
+            deps = _deps(
+                deps = dependencies,
+                deps_by_platform = dependencies_by_platform,
+                tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL),
+                select = getattr(native, "select", select),
+            ),
+            tags = tags,
+            visibility = impl_vis,
+        )
+
+def _config_settings(dependencies_by_platform, native = native, **kwargs):
+    """Generate config settings for the targets.
+
+    Args:
+        dependencies_by_platform: {type}`list[str]` platform keys, can be
+            one of the following formats:
+            * `//conditions:default`
+            * `@platforms//os:{value}`
+            * `@platforms//cpu:{value}`
+            * `@//python/config_settings:is_python_3.{minor_version}`
+            * `{os}_{cpu}`
+            * `cp3{minor_version}_{os}_{cpu}`
+        native: {type}`native` The native struct for overriding in tests.
+        **kwargs: Extra kwargs to pass to the rule.
+    """
+    for p in dependencies_by_platform:
+        if p.startswith("@") or p.endswith("default"):
+            continue
+
+        abi, _, tail = p.partition("_")
+        if not abi.startswith("cp"):
+            tail = p
+            abi = ""
+
+        os, _, arch = tail.partition("_")
+        os = "" if os == "anyos" else os
+        arch = "" if arch == "anyarch" else arch
+
+        _kwargs = dict(kwargs)
+        if arch:
+            _kwargs.setdefault("constraint_values", []).append("@platforms//cpu:{}".format(arch))
+        if os:
+            _kwargs.setdefault("constraint_values", []).append("@platforms//os:{}".format(os))
+
+        if abi:
+            _kwargs["flag_values"] = {
+                "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}".format(
+                    minor_version = abi[len("cp3"):],
+                ),
+            }
+
+        native.config_setting(
+            name = "is_{name}".format(
+                name = p.replace("cp3", "python_3."),
+            ),
+            **_kwargs
+        )
+
+def _plat_label(plat):
+    if plat.endswith("default"):
+        return plat
+    elif plat.startswith("@//"):
+        return Label(plat.strip("@"))
+    elif plat.startswith("@"):
+        return plat
+    else:
+        return ":is_" + plat.replace("cp3", "python_3.")
+
+def _deps(deps, deps_by_platform, tmpl, select = select):
+    deps = [tmpl.format(d) for d in sorted(deps)]
+
+    if not deps_by_platform:
+        return deps
+
+    deps_by_platform = {
+        _plat_label(p): [
+            tmpl.format(d)
+            for d in sorted(deps)
+        ]
+        for p, deps in sorted(deps_by_platform.items())
+    }
+
+    # Add the default, which means that we will be just using the dependencies in
+    # `deps` for platforms that are not handled in a special way by the packages
+    deps_by_platform.setdefault("//conditions:default", [])
+
+    if not deps:
+        return select(deps_by_platform)
+    else:
+        return deps + select(deps_by_platform)
diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl
index 9453011..b0d8f6d 100644
--- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl
+++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl
@@ -19,560 +19,85 @@
 
 _tests = []
 
-def _test_simple(env):
+def _test_all(env):
     want = """\
-load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets")
 
 package(default_visibility = ["//visibility:public"])
 
-filegroup(
-    name = "dist_info",
-    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "data",
-    srcs = glob(["data/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "whl",
-    srcs = ["foo.whl"],
-    data = [
-        "@pypi_bar_baz//:whl",
-        "@pypi_foo//:whl",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-py_library(
-    name = "pkg",
-    srcs = glob(
-        ["site-packages/**/*.py"],
-        exclude=[],
-        # Empty sources are allowed to support wheels that don't have any
-        # pure-Python code, e.g. pymssql, which is written in Cython.
-        allow_empty = True,
-    ),
-    data = [] + glob(
-        ["site-packages/**/*"],
-        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
-    ),
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["site-packages"],
-    deps = [
-        "@pypi_bar_baz//:pkg",
-        "@pypi_foo//:pkg",
-    ],
-    tags = ["tag1", "tag2"],
-    visibility = ["//visibility:public"],
-)
-"""
-    actual = generate_whl_library_build_bazel(
-        dep_template = "@pypi_{name}//:{target}",
-        whl_name = "foo.whl",
-        dependencies = ["foo", "bar-baz"],
-        dependencies_by_platform = {},
-        data_exclude = [],
-        tags = ["tag1", "tag2"],
-        entry_points = {},
-        annotation = None,
-    )
-    env.expect.that_str(actual).equals(want)
-
-_tests.append(_test_simple)
-
-def _test_dep_selects(env):
-    want = """\
-load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
-
-package(default_visibility = ["//visibility:public"])
-
-filegroup(
-    name = "dist_info",
-    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "data",
-    srcs = glob(["data/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "whl",
-    srcs = ["foo.whl"],
-    data = [
-        "@pypi_bar_baz//:whl",
-        "@pypi_foo//:whl",
-    ] + select(
-        {
-            "@//python/config_settings:is_python_3.9": ["@pypi_py39_dep//:whl"],
-            "@platforms//cpu:aarch64": ["@pypi_arm_dep//:whl"],
-            "@platforms//os:windows": ["@pypi_win_dep//:whl"],
-            ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:whl"],
-            ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"],
-            ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:whl"],
-            ":is_linux_x86_64": ["@pypi_linux_intel_dep//:whl"],
-            "//conditions:default": [],
-        },
-    ),
-    visibility = ["//visibility:public"],
-)
-
-py_library(
-    name = "pkg",
-    srcs = glob(
-        ["site-packages/**/*.py"],
-        exclude=[],
-        # Empty sources are allowed to support wheels that don't have any
-        # pure-Python code, e.g. pymssql, which is written in Cython.
-        allow_empty = True,
-    ),
-    data = [] + glob(
-        ["site-packages/**/*"],
-        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
-    ),
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["site-packages"],
-    deps = [
-        "@pypi_bar_baz//:pkg",
-        "@pypi_foo//:pkg",
-    ] + select(
-        {
-            "@//python/config_settings:is_python_3.9": ["@pypi_py39_dep//:pkg"],
-            "@platforms//cpu:aarch64": ["@pypi_arm_dep//:pkg"],
-            "@platforms//os:windows": ["@pypi_win_dep//:pkg"],
-            ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:pkg"],
-            ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"],
-            ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"],
-            ":is_linux_x86_64": ["@pypi_linux_intel_dep//:pkg"],
-            "//conditions:default": [],
-        },
-    ),
-    tags = ["tag1", "tag2"],
-    visibility = ["//visibility:public"],
-)
-
-config_setting(
-    name = "is_python_3.10_linux_ppc",
-    flag_values = {
-        "@rules_python//python/config_settings:python_version_major_minor": "3.10",
+whl_library_targets(
+    copy_executables = {
+        "exec_src": "exec_dest",
     },
-    constraint_values = [
-        "@platforms//cpu:ppc",
-        "@platforms//os:linux",
-    ],
-    visibility = ["//visibility:private"],
-)
-
-config_setting(
-    name = "is_python_3.9_anyos_aarch64",
-    flag_values = {
-        "@rules_python//python/config_settings:python_version_major_minor": "3.9",
+    copy_files = {
+        "file_src": "file_dest",
     },
-    constraint_values = ["@platforms//cpu:aarch64"],
-    visibility = ["//visibility:private"],
-)
-
-config_setting(
-    name = "is_python_3.9_linux_anyarch",
-    flag_values = {
-        "@rules_python//python/config_settings:python_version_major_minor": "3.9",
+    data = ["extra_target"],
+    data_exclude = [
+        "exclude_via_attr",
+        "data_exclude_all",
+    ],
+    dep_template = "@pypi//{name}:{target}",
+    dependencies = [
+        "foo",
+        "bar-baz",
+        "qux",
+    ],
+    dependencies_by_platform = {
+        "linux_x86_64": [
+            "box",
+            "box-amd64",
+        ],
+        "windows_x86_64": ["fox"],
+        "@platforms//os:linux": ["box"],
     },
-    constraint_values = ["@platforms//os:linux"],
-    visibility = ["//visibility:private"],
-)
-
-config_setting(
-    name = "is_linux_x86_64",
-    constraint_values = [
-        "@platforms//cpu:x86_64",
-        "@platforms//os:linux",
+    entry_points = {
+        "foo": "bar.py",
+    },
+    group_deps = [
+        "foo",
+        "fox",
+        "qux",
     ],
-    visibility = ["//visibility:private"],
-)
-"""
-    actual = generate_whl_library_build_bazel(
-        dep_template = "@pypi_{name}//:{target}",
-        whl_name = "foo.whl",
-        dependencies = ["foo", "bar-baz"],
-        dependencies_by_platform = {
-            "@//python/config_settings:is_python_3.9": ["py39_dep"],
-            "@platforms//cpu:aarch64": ["arm_dep"],
-            "@platforms//os:windows": ["win_dep"],
-            "cp310_linux_ppc": ["py310_linux_ppc_dep"],
-            "cp39_anyos_aarch64": ["py39_arm_dep"],
-            "cp39_linux_anyarch": ["py39_linux_dep"],
-            "linux_x86_64": ["linux_intel_dep"],
-        },
-        data_exclude = [],
-        tags = ["tag1", "tag2"],
-        entry_points = {},
-        annotation = None,
-    )
-    env.expect.that_str(actual.replace("@@", "@")).equals(want)
-
-_tests.append(_test_dep_selects)
-
-def _test_with_annotation(env):
-    want = """\
-load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
-
-package(default_visibility = ["//visibility:public"])
-
-filegroup(
-    name = "dist_info",
-    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "data",
-    srcs = glob(["data/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "whl",
-    srcs = ["foo.whl"],
-    data = [
-        "@pypi_bar_baz//:whl",
-        "@pypi_foo//:whl",
+    group_name = "qux",
+    name = "foo.whl",
+    srcs_exclude = ["srcs_exclude_all"],
+    tags = [
+        "tag2",
+        "tag1",
     ],
-    visibility = ["//visibility:public"],
-)
-
-py_library(
-    name = "pkg",
-    srcs = glob(
-        ["site-packages/**/*.py"],
-        exclude=["srcs_exclude_all"],
-        # Empty sources are allowed to support wheels that don't have any
-        # pure-Python code, e.g. pymssql, which is written in Cython.
-        allow_empty = True,
-    ),
-    data = ["file_dest", "exec_dest"] + glob(
-        ["site-packages/**/*"],
-        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD", "data_exclude_all"],
-    ),
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["site-packages"],
-    deps = [
-        "@pypi_bar_baz//:pkg",
-        "@pypi_foo//:pkg",
-    ],
-    tags = ["tag1", "tag2"],
-    visibility = ["//visibility:public"],
-)
-
-copy_file(
-    name = "file_dest.copy",
-    src = "file_src",
-    out = "file_dest",
-    is_executable = False,
-)
-
-copy_file(
-    name = "exec_dest.copy",
-    src = "exec_src",
-    out = "exec_dest",
-    is_executable = True,
 )
 
 # SOMETHING SPECIAL AT THE END
 """
     actual = generate_whl_library_build_bazel(
-        dep_template = "@pypi_{name}//:{target}",
-        whl_name = "foo.whl",
-        dependencies = ["foo", "bar-baz"],
-        dependencies_by_platform = {},
-        data_exclude = [],
-        tags = ["tag1", "tag2"],
-        entry_points = {},
+        dep_template = "@pypi//{name}:{target}",
+        name = "foo.whl",
+        dependencies = ["foo", "bar-baz", "qux"],
+        dependencies_by_platform = {
+            "linux_x86_64": ["box", "box-amd64"],
+            "windows_x86_64": ["fox"],
+            "@platforms//os:linux": ["box"],  # buildifier: disable=unsorted-dict-items to check that we sort inside the test
+        },
+        tags = ["tag2", "tag1"],
+        entry_points = {
+            "foo": "bar.py",
+        },
+        data_exclude = ["exclude_via_attr"],
         annotation = struct(
             copy_files = {"file_src": "file_dest"},
             copy_executables = {"exec_src": "exec_dest"},
-            data = [],
+            data = ["extra_target"],
             data_exclude_glob = ["data_exclude_all"],
             srcs_exclude_glob = ["srcs_exclude_all"],
             additive_build_content = """# SOMETHING SPECIAL AT THE END""",
         ),
-    )
-    env.expect.that_str(actual).equals(want)
-
-_tests.append(_test_with_annotation)
-
-def _test_with_entry_points(env):
-    want = """\
-load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
-
-package(default_visibility = ["//visibility:public"])
-
-filegroup(
-    name = "dist_info",
-    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "data",
-    srcs = glob(["data/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "whl",
-    srcs = ["foo.whl"],
-    data = [
-        "@pypi_bar_baz//:whl",
-        "@pypi_foo//:whl",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-py_library(
-    name = "pkg",
-    srcs = glob(
-        ["site-packages/**/*.py"],
-        exclude=[],
-        # Empty sources are allowed to support wheels that don't have any
-        # pure-Python code, e.g. pymssql, which is written in Cython.
-        allow_empty = True,
-    ),
-    data = [] + glob(
-        ["site-packages/**/*"],
-        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
-    ),
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["site-packages"],
-    deps = [
-        "@pypi_bar_baz//:pkg",
-        "@pypi_foo//:pkg",
-    ],
-    tags = ["tag1", "tag2"],
-    visibility = ["//visibility:public"],
-)
-
-py_binary(
-    name = "rules_python_wheel_entry_point_fizz",
-    srcs = ["buzz.py"],
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["."],
-    deps = [":pkg"],
-)
-"""
-    actual = generate_whl_library_build_bazel(
-        dep_template = "@pypi_{name}//:{target}",
-        whl_name = "foo.whl",
-        dependencies = ["foo", "bar-baz"],
-        dependencies_by_platform = {},
-        data_exclude = [],
-        tags = ["tag1", "tag2"],
-        entry_points = {"fizz": "buzz.py"},
-        annotation = None,
-    )
-    env.expect.that_str(actual).equals(want)
-
-_tests.append(_test_with_entry_points)
-
-def _test_group_member(env):
-    want = """\
-load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
-
-package(default_visibility = ["//visibility:public"])
-
-filegroup(
-    name = "dist_info",
-    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "data",
-    srcs = glob(["data/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "_whl",
-    srcs = ["foo.whl"],
-    data = ["@pypi_bar_baz//:whl"] + select(
-        {
-            "@platforms//os:linux": ["@pypi_box//:whl"],
-            ":is_linux_x86_64": [
-                "@pypi_box//:whl",
-                "@pypi_box_amd64//:whl",
-            ],
-            "//conditions:default": [],
-        },
-    ),
-    visibility = ["@pypi__groups//:__pkg__"],
-)
-
-py_library(
-    name = "_pkg",
-    srcs = glob(
-        ["site-packages/**/*.py"],
-        exclude=[],
-        # Empty sources are allowed to support wheels that don't have any
-        # pure-Python code, e.g. pymssql, which is written in Cython.
-        allow_empty = True,
-    ),
-    data = [] + glob(
-        ["site-packages/**/*"],
-        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
-    ),
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["site-packages"],
-    deps = ["@pypi_bar_baz//:pkg"] + select(
-        {
-            "@platforms//os:linux": ["@pypi_box//:pkg"],
-            ":is_linux_x86_64": [
-                "@pypi_box//:pkg",
-                "@pypi_box_amd64//:pkg",
-            ],
-            "//conditions:default": [],
-        },
-    ),
-    tags = [],
-    visibility = ["@pypi__groups//:__pkg__"],
-)
-
-config_setting(
-    name = "is_linux_x86_64",
-    constraint_values = [
-        "@platforms//cpu:x86_64",
-        "@platforms//os:linux",
-    ],
-    visibility = ["//visibility:private"],
-)
-
-alias(
-    name = "pkg",
-    actual = "@pypi__groups//:qux_pkg",
-)
-
-alias(
-    name = "whl",
-    actual = "@pypi__groups//:qux_whl",
-)
-"""
-    actual = generate_whl_library_build_bazel(
-        dep_template = "@pypi_{name}//:{target}",
-        whl_name = "foo.whl",
-        dependencies = ["foo", "bar-baz", "qux"],
-        dependencies_by_platform = {
-            "linux_x86_64": ["box", "box-amd64"],
-            "windows_x86_64": ["fox"],
-            "@platforms//os:linux": ["box"],  # buildifier: disable=unsorted-dict-items to check that we sort inside the test
-        },
-        tags = [],
-        entry_points = {},
-        data_exclude = [],
-        annotation = None,
         group_name = "qux",
         group_deps = ["foo", "fox", "qux"],
     )
     env.expect.that_str(actual.replace("@@", "@")).equals(want)
 
-_tests.append(_test_group_member)
-
-def _test_group_member_deps_to_hub(env):
-    want = """\
-load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
-
-package(default_visibility = ["//visibility:public"])
-
-filegroup(
-    name = "dist_info",
-    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "data",
-    srcs = glob(["data/**"], allow_empty = True),
-)
-
-filegroup(
-    name = "whl",
-    srcs = ["foo.whl"],
-    data = ["@pypi//bar_baz:whl"] + select(
-        {
-            "@platforms//os:linux": ["@pypi//box:whl"],
-            ":is_linux_x86_64": [
-                "@pypi//box:whl",
-                "@pypi//box_amd64:whl",
-            ],
-            "//conditions:default": [],
-        },
-    ),
-    visibility = ["@pypi//:__subpackages__"],
-)
-
-py_library(
-    name = "pkg",
-    srcs = glob(
-        ["site-packages/**/*.py"],
-        exclude=[],
-        # Empty sources are allowed to support wheels that don't have any
-        # pure-Python code, e.g. pymssql, which is written in Cython.
-        allow_empty = True,
-    ),
-    data = [] + glob(
-        ["site-packages/**/*"],
-        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
-    ),
-    # This makes this directory a top-level in the python import
-    # search path for anything that depends on this.
-    imports = ["site-packages"],
-    deps = ["@pypi//bar_baz:pkg"] + select(
-        {
-            "@platforms//os:linux": ["@pypi//box:pkg"],
-            ":is_linux_x86_64": [
-                "@pypi//box:pkg",
-                "@pypi//box_amd64:pkg",
-            ],
-            "//conditions:default": [],
-        },
-    ),
-    tags = [],
-    visibility = ["@pypi//:__subpackages__"],
-)
-
-config_setting(
-    name = "is_linux_x86_64",
-    constraint_values = [
-        "@platforms//cpu:x86_64",
-        "@platforms//os:linux",
-    ],
-    visibility = ["//visibility:private"],
-)
-"""
-    actual = generate_whl_library_build_bazel(
-        dep_template = "@pypi//{name}:{target}",
-        whl_name = "foo.whl",
-        dependencies = ["foo", "bar-baz", "qux"],
-        dependencies_by_platform = {
-            "linux_x86_64": ["box", "box-amd64"],
-            "windows_x86_64": ["fox"],
-            "@platforms//os:linux": ["box"],  # buildifier: disable=unsorted-dict-items to check that we sort inside the test
-        },
-        tags = [],
-        entry_points = {},
-        data_exclude = [],
-        annotation = None,
-        group_name = "qux",
-        group_deps = ["foo", "fox", "qux"],
-    )
-    env.expect.that_str(actual.replace("@@", "@")).equals(want)
-
-_tests.append(_test_group_member_deps_to_hub)
+_tests.append(_test_all)
 
 def generate_whl_library_build_bazel_test_suite(name):
     """Create the test suite.
diff --git a/tests/pypi/whl_library_targets/BUILD.bazel b/tests/pypi/whl_library_targets/BUILD.bazel
new file mode 100644
index 0000000..f3d25c2
--- /dev/null
+++ b/tests/pypi/whl_library_targets/BUILD.bazel
@@ -0,0 +1,5 @@
+load(":whl_library_targets_tests.bzl", "whl_library_targets_test_suite")
+
+whl_library_targets_test_suite(
+    name = "whl_library_targets_tests",
+)
diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl
new file mode 100644
index 0000000..9694eee
--- /dev/null
+++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl
@@ -0,0 +1,349 @@
+# 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("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/private/pypi:whl_library_targets.bzl", "whl_library_targets")  # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_filegroups(env):
+    calls = []
+
+    def glob(match, *, allow_empty):
+        env.expect.that_bool(allow_empty).equals(True)
+        return match
+
+    whl_library_targets(
+        name = "",
+        dep_template = "",
+        native = struct(
+            filegroup = lambda **kwargs: calls.append(kwargs),
+            glob = glob,
+        ),
+        rules = struct(),
+    )
+
+    env.expect.that_collection(calls).contains_exactly([
+        {
+            "name": "dist_info",
+            "srcs": ["site-packages/*.dist-info/**"],
+            "visibility": ["//visibility:public"],
+        },
+        {
+            "name": "data",
+            "srcs": ["data/**"],
+            "visibility": ["//visibility:public"],
+        },
+        {
+            "name": "whl",
+            "srcs": [""],
+            "data": [],
+            "visibility": ["//visibility:public"],
+        },
+    ])  # buildifier: @unsorted-dict-items
+
+_tests.append(_test_filegroups)
+
+def _test_platforms(env):
+    calls = []
+
+    whl_library_targets(
+        name = "",
+        dep_template = None,
+        dependencies_by_platform = {
+            "@//python/config_settings:is_python_3.9": ["py39_dep"],
+            "@platforms//cpu:aarch64": ["arm_dep"],
+            "@platforms//os:windows": ["win_dep"],
+            "cp310_linux_ppc": ["py310_linux_ppc_dep"],
+            "cp39_anyos_aarch64": ["py39_arm_dep"],
+            "cp39_linux_anyarch": ["py39_linux_dep"],
+            "linux_x86_64": ["linux_intel_dep"],
+        },
+        filegroups = {},
+        native = struct(
+            config_setting = lambda **kwargs: calls.append(kwargs),
+        ),
+        rules = struct(),
+    )
+
+    env.expect.that_collection(calls).contains_exactly([
+        {
+            "name": "is_python_3.10_linux_ppc",
+            "flag_values": {
+                "@rules_python//python/config_settings:python_version_major_minor": "3.10",
+            },
+            "constraint_values": [
+                "@platforms//cpu:ppc",
+                "@platforms//os:linux",
+            ],
+            "visibility": ["//visibility:private"],
+        },
+        {
+            "name": "is_python_3.9_anyos_aarch64",
+            "flag_values": {
+                "@rules_python//python/config_settings:python_version_major_minor": "3.9",
+            },
+            "constraint_values": ["@platforms//cpu:aarch64"],
+            "visibility": ["//visibility:private"],
+        },
+        {
+            "name": "is_python_3.9_linux_anyarch",
+            "flag_values": {
+                "@rules_python//python/config_settings:python_version_major_minor": "3.9",
+            },
+            "constraint_values": ["@platforms//os:linux"],
+            "visibility": ["//visibility:private"],
+        },
+        {
+            "name": "is_linux_x86_64",
+            "constraint_values": [
+                "@platforms//cpu:x86_64",
+                "@platforms//os:linux",
+            ],
+            "visibility": ["//visibility:private"],
+        },
+    ])  # buildifier: @unsorted-dict-items
+
+_tests.append(_test_platforms)
+
+def _test_copy(env):
+    calls = []
+
+    whl_library_targets(
+        name = "",
+        dep_template = None,
+        dependencies_by_platform = {},
+        filegroups = {},
+        copy_files = {"file_src": "file_dest"},
+        copy_executables = {"exec_src": "exec_dest"},
+        native = struct(),
+        rules = struct(
+            copy_file = lambda **kwargs: calls.append(kwargs),
+        ),
+    )
+
+    env.expect.that_collection(calls).contains_exactly([
+        {
+            "name": "file_dest.copy",
+            "out": "file_dest",
+            "src": "file_src",
+            "visibility": ["//visibility:public"],
+        },
+        {
+            "is_executable": True,
+            "name": "exec_dest.copy",
+            "out": "exec_dest",
+            "src": "exec_src",
+            "visibility": ["//visibility:public"],
+        },
+    ])
+
+_tests.append(_test_copy)
+
+def _test_entrypoints(env):
+    calls = []
+
+    whl_library_targets(
+        name = "",
+        dep_template = None,
+        dependencies_by_platform = {},
+        filegroups = {},
+        entry_points = {
+            "fizz": "buzz.py",
+        },
+        native = struct(),
+        rules = struct(
+            py_binary = lambda **kwargs: calls.append(kwargs),
+        ),
+    )
+
+    env.expect.that_collection(calls).contains_exactly([
+        {
+            "name": "rules_python_wheel_entry_point_fizz",
+            "srcs": ["buzz.py"],
+            "deps": [":pkg"],
+            "imports": ["."],
+            "visibility": ["//visibility:public"],
+        },
+    ])  # buildifier: @unsorted-dict-items
+
+_tests.append(_test_entrypoints)
+
+def _test_whl_and_library_deps(env):
+    filegroup_calls = []
+    py_library_calls = []
+
+    whl_library_targets(
+        name = "foo.whl",
+        dep_template = "@pypi_{name}//:{target}",
+        dependencies = ["foo", "bar-baz"],
+        dependencies_by_platform = {
+            "@//python/config_settings:is_python_3.9": ["py39_dep"],
+            "@platforms//cpu:aarch64": ["arm_dep"],
+            "@platforms//os:windows": ["win_dep"],
+            "cp310_linux_ppc": ["py310_linux_ppc_dep"],
+            "cp39_anyos_aarch64": ["py39_arm_dep"],
+            "cp39_linux_anyarch": ["py39_linux_dep"],
+            "linux_x86_64": ["linux_intel_dep"],
+        },
+        data_exclude = [],
+        tags = ["tag1", "tag2"],
+        # Overrides for testing
+        filegroups = {},
+        native = struct(
+            filegroup = lambda **kwargs: filegroup_calls.append(kwargs),
+            config_setting = lambda **_: None,
+            glob = _glob,
+            select = _select,
+        ),
+        rules = struct(
+            py_library = lambda **kwargs: py_library_calls.append(kwargs),
+        ),
+    )
+
+    env.expect.that_collection(filegroup_calls).contains_exactly([
+        {
+            "name": "whl",
+            "srcs": ["foo.whl"],
+            "data": [
+                "@pypi_bar_baz//:whl",
+                "@pypi_foo//:whl",
+            ] + _select(
+                {
+                    Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:whl"],
+                    "@platforms//cpu:aarch64": ["@pypi_arm_dep//:whl"],
+                    "@platforms//os:windows": ["@pypi_win_dep//:whl"],
+                    ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:whl"],
+                    ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"],
+                    ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:whl"],
+                    ":is_linux_x86_64": ["@pypi_linux_intel_dep//:whl"],
+                    "//conditions:default": [],
+                },
+            ),
+            "visibility": ["//visibility:public"],
+        },
+    ])  # buildifier: @unsorted-dict-items
+    env.expect.that_collection(py_library_calls).contains_exactly([
+        {
+            "name": "pkg",
+            "srcs": _glob(
+                ["site-packages/**/*.py"],
+                exclude = [],
+                allow_empty = True,
+            ),
+            "data": [] + _glob(
+                ["site-packages/**/*"],
+                exclude = ["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+            ),
+            "imports": ["site-packages"],
+            "deps": [
+                "@pypi_bar_baz//:pkg",
+                "@pypi_foo//:pkg",
+            ] + _select(
+                {
+                    Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:pkg"],
+                    "@platforms//cpu:aarch64": ["@pypi_arm_dep//:pkg"],
+                    "@platforms//os:windows": ["@pypi_win_dep//:pkg"],
+                    ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:pkg"],
+                    ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"],
+                    ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"],
+                    ":is_linux_x86_64": ["@pypi_linux_intel_dep//:pkg"],
+                    "//conditions:default": [],
+                },
+            ),
+            "tags": ["tag1", "tag2"],
+            "visibility": ["//visibility:public"],
+        },
+    ])  # buildifier: @unsorted-dict-items
+
+_tests.append(_test_whl_and_library_deps)
+
+def _test_group(env):
+    alias_calls = []
+    py_library_calls = []
+
+    whl_library_targets(
+        name = "foo.whl",
+        dep_template = "@pypi_{name}//:{target}",
+        dependencies = ["foo", "bar-baz", "qux"],
+        dependencies_by_platform = {
+            "linux_x86_64": ["box", "box-amd64"],
+            "windows_x86_64": ["fox"],
+            "@platforms//os:linux": ["box"],  # buildifier: disable=unsorted-dict-items to check that we sort inside the test
+        },
+        tags = [],
+        entry_points = {},
+        data_exclude = [],
+        group_name = "qux",
+        group_deps = ["foo", "fox", "qux"],
+        # Overrides for testing
+        filegroups = {},
+        native = struct(
+            config_setting = lambda **_: None,
+            glob = _glob,
+            alias = lambda **kwargs: alias_calls.append(kwargs),
+            select = _select,
+        ),
+        rules = struct(
+            py_library = lambda **kwargs: py_library_calls.append(kwargs),
+        ),
+    )
+
+    env.expect.that_collection(alias_calls).contains_exactly([
+        {"name": "pkg", "actual": "@pypi__groups//:qux_pkg", "visibility": ["//visibility:public"]},
+        {"name": "whl", "actual": "@pypi__groups//:qux_whl", "visibility": ["//visibility:public"]},
+    ])  # buildifier: @unsorted-dict-items
+    env.expect.that_collection(py_library_calls).contains_exactly([
+        {
+            "name": "_pkg",
+            "srcs": _glob(["site-packages/**/*.py"], exclude = [], allow_empty = True),
+            "data": [] + _glob(
+                ["site-packages/**/*"],
+                exclude = ["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+            ),
+            "imports": ["site-packages"],
+            "deps": ["@pypi_bar_baz//:pkg"] + _select({
+                "@platforms//os:linux": ["@pypi_box//:pkg"],
+                ":is_linux_x86_64": ["@pypi_box//:pkg", "@pypi_box_amd64//:pkg"],
+                "//conditions:default": [],
+            }),
+            "tags": [],
+            "visibility": ["@pypi__groups//:__pkg__"],
+        },
+    ])  # buildifier: @unsorted-dict-items
+
+_tests.append(_test_group)
+
+def _glob(*args, **kwargs):
+    return [struct(
+        glob = args,
+        kwargs = kwargs,
+    )]
+
+def _select(*args, **kwargs):
+    """We need to have this mock select because we still need to support bazel 6."""
+    return [struct(
+        select = args,
+        kwargs = kwargs,
+    )]
+
+def whl_library_targets_test_suite(name):
+    """create the test suite.
+
+    args:
+        name: the name of the test suite
+    """
+    test_suite(name = name, basic_tests = _tests)