Add a non-strict autodetecting Python toolchain

This toolchain works just like the standard autodetecting toolchain, except that if it falls back on the `python` command, it doesn't care what version that interpreter is. This allows users to opt into the legacy behavior of #4815 while still enabling Python toolchains.

This is particularly useful for Mac users who do not have a Python 3 runtime installed. Naturally, such users can only have Python 2 code in their builds, but it's still possible that these Python targets get analyzed by Bazel as PY3. For example, the target author may have forgotten to set the `python_version = "PY2"` attribute, or the downstream user may have not added `--host_force_python=PY2` to their bazelrc. Previously this worked because under #4815 Bazel would just use Python 2 for PY3 targets. Strict version checking breaks these builds, but the new non-strict toolchain provides a workaround.

Fixes #8547

RELNOTES: None
PiperOrigin-RevId: 251535571
diff --git a/tools/python/pywrapper_test.py b/tools/python/pywrapper_test.py
index fa41239..07732a6 100644
--- a/tools/python/pywrapper_test.py
+++ b/tools/python/pywrapper_test.py
@@ -94,14 +94,20 @@
         path, msg="Could not locate '%s' command on PATH" % cmd)
     self.CopyFile(path, os.path.join("dir", cmd), executable=True)
 
+  def locate_runfile(self, runfile_path):
+    resolved_path = self.Rlocation(runfile_path)
+    self.assertIsNotNone(
+        resolved_path, msg="Could not locate %s in runfiles" % runfile_path)
+    return resolved_path
+
   def setUp(self):
     super(PywrapperTest, self).setUp()
 
-    # Locate script under test.
-    wrapper_path = self.Rlocation("io_bazel/tools/python/py2wrapper.sh")
-    self.assertIsNotNone(
-        wrapper_path, msg="Could not locate py2wrapper.sh in runfiles")
-    self.wrapper_path = wrapper_path
+    # Locate scripts under test.
+    self.wrapper_path = \
+        self.locate_runfile("io_bazel/tools/python/py2wrapper.sh")
+    self.nonstrict_wrapper_path = \
+        self.locate_runfile("io_bazel/tools/python/py2wrapper_nonstrict.sh")
 
     # Setup scratch directory with all executables the script depends on.
     #
@@ -112,10 +118,10 @@
     self.setup_tool("echo")
     self.setup_tool("grep")
 
-  def run_wrapper(self, title_for_logging=None):
+  def run_with_restricted_path(self, program, title_for_logging=None):
     new_env = dict(os.environ)
     new_env["PATH"] = self.Path("dir")
-    proc = subprocess.Popen([self.wrapper_path],
+    proc = subprocess.Popen([program],
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             universal_newlines=True,
@@ -136,6 +142,13 @@
           """) % (title_for_logging, proc.returncode, out, err))
     return proc.returncode, out, err
 
+  def run_wrapper(self, title_for_logging):
+    return self.run_with_restricted_path(self.wrapper_path, title_for_logging)
+
+  def run_nonstrict_wrapper(self, title_for_logging):
+    return self.run_with_restricted_path(self.nonstrict_wrapper_path,
+                                         title_for_logging)
+
   def assert_wrapper_success(self, returncode, out, err):
     self.assertEqual(returncode, 0, msg="Expected to exit without error")
     self.assertEqual(
@@ -190,6 +203,13 @@
     self.assert_wrapper_failure(returncode, out, err,
                                 "Neither 'python2' nor 'python' were found")
 
+  def test_wrong_version_ok_for_nonstrict(self):
+    self.ScratchFile(
+        "dir/python2", MockPythonLines.WRONG_VERSION, executable=True)
+    returncode, out, err = \
+        self.run_nonstrict_wrapper("test_wrong_version_ok_for_nonstrict")
+    self.assert_wrapper_success(returncode, out, err)
+
 
 if __name__ == "__main__":
   unittest.main()