refactor(bzlmod)!: simplify pip.parse repository layout (#1395)

Before this PR we would generate extra `alias` repos and the extra `hub`
repo
for the `entry_point` macro usage. This PR removes the extras and
delegates the
creation of version-aware aliases to the `render_pkg_aliases` internal
function. This reduces the number of repositories created by the
`pip.parse`
extension.

Fixes #1255.

BREAKING CHANGE:

Note that this only affects bzlmod support, which is still beta.

* Bzlmod `pip.parse` no longer generates `{hub_name}_{py_version}` hub
repos.
* Bzlmod `pip.parse` no longer generates `{hub_name}_{distribution}` hub
repos.

These repos aren't part of a public API, but were typically used for the
`entry_point`
macros. Instead, use `py_console_script_binary`, which is the supported
replacement
for entry points under bzlmod. Directly referencing the underlying
distribution
repos remains unsupported.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e1bf1f..ed3a60d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -58,6 +58,11 @@
 * (bzlmod) The `entry_point` macro is no longer supported and has been removed
   in favour of the `py_console_script_binary` macro for `bzlmod` users.
 
+* (bzlmod) The `pip.parse` no longer generates `{hub_name}_{py_version}` hub repos
+  as the `entry_point` macro has been superseded by `py_console_script_binary`.
+
+* (bzlmod) The `pip.parse` no longer generates `{hub_name}_{distribution}` hub repos.
+
 ### Fixed
 
 * (whl_library) No longer restarts repository rule when fetching external
diff --git a/docs/pip_repository.md b/docs/pip_repository.md
index 8536052..453ca29 100644
--- a/docs/pip_repository.md
+++ b/docs/pip_repository.md
@@ -7,7 +7,7 @@
 ## pip_hub_repository_bzlmod
 
 <pre>
-pip_hub_repository_bzlmod(<a href="#pip_hub_repository_bzlmod-name">name</a>, <a href="#pip_hub_repository_bzlmod-repo_mapping">repo_mapping</a>, <a href="#pip_hub_repository_bzlmod-repo_name">repo_name</a>, <a href="#pip_hub_repository_bzlmod-whl_library_alias_names">whl_library_alias_names</a>)
+pip_hub_repository_bzlmod(<a href="#pip_hub_repository_bzlmod-name">name</a>, <a href="#pip_hub_repository_bzlmod-default_version">default_version</a>, <a href="#pip_hub_repository_bzlmod-repo_mapping">repo_mapping</a>, <a href="#pip_hub_repository_bzlmod-repo_name">repo_name</a>, <a href="#pip_hub_repository_bzlmod-whl_map">whl_map</a>)
 </pre>
 
 A rule for bzlmod mulitple pip repository creation. PRIVATE USE ONLY.
@@ -18,9 +18,10 @@
 | Name  | Description | Type | Mandatory | Default |
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <a id="pip_hub_repository_bzlmod-name"></a>name |  A unique name for this repository.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="pip_hub_repository_bzlmod-default_version"></a>default_version |  This is the default python version in the format of X.Y.Z. This should match what is setup by the 'python' extension using the 'is_default = True' setting.   | String | required |  |
 | <a id="pip_hub_repository_bzlmod-repo_mapping"></a>repo_mapping |  A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.&lt;p&gt;For example, an entry <code>"@foo": "@bar"</code> declares that, for any time this repository depends on <code>@foo</code> (such as a dependency on <code>@foo//some:target</code>, it should actually resolve that dependency within globally-declared <code>@bar</code> (<code>@bar//some:target</code>).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
 | <a id="pip_hub_repository_bzlmod-repo_name"></a>repo_name |  The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.   | String | required |  |
-| <a id="pip_hub_repository_bzlmod-whl_library_alias_names"></a>whl_library_alias_names |  The list of whl alias that we use to build aliases and the whl names   | List of strings | required |  |
+| <a id="pip_hub_repository_bzlmod-whl_map"></a>whl_map |  The wheel map where values are python versions   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> List of strings</a> | required |  |
 
 
 <a id="pip_repository"></a>
@@ -101,31 +102,6 @@
 | <a id="pip_repository-timeout"></a>timeout |  Timeout (in seconds) on the rule's execution duration.   | Integer | optional | <code>600</code> |
 
 
-<a id="pip_repository_bzlmod"></a>
-
-## pip_repository_bzlmod
-
-<pre>
-pip_repository_bzlmod(<a href="#pip_repository_bzlmod-name">name</a>, <a href="#pip_repository_bzlmod-repo_mapping">repo_mapping</a>, <a href="#pip_repository_bzlmod-repo_name">repo_name</a>, <a href="#pip_repository_bzlmod-requirements_darwin">requirements_darwin</a>, <a href="#pip_repository_bzlmod-requirements_linux">requirements_linux</a>,
-                      <a href="#pip_repository_bzlmod-requirements_lock">requirements_lock</a>, <a href="#pip_repository_bzlmod-requirements_windows">requirements_windows</a>)
-</pre>
-
-A rule for bzlmod pip_repository creation. Intended for private use only.
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="pip_repository_bzlmod-name"></a>name |  A unique name for this repository.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="pip_repository_bzlmod-repo_mapping"></a>repo_mapping |  A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.&lt;p&gt;For example, an entry <code>"@foo": "@bar"</code> declares that, for any time this repository depends on <code>@foo</code> (such as a dependency on <code>@foo//some:target</code>, it should actually resolve that dependency within globally-declared <code>@bar</code> (<code>@bar//some:target</code>).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
-| <a id="pip_repository_bzlmod-repo_name"></a>repo_name |  The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name   | String | required |  |
-| <a id="pip_repository_bzlmod-requirements_darwin"></a>requirements_darwin |  Override the requirements_lock attribute when the host platform is Mac OS   | <a href="https://bazel.build/concepts/labels">Label</a> | optional | <code>None</code> |
-| <a id="pip_repository_bzlmod-requirements_linux"></a>requirements_linux |  Override the requirements_lock attribute when the host platform is Linux   | <a href="https://bazel.build/concepts/labels">Label</a> | optional | <code>None</code> |
-| <a id="pip_repository_bzlmod-requirements_lock"></a>requirements_lock |  A fully resolved 'requirements.txt' pip requirement file containing the transitive set of your dependencies. If this file is passed instead of 'requirements' no resolve will take place and pip_repository will create individual repositories for each of your dependencies so that wheels are fetched/built only for the targets specified by 'build/run/test'.   | <a href="https://bazel.build/concepts/labels">Label</a> | optional | <code>None</code> |
-| <a id="pip_repository_bzlmod-requirements_windows"></a>requirements_windows |  Override the requirements_lock attribute when the host platform is Windows   | <a href="https://bazel.build/concepts/labels">Label</a> | optional | <code>None</code> |
-
-
 <a id="whl_library"></a>
 
 ## whl_library
diff --git a/python/extensions/pip.bzl b/python/extensions/pip.bzl
index 3ba0d3e..f94f18c 100644
--- a/python/extensions/pip.bzl
+++ b/python/extensions/pip.bzl
@@ -15,17 +15,16 @@
 "pip module extension for use with bzlmod"
 
 load("@pythons_hub//:interpreters.bzl", "DEFAULT_PYTHON_VERSION", "INTERPRETER_LABELS")
-load("//python:pip.bzl", "whl_library_alias")
 load(
     "//python/pip_install:pip_repository.bzl",
     "locked_requirements_label",
     "pip_hub_repository_bzlmod",
     "pip_repository_attrs",
-    "pip_repository_bzlmod",
     "use_isolated",
     "whl_library",
 )
 load("//python/pip_install:requirements_parser.bzl", parse_requirements = "parse")
+load("//python/private:full_version.bzl", "full_version")
 load("//python/private:normalize_name.bzl", "normalize_name")
 load("//python/private:version_label.bzl", "version_label")
 
@@ -78,11 +77,11 @@
             whl_mods = whl_mods,
         )
 
-def _create_versioned_pip_and_whl_repos(module_ctx, pip_attr, whl_map):
+def _create_whl_repos(module_ctx, pip_attr, whl_map):
     python_interpreter_target = pip_attr.python_interpreter_target
 
     # if we do not have the python_interpreter set in the attributes
-    # we programtically find it.
+    # we programmatically find it.
     hub_name = pip_attr.hub_name
     if python_interpreter_target == None:
         python_name = "python_" + version_label(pip_attr.python_version, sep = "_")
@@ -104,23 +103,12 @@
     requrements_lock = locked_requirements_label(module_ctx, pip_attr)
 
     # Parse the requirements file directly in starlark to get the information
-    # needed for the whl_libary declarations below. This is needed to contain
-    # the pip_repository logic to a single module extension.
+    # needed for the whl_libary declarations below.
     requirements_lock_content = module_ctx.read(requrements_lock)
     parse_result = parse_requirements(requirements_lock_content)
     requirements = parse_result.requirements
     extra_pip_args = pip_attr.extra_pip_args + parse_result.options
 
-    # Create the repository where users load the `requirement` macro. Under bzlmod
-    # this does not create the install_deps() macro.
-    # TODO: we may not need this repository once we have entry points
-    # supported. For now a user can access this repository and use
-    # the entrypoint functionality.
-    pip_repository_bzlmod(
-        name = pip_name,
-        repo_name = pip_name,
-        requirements_lock = pip_attr.requirements_lock,
-    )
     if hub_name not in whl_map:
         whl_map[hub_name] = {}
 
@@ -157,12 +145,12 @@
         if whl_name not in whl_map[hub_name]:
             whl_map[hub_name][whl_name] = {}
 
-        whl_map[hub_name][whl_name][pip_attr.python_version] = pip_name + "_"
+        whl_map[hub_name][whl_name][full_version(pip_attr.python_version)] = pip_name + "_"
 
 def _pip_impl(module_ctx):
-    """Implementation of a class tag that creates the pip hub(s) and corresponding pip spoke, alias and whl repositories.
+    """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
 
-    This implmentation iterates through all of the `pip.parse` calls and creates
+    This implementation iterates through all of the `pip.parse` calls and creates
     different pip hub repositories based on the "hub_name".  Each of the
     pip calls create spoke repos that uses a specific Python interpreter.
 
@@ -196,52 +184,33 @@
     Both of these pip spokes contain requirements files that includes websocket
     and its dependencies.
 
-    Two different repositories are created for the two spokes:
-
-    - @@rules_python~override~pip~pip_39
-    - @@rules_python~override~pip~pip_310
-
-    The different spoke names are a combination of the hub_name and the Python version.
-    In the future we may remove this repository, but we do not support entry points.
-    yet, and that functionality exists in these repos.
-
     We also need repositories for the wheels that the different pip spokes contain.
     For each Python version a different wheel repository is created. In our example
-    each pip spoke had a requirments file that contained websockets. We
+    each pip spoke had a requirements file that contained websockets. We
     then create two different wheel repositories that are named the following.
 
     - @@rules_python~override~pip~pip_39_websockets
     - @@rules_python~override~pip~pip_310_websockets
 
-    And if the wheel has any other dependies subsequest wheels are created in the same fashion.
+    And if the wheel has any other dependencies subsequent wheels are created in the same fashion.
 
-    We also create a repository for the wheel alias.  We want to just use the syntax
-    'requirement("websockets")' we need to have an alias repository that is named:
-
-    - @@rules_python~override~pip~pip_websockets
-
-    This repository contains alias statements for the different wheel components (pkg, data, etc).
-    Each of those aliases has a select that resolves to a spoke repository depending on
-    the Python version.
+    The hub repository has aliases for `pkg`, `data`, etc, which have a select that resolves to
+    a spoke repository depending on the Python version.
 
     Also we may have more than one hub as defined in a MODULES.bazel file.  So we could have multiple
     hubs pointing to various different pip spokes.
 
-    Some other business rules notes.  A hub can only have one spoke per Python version.  We cannot
+    Some other business rules notes. A hub can only have one spoke per Python version.  We cannot
     have a hub named "pip" that has two spokes that use the Python 3.9 interpreter.  Second
-    we cannot have the same hub name used in submodules.  The hub name has to be globally
+    we cannot have the same hub name used in sub-modules.  The hub name has to be globally
     unique.
 
-    This implementation reuses elements of non-bzlmod code and also reuses the first implementation
-    of pip bzlmod, but adds the capability to have multiple pip.parse calls.
-
     This implementation also handles the creation of whl_modification JSON files that are used
-    during the creation of wheel libraries.  These JSON files used via the annotations argument
+    during the creation of wheel libraries. These JSON files used via the annotations argument
     when calling wheel_installer.py.
 
     Args:
         module_ctx: module contents
-
     """
 
     # Build all of the wheel modifications if the tag class is called.
@@ -259,63 +228,46 @@
     for mod in module_ctx.modules:
         for pip_attr in mod.tags.parse:
             hub_name = pip_attr.hub_name
-            if hub_name in pip_hub_map:
-                # We cannot have two hubs with the same name in different
-                # modules.
-                if pip_hub_map[hub_name].module_name != mod.name:
-                    fail((
-                        "Duplicate cross-module pip hub named '{hub}': pip hub " +
-                        "names must be unique across modules. First defined " +
-                        "by module '{first_module}', second attempted by " +
-                        "module '{second_module}'"
-                    ).format(
-                        hub = hub_name,
-                        first_module = pip_hub_map[hub_name].module_name,
-                        second_module = mod.name,
-                    ))
-
-                if pip_attr.python_version in pip_hub_map[hub_name].python_versions:
-                    fail((
-                        "Duplicate pip python version '{version}' for hub " +
-                        "'{hub}' in module '{module}': the Python versions " +
-                        "used for a hub must be unique"
-                    ).format(
-                        hub = hub_name,
-                        module = mod.name,
-                        version = pip_attr.python_version,
-                    ))
-                else:
-                    pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version)
-            else:
+            if hub_name not in pip_hub_map:
                 pip_hub_map[pip_attr.hub_name] = struct(
                     module_name = mod.name,
                     python_versions = [pip_attr.python_version],
                 )
+            elif pip_hub_map[hub_name].module_name != mod.name:
+                # We cannot have two hubs with the same name in different
+                # modules.
+                fail((
+                    "Duplicate cross-module pip hub named '{hub}': pip hub " +
+                    "names must be unique across modules. First defined " +
+                    "by module '{first_module}', second attempted by " +
+                    "module '{second_module}'"
+                ).format(
+                    hub = hub_name,
+                    first_module = pip_hub_map[hub_name].module_name,
+                    second_module = mod.name,
+                ))
 
-            _create_versioned_pip_and_whl_repos(module_ctx, pip_attr, hub_whl_map)
+            elif pip_attr.python_version in pip_hub_map[hub_name].python_versions:
+                fail((
+                    "Duplicate pip python version '{version}' for hub " +
+                    "'{hub}' in module '{module}': the Python versions " +
+                    "used for a hub must be unique"
+                ).format(
+                    hub = hub_name,
+                    module = mod.name,
+                    version = pip_attr.python_version,
+                ))
+            else:
+                pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version)
+
+            _create_whl_repos(module_ctx, pip_attr, hub_whl_map)
 
     for hub_name, whl_map in hub_whl_map.items():
-        for whl_name, version_map in whl_map.items():
-            if DEFAULT_PYTHON_VERSION in version_map:
-                whl_default_version = DEFAULT_PYTHON_VERSION
-            else:
-                whl_default_version = None
-
-            # Create the alias repositories which contains different select
-            # statements  These select statements point to the different pip
-            # whls that are based on a specific version of Python.
-            whl_library_alias(
-                name = hub_name + "_" + whl_name,
-                wheel_name = whl_name,
-                default_version = whl_default_version,
-                version_map = version_map,
-            )
-
-        # Create the hub repository for pip.
         pip_hub_repository_bzlmod(
             name = hub_name,
             repo_name = hub_name,
-            whl_library_alias_names = whl_map.keys(),
+            whl_map = whl_map,
+            default_version = full_version(DEFAULT_PYTHON_VERSION),
         )
 
 def _pip_parse_ext_attrs():
diff --git a/python/pip_install/pip_hub_repository_requirements_bzlmod.bzl.tmpl b/python/pip_install/pip_hub_repository_requirements_bzlmod.bzl.tmpl
deleted file mode 100644
index 53d4ee9..0000000
--- a/python/pip_install/pip_hub_repository_requirements_bzlmod.bzl.tmpl
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Starlark representation of locked requirements.
-
-@generated by rules_python pip_parse repository rule
-from %%REQUIREMENTS_LOCK%%.
-
-This file is different from the other bzlmod template
-because we do not support entry_point yet.
-"""
-
-all_requirements = %%ALL_REQUIREMENTS%%
-
-all_whl_requirements = %%ALL_WHL_REQUIREMENTS%%
-
-all_data_requirements = %%ALL_DATA_REQUIREMENTS%%
-
-def _clean_name(name):
-    return name.replace("-", "_").replace(".", "_").lower()
-
-def requirement(name):
-    return "%%MACRO_TMPL%%".format(_clean_name(name), "pkg")
-
-def whl_requirement(name):
-    return "%%MACRO_TMPL%%".format(_clean_name(name), "whl")
-
-def data_requirement(name):
-    return "%%MACRO_TMPL%%".format(_clean_name(name), "data")
-
-def dist_info_requirement(name):
-    return "%%MACRO_TMPL%%".format(_clean_name(name), "dist_info")
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index abe3ca7..ea8b9eb 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -267,10 +267,14 @@
 """)
     return requirements_txt
 
-def _create_pip_repository_bzlmod(rctx, bzl_packages, requirements):
-    repo_name = rctx.attr.repo_name
-    build_contents = _BUILD_FILE_CONTENTS
-    aliases = render_pkg_aliases(repo_name = repo_name, bzl_packages = bzl_packages)
+def _pip_hub_repository_bzlmod_impl(rctx):
+    bzl_packages = rctx.attr.whl_map.keys()
+    aliases = render_pkg_aliases(
+        repo_name = rctx.attr.repo_name,
+        rules_python = rctx.attr._template.workspace_name,
+        default_version = rctx.attr.default_version,
+        whl_map = rctx.attr.whl_map,
+    )
     for path, contents in aliases.items():
         rctx.file(path, contents)
 
@@ -280,7 +284,7 @@
     # `requirement`, et al. macros.
     macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name)
 
-    rctx.file("BUILD.bazel", build_contents)
+    rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
     rctx.template("requirements.bzl", rctx.attr._template, substitutions = {
         "%%ALL_DATA_REQUIREMENTS%%": _format_repr_list([
             macro_tmpl.format(p, "data")
@@ -296,24 +300,26 @@
         ]),
         "%%MACRO_TMPL%%": macro_tmpl,
         "%%NAME%%": rctx.attr.name,
-        "%%REQUIREMENTS_LOCK%%": requirements,
     })
 
-def _pip_hub_repository_bzlmod_impl(rctx):
-    bzl_packages = rctx.attr.whl_library_alias_names
-    _create_pip_repository_bzlmod(rctx, bzl_packages, "")
-
 pip_hub_repository_bzlmod_attrs = {
+    "default_version": attr.string(
+        mandatory = True,
+        doc = """\
+This is the default python version in the format of X.Y.Z. This should match
+what is setup by the 'python' extension using the 'is_default = True'
+setting.""",
+    ),
     "repo_name": attr.string(
         mandatory = True,
         doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.",
     ),
-    "whl_library_alias_names": attr.string_list(
+    "whl_map": attr.string_list_dict(
         mandatory = True,
-        doc = "The list of whl alias that we use to build aliases and the whl names",
+        doc = "The wheel map where values are python versions",
     ),
     "_template": attr.label(
-        default = ":pip_hub_repository_requirements_bzlmod.bzl.tmpl",
+        default = ":pip_repository_requirements_bzlmod.bzl.tmpl",
     ),
 }
 
@@ -323,52 +329,6 @@
     implementation = _pip_hub_repository_bzlmod_impl,
 )
 
-def _pip_repository_bzlmod_impl(rctx):
-    requirements_txt = locked_requirements_label(rctx, rctx.attr)
-    content = rctx.read(requirements_txt)
-    parsed_requirements_txt = parse_requirements(content)
-
-    packages = [(normalize_name(name), requirement) for name, requirement in parsed_requirements_txt.requirements]
-
-    bzl_packages = sorted([name for name, _ in packages])
-    _create_pip_repository_bzlmod(rctx, bzl_packages, str(requirements_txt))
-
-pip_repository_bzlmod_attrs = {
-    "repo_name": attr.string(
-        mandatory = True,
-        doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name",
-    ),
-    "requirements_darwin": attr.label(
-        allow_single_file = True,
-        doc = "Override the requirements_lock attribute when the host platform is Mac OS",
-    ),
-    "requirements_linux": attr.label(
-        allow_single_file = True,
-        doc = "Override the requirements_lock attribute when the host platform is Linux",
-    ),
-    "requirements_lock": attr.label(
-        allow_single_file = True,
-        doc = """
-A fully resolved 'requirements.txt' pip requirement file containing the transitive set of your dependencies. If this file is passed instead
-of 'requirements' no resolve will take place and pip_repository will create individual repositories for each of your dependencies so that
-wheels are fetched/built only for the targets specified by 'build/run/test'.
-""",
-    ),
-    "requirements_windows": attr.label(
-        allow_single_file = True,
-        doc = "Override the requirements_lock attribute when the host platform is Windows",
-    ),
-    "_template": attr.label(
-        default = ":pip_repository_requirements_bzlmod.bzl.tmpl",
-    ),
-}
-
-pip_repository_bzlmod = repository_rule(
-    attrs = pip_repository_bzlmod_attrs,
-    doc = """A rule for bzlmod pip_repository creation. Intended for private use only.""",
-    implementation = _pip_repository_bzlmod_impl,
-)
-
 def _pip_repository_impl(rctx):
     requirements_txt = locked_requirements_label(rctx, rctx.attr)
     content = rctx.read(requirements_txt)
diff --git a/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl b/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl
index 00580f5..c72187c 100644
--- a/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl
+++ b/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl
@@ -1,7 +1,6 @@
 """Starlark representation of locked requirements.
 
-@generated by rules_python pip_parse repository rule
-from %%REQUIREMENTS_LOCK%%.
+@generated by rules_python pip.parse bzlmod extension.
 """
 
 all_requirements = %%ALL_REQUIREMENTS%%