fix(bzlmod): use X.Y select for the hub repo (#1696)

This allows better interoperability between toolchains and the wheels
built by the `whl_library`, as wheels are usually compatible across all
of the python patch versions.

---------

Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c689e9d..66f0fc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,10 @@
 * **BREAKING** (wheel) The `incompatible_normalize_name` and
   `incompatible_normalize_version` flags have been removed. They had been
   flipped to `True` in 0.27.0 release.
+* (bzlmod) The pip hub repository now uses the newly introduced config settings
+  using the `X.Y` python version notation. This improves cross module
+  interoperability and allows to share wheels built by interpreters using
+  different patch versions.
 
 ### Fixed
 
diff --git a/examples/multi_python_versions/tests/my_lib_test.py b/examples/multi_python_versions/tests/my_lib_test.py
index 1d4880f..3f334f5 100644
--- a/examples/multi_python_versions/tests/my_lib_test.py
+++ b/examples/multi_python_versions/tests/my_lib_test.py
@@ -23,5 +23,7 @@
 if not my_lib.websockets_is_for_python_version(
     workspace_version
 ) and not my_lib.websockets_is_for_python_version(bzlmod_version):
-    print("expected package for Python version is different than returned")
+    print("expected package for Python version is different than returned\n"
+          f"expected either {workspace_version} or {bzlmod_version}\n"
+          f"but got {my_lib.websockets.__file__}")
     sys.exit(1)
diff --git a/python/config_settings/config_settings.bzl b/python/config_settings/config_settings.bzl
index bd4a1b2..f1e142c 100644
--- a/python/config_settings/config_settings.bzl
+++ b/python/config_settings/config_settings.bzl
@@ -29,6 +29,7 @@
 
     # Maps e.g. "3.8" -> ["3.8.1", "3.8.2", etc]
     minor_to_micro_versions = {}
+
     allowed_flag_values = []
     for micro_version in python_versions:
         minor, _, _ = micro_version.rpartition(".")
@@ -37,9 +38,12 @@
 
     string_flag(
         name = "python_version",
-        # TODO: The default here should somehow match the MODULE config
-        build_setting_default = python_versions[0],
-        values = sorted(allowed_flag_values),
+        # TODO: The default here should somehow match the MODULE config. Until
+        # then, use the empty string to indicate an unknown version. This
+        # also prevents version-unaware targets from inadvertently matching
+        # a select condition when they shouldn't.
+        build_setting_default = "",
+        values = [""] + sorted(allowed_flag_values),
         visibility = ["//visibility:public"],
     )
 
@@ -53,7 +57,6 @@
             name = equals_minor_version_name,
             flag_values = {":python_version": minor_version},
         )
-
         matches_minor_version_names = [equals_minor_version_name]
 
         for micro_version in micro_versions:
diff --git a/python/private/bzlmod/pip.bzl b/python/private/bzlmod/pip.bzl
index ae19dad..ce3ddde 100644
--- a/python/private/bzlmod/pip.bzl
+++ b/python/private/bzlmod/pip.bzl
@@ -25,12 +25,29 @@
     "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:parse_whl_name.bzl", "parse_whl_name")
 load("//python/private:version_label.bzl", "version_label")
 load(":pip_repository.bzl", "pip_repository")
 
+def _parse_version(version):
+    major, _, version = version.partition(".")
+    minor, _, version = version.partition(".")
+    patch, _, version = version.partition(".")
+    build, _, version = version.partition(".")
+
+    return struct(
+        # use semver vocabulary here
+        major = major,
+        minor = minor,
+        patch = patch,  # this is called `micro` in the Python interpreter versioning scheme
+        build = build,
+    )
+
+def _major_minor_version(version):
+    version = _parse_version(version)
+    return "{}.{}".format(version.major, version.minor)
+
 def _whl_mods_impl(mctx):
     """Implementation of the pip.whl_mods tag class.
 
@@ -190,7 +207,7 @@
         if whl_name not in whl_map[hub_name]:
             whl_map[hub_name][whl_name] = {}
 
-        whl_map[hub_name][whl_name][full_version(pip_attr.python_version)] = pip_name + "_"
+        whl_map[hub_name][whl_name][_major_minor_version(pip_attr.python_version)] = pip_name + "_"
 
 def _pip_impl(module_ctx):
     """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
@@ -341,7 +358,7 @@
             name = hub_name,
             repo_name = hub_name,
             whl_map = whl_map,
-            default_version = full_version(DEFAULT_PYTHON_VERSION),
+            default_version = _major_minor_version(DEFAULT_PYTHON_VERSION),
         )
 
 def _pip_parse_ext_attrs():
diff --git a/python/private/bzlmod/pip_repository.bzl b/python/private/bzlmod/pip_repository.bzl
index 9e6b0f4..8ea5ee7 100644
--- a/python/private/bzlmod/pip_repository.bzl
+++ b/python/private/bzlmod/pip_repository.bzl
@@ -63,7 +63,7 @@
     "default_version": attr.string(
         mandatory = True,
         doc = """\
-This is the default python version in the format of X.Y.Z. This should match
+This is the default python version in the format of X.Y. This should match
 what is setup by the 'python' extension using the 'is_default = True'
 setting.""",
     ),
diff --git a/tests/pip_hub_repository/render_pkg_aliases/render_pkg_aliases_test.bzl b/tests/pip_hub_repository/render_pkg_aliases/render_pkg_aliases_test.bzl
index dff7cd0..3f99ef3 100644
--- a/tests/pip_hub_repository/render_pkg_aliases/render_pkg_aliases_test.bzl
+++ b/tests/pip_hub_repository/render_pkg_aliases/render_pkg_aliases_test.bzl
@@ -73,11 +73,11 @@
 
 def _test_bzlmod_aliases(env):
     actual = render_pkg_aliases(
-        default_version = "3.2.3",
+        default_version = "3.2",
         repo_name = "pypi",
         rules_python = "rules_python",
         whl_map = {
-            "bar-baz": ["3.2.3"],
+            "bar-baz": ["3.2"],
         },
     )
 
@@ -94,7 +94,7 @@
     name = "pkg",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:pkg",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:pkg",
             "//conditions:default": "@pypi_32_bar_baz//:pkg",
         },
     ),
@@ -104,7 +104,7 @@
     name = "whl",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:whl",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:whl",
             "//conditions:default": "@pypi_32_bar_baz//:whl",
         },
     ),
@@ -114,7 +114,7 @@
     name = "data",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:data",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:data",
             "//conditions:default": "@pypi_32_bar_baz//:data",
         },
     ),
@@ -124,7 +124,7 @@
     name = "dist_info",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:dist_info",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:dist_info",
             "//conditions:default": "@pypi_32_bar_baz//:dist_info",
         },
     ),
@@ -141,7 +141,7 @@
         repo_name = "pypi",
         rules_python = "rules_python",
         whl_map = {
-            "bar-baz": ["3.2.3", "3.1.3"],
+            "bar-baz": ["3.2", "3.1"],
         },
     )
 
@@ -154,7 +154,7 @@
 
 The current build configuration's Python version doesn't match any of the Python
 versions available for this wheel. This wheel supports the following Python versions:
-    3.1.3, 3.2.3
+    3.1, 3.2
 
 As matched by the `@rules_python//python/config_settings:is_python_<version>`
 configuration settings.
@@ -177,8 +177,8 @@
     name = "pkg",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:pkg",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:pkg",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:pkg",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:pkg",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -188,8 +188,8 @@
     name = "whl",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:whl",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:whl",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:whl",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:whl",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -199,8 +199,8 @@
     name = "data",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:data",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:data",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:data",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:data",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -210,8 +210,8 @@
     name = "dist_info",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:dist_info",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:dist_info",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:dist_info",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:dist_info",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -224,11 +224,18 @@
 
 def _test_bzlmod_aliases_for_non_root_modules(env):
     actual = render_pkg_aliases(
-        default_version = "3.2.4",
+        # NOTE @aignas 2024-01-17: if the default X.Y version coincides with the
+        # versions that are used in the root module, then this would be the same as
+        # as _test_bzlmod_aliases.
+        #
+        # However, if the root module uses a different default version than the
+        # non-root module, then we will have a no-match-error because the default_version
+        # is not in the list of the versions in the whl_map.
+        default_version = "3.3",
         repo_name = "pypi",
         rules_python = "rules_python",
         whl_map = {
-            "bar-baz": ["3.2.3", "3.1.3"],
+            "bar-baz": ["3.2", "3.1"],
         },
     )
 
@@ -241,7 +248,7 @@
 
 The current build configuration's Python version doesn't match any of the Python
 versions available for this wheel. This wheel supports the following Python versions:
-    3.1.3, 3.2.3
+    3.1, 3.2
 
 As matched by the `@rules_python//python/config_settings:is_python_<version>`
 configuration settings.
@@ -264,8 +271,8 @@
     name = "pkg",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:pkg",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:pkg",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:pkg",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:pkg",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -275,8 +282,8 @@
     name = "whl",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:whl",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:whl",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:whl",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:whl",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -286,8 +293,8 @@
     name = "data",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:data",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:data",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:data",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:data",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -297,8 +304,8 @@
     name = "dist_info",
     actual = select(
         {
-            "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:dist_info",
-            "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:dist_info",
+            "@@rules_python//python/config_settings:is_python_3.1": "@pypi_31_bar_baz//:dist_info",
+            "@@rules_python//python/config_settings:is_python_3.2": "@pypi_32_bar_baz//:dist_info",
         },
         no_match_error = _NO_MATCH_ERROR,
     ),
@@ -311,12 +318,12 @@
 
 def _test_bzlmod_aliases_are_created_for_all_wheels(env):
     actual = render_pkg_aliases(
-        default_version = "3.2.3",
+        default_version = "3.2",
         repo_name = "pypi",
         rules_python = "rules_python",
         whl_map = {
-            "bar": ["3.1.2", "3.2.3"],
-            "foo": ["3.1.2", "3.2.3"],
+            "bar": ["3.1", "3.2"],
+            "foo": ["3.1", "3.2"],
         },
     )