refactor(internal): make the usage of MINOR_MAPPING variable explicit in full_version (#2219)

This PR just makes the `MINOR_MAPPING` overridable and explicit in
many macros/rules that we own. Even though technically new API is
exposed, I am not sure if it is possible to use it and I am not sure
if we should advertise it.

Explicit minor_mapping results in easier wiring of `python.override`
`bzlmod` extension tag class planned for #2081.
diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock
index d747ed3..af31a12 100644
--- a/examples/bzlmod/MODULE.bazel.lock
+++ b/examples/bzlmod/MODULE.bazel.lock
@@ -1231,7 +1231,7 @@
     },
     "@@rules_python~//python/extensions:pip.bzl%pip": {
       "general": {
-        "bzlTransitiveDigest": "vzdh1M3LRVqyF10AVUO1+FOE7CZwlZaFT+7RgQ4OKXg=",
+        "bzlTransitiveDigest": "QxV2PiqVV2B5LpnSrlzLgYyKNbUEXyVc1u+ahMrefws=",
         "usagesDigest": "MChlcSw99EuW3K7OOoMcXQIdcJnEh6YmfyjJm+9mxIg=",
         "recordedFileInputs": {
           "@@other_module~//requirements_lock_3_11.txt": "a7d0061366569043d5efcf80e34a32c732679367cb3c831c4cdc606adc36d314",
@@ -6140,7 +6140,7 @@
     },
     "@@rules_python~//python/private/pypi:pip.bzl%pip_internal": {
       "general": {
-        "bzlTransitiveDigest": "TgRegkReKbGzK4VxYz9up697gcf5Q8NFuZYnZHryck8=",
+        "bzlTransitiveDigest": "P0W31OsSgVVNQ3oRHHFiRWK7NLBLyI+KbQQBCPhou7w=",
         "usagesDigest": "Y8ihY+R57BAFhalrVLVdJFrpwlbsiKz9JPJ99ljF7HA=",
         "recordedFileInputs": {
           "@@rules_python~//tools/publish/requirements.txt": "031e35d03dde03ae6305fe4b3d1f58ad7bdad857379752deede0f93649991b8a",
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 7b913df..3d23614 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -114,7 +114,6 @@
 bzl_library(
     name = "full_version_bzl",
     srcs = ["full_version.bzl"],
-    deps = ["//python:versions_bzl"],
 )
 
 bzl_library(
@@ -132,6 +131,7 @@
     name = "python_bzl",
     srcs = ["python.bzl"],
     deps = [
+        ":full_version_bzl",
         ":pythons_hub_bzl",
         ":repo_utils_bzl",
         ":toolchains_repo_bzl",
@@ -164,7 +164,6 @@
     deps = [
         ":full_version_bzl",
         ":py_toolchain_suite_bzl",
-        "//python:versions_bzl",
     ],
 )
 
diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl
index 99b8b94..301a97b 100644
--- a/python/private/config_settings.bzl
+++ b/python/private/config_settings.bzl
@@ -27,14 +27,15 @@
     micro, _, s = s.partition(".")
     return (int(major), int(minor), int(micro))
 
-def _flag_values(python_versions):
+def _flag_values(*, python_versions, minor_mapping):
     """Construct a map of python_version to a list of toolchain values.
 
     This mapping maps the concept of a config setting to a list of compatible toolchain versions.
     For using this in the code, the VERSION_FLAG_VALUES should be used instead.
 
     Args:
-        python_versions: list of strings; all X.Y.Z python versions
+        python_versions: {type}`list[str]` X.Y.Z` python versions.
+        minor_mapping: {type}`dict[str, str]` `X.Y` to `X.Y.Z` mapping.
 
     Returns:
         A `map[str, list[str]]`. Each key is a python_version flag value. Each value
@@ -61,13 +62,13 @@
         ret.setdefault(minor_version, [minor_version]).append(micro_version)
 
         # Ensure that is_python_3.9.8 is matched if python_version is set
-        # to 3.9 if MINOR_MAPPING points to 3.9.8
-        default_micro_version = MINOR_MAPPING[minor_version]
+        # to 3.9 if minor_mapping points to 3.9.8
+        default_micro_version = minor_mapping[minor_version]
         ret[micro_version] = [micro_version, minor_version] if default_micro_version == micro_version else [micro_version]
 
     return ret
 
-VERSION_FLAG_VALUES = _flag_values(TOOL_VERSIONS.keys())
+VERSION_FLAG_VALUES = _flag_values(python_versions = TOOL_VERSIONS.keys(), minor_mapping = MINOR_MAPPING)
 
 def is_python_config_setting(name, *, python_version, reuse_conditions = None, **kwargs):
     """Create a config setting for matching 'python_version' configuration flag.
diff --git a/python/private/full_version.bzl b/python/private/full_version.bzl
index 98eeee5..0292d6c 100644
--- a/python/private/full_version.bzl
+++ b/python/private/full_version.bzl
@@ -14,20 +14,19 @@
 
 """A small helper to ensure that we are working with full versions."""
 
-load("//python:versions.bzl", "MINOR_MAPPING")
-
-def full_version(version):
+def full_version(*, version, minor_mapping):
     """Return a full version.
 
     Args:
-        version: the version in `X.Y` or `X.Y.Z` format.
+        version: {type}`str` the version in `X.Y` or `X.Y.Z` format.
+        minor_mapping: {type}`dict[str, str]` mapping between `X.Y` to `X.Y.Z` format.
 
     Returns:
         a full version given the version string. If the string is already a
         major version then we return it as is.
     """
-    if version in MINOR_MAPPING:
-        return MINOR_MAPPING[version]
+    if version in minor_mapping:
+        return minor_mapping[version]
 
     parts = version.split(".")
     if len(parts) == 3:
@@ -36,7 +35,7 @@
         fail(
             "Unknown Python version '{}', available values are: {}".format(
                 version,
-                ",".join(MINOR_MAPPING.keys()),
+                ",".join(minor_mapping.keys()),
             ),
         )
     else:
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index 3b11dbe..21f69bf 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -156,7 +156,10 @@
 bzl_library(
     name = "multi_pip_parse_bzl",
     srcs = ["multi_pip_parse.bzl"],
-    deps = ["pip_repository_bzl"],
+    deps = [
+        "pip_repository_bzl",
+        "//python/private:text_util_bzl",
+    ],
 )
 
 bzl_library(
diff --git a/python/private/pypi/multi_pip_parse.bzl b/python/private/pypi/multi_pip_parse.bzl
index fe9e2db..6e824f6 100644
--- a/python/private/pypi/multi_pip_parse.bzl
+++ b/python/private/pypi/multi_pip_parse.bzl
@@ -14,6 +14,7 @@
 
 """A pip_parse implementation for version aware toolchains in WORKSPACE."""
 
+load("//python/private:text_util.bzl", "render")
 load(":pip_repository.bzl", pip_parse = "pip_repository")
 
 def _multi_pip_parse_impl(rctx):
@@ -97,6 +98,7 @@
             name = "{name}_" + wheel_name,
             wheel_name = wheel_name,
             default_version = "{default_version}",
+            minor_mapping = {minor_mapping},
             version_map = _version_map[wheel_name],
         )
 """.format(
@@ -107,6 +109,7 @@
         process_requirements_calls = "\n".join(process_requirements_calls),
         rules_python = rules_python,
         default_version = rctx.attr.default_version,
+        minor_mapping = render.indent(render.dict(rctx.attr.minor_mapping)).lstrip(),
     )
     rctx.file("requirements.bzl", requirements_bzl)
     rctx.file("BUILD.bazel", "exports_files(['requirements.bzl'])")
@@ -115,12 +118,13 @@
     _multi_pip_parse_impl,
     attrs = {
         "default_version": attr.string(),
+        "minor_mapping": attr.string_dict(),
         "pip_parses": attr.string_dict(),
         "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
     },
 )
 
-def multi_pip_parse(name, default_version, python_versions, python_interpreter_target, requirements_lock, **kwargs):
+def multi_pip_parse(name, default_version, python_versions, python_interpreter_target, requirements_lock, minor_mapping, **kwargs):
     """NOT INTENDED FOR DIRECT USE!
 
     This is intended to be used by the multi_pip_parse implementation in the template of the
@@ -128,10 +132,11 @@
 
     Args:
         name: the name of the multi_pip_parse repository.
-        default_version: the default Python version.
-        python_versions: all Python toolchain versions currently registered.
-        python_interpreter_target: a dictionary which keys are Python versions and values are resolved host interpreters.
-        requirements_lock: a dictionary which keys are Python versions and values are locked requirements files.
+        default_version: {type}`str` the default Python version.
+        python_versions: {type}`list[str]` all Python toolchain versions currently registered.
+        python_interpreter_target: {type}`dict[str, Label]` a dictionary which keys are Python versions and values are resolved host interpreters.
+        requirements_lock: {type}`dict[str, Label]` a dictionary which keys are Python versions and values are locked requirements files.
+        minor_mapping: {type}`dict[str, str]` mapping between `X.Y` to `X.Y.Z` format.
         **kwargs: extra arguments passed to all wrapped pip_parse.
 
     Returns:
@@ -157,4 +162,5 @@
         name = name,
         default_version = default_version,
         pip_parses = pip_parses,
+        minor_mapping = minor_mapping,
     )
diff --git a/python/private/pypi/whl_library_alias.bzl b/python/private/pypi/whl_library_alias.bzl
index 263d7ec..d34b34a 100644
--- a/python/private/pypi/whl_library_alias.bzl
+++ b/python/private/pypi/whl_library_alias.bzl
@@ -29,6 +29,7 @@
         build_content.append(_whl_library_render_alias_target(
             alias_name = alias_name,
             default_repo_prefix = default_repo_prefix,
+            minor_mapping = rctx.attr.minor_mapping,
             rules_python = rules_python,
             version_map = version_map,
             wheel_name = rctx.attr.wheel_name,
@@ -36,8 +37,10 @@
     rctx.file("BUILD.bazel", "\n".join(build_content))
 
 def _whl_library_render_alias_target(
+        *,
         alias_name,
         default_repo_prefix,
+        minor_mapping,
         rules_python,
         version_map,
         wheel_name):
@@ -48,7 +51,7 @@
     for [python_version, repo_prefix] in version_map:
         alias.append("""\
         "@{rules_python}//python/config_settings:is_python_{full_python_version}": "{actual}",""".format(
-            full_python_version = full_version(python_version),
+            full_python_version = full_version(version = python_version, minor_mapping = minor_mapping),
             actual = "@{repo_prefix}{wheel_name}//:{alias_name}".format(
                 repo_prefix = repo_prefix,
                 wheel_name = wheel_name,
@@ -92,6 +95,7 @@
                   "not specified, then the default rules won't be able to " +
                   "resolve a wheel and an error will occur.",
         ),
+        "minor_mapping": attr.string_dict(mandatory = True),
         "version_map": attr.string_dict(mandatory = True),
         "wheel_name": attr.string(mandatory = True),
         "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
diff --git a/python/private/python.bzl b/python/private/python.bzl
index e1d13b9..9a9a240 100644
--- a/python/private/python.bzl
+++ b/python/private/python.bzl
@@ -16,9 +16,10 @@
 
 load("@bazel_features//:features.bzl", "bazel_features")
 load("//python:repositories.bzl", "python_register_toolchains")
-load("//python:versions.bzl", "TOOL_VERSIONS")
-load("//python/private:repo_utils.bzl", "repo_utils")
+load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
+load(":full_version.bzl", "full_version")
 load(":pythons_hub.bzl", "hub_repo")
+load(":repo_utils.bzl", "repo_utils")
 load(":text_util.bzl", "render")
 load(":toolchains_repo.bzl", "multi_toolchain_aliases")
 load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER")
@@ -184,6 +185,11 @@
         fail("more than {} python versions are not supported".format(_MAX_NUM_TOOLCHAINS))
 
     return struct(
+        debug_info = debug_info,
+        default_python_version = toolchains[-1].python_version,
+        defaults = {
+            "ignore_root_user_error": ignore_root_user_error,
+        },
         toolchains = [
             struct(
                 python_version = t.python_version,
@@ -192,11 +198,6 @@
             )
             for t in toolchains
         ],
-        debug_info = debug_info,
-        default_python_version = toolchains[-1].python_version,
-        defaults = {
-            "ignore_root_user_error": ignore_root_user_error,
-        },
     )
 
 def _python_impl(module_ctx):
@@ -207,6 +208,7 @@
             name = toolchain_info.name,
             python_version = toolchain_info.python_version,
             register_coverage_tool = toolchain_info.register_coverage_tool,
+            minor_mapping = MINOR_MAPPING,
             **py.defaults
         )
 
@@ -220,7 +222,10 @@
             render.toolchain_prefix(index, toolchain.name, _TOOLCHAIN_INDEX_PAD_LENGTH)
             for index, toolchain in enumerate(py.toolchains)
         ],
-        toolchain_python_versions = [t.python_version for t in py.toolchains],
+        toolchain_python_versions = [
+            full_version(version = t.python_version, minor_mapping = MINOR_MAPPING)
+            for t in py.toolchains
+        ],
         # The last toolchain is the default; it can't have version constraints
         # Despite the implication of the arg name, the values are strs, not bools
         toolchain_set_python_version_constraints = [
diff --git a/python/private/python_repositories.bzl b/python/private/python_repositories.bzl
index b65127b..c4988ee 100644
--- a/python/private/python_repositories.bzl
+++ b/python/private/python_repositories.bzl
@@ -22,6 +22,7 @@
 load(
     "//python:versions.bzl",
     "DEFAULT_RELEASE_BASE_URL",
+    "MINOR_MAPPING",
     "PLATFORMS",
     "TOOL_VERSIONS",
     "get_release_info",
@@ -583,6 +584,7 @@
         register_coverage_tool = False,
         set_python_version_constraint = False,
         tool_versions = None,
+        minor_mapping = None,
         **kwargs):
     """Convenience macro for users which does typical setup.
 
@@ -607,6 +609,8 @@
         tool_versions: {type}`dict` contains a mapping of version with SHASUM
             and platform info. If not supplied, the defaults in
             python/versions.bzl will be used.
+        minor_mapping: {type}`dict[str, str]` contains a mapping from `X.Y` to `X.Y.Z`
+            version.
         **kwargs: passed to each {obj}`python_repository` call.
     """
 
@@ -616,8 +620,9 @@
 
     base_url = kwargs.pop("base_url", DEFAULT_RELEASE_BASE_URL)
     tool_versions = tool_versions or TOOL_VERSIONS
+    minor_mapping = minor_mapping or MINOR_MAPPING
 
-    python_version = full_version(python_version)
+    python_version = full_version(version = python_version, minor_mapping = minor_mapping)
 
     toolchain_repo_name = "{name}_toolchains".format(name = name)
 
@@ -716,6 +721,7 @@
         name,
         python_versions,
         default_version = None,
+        minor_mapping = None,
         **kwargs):
     """Convenience macro for registering multiple Python toolchains.
 
@@ -724,11 +730,15 @@
         python_versions: {type}`list[str]` the Python versions.
         default_version: {type}`str` the default Python version. If not set,
             the first version in python_versions is used.
+        minor_mapping: {type}`dict[str, str]` mapping between `X.Y` to `X.Y.Z`
+            format. Defaults to the value in `//python:versions.bzl`.
         **kwargs: passed to each {obj}`python_register_toolchains` call.
     """
     if len(python_versions) == 0:
         fail("python_versions must not be empty")
 
+    minor_mapping = minor_mapping or MINOR_MAPPING
+
     if not default_version:
         default_version = python_versions.pop(0)
     for python_version in python_versions:
@@ -742,12 +752,14 @@
             name = name + "_" + python_version.replace(".", "_"),
             python_version = python_version,
             set_python_version_constraint = True,
+            minor_mapping = minor_mapping,
             **kwargs
         )
     python_register_toolchains(
         name = name + "_" + default_version.replace(".", "_"),
         python_version = default_version,
         set_python_version_constraint = False,
+        minor_mapping = minor_mapping,
         **kwargs
     )
 
@@ -757,4 +769,5 @@
             python_version: name + "_" + python_version.replace(".", "_")
             for python_version in (python_versions + [default_version])
         },
+        minor_mapping = minor_mapping,
     )
diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl
index 7a8c874..da6c80d 100644
--- a/python/private/pythons_hub.bzl
+++ b/python/private/pythons_hub.bzl
@@ -14,7 +14,6 @@
 
 "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels"
 
-load("//python/private:full_version.bzl", "full_version")
 load(
     "//python/private:toolchains_repo.bzl",
     "python_toolchain_build_file_content",
@@ -59,7 +58,7 @@
         [
             python_toolchain_build_file_content(
                 prefix = prefixes[i],
-                python_version = full_version(python_versions[i]),
+                python_version = python_versions[i],
                 set_python_version_constraint = set_python_version_constraints[i],
                 user_repository_name = user_repository_names[i],
             )
@@ -123,7 +122,7 @@
     implementation = _hub_repo_impl,
     attrs = {
         "default_python_version": attr.string(
-            doc = "Default Python version for the build.",
+            doc = "Default Python version for the build in `X.Y` or `X.Y.Z` format.",
             mandatory = True,
         ),
         "toolchain_prefixes": attr.string_list(
@@ -131,7 +130,7 @@
             mandatory = True,
         ),
         "toolchain_python_versions": attr.string_list(
-            doc = "List of Python versions for the toolchains",
+            doc = "List of Python versions for the toolchains. In `X.Y.Z` format.",
             mandatory = True,
         ),
         "toolchain_set_python_version_constraints": attr.string_list(
diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl
index 21c4e90..528b86f 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -358,11 +358,13 @@
         name = name,
         python_versions = {python_versions},
         requirements_lock = requirements_lock,
+        minor_mapping = {minor_mapping},
         **kwargs
     )
 
 """.format(
         python_versions = rctx.attr.python_versions.keys(),
+        minor_mapping = render.indent(render.dict(rctx.attr.minor_mapping), indent = " " * 8).lstrip(),
         rules_python = rules_python,
     )
     rctx.file("pip.bzl", content = pip_bzl)
@@ -371,6 +373,7 @@
 multi_toolchain_aliases = repository_rule(
     _multi_toolchain_aliases_impl,
     attrs = {
+        "minor_mapping": attr.string_dict(doc = "The mapping between `X.Y` and `X.Y.Z` python version values"),
         "python_versions": attr.string_dict(doc = "The Python versions."),
         "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
     },