feat(toolchain): support specifying x.y versions in transitions (#1720)

This is inspired by how rules_go is registering their toolchains.

Their toolchains have multiple `target_settings` values. This
allows for a simpler passing of `X.Y` version to the `py_binary` and
`py_test` rules and does not strictly require us to provide the APIs
that pass the full python version value as the closure. This is only
possible because #1555 introduced working aliases and now we can also
have this.

Summary:
- refactor: move the toolchain_def to starlark as opposed to templating
- refactor: move the version setting as well
- feat: support matching on X.Y versions
- feat: X.Y.Z will match if X.Y is used as python_version flag and the
  MINOR_MAPPING has `"X.Y": "X.Y.Z"`.
- test: add tests checking the generated config settings.
- doc: add an example of how we could use the transition files directly

See
https://github.com/bazelbuild/rules_go/blob/master/go/private/go_toolchain.bzl#L181

---------

Co-authored-by: Richard Levasseur <richardlev@gmail.com>
diff --git a/.bazelrc b/.bazelrc
index eb00db9..c911ea5 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -4,8 +4,8 @@
 # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
 # To update these lines, execute
 # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages`
-build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
-query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
+build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
+query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
 
 test --test_output=errors
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5416787..a137e9d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,6 +47,12 @@
 * New Python versions available: `3.11.7`, `3.12.1` using
   https://github.com/indygreg/python-build-standalone/releases/tag/20240107.
 
+* (toolchain) Allow setting `x.y` as the `python_version` parameter in
+  the version-aware `py_binary` and `py_test` rules. This allows users to
+  use the same rule import for testing with specific Python versions and
+  rely on toolchain configuration and how the latest version takes precedence
+  if e.g. `3.8` is selected. That also simplifies `.bazelrc` for any users
+  that set the default `python_version` string flag in that way.
 ## 0.29.0 - 2024-01-22
 
 [0.29.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.29.0
diff --git a/examples/bzlmod/tests/BUILD.bazel b/examples/bzlmod/tests/BUILD.bazel
index ce7079c..9f7aa1b 100644
--- a/examples/bzlmod/tests/BUILD.bazel
+++ b/examples/bzlmod/tests/BUILD.bazel
@@ -2,6 +2,8 @@
 load("@python_versions//3.11:defs.bzl", py_binary_3_11 = "py_binary", py_test_3_11 = "py_test")
 load("@python_versions//3.9:defs.bzl", py_binary_3_9 = "py_binary", py_test_3_9 = "py_test")
 load("@rules_python//python:defs.bzl", "py_binary", "py_test")
+load("@rules_python//python:versions.bzl", "MINOR_MAPPING")
+load("@rules_python//python/config_settings:transition.bzl", py_versioned_binary = "py_binary", py_versioned_test = "py_test")
 
 py_binary(
     name = "version_default",
@@ -27,6 +29,13 @@
     main = "version.py",
 )
 
+py_versioned_binary(
+    name = "version_3_10_versioned",
+    srcs = ["version.py"],
+    main = "version.py",
+    python_version = "3.10",
+)
+
 # This is a work in progress and the commented
 # tests will not work  until we can support
 # multiple pips with bzlmod.
@@ -52,6 +61,28 @@
     deps = ["//libs/my_lib"],
 )
 
+py_versioned_test(
+    name = "my_lib_versioned_test",
+    srcs = ["my_lib_test.py"],
+    main = "my_lib_test.py",
+    python_version = "3.10",
+    deps = select(
+        {
+            "@rules_python//python/config_settings:is_python_" + MINOR_MAPPING["3.10"]: ["//libs/my_lib"],
+        },
+        no_match_error = """\
+This test is failing to find dependencies and it seems that the is_python_{version}
+does not match the transitioned configuration of python-version 3.10. Please
+look at the
+
+    @rules_python//python/config_settings:config_settings.bzl
+
+to fix any bugs.""".format(
+            version = MINOR_MAPPING["3.10"],
+        ),
+    ),
+)
+
 py_test(
     name = "version_default_test",
     srcs = ["version_test.py"],
@@ -73,6 +104,14 @@
     main = "version_test.py",
 )
 
+py_versioned_test(
+    name = "version_versioned_test",
+    srcs = ["version_test.py"],
+    env = {"VERSION_CHECK": "3.10"},
+    main = "version_test.py",
+    python_version = "3.10",
+)
+
 py_test_3_11(
     name = "version_3_11_test",
     srcs = ["version_test.py"],
@@ -104,6 +143,19 @@
     main = "cross_version_test.py",
 )
 
+py_versioned_test(
+    name = "version_3_10_takes_3_9_subprocess_test_2",
+    srcs = ["cross_version_test.py"],
+    data = [":version_3_9"],
+    env = {
+        "SUBPROCESS_VERSION_CHECK": "3.9",
+        "SUBPROCESS_VERSION_PY_BINARY": "$(rootpath :version_3_9)",
+        "VERSION_CHECK": "3.10",
+    },
+    main = "cross_version_test.py",
+    python_version = "3.10",
+)
+
 sh_test(
     name = "version_test_binary_default",
     srcs = ["version_test.sh"],
diff --git a/python/config_settings/config_settings.bzl b/python/config_settings/config_settings.bzl
index f1e142c..9e6bbd6 100644
--- a/python/config_settings/config_settings.bzl
+++ b/python/config_settings/config_settings.bzl
@@ -17,6 +17,7 @@
 
 load("@bazel_skylib//lib:selects.bzl", "selects")
 load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
+load("//python:versions.bzl", "MINOR_MAPPING")
 
 def construct_config_settings(name, python_versions):
     """Constructs a set of configs for all Python versions.
@@ -36,6 +37,8 @@
         minor_to_micro_versions.setdefault(minor, []).append(micro_version)
         allowed_flag_values.append(micro_version)
 
+    allowed_flag_values.extend(list(minor_to_micro_versions))
+
     string_flag(
         name = "python_version",
         # TODO: The default here should somehow match the MODULE config. Until
@@ -59,14 +62,44 @@
         )
         matches_minor_version_names = [equals_minor_version_name]
 
+        default_micro_version = MINOR_MAPPING[minor_version]
+
         for micro_version in micro_versions:
             is_micro_version_name = "is_python_" + micro_version
+            if default_micro_version != micro_version:
+                native.config_setting(
+                    name = is_micro_version_name,
+                    flag_values = {":python_version": micro_version},
+                    visibility = ["//visibility:public"],
+                )
+                matches_minor_version_names.append(is_micro_version_name)
+                continue
+
+            # 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
+            equals_micro_name = "_python_version_flag_equals_" + micro_version
             native.config_setting(
-                name = is_micro_version_name,
+                name = equals_micro_name,
                 flag_values = {":python_version": micro_version},
+            )
+
+            # An alias pointing to an underscore-prefixed config_setting_group
+            # is used because config_setting_group creates
+            # `is_{minor}_N` targets, which are easily confused with the
+            # `is_{minor}.{micro}` (dot) targets.
+            selects.config_setting_group(
+                name = "_" + is_micro_version_name,
+                match_any = [
+                    equals_micro_name,
+                    equals_minor_version_name,
+                ],
+            )
+            native.alias(
+                name = is_micro_version_name,
+                actual = "_" + is_micro_version_name,
                 visibility = ["//visibility:public"],
             )
-            matches_minor_version_names.append(is_micro_version_name)
+            matches_minor_version_names.append(equals_micro_name)
 
         # This is prefixed with an underscore to prevent confusion due to how
         # config_setting_group is implemented and how our micro-version targets
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 25937f0..1017b09 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -162,6 +162,14 @@
 )
 
 bzl_library(
+    name = "py_toolchain_suite_bzl",
+    srcs = ["py_toolchain_suite.bzl"],
+    deps = [
+        "@bazel_skylib//lib:selects",
+    ],
+)
+
+bzl_library(
     name = "py_wheel_bzl",
     srcs = ["py_wheel.bzl"],
     visibility = ["//:__subpackages__"],
diff --git a/python/private/bzlmod/BUILD.bazel b/python/private/bzlmod/BUILD.bazel
index a312922..b636cca 100644
--- a/python/private/bzlmod/BUILD.bazel
+++ b/python/private/bzlmod/BUILD.bazel
@@ -73,6 +73,6 @@
     deps = [
         "//python:versions_bzl",
         "//python/private:full_version_bzl",
-        "//python/private:toolchains_repo_bzl",
+        "//python/private:py_toolchain_suite_bzl",
     ],
 )
diff --git a/python/private/bzlmod/pythons_hub.bzl b/python/private/bzlmod/pythons_hub.bzl
index 3889e13..b002956 100644
--- a/python/private/bzlmod/pythons_hub.bzl
+++ b/python/private/bzlmod/pythons_hub.bzl
@@ -20,7 +20,6 @@
     "//python/private:toolchains_repo.bzl",
     "get_host_os_arch",
     "get_host_platform",
-    "get_repository_name",
     "python_toolchain_build_file_content",
 )
 
@@ -31,6 +30,7 @@
 
 _HUB_BUILD_FILE_TEMPLATE = """\
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("@@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_suite")
 
 bzl_library(
     name = "interpreters_bzl",
@@ -56,19 +56,24 @@
     if not _have_same_length(python_versions, set_python_version_constraints, user_repository_names):
         fail("all lists must have the same length")
 
-    rules_python = get_repository_name(workspace_location)
-
     # Iterate over the length of python_versions and call
     # build the toolchain content by calling python_toolchain_build_file_content
-    toolchains = "\n".join([python_toolchain_build_file_content(
-        prefix = prefixes[i],
-        python_version = full_version(python_versions[i]),
-        set_python_version_constraint = set_python_version_constraints[i],
-        user_repository_name = user_repository_names[i],
-        rules_python = rules_python,
-    ) for i in range(len(python_versions))])
+    toolchains = "\n".join(
+        [
+            python_toolchain_build_file_content(
+                prefix = prefixes[i],
+                python_version = full_version(python_versions[i]),
+                set_python_version_constraint = set_python_version_constraints[i],
+                user_repository_name = user_repository_names[i],
+            )
+            for i in range(len(python_versions))
+        ],
+    )
 
-    return _HUB_BUILD_FILE_TEMPLATE.format(toolchains = toolchains)
+    return _HUB_BUILD_FILE_TEMPLATE.format(
+        toolchains = toolchains,
+        rules_python = workspace_location.workspace_name,
+    )
 
 _interpreters_bzl_template = """
 INTERPRETER_LABELS = {{
diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl
new file mode 100644
index 0000000..5b4b6f8
--- /dev/null
+++ b/python/private/py_toolchain_suite.bzl
@@ -0,0 +1,81 @@
+# Copyright 2022 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.
+
+"""Create the toolchain defs in a BUILD.bazel file."""
+
+load("@bazel_skylib//lib:selects.bzl", "selects")
+
+_py_toolchain_type = Label("@bazel_tools//tools/python:toolchain_type")
+_py_cc_toolchain_type = Label("//python/cc:toolchain_type")
+
+def py_toolchain_suite(*, prefix, user_repository_name, python_version, set_python_version_constraint, **kwargs):
+    """For internal use only.
+
+    Args:
+        prefix: Prefix for toolchain target names.
+        user_repository_name: The name of the user repository.
+        python_version: The full (X.Y.Z) version of the interpreter.
+        set_python_version_constraint: True or False as a string.
+        **kwargs: extra args passed to the `toolchain` calls.
+
+    """
+
+    # We have to use a String value here because bzlmod is passing in a
+    # string as we cannot have list of bools in build rule attribues.
+    # This if statement does not appear to work unless it is in the
+    # toolchain file.
+    if set_python_version_constraint == "True":
+        major_minor, _, _ = python_version.rpartition(".")
+
+        selects.config_setting_group(
+            name = prefix + "_version_setting",
+            match_any = [
+                Label("//python/config_settings:is_python_%s" % v)
+                for v in [
+                    major_minor,
+                    python_version,
+                ]
+            ],
+            visibility = ["//visibility:private"],
+        )
+        target_settings = [prefix + "_version_setting"]
+    elif set_python_version_constraint == "False":
+        target_settings = []
+    else:
+        fail(("Invalid set_python_version_constraint value: got {} {}, wanted " +
+              "either the string 'True' or the string 'False'; " +
+              "(did you convert bool to string?)").format(
+            type(set_python_version_constraint),
+            repr(set_python_version_constraint),
+        ))
+
+    native.toolchain(
+        name = "{prefix}_toolchain".format(prefix = prefix),
+        toolchain = "@{user_repository_name}//:python_runtimes".format(
+            user_repository_name = user_repository_name,
+        ),
+        toolchain_type = _py_toolchain_type,
+        target_settings = target_settings,
+        **kwargs
+    )
+
+    native.toolchain(
+        name = "{prefix}_py_cc_toolchain".format(prefix = prefix),
+        toolchain = "@{user_repository_name}//:py_cc_toolchain".format(
+            user_repository_name = user_repository_name,
+        ),
+        toolchain_type = _py_cc_toolchain_type,
+        target_settings = target_settings,
+        **kwargs
+    )
diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl
index c7b6178..17ef1a3 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -40,8 +40,7 @@
         prefix,
         python_version,
         set_python_version_constraint,
-        user_repository_name,
-        rules_python):
+        user_repository_name):
     """Creates the content for toolchain definitions for a build file.
 
     Args:
@@ -51,58 +50,29 @@
             have the Python version constraint added as a requirement for
             matching the toolchain, "False" if not.
         user_repository_name: names for the user repos
-        rules_python: rules_python label
 
     Returns:
         build_content: Text containing toolchain definitions
     """
-    if set_python_version_constraint == "True":
-        constraint = "{rules_python}//python/config_settings:is_python_{python_version}".format(
-            rules_python = rules_python,
-            python_version = python_version,
-        )
-        target_settings = '["{}"]'.format(constraint)
-    elif set_python_version_constraint == "False":
-        target_settings = "[]"
-    else:
-        fail(("Invalid set_python_version_constraint value: got {} {}, wanted " +
-              "either the string 'True' or the string 'False'; " +
-              "(did you convert bool to string?)").format(
-            type(set_python_version_constraint),
-            repr(set_python_version_constraint),
-        ))
 
     # We create a list of toolchain content from iterating over
     # the enumeration of PLATFORMS.  We enumerate PLATFORMS in
     # order to get us an index to increment the increment.
-    return "".join([
-        """
-toolchain(
-    name = "{prefix}{platform}_toolchain",
+    return "\n\n".join([
+        """\
+py_toolchain_suite(
+    user_repository_name = "{user_repository_name}_{platform}",
+    prefix = "{prefix}{platform}",
     target_compatible_with = {compatible_with},
-    target_settings = {target_settings},
-    toolchain = "@{user_repository_name}_{platform}//:python_runtimes",
-    toolchain_type = "@bazel_tools//tools/python:toolchain_type",
-)
-
-toolchain(
-    name = "{prefix}{platform}_py_cc_toolchain",
-    target_compatible_with = {compatible_with},
-    target_settings = {target_settings},
-    toolchain = "@{user_repository_name}_{platform}//:py_cc_toolchain",
-    toolchain_type = "@rules_python//python/cc:toolchain_type",
-
-)
-""".format(
+    python_version = "{python_version}",
+    set_python_version_constraint = "{set_python_version_constraint}",
+)""".format(
             compatible_with = meta.compatible_with,
             platform = platform,
-            # We have to use a String value here because bzlmod is passing in a
-            # string as we cannot have list of bools in build rule attribues.
-            # This if statement does not appear to work unless it is in the
-            # toolchain file.
-            target_settings = target_settings,
+            set_python_version_constraint = set_python_version_constraint,
             user_repository_name = user_repository_name,
             prefix = prefix,
+            python_version = python_version,
         )
         for platform, meta in PLATFORMS.items()
     ])
@@ -116,17 +86,17 @@
 # python_register_toolchains macro so you don't normally need to interact with
 # these targets.
 
-"""
+load("@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_suite")
 
-    # Get the repository name
-    rules_python = get_repository_name(rctx.attr._rules_python_workspace)
+""".format(
+        rules_python = rctx.attr._rules_python_workspace.workspace_name,
+    )
 
     toolchains = python_toolchain_build_file_content(
         prefix = "",
         python_version = rctx.attr.python_version,
         set_python_version_constraint = str(rctx.attr.set_python_version_constraint),
         user_repository_name = rctx.attr.user_repository_name,
-        rules_python = rules_python,
     )
 
     rctx.file("BUILD.bazel", build_content + toolchains)
diff --git a/tests/toolchains/config_settings/BUILD.bazel b/tests/toolchains/config_settings/BUILD.bazel
new file mode 100644
index 0000000..babd19f
--- /dev/null
+++ b/tests/toolchains/config_settings/BUILD.bazel
@@ -0,0 +1,54 @@
+# 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("@bazel_skylib//rules:diff_test.bzl", "diff_test")
+
+# Do bazel query for the following targets and put the results into a file
+_TARGETS = [
+    "is_python_3.12.0",
+    "is_python_3.12.1",
+    "_is_python_3.12.1",
+    "_python_version_flag_equals_3.12.1",
+    "_python_version_flag_equals_3.12",
+]
+
+[
+    genquery(
+        name = target,
+        expression = "//python/config_settings:" + target,
+        opts = ["--output=build"],
+        scope = ["//python/config_settings:" + target],
+    )
+    for target in _TARGETS
+]
+
+genrule(
+    name = "config_settings_query",
+    srcs = _TARGETS,
+    outs = ["config_settings_query.txt"],
+    # Strip comments that are specific to the host it is being run on and make the
+    # expectation output more maintainable.
+    cmd = "sed -e '/^#/d' -e '/^  generator_/d' $(SRCS) >$@",
+    target_compatible_with = select({
+        # We don't have sed available on Windows
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+)
+
+diff_test(
+    name = "config_settings_test",
+    file1 = "want",
+    file2 = "config_settings_query",
+)
diff --git a/tests/toolchains/config_settings/want b/tests/toolchains/config_settings/want
new file mode 100644
index 0000000..0ecbd67
--- /dev/null
+++ b/tests/toolchains/config_settings/want
@@ -0,0 +1,27 @@
+config_setting(
+  name = "is_python_3.12.0",
+  visibility = ["//visibility:public"],
+  flag_values = {"//python/config_settings:python_version": "3.12.0"},
+)
+
+alias(
+  name = "is_python_3.12.1",
+  visibility = ["//visibility:public"],
+  actual = "//python/config_settings:_is_python_3.12.1",
+)
+
+alias(
+  name = "_is_python_3.12.1",
+  actual = select({"//python/config_settings:_python_version_flag_equals_3.12.1": "//python/config_settings:_python_version_flag_equals_3.12.1", "//conditions:default": "//python/config_settings:_python_version_flag_equals_3.12"}),
+)
+
+config_setting(
+  name = "_python_version_flag_equals_3.12.1",
+  flag_values = {"//python/config_settings:python_version": "3.12.1"},
+)
+
+config_setting(
+  name = "_python_version_flag_equals_3.12",
+  flag_values = {"//python/config_settings:python_version": "3.12"},
+)
+