feat: add //python:none as public target to disable exec_interpreter (#2226)

When writing the toolchain docs, I realized there wasn't a public target
to use for
disabling the exec_interpreter.

Fixed by adding an alias to the internal target.

Along the way:
* Add the exec tools and cc toolchains to the doc gen
* A few improvements to the cc/exec tools docs
* Add public bzl file for py_exec_tools_toolchain and PyExecToolsInfo
* Fix a bug in sphinx_bzl where local names were hiding global names
even if the
  requested type didn't match (e.g. a macro foo referring to rule foo)
* Fix xrefs in the python/cc/index.md; it wasn't setting the default
domain to bzl
* Fix object type definition for attributes: the object type name was
"attribute",
  but everything else was using "attr"; switched to "attr"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4c9920..33aecf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -73,7 +73,8 @@
   `TOOL_VERSIONS` for registering patched toolchains please consider setting
   the `patch_strip` explicitly to `1` if you depend on this value - in the
   future the value may change to default to `0`.
-
+* (toolchains) Added `//python:none`, a special target for use with
+  {obj}`py_exec_tools_toolchain.exec_interpreter` to treat the value as `None`.
 
 ### Removed
 * (toolchains): Removed accidentally exposed `http_archive` symbol from
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 2b407db..149e2c5 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -72,7 +72,6 @@
     deps = [
         ":bzl_api_docs",
         ":py_api_srcs",
-        ":py_cc_toolchain",
         ":py_runtime_pair",
         "//sphinxdocs/docs:docs_lib",
     ],
@@ -86,16 +85,18 @@
         "//python:pip_bzl",
         "//python:py_binary_bzl",
         "//python:py_cc_link_params_info_bzl",
+        "//python:py_exec_tools_info_bzl",
+        "//python:py_exec_tools_toolchain_bzl",
         "//python:py_executable_info_bzl",
         "//python:py_library_bzl",
         "//python:py_runtime_bzl",
         "//python:py_runtime_info_bzl",
         "//python:py_test_bzl",
         "//python:repositories_bzl",
+        "//python/cc:py_cc_toolchain_bzl",
         "//python/cc:py_cc_toolchain_info_bzl",
         "//python/entry_points:py_console_script_binary_bzl",
-        "//python/private:py_exec_tools_info_bzl",
-        "//python/private:py_exec_tools_toolchain_bzl",
+        "//python/private:py_cc_toolchain_rule_bzl",
         "//python/private/common:py_binary_rule_bazel_bzl",
         "//python/private/common:py_library_rule_bazel_bzl",
         "//python/private/common:py_runtime_rule_bzl",
@@ -113,16 +114,6 @@
 )
 
 sphinx_stardoc(
-    name = "py_cc_toolchain",
-    src = "//python/private:py_cc_toolchain_rule.bzl",
-    prefix = "api/rules_python/",
-    public_load_path = "//python/cc:py_cc_toolchain.bzl",
-    tags = ["docs"],
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-    deps = ["//python/cc:py_cc_toolchain_bzl"],
-)
-
-sphinx_stardoc(
     name = "py_runtime_pair",
     src = "//python/private:py_runtime_pair_rule_bzl",
     prefix = "api/rules_python/",
diff --git a/docs/api/rules_python/python/cc/index.md b/docs/api/rules_python/python/cc/index.md
index 233b130..82c5934 100644
--- a/docs/api/rules_python/python/cc/index.md
+++ b/docs/api/rules_python/python/cc/index.md
@@ -1,3 +1,5 @@
+:::{default-domain} bzl
+:::
 :::{bzl:currentfile} //python/cc:BUILD.bazel
 :::
 # //python/cc
@@ -31,4 +33,9 @@
 Toolchain type identifier for the Python C toolchain.
 
 This toolchain type is typically implemented by {obj}`py_cc_toolchain`.
+
+::::{seealso}
+{any}`Custom Toolchains` for how to define custom toolchains
+::::
+
 :::
diff --git a/docs/api/rules_python/python/index.md b/docs/api/rules_python/python/index.md
index 6ce5e7c..bc5a731 100644
--- a/docs/api/rules_python/python/index.md
+++ b/docs/api/rules_python/python/index.md
@@ -10,7 +10,7 @@
 Identifier for the toolchain type for the target platform.
 
 This toolchain type gives information about the runtime for the target platform.
-It is typically implemented by the {obj}`py_runtime` rule
+It is typically implemented by the {obj}`py_runtime` rule.
 
 ::::{seealso}
 {any}`Custom Toolchains` for how to define custom toolchains
@@ -21,6 +21,14 @@
 :::{bzl:target} exec_tools_toolchain_type
 
 Identifier for the toolchain type for exec tools used to build Python targets.
+
+This toolchain type gives information about tools needed to build Python targets
+at build time. It is typically implemented by the {obj}`py_exec_tools_toolchain`
+rule.
+
+::::{seealso}
+{any}`Custom Toolchains` for how to define custom toolchains
+::::
 :::
 
 :::{bzl:target} current_py_toolchain
@@ -28,7 +36,7 @@
 Helper target to resolve to the consumer's current Python toolchain. This target
 provides:
 
-* `PyRuntimeInfo`: The consuming target's target toolchain information
+* {obj}`PyRuntimeInfo`: The consuming target's target toolchain information
 
 :::
 
@@ -42,3 +50,16 @@
 :::
 ::::
 
+:::{target} none
+A special target so that label attributes with default values can be set to
+`None`.
+
+Bazel interprets `None` to mean "use the default value", which
+makes it impossible to have a label attribute with a default value that is
+optional. To work around this, a target with a special provider is used;
+internally rules check for this, then treat the value as `None`.
+
+::::{versionadded} 0.36.0
+::::
+
+:::
diff --git a/docs/toolchains.md b/docs/toolchains.md
index b5f664f..2ac0099 100644
--- a/docs/toolchains.md
+++ b/docs/toolchains.md
@@ -395,7 +395,7 @@
 
 py_exec_tools_toolchain(
     name = "exec_tools_toolchain_impl",
-    exec_interpreter = "@rules_python/python:null_target",
+    exec_interpreter = "@rules_python/python:none",
     precompiler = "precompiler-cpython-3.12"
 )
 
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index 40880a1..6fcde38 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -140,6 +140,18 @@
 )
 
 bzl_library(
+    name = "py_exec_tools_info_bzl",
+    srcs = ["py_exec_tools_info.bzl"],
+    deps = ["//python/private:py_exec_tools_info_bzl"],
+)
+
+bzl_library(
+    name = "py_exec_tools_toolchain_bzl",
+    srcs = ["py_exec_tools_toolchain.bzl"],
+    deps = ["//python/private:py_exec_tools_toolchain_bzl"],
+)
+
+bzl_library(
     name = "py_executable_info_bzl",
     srcs = ["py_executable_info.bzl"],
     deps = ["//python/private:py_executable_info_bzl"],
@@ -308,6 +320,12 @@
     visibility = ["//visibility:public"],
 )
 
+# Special target to indicate `None` for label attributes a default value.
+alias(
+    name = "none",
+    actual = "//python/private:sentinel",
+)
+
 # Definitions for a Python toolchain that, at execution time, attempts to detect
 # a platform runtime having the appropriate major Python version. Consider this
 # a toolchain of last resort.
diff --git a/python/cc/BUILD.bazel b/python/cc/BUILD.bazel
index d384d05..f4e4aeb 100644
--- a/python/cc/BUILD.bazel
+++ b/python/cc/BUILD.bazel
@@ -40,7 +40,7 @@
     name = "py_cc_toolchain_bzl",
     srcs = ["py_cc_toolchain.bzl"],
     visibility = ["//visibility:public"],
-    deps = ["//python/private:py_cc_toolchain_bzl"],
+    deps = ["//python/private:py_cc_toolchain_macro_bzl"],
 )
 
 bzl_library(
diff --git a/python/cc/py_cc_toolchain_info.bzl b/python/cc/py_cc_toolchain_info.bzl
index 9ea394a..3164f89 100644
--- a/python/cc/py_cc_toolchain_info.bzl
+++ b/python/cc/py_cc_toolchain_info.bzl
@@ -1,4 +1,4 @@
-# Copyright 2023 The Bazel Authors. All rights reserved.
+# 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.
@@ -11,11 +11,12 @@
 # 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.
+"""Provider for C/C++ information from the toolchain.
 
-"""Provider for C/C++ information about the Python runtime.
-
-NOTE: This is a beta-quality feature. APIs subject to change until
-https://github.com/bazelbuild/rules_python/issues/824 is considered done.
+:::{seealso}
+* {any}`Custom toolchains` for how to define custom toolchains.
+* {obj}`py_cc_toolchain` rule for defining the toolchain.
+:::
 """
 
 load("//python/private:py_cc_toolchain_info.bzl", _PyCcToolchainInfo = "PyCcToolchainInfo")
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index a35e2f7..e0de7d3 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -168,15 +168,16 @@
 )
 
 bzl_library(
-    name = "py_cc_toolchain_bzl",
-    srcs = [
-        "py_cc_toolchain_macro.bzl",
-        "py_cc_toolchain_rule.bzl",
+    name = "py_cc_toolchain_macro_bzl",
+    srcs = ["py_cc_toolchain_macro.bzl"],
+    deps = [
+        ":py_cc_toolchain_rule_bzl",
     ],
-    visibility = [
-        "//docs:__subpackages__",
-        "//python/cc:__pkg__",
-    ],
+)
+
+bzl_library(
+    name = "py_cc_toolchain_rule_bzl",
+    srcs = ["py_cc_toolchain_rule.bzl"],
     deps = [
         ":py_cc_toolchain_info_bzl",
         ":rules_cc_srcs_bzl",
@@ -188,7 +189,6 @@
 bzl_library(
     name = "py_cc_toolchain_info_bzl",
     srcs = ["py_cc_toolchain_info.bzl"],
-    visibility = ["//python/cc:__pkg__"],
 )
 
 bzl_library(
@@ -370,7 +370,6 @@
     [
         "coverage.patch",
         "repack_whl.py",
-        "py_cc_toolchain_rule.bzl",
         "py_package.bzl",
         "py_wheel.bzl",
         "py_wheel_normalize_pep440.bzl",
diff --git a/python/private/py_cc_toolchain_macro.bzl b/python/private/py_cc_toolchain_macro.bzl
index 35276f7..416caac 100644
--- a/python/private/py_cc_toolchain_macro.bzl
+++ b/python/private/py_cc_toolchain_macro.bzl
@@ -22,8 +22,10 @@
 def py_cc_toolchain(**kwargs):
     """Creates a py_cc_toolchain target.
 
+    This is a macro around the {rule}`py_cc_toolchain` rule.
+
     Args:
-        **kwargs: Keyword args to pass onto underlying rule.
+        **kwargs: Keyword args to pass onto underlying {rule}`py_cc_toolchain` rule.
     """
 
     #  This tag is added to easily identify usages through other macros.
diff --git a/python/private/py_cc_toolchain_rule.bzl b/python/private/py_cc_toolchain_rule.bzl
index 2c52a2e..279f86c 100644
--- a/python/private/py_cc_toolchain_rule.bzl
+++ b/python/private/py_cc_toolchain_rule.bzl
@@ -78,5 +78,11 @@
 
 This rule carries information about the C/C++ side of a Python runtime, e.g.
 headers, shared libraries, etc.
+
+This provides `ToolchainInfo` with the following attributes:
+* `py_cc_toolchain`: {type}`PyCcToolchainInfo`
+* `toolchain_label`: {type}`Label` _only present when `--visibile_for_testing=True`
+  for internal testing_. The rule's label; this allows identifying what toolchain
+  implmentation was selected for testing purposes.
 """,
 )
diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl
index 26c09ca..957448f 100644
--- a/python/private/py_exec_tools_toolchain.bzl
+++ b/python/private/py_exec_tools_toolchain.bzl
@@ -39,20 +39,42 @@
 
 py_exec_tools_toolchain = rule(
     implementation = _py_exec_tools_toolchain_impl,
+    doc = """
+Provides a toolchain for build time tools.
+
+This provides `ToolchainInfo` with the following attributes:
+* `exec_tools`: {type}`PyExecToolsInfo` 
+* `toolchain_label`: {type}`Label` _only present when `--visibile_for_testing=True`
+  for internal testing_. The rule's label; this allows identifying what toolchain
+  implmentation was selected for testing purposes.
+""",
     attrs = {
         "exec_interpreter": attr.label(
             default = "//python/private:current_interpreter_executable",
             cfg = "exec",
             doc = """
-The interpreter to use in the exec config. To disable, specify the
-special target `//python/private:sentinel`. See PyExecToolsInfo.exec_interpreter
-for further docs.
+An interpreter that is directly usable in the exec configuration
+
+If not specified, the interpreter from {obj}`//python:toolchain_type` will
+be used.
+
+To disable, specify the special target {obj}`//python:none`; the raw value `None`
+will use the default.
+
+:::{note}
+This is only useful for `ctx.actions.run` calls that _directly_ invoke the
+interpreter, which is fairly uncommon and low level. It is better to use a
+`cfg="exec"` attribute that points to a `py_binary` rule instead, which will
+handle all the necessary transitions and runtime setup to invoke a program.
+:::
+
+See {obj}`PyExecToolsInfo.exec_interpreter` for further docs.
 """,
         ),
         "precompiler": attr.label(
             allow_files = True,
             cfg = "exec",
-            doc = "See PyExecToolsInfo.precompiler",
+            doc = "See {obj}`PyExecToolsInfo.precompiler`",
         ),
         "_visible_for_testing": attr.label(
             default = "//python/private:visible_for_testing",
diff --git a/python/py_exec_tools_info.bzl b/python/py_exec_tools_info.bzl
new file mode 100644
index 0000000..4384123
--- /dev/null
+++ b/python/py_exec_tools_info.bzl
@@ -0,0 +1,24 @@
+# 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.
+"""Provider for the exec tools toolchain.
+
+:::{seealso}
+* {any}`Custom toolchains` for how to define custom toolchains.
+* {obj}`py_cc_toolchain` rule for defining the toolchain.
+:::
+"""
+
+load("//python/private:py_exec_tools_info.bzl", _PyExecToolsInfo = "PyExecToolsInfo")
+
+PyExecToolsInfo = _PyExecToolsInfo
diff --git a/python/py_exec_tools_toolchain.bzl b/python/py_exec_tools_toolchain.bzl
new file mode 100644
index 0000000..6e0a663
--- /dev/null
+++ b/python/py_exec_tools_toolchain.bzl
@@ -0,0 +1,18 @@
+# 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.
+"""Toolchain for build-time tools."""
+
+load("//python/private:py_exec_tools_toolchain.bzl", _py_exec_tools_toolchain = "py_exec_tools_toolchain")
+
+py_exec_tools_toolchain = _py_exec_tools_toolchain
diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt
index fd2bac7..969c772 100644
--- a/sphinxdocs/inventories/bazel_inventory.txt
+++ b/sphinxdocs/inventories/bazel_inventory.txt
@@ -59,7 +59,7 @@
 ctx.workspace_name bzl:obj 1 rules/lib/builtins/ctx#workspace_name -
 depset bzl:type 1 rules/lib/depset -
 dict bzl:type 1 rules/lib/dict -
-exec_compatible_with bzl:attribute 1 reference/be/common-definitions#common.exec_compatible_with -
+exec_compatible_with bzl:attr 1 reference/be/common-definitions#common.exec_compatible_with -
 int bzl:type 1 rules/lib/int -
 label bzl:type 1 concepts/labels -
 list bzl:type 1 rules/lib/list -
@@ -133,13 +133,13 @@
 runfiles.symlinks bzl:type 1 rules/lib/builtins/runfiles#symlinks -
 str bzl:type 1 rules/lib/string -
 struct bzl:type 1 rules/lib/builtins/struct -
-target_compatible_with bzl:attribute 1 reference/be/common-definitions#common.target_compatible_with -
+target_compatible_with bzl:attr 1 reference/be/common-definitions#common.target_compatible_with -
 testing bzl:obj 1 rules/lib/toplevel/testing -
 testing.ExecutionInfo bzl:function 1 rules/lib/toplevel/testing#ExecutionInfo -
 testing.TestEnvironment bzl:function 1 rules/lib/toplevel/testing#TestEnvironment -
 testing.analysis_test bzl:rule 1 rules/lib/toplevel/testing#analysis_test -
 toolchain bzl:rule 1 reference/be/platforms-and-toolchains#toolchain -
 toolchain.exec_compatible_with bzl:rule 1 reference/be/platforms-and-toolchains#toolchain.exec_compatible_with -
-toolchain.target_settings bzl:attribute 1 reference/be/platforms-and-toolchains#toolchain.target_settings -
-toolchain.target_compatible_with bzl:attribute 1 reference/be/platforms-and-toolchains#toolchain.target_compatible_with -
+toolchain.target_settings bzl:attr 1 reference/be/platforms-and-toolchains#toolchain.target_settings -
+toolchain.target_compatible_with bzl:attr 1 reference/be/platforms-and-toolchains#toolchain.target_compatible_with -
 toolchain_type bzl:type 1 rules/lib/builtins/toolchain_type.html -
diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py
index cbd35a9..54b1285 100644
--- a/sphinxdocs/src/sphinx_bzl/bzl.py
+++ b/sphinxdocs/src/sphinx_bzl/bzl.py
@@ -1439,9 +1439,7 @@
     object_types = {
         "arg": domains.ObjType("arg", "arg", "obj"),  # macro/function arg
         "aspect": domains.ObjType("aspect", "aspect", "obj"),
-        "attribute": domains.ObjType(
-            "attribute", "attribute", "attr", "obj"
-        ),  # rule attribute
+        "attr": domains.ObjType("attr", "attr", "obj"),  # rule attribute
         "function": domains.ObjType("function", "func", "obj"),
         "method": domains.ObjType("method", "method", "obj"),
         "module-extension": domains.ObjType(
@@ -1460,6 +1458,7 @@
         # types are objects that have a constructor and methods/attrs
         "type": domains.ObjType("type", "type", "obj"),
     }
+
     # This controls:
     # * What is recognized when parsing, e.g. ":bzl:ref:`foo`" requires
     # "ref" to be in the role dict below.
@@ -1508,7 +1507,7 @@
         # dict[str, dict[str, _ObjectEntry]]
         "doc_names": {},
         # Objects by a shorter or alternative name
-        # dict[str, _ObjectEntry]
+        # dict[str, dict[str id, _ObjectEntry]]
         "alt_names": {},
     }
 
@@ -1588,8 +1587,14 @@
         # Note that the flag value could contain `=`
         if "=" in target:
             target = target[: target.find("=")]
+
         if target in self.data["doc_names"].get(fromdocname, {}):
-            return self.data["doc_names"][fromdocname][target]
+            entry = self.data["doc_names"][fromdocname][target]
+            # Prevent a local doc name masking a global alt name when its of
+            # a different type. e.g. when the macro `foo` refers to the
+            # rule `foo` in another doc.
+            if object_type in self.object_types[entry.object_type].roles:
+                return entry
 
         if object_type == "obj":
             search_space = self.data["objects"]
@@ -1600,7 +1605,15 @@
 
         _log_debug("find_entry: alt_names=%s", sorted(self.data["alt_names"].keys()))
         if target in self.data["alt_names"]:
-            return self.data["alt_names"][target]
+            # Give preference to shorter object ids. This is a work around
+            # to allow e.g. `FooInfo` to refer to the FooInfo type rather than
+            # the `FooInfo` constructor.
+            entries = sorted(
+                self.data["alt_names"][target].items(), key=lambda item: len(item[0])
+            )
+            for _, entry in entries:
+                if object_type in self.object_types[entry.object_type].roles:
+                    return entry
 
         return None
 
@@ -1633,17 +1646,8 @@
         alt_names.append(label + (f"%{symbol}" if symbol else ""))
 
         for alt_name in sorted(set(alt_names)):
-            if alt_name in self.data["alt_names"]:
-                existing = self.data["alt_names"][alt_name]
-                # This situation usually occurs for the constructor function
-                # of a provider, but could occur for e.g. an exported struct
-                # with an attribute the same name as the struct. For lack
-                # of a better option, take the shorter entry, on the assumption
-                # it refers to some container of the longer entry.
-                if len(entry.full_id) < len(existing.full_id):
-                    self.data["alt_names"][alt_name] = entry
-            else:
-                self.data["alt_names"][alt_name] = entry
+            self.data["alt_names"].setdefault(alt_name, {})
+            self.data["alt_names"][alt_name][entry.full_id] = entry
 
         docname = entry.index_entry.docname
         self.data["doc_names"].setdefault(docname, {})
@@ -1653,11 +1657,11 @@
         self, docnames: list[str], otherdata: dict[str, typing.Any]
     ) -> None:
         # Merge in simple dict[key, value] data
-        for top_key in ("objects", "alt_names"):
+        for top_key in ("objects",):
             self.data[top_key].update(otherdata.get(top_key, {}))
 
         # Merge in two-level dict[top_key, dict[sub_key, value]] data
-        for top_key in ("objects_by_type", "doc_names"):
+        for top_key in ("objects_by_type", "doc_names", "alt_names"):
             existing_top_map = self.data[top_key]
             for sub_key, sub_values in otherdata.get(top_key, {}).items():
                 if sub_key not in existing_top_map: