Make Python rules require the new Python toolchain type

This makes py_binary / py_test require the new Python toolchain type (@bazel_tools//tools/python:toolchain_type). It does *not* make the rules actually use Python runtimes obtained via the toolchain; that happens in a follow-up CL.

A default toolchain is defined and registered automatically in order to prevent analysis of Python rules from failing frivolously. This default is simply a stub that fails at execution time, but a follow-up CL will change it to autodetect the Python interpreter at runtime. Registration of the toolchain occurs via a WORKSPACE suffix file, so it happens automatically for every workspace.

Note that workspace suffixes are disabled in BazelAnalysisMock#createRuleClassProvider, so we need to add it to the analysis mock's boilerplate workspace content. Tests that want to register a specific toolchain (and have it take precedence over the boilerplate) should use --extra_toolchains=... in the configuration, rather than manipulate the WORKSPACE file.

tools/python/BUILD is refactored into tools/python/BUILD.tools so that the newly added toolchain definitions can be bootstrapped with Bazel 0.23, which does not have the required PyRuntimeInfo provider type.

As a drive-by cleanup, the ":py_interpreter" attribute is pushed down from PyBaseRule to PyBinaryBaseRule.

Work toward #7375.

RELNOTES: None
PiperOrigin-RevId: 236897042
diff --git a/scripts/bootstrap/compile.sh b/scripts/bootstrap/compile.sh
index 3dfceac..8a890eb 100755
--- a/scripts/bootstrap/compile.sh
+++ b/scripts/bootstrap/compile.sh
@@ -255,8 +255,13 @@
   link_file "${PWD}/tools/java/runfiles/Util.java" "${BAZEL_TOOLS_REPO}/tools/java/runfiles/Util.java"
   link_file "${PWD}/tools/java/runfiles/BUILD.tools" "${BAZEL_TOOLS_REPO}/tools/java/runfiles/BUILD"
 
+  # Create @bazel_tools/tools/python/BUILD
+  mkdir -p ${BAZEL_TOOLS_REPO}/tools/python
+  link_file "${PWD}/tools/python/BUILD.tools" "${BAZEL_TOOLS_REPO}/tools/python/BUILD"
+
   # Create the rest of @bazel_tools//tools/...
   link_children "${PWD}" tools/cpp "${BAZEL_TOOLS_REPO}"
+  link_children "${PWD}" tools/python "${BAZEL_TOOLS_REPO}"
   link_children "${PWD}" tools "${BAZEL_TOOLS_REPO}"
 
   # The BUILD file needed for @remote_java_tools.
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
index dea4426..b20b2cb 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
@@ -353,6 +353,13 @@
           builder.addRuleDefinition(new PyRuntimeRule());
 
           builder.addSkylarkBootstrap(new PyBootstrap(PyInfo.PROVIDER, PyRuntimeInfo.PROVIDER));
+
+          try {
+            builder.addWorkspaceFileSuffix(
+                ResourceFileLoader.loadResource(BazelPyBinaryRule.class, "python.WORKSPACE"));
+          } catch (IOException e) {
+            throw new IllegalStateException(e);
+          }
         }
 
         @Override
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java
index 3610423..2bc2a56 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java
@@ -129,10 +129,6 @@
               attr("srcs_version", STRING)
                   .value(PythonVersion.DEFAULT_SRCS_VALUE.toString())
                   .allowedValues(new AllowedValueSet(PythonVersion.SRCS_STRINGS)))
-          // TODO(brandjon): Consider adding to py_interpreter a .mandatoryNativeProviders() of
-          // PyRuntimeInfoProvider. (Add a test case to PythonConfigurationTest for violations of
-          // this requirement.) Probably moot now that this is going to be replaced by toolchains.
-          .add(attr(":py_interpreter", LABEL).value(PY_INTERPRETER))
           // do not depend on lib2to3:2to3 rule, because it creates circular dependencies
           // 2to3 is itself written in Python and depends on many libraries.
           .add(
@@ -253,6 +249,11 @@
           </ul>
           <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
           .add(attr("stamp", TRISTATE).value(TriState.AUTO))
+          // TODO(brandjon): Consider adding to py_interpreter a .mandatoryNativeProviders() of
+          // PyRuntimeInfoProvider. (Add a test case to PythonConfigurationTest for violations of
+          // this requirement.) Probably moot now that this is going to be replaced by toolchains.
+          .add(attr(":py_interpreter", LABEL).value(PY_INTERPRETER))
+          .addRequiredToolchains(env.getToolsLabel("//tools/python:toolchain_type"))
           .build();
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python.WORKSPACE b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python.WORKSPACE
new file mode 100644
index 0000000..baf03b5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python.WORKSPACE
@@ -0,0 +1 @@
+register_toolchains("@bazel_tools//tools/python:all")
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index bf12b84..10960aa 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -78,6 +78,7 @@
             "bind(name = 'android/sdk', actual='@bazel_tools//tools/android:sdk')",
             "register_toolchains('@bazel_tools//tools/cpp:all')",
             "register_toolchains('@bazel_tools//tools/jdk:all')",
+            "register_toolchains('@bazel_tools//tools/python:all')",
             "local_repository(name = 'local_config_platform', path = '"
                 + localConfigPlatformWorkspace
                 + "')"));
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java b/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java
index c1884e0..224bcb4 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java
@@ -38,6 +38,7 @@
         TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/python/BUILD",
         "package(default_visibility=['//visibility:public'])",
         "load(':python_version.bzl', 'define_python_version_flag')",
+        "load('//tools/python:toolchain.bzl', 'py_runtime_pair')",
         "define_python_version_flag(",
         "    name = 'python_version',",
         ")",
@@ -52,6 +53,12 @@
         "toolchain_type(name = 'toolchain_type')",
         "constraint_setting(name = 'py2_interpreter_path')",
         "constraint_setting(name = 'py3_interpreter_path')",
+        "py_runtime_pair(name = 'dummy_py_runtime_pair')",
+        "toolchain(",
+        "    name = 'dummy_toolchain',",
+        "    toolchain = ':dummy_py_runtime_pair',",
+        "    toolchain_type = ':toolchain_type',",
+        ")",
         "exports_files(['precompile.py'])",
         "sh_binary(name='2to3', srcs=['2to3.sh'])");
   }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/PythonToolchainTest.java b/src/test/java/com/google/devtools/build/lib/rules/python/PythonToolchainTest.java
index 273dc9b..dae74c9 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/PythonToolchainTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/PythonToolchainTest.java
@@ -104,8 +104,8 @@
         "    name = 'mytarget',",
         ")");
     // Register the toolchain and ask for the platform.
-    rewriteWorkspace("register_toolchains('//pkg:my_toolchain')");
-    useConfiguration("--platforms=//platforms:my_platform");
+    useConfiguration(
+        "--platforms=//platforms:my_platform", "--extra_toolchains=//pkg:my_toolchain");
 
     getConfiguredTarget("//pkg:mytarget");
     assertContainsEvent("PY2 path: /system/python2");
diff --git a/tools/python/BUILD b/tools/python/BUILD
index e7b3dba..2bdfb3e 100644
--- a/tools/python/BUILD
+++ b/tools/python/BUILD
@@ -1,12 +1,5 @@
-load(":python_version.bzl", "define_python_version_flag")
-
 package(default_visibility = ["//visibility:public"])
 
-sh_binary(
-    name = "2to3",
-    srcs = ["2to3.sh"],
-)
-
 filegroup(
     name = "srcs",
     srcs = glob(["**"]) + [
@@ -29,60 +22,3 @@
     ],
     visibility = ["//tools:__pkg__"],
 )
-
-# This target can be used to inspect the current Python major version. To use,
-# put it in the `flag_values` attribute of a `config_setting` and test it
-# against the values "PY2" or "PY3". It will always match one or the other.
-#
-# If you do not need to test any other flags in combination with the Python
-# version, then as a convenience you may use the predefined `config_setting`s
-# `@bazel_tools//tools/python:PY2` and `@bazel_tools//tools/python:PY3`.
-#
-# Example usage:
-#
-#     config_setting(
-#         name = "py3_on_arm",
-#         values = {"cpu": "arm"},
-#         flag_values = {"@bazel_tools//tools/python:python_version": "PY3"},
-#     )
-#
-#     my_target(
-#         ...
-#         some_attr = select({
-#             ":py3_on_arm": ...,
-#             ...
-#         }),
-#         ...
-#     )
-#
-# Caution: Do not `select()` on the built-in command-line flags `--force_python`
-# or `--python_version`, as they do not always reflect the true Python version
-# of the current target. `select()`-ing on them can lead to action conflicts and
-# will be disallowed.
-define_python_version_flag(
-    name = "python_version",
-)
-
-config_setting(
-    name = "PY2",
-    flag_values = {":python_version": "PY2"},
-    visibility = ["//visibility:public"],
-)
-
-config_setting(
-    name = "PY3",
-    flag_values = {":python_version": "PY3"},
-    visibility = ["//visibility:public"],
-)
-
-# The toolchain type for Python rules. Provides a Python 2 and/or Python 3
-# runtime.
-toolchain_type(name = "toolchain_type")
-
-# A constraint_setting to use for constraints related to the location of the
-# system Python 2 interpreter on a platform.
-constraint_setting(name = "py2_interpreter_path")
-
-# A constraint_setting to use for constraints related to the location of the
-# system Python 3 interpreter on a platform.
-constraint_setting(name = "py3_interpreter_path")
diff --git a/tools/python/BUILD.tools b/tools/python/BUILD.tools
new file mode 100644
index 0000000..1efd754
--- /dev/null
+++ b/tools/python/BUILD.tools
@@ -0,0 +1,81 @@
+load(":python_version.bzl", "define_python_version_flag")
+load(":toolchain.bzl", "py_runtime_pair")
+
+package(default_visibility = ["//visibility:public"])
+
+sh_binary(
+    name = "2to3",
+    srcs = ["2to3.sh"],
+)
+
+# This target can be used to inspect the current Python major version. To use,
+# put it in the `flag_values` attribute of a `config_setting` and test it
+# against the values "PY2" or "PY3". It will always match one or the other.
+#
+# If you do not need to test any other flags in combination with the Python
+# version, then as a convenience you may use the predefined `config_setting`s
+# `@bazel_tools//tools/python:PY2` and `@bazel_tools//tools/python:PY3`.
+#
+# Example usage:
+#
+#     config_setting(
+#         name = "py3_on_arm",
+#         values = {"cpu": "arm"},
+#         flag_values = {"@bazel_tools//tools/python:python_version": "PY3"},
+#     )
+#
+#     my_target(
+#         ...
+#         some_attr = select({
+#             ":py3_on_arm": ...,
+#             ...
+#         }),
+#         ...
+#     )
+#
+# Caution: Do not `select()` on the built-in command-line flags `--force_python`
+# or `--python_version`, as they do not always reflect the true Python version
+# of the current target. `select()`-ing on them can lead to action conflicts and
+# will be disallowed.
+define_python_version_flag(
+    name = "python_version",
+)
+
+config_setting(
+    name = "PY2",
+    flag_values = {":python_version": "PY2"},
+    visibility = ["//visibility:public"],
+)
+
+config_setting(
+    name = "PY3",
+    flag_values = {":python_version": "PY3"},
+    visibility = ["//visibility:public"],
+)
+
+# The toolchain type for Python rules. Provides a Python 2 and/or Python 3
+# runtime.
+toolchain_type(name = "toolchain_type")
+
+# A constraint_setting to use for constraints related to the location of the
+# system Python 2 interpreter on a platform.
+constraint_setting(name = "py2_interpreter_path")
+
+# A constraint_setting to use for constraints related to the location of the
+# system Python 3 interpreter on a platform.
+constraint_setting(name = "py3_interpreter_path")
+
+# A Python toolchain that, at execution time, attempts to detect a platform
+# runtime having the appropriate major Python version.
+
+py_runtime_pair(
+    name = "autodetecting_py_runtime_pair",
+    # TODO(brandjon): Not yet implemented. Currently this provides no runtimes,
+    # so it will just fail at analysis time if you attempt to use it.
+)
+
+toolchain(
+    name = "autodetecting_toolchain",
+    toolchain = ":autodetecting_py_runtime_pair",
+    toolchain_type = ":toolchain_type",
+)