docs: make readthedocs render a bit nicer and port docs over to Sphinx (#1511)

This makes the Sphinx-based docs hosted on readthedocs render a bit more
nicely, fixes a few issues, and adds some features to //sphinxdocs

This also moves all the docs onto Sphinx, deleting the checked-in
documentation.

Doc fixes/improvements:
* Ports various docs over to Sphinx pages. They're split out from the
readme file.
* Version RTD is building is reflected in the docs
* Fixes some references to github files
* Includes the custom CSS file that styled the api docs
* Removes `-Q` from doc building; all warnings should be fixed now
* Added Bazel inventory file. Bazel doesn't provide one, but we can
manually provide on
  and still use intersphinx functionality.
* Added `gh-path` custom role. This is a shortcut for writing the whole
github URL.
* Sets the primary domain to None. The default is py, which we don't use
much of, so it
  just results in confusing crossref errors.
* Enable nitpicky mode to catch more errors.
* Remove the `starlark` marker from codeblocks; that name isn't
recognized by Sphinx.
  The highlighting is still sufficient.
* Adds a glossary

Sphinxdocs improvements:
* Added a flag to pass along arbitrary `-D` args to the Sphinx
invocations. This allows
e.g., the `version` setting of the docs to be set on the command line
from the
  `READTHEDOCS_VERSION` environment variable
* Added inventory file generation. These are files that allow
referencing external
  projects using intersphinx.
* `sphinx_stardocs` have their public load path set as their page title.
This groups the
  API docs more naturally by file. The path can be customized.
* `sphinx_stardocs` can have a footer specified for generated pages.
This allows easily
  added a list of link labels for easy re-use.
* `readthedocs_install` now tries harder to find an open port
* The conf.py file is moved into the generated sources directly. This
was done because some
config settings are relative to the conf.py file, which was being placed
one directory
  above the regular sources.

Fixes #1484, #1481
diff --git a/.bazelrc b/.bazelrc
index 22f7028..67f2973 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -23,3 +23,7 @@
 # TODO: migrate all dependencies from WORKSPACE to MODULE.bazel
 # https://github.com/bazelbuild/rules_python/issues/1469
 common --noexperimental_enable_bzlmod
+
+# Additional config to use for readthedocs builds.
+# See .readthedocs.yml for additional flags
+build:rtd --stamp
diff --git a/.readthedocs.yml b/.readthedocs.yml
index d1cdbc0..9d59380 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -6,5 +6,6 @@
   tools:
     nodejs: "19"
   commands:
+    - env
     - npm install -g @bazel/bazelisk
-    - bazel run //docs/sphinx:readthedocs_install
+    - bazel run --config=rtd --//sphinxdocs:extra_defines=version=$READTHEDOCS_VERSION //docs/sphinx:readthedocs_install
diff --git a/README.md b/README.md
index 6ac8b7b..546af97 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,7 @@
 `py_binary`, `py_test`, `py_proto_library`, and related symbols that provide the basis for Python
 support in Bazel. It also contains package installation rules for integrating with PyPI and other indices. 
 
-Documentation for rules_python  lives in the
-[`docs/`](https://github.com/bazelbuild/rules_python/tree/main/docs)
-directory and in the
+Documentation for rules_python is at <https://rules-python.readthedocs.io> and in the
 [Bazel Build Encyclopedia](https://docs.bazel.build/versions/master/be/python.html).
 
 Examples live in the [examples](examples) directory.
@@ -25,356 +23,13 @@
 
 The Bazel community maintains this repository. Neither Google nor the Bazel team provides support for the code. However, this repository is part of the test suite used to vet new Bazel releases. See [How to contribute](CONTRIBUTING.md) page for information on our development workflow.
 
+## Documentation
+
+For detailed documentation, see <https://rules-python.readthedocs.io>
+
 ## Bzlmod support
 
 - Status: Beta
 - Full Feature Parity: No
 
 See [Bzlmod support](BZLMOD_SUPPORT.md) for more details.
-
-## Getting started
-
-The following two sections cover using `rules_python` with bzlmod and
-the older way of configuring bazel with a `WORKSPACE` file.
-
-### Using bzlmod
-
-**IMPORTANT: bzlmod support is still in Beta; APIs are subject to change.**
-
-The first step to using rules_python with bzlmod is to add the dependency to
-your MODULE.bazel file:
-
-```starlark
-# Update the version "0.0.0" to the release found here:
-# https://github.com/bazelbuild/rules_python/releases.
-bazel_dep(name = "rules_python", version = "0.0.0")
-```
-
-Once added, you can load the rules and use them:
-
-```starlark
-load("@rules_python//python:py_binary.bzl", "py_binary")
-
-py_binary(...)
-```
-
-Depending on what you're doing, you likely want to do some additional
-configuration to control what Python version is used; read the following
-sections for how to do that.
-
-#### Toolchain registration with bzlmod
-
-A default toolchain is automatically configured depending on
-`rules_python`. Note, however, the version used tracks the most recent Python
-release and will change often.
-
-If you want to use a specific Python version for your programs, then how
-to do so depends on if you're configuring the root module or not. The root
-module is special because it can set the *default* Python version, which
-is used by the version-unaware rules (e.g. `//python:py_binary.bzl` et al). For
-submodules, it's recommended to use the version-aware rules to pin your programs
-to a specific Python version so they don't accidentally run with a different
-version configured by the root module.
-
-##### Configuring and using the default Python version
-
-To specify what the default Python version is, set `is_default = True` when
-calling `python.toolchain()`. This can only be done by the root module; it is
-silently ignored if a submodule does it. Similarly, using the version-unaware
-rules (which always use the default Python version) should only be done by the
-root module. If submodules use them, then they may run with a different Python
-version than they expect.
-
-```starlark
-python = use_extension("@rules_python//python/extensions:python.bzl", "python")
-
-python.toolchain(
-    python_version = "3.11",
-    is_default = True,
-)
-```
-
-Then use the base rules from e.g. `//python:py_binary.bzl`.
-
-##### Pinning to a Python version
-
-Pinning to a version allows targets to force that a specific Python version is
-used, even if the root module configures a different version as a default. This
-is most useful for two cases:
-
-1. For submodules to ensure they run with the appropriate Python version
-2. To allow incremental, per-target, upgrading to newer Python versions,
-   typically in a mono-repo situation.
-
-To configure a submodule with the version-aware rules, request the particular
-version you need, then use the `@python_versions` repo to use the rules that
-force specific versions:
-
-```starlark
-python = use_extension("@rules_python//python/extensions:python.bzl", "python")
-
-python.toolchain(
-    python_version = "3.11",
-)
-use_repo(python, "python_versions")
-```
-
-Then use e.g. `load("@python_versions//3.11:defs.bzl", "py_binary")` to use
-the rules that force that particular version. Multiple versions can be specified
-and use within a single build.
-
-For more documentation, see the bzlmod examples under the [examples](examples) folder.  Look for the examples that contain a `MODULE.bazel` file.
-
-##### Other toolchain details
-
-The `python.toolchain()` call makes its contents available under a repo named
-`python_X_Y`, where X and Y are the major and minor versions. For example,
-`python.toolchain(python_version="3.11")` creates the repo `@python_3_11`.
-Remember to call `use_repo()` to make repos visible to your module:
-`use_repo(python, "python_3_11")`
-
-### Using a WORKSPACE file
-
-To import rules_python in your project, you first need to add it to your
-`WORKSPACE` file, using the snippet provided in the
-[release you choose](https://github.com/bazelbuild/rules_python/releases)
-
-To depend on a particular unreleased version, you can do the following:
-
-```starlark
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-
-
-# Update the SHA and VERSION to the lastest version available here:
-# https://github.com/bazelbuild/rules_python/releases.
-
-SHA="84aec9e21cc56fbc7f1335035a71c850d1b9b5cc6ff497306f84cced9a769841"
-
-VERSION="0.23.1"
-
-http_archive(
-    name = "rules_python",
-    sha256 = SHA,
-    strip_prefix = "rules_python-{}".format(VERSION),
-    url = "https://github.com/bazelbuild/rules_python/releases/download/{}/rules_python-{}.tar.gz".format(VERSION,VERSION),
-)
-
-load("@rules_python//python:repositories.bzl", "py_repositories")
-
-py_repositories()
-```
-
-#### Toolchain registration
-
-To register a hermetic Python toolchain rather than rely on a system-installed interpreter for runtime execution, you can add to the `WORKSPACE` file:
-
-```starlark
-load("@rules_python//python:repositories.bzl", "python_register_toolchains")
-
-python_register_toolchains(
-    name = "python_3_11",
-    # Available versions are listed in @rules_python//python:versions.bzl.
-    # We recommend using the same version your team is already standardized on.
-    python_version = "3.11",
-)
-
-load("@python_3_11//:defs.bzl", "interpreter")
-
-load("@rules_python//python:pip.bzl", "pip_parse")
-
-pip_parse(
-    ...
-    python_interpreter_target = interpreter,
-    ...
-)
-```
-
-After registration, your Python targets will use the toolchain's interpreter during execution, but a system-installed interpreter
-is still used to 'bootstrap' Python targets (see https://github.com/bazelbuild/rules_python/issues/691).
-You may also find some quirks while using this toolchain. Please refer to [python-build-standalone documentation's _Quirks_ section](https://python-build-standalone.readthedocs.io/en/latest/quirks.html).
-
-### Toolchain usage in other rules
-
-Python toolchains can be utilized in other bazel rules, such as `genrule()`, by adding the `toolchains=["@rules_python//python:current_py_toolchain"]` attribute. You can obtain the path to the Python interpreter using the `$(PYTHON2)` and `$(PYTHON3)` ["Make" Variables](https://bazel.build/reference/be/make-variables). See the [`test_current_py_toolchain`](tests/load_from_macro/BUILD.bazel) target for an example.
-
-### "Hello World"
-
-Once you've imported the rule set into your `WORKSPACE` using any of these
-methods, you can then load the core rules in your `BUILD` files with the following:
-
-```starlark
-load("@rules_python//python:defs.bzl", "py_binary")
-
-py_binary(
-  name = "main",
-  srcs = ["main.py"],
-)
-```
-
-## Using dependencies from PyPI
-
-Using PyPI packages (aka "pip install") involves two main steps.
-
-1. [Installing third_party packages](#installing-third_party-packages)
-2. [Using third_party packages as dependencies](#using-third_party-packages-as-dependencies)
-
-### Installing third_party packages
-
-#### Using bzlmod
-
-To add pip dependencies to your `MODULE.bazel` file, use the `pip.parse` extension, and call it to create the central external repo and individual wheel external repos. Include in the `MODULE.bazel` the toolchain extension as shown in the first bzlmod example above.
-
-```starlark
-pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
-pip.parse(
-    hub_name = "my_deps",
-    python_version = "3.11",
-    requirements_lock = "//:requirements_lock_3_11.txt",
-)
-use_repo(pip, "my_deps")
-```
-For more documentation, including how the rules can update/create a requirements file, see the bzlmod examples under the [examples](examples) folder.
-
-#### Using a WORKSPACE file
-
-To add pip dependencies to your `WORKSPACE`, load the `pip_parse` function and call it to create the central external repo and individual wheel external repos.
-
-```starlark
-load("@rules_python//python:pip.bzl", "pip_parse")
-
-# Create a central repo that knows about the dependencies needed from
-# requirements_lock.txt.
-pip_parse(
-   name = "my_deps",
-   requirements_lock = "//path/to:requirements_lock.txt",
-)
-# Load the starlark macro, which will define your dependencies.
-load("@my_deps//:requirements.bzl", "install_deps")
-# Call it to define repos for your requirements.
-install_deps()
-```
-
-#### pip rules
-
-Note that since `pip_parse` is a repository rule and therefore executes pip at WORKSPACE-evaluation time, Bazel has no
-information about the Python toolchain and cannot enforce that the interpreter
-used to invoke pip matches the interpreter used to run `py_binary` targets. By
-default, `pip_parse` uses the system command `"python3"`. To override this, pass in the
-`python_interpreter` attribute or `python_interpreter_target` attribute to `pip_parse`.
-
-You can have multiple `pip_parse`s in the same workspace.  Or use the pip extension multiple times when using bzlmod.
-This configuration will create multiple external repos that have no relation to one another 
-and may result in downloading the same wheels numerous times.
-
-As with any repository rule, if you would like to ensure that `pip_parse` is
-re-executed to pick up a non-hermetic change to your environment (e.g.,
-updating your system `python` interpreter), you can force it to re-execute by running
-`bazel sync --only [pip_parse name]`.
-
-Note: The `pip_install` rule is deprecated. `pip_parse` offers identical functionality, and both `pip_install` and `pip_parse` now have the same implementation. The name `pip_install` may be removed in a future version of the rules.
-
-The maintainers have made all reasonable efforts to facilitate a smooth transition. Still, some users of `pip_install` will need to replace their existing `requirements.txt` with a fully resolved set of dependencies using a tool such as `pip-tools` or the `compile_pip_requirements` repository rule.
-
-### Using third_party packages as dependencies
-
-Each extracted wheel repo contains a `py_library` target representing
-the wheel's contents. There are two ways to access this library. The
-first uses the `requirement()` function defined in the central
-repo's `//:requirements.bzl` file. This function maps a pip package
-name to a label:
-
-```starlark
-load("@my_deps//:requirements.bzl", "requirement")
-
-py_library(
-    name = "mylib",
-    srcs = ["mylib.py"],
-    deps = [
-        ":myotherlib",
-        requirement("some_pip_dep"),
-        requirement("another_pip_dep"),
-    ]
-)
-```
-
-The reason `requirement()` exists is that the pattern for the labels,
-while not expected to change frequently, is not guaranteed to be
-stable. Using `requirement()` ensures you do not have to refactor
-your `BUILD` files if the pattern changes.
-
-On the other hand, using `requirement()` has several drawbacks; see
-[this issue][requirements-drawbacks] for an enumeration. If you don't
-want to use `requirement()`, you can use the library
-labels directly instead. For `pip_parse`, the labels are of the following form:
-
-```starlark
-@{name}_{package}//:pkg
-```
-
-Here `name` is the `name` attribute that was passed to `pip_parse` and
-`package` is the pip package name with characters that are illegal in
-Bazel label names (e.g. `-`, `.`) replaced with `_`. If you need to
-update `name` from "old" to "new", then you can run the following
-buildozer command:
-
-```shell
-buildozer 'substitute deps @old_([^/]+)//:pkg @new_${1}//:pkg' //...:*
-```
-
-For `pip_install`, the labels are instead of the form:
-
-```starlark
-@{name}//pypi__{package}
-```
-
-[requirements-drawbacks]: https://github.com/bazelbuild/rules_python/issues/414
-
-#### 'Extras' dependencies
-
-Any 'extras' specified in the requirements lock file will be automatically added as transitive dependencies of the package. In the example above, you'd just put `requirement("useful_dep")`.
-
-### Consuming Wheel Dists Directly
-
-If you need to depend on the wheel dists themselves, for instance, to pass them
-to some other packaging tool, you can get a handle to them with the `whl_requirement` macro. For example:
-
-```starlark
-filegroup(
-    name = "whl_files",
-    data = [
-        whl_requirement("boto3"),
-    ]
-)
-```
-# Python Gazelle plugin
-
-[Gazelle](https://github.com/bazelbuild/bazel-gazelle)
-is a build file generator for Bazel projects. It can create new `BUILD.bazel` files for a project that follows language conventions and update existing build files to include new sources, dependencies, and options.
-
-Bazel may run Gazelle using the Gazelle rule, or it may be installed and run as a command line tool.
-
-See the documentation for Gazelle with rules_python [here](gazelle).
-
-## Migrating from the bundled rules
-
-The core rules are currently available in Bazel as built-in symbols, but this
-form is deprecated. Instead, you should depend on rules_python in your
-`WORKSPACE` file and load the Python rules from
-`@rules_python//python:defs.bzl`.
-
-A [buildifier](https://github.com/bazelbuild/buildtools/blob/master/buildifier/README.md)
-fix is available to automatically migrate `BUILD` and `.bzl` files to add the
-appropriate `load()` statements and rewrite uses of `native.py_*`.
-
-```sh
-# Also consider using the -r flag to modify an entire workspace.
-buildifier --lint=fix --warnings=native-py <files>
-```
-
-Currently, the `WORKSPACE` file needs to be updated manually as per [Getting
-started](#Getting-started) above.
-
-Note that Starlark-defined bundled symbols underneath
-`@bazel_tools//tools/python` are also deprecated. These are not yet rewritten
-by buildifier.
-
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 918a87a..c334fbc 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -13,9 +13,6 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
-load("@bazel_skylib//rules:write_file.bzl", "write_file")
-load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc")
 
 # NOTE: Only public visibility for historical reasons.
 # This package is only for rules_python to generate its own docs.
@@ -23,17 +20,6 @@
 
 licenses(["notice"])  # Apache 2.0
 
-_DOCS = [
-    "packaging",
-    "pip",
-    "py_cc_toolchain",
-    "py_cc_toolchain_info",
-    # TODO @aignas 2023-10-09: move some of the example code from the `.bzl` files
-    # to the markdown once #1476 is merged.
-    "py_console_script_binary",
-    "python",
-]
-
 # Temporary compatibility aliases for some other projects depending on the old
 # bzl_library targets.
 alias(
@@ -64,124 +50,3 @@
     deprecation = "Use //python/pip_install:pip_repository_bzl instead; Both the requirements " +
                   "parser and targets under //docs are internal",
 )
-
-# TODO: Stardoc does not guarantee consistent outputs accross platforms (Unix/Windows).
-# As a result we do not build or test docs on Windows.
-_TARGET_COMPATIBLE_WITH = select({
-    "@platforms//os:linux": [],
-    "@platforms//os:macos": [],
-    "//conditions:default": ["@platforms//:incompatible"],
-})
-
-stardoc(
-    name = "core-docs",
-    out = "python.md.gen",
-    input = "//python:defs.bzl",
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-    deps = [
-        "//python:defs_bzl",
-    ],
-)
-
-stardoc(
-    name = "pip-docs",
-    out = "pip.md.gen",
-    input = "//python:pip.bzl",
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-    deps = [
-        "//python:pip_bzl",
-    ],
-)
-
-stardoc(
-    name = "py-console-script-binary",
-    out = "py_console_script_binary.md.gen",
-    input = "//python/entry_points:py_console_script_binary.bzl",
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-    deps = [
-        "//python/entry_points:py_console_script_binary_bzl",
-    ],
-)
-
-stardoc(
-    name = "packaging-docs",
-    out = "packaging.md.gen",
-    input = "//python:packaging.bzl",
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-    deps = ["//python:packaging_bzl"],
-)
-
-stardoc(
-    name = "py_cc_toolchain-docs",
-    out = "py_cc_toolchain.md.gen",
-    # NOTE: The public file isn't used as the input because it would document
-    # the macro, which doesn't have the attribute documentation. The macro
-    # doesn't do anything interesting to users, so bypass it to avoid having to
-    # copy/paste all the rule's doc in the macro.
-    input = "//python/private:py_cc_toolchain_rule.bzl",
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-    deps = ["//python/private:py_cc_toolchain_bzl"],
-)
-
-stardoc(
-    name = "py_cc_toolchain_info-docs",
-    out = "py_cc_toolchain_info.md.gen",
-    input = "//python/cc:py_cc_toolchain_info.bzl",
-    deps = ["//python/cc:py_cc_toolchain_info_bzl"],
-)
-
-[
-    # retain any modifications made by the maintainers above the generated part
-    genrule(
-        name = "merge_" + k,
-        srcs = [
-            k + ".md",
-            k + ".md.gen",
-        ],
-        outs = [k + ".md_"],
-        cmd = ";".join([
-            "sed -En '/{comment_bait}/q;p' <$(location {first}) > $@",
-            "sed -E 's/{comment_doc}/{comment_note}/g' $(location {second}) >> $@",
-        ]).format(
-            comment_bait = "Stardoc: http:..skydoc.bazel.build -->",
-            comment_doc = "^<!.*(Stardoc:.*skydoc.bazel.build.*)",
-            comment_note = "<!-- Everything including and below this line replaced " +
-                           "with output from \\1",
-            first = k + ".md",
-            second = k + ".md.gen",
-        ),
-    )
-    for k in _DOCS
-]
-
-[
-    diff_test(
-        name = "check_" + k,
-        failure_message = "Please run:   bazel run //docs:update",
-        file1 = k + ".md",
-        file2 = k + ".md_",
-        tags = ["doc_check_test"],
-        target_compatible_with = _TARGET_COMPATIBLE_WITH,
-    )
-    for k in _DOCS
-]
-
-write_file(
-    name = "gen_update",
-    out = "update.sh",
-    content = [
-        "#!/usr/bin/env bash",
-        "cd $BUILD_WORKSPACE_DIRECTORY",
-    ] + [
-        "cp -fv bazel-bin/docs/{0}.md_ docs/{0}.md".format(k)
-        for k in _DOCS
-    ],
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-)
-
-sh_binary(
-    name = "update",
-    srcs = ["update.sh"],
-    data = ["merge_" + k for k in _DOCS],
-    target_compatible_with = _TARGET_COMPATIBLE_WITH,
-)
diff --git a/docs/coverage.md b/docs/coverage.md
deleted file mode 100644
index 63f2578..0000000
--- a/docs/coverage.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Setting up coverage
-
-As of Bazel 6, the Python toolchains and bootstrap logic supports providing
-coverage information using the `coverage` library.
-
-As of `rules_python` version `0.18.1`, builtin coverage support can be enabled
-when configuring toolchains.
-
-## Enabling `rules_python` coverage support
-
-Enabling the coverage support bundled with `rules_python` just requires setting an
-argument when registerting toolchains.
-
-For Bzlmod:
-
-```starlark
-python.toolchain(
-    "@python3_9_toolchains//:all",
-    configure_coverage_tool = True,
-)
-```
-
-For WORKSPACE configuration:
-
-```starlark
-python_register_toolchains(
-   register_coverage_tool = True,
-)
-```
-
-NOTE: This will implicitly add the version of `coverage` bundled with
-`rules_python` to the dependencies of `py_test` rules when `bazel coverage` is
-run. If a target already transitively depends on a different version of
-`coverage`, then behavior is undefined -- it is undefined which version comes
-first in the import path. If you find yourself in this situation, then you'll
-need to manually configure coverage (see below).
-
-## Manually configuring coverage
-
-To manually configure coverage support, you'll need to set the
-`py_runtime.coverage_tool` attribute. This attribute is a target that specifies
-the coverage entry point file and, optionally, client libraries that are added
-to `py_test` targets. Typically, this would be a `filegroup` that looked like:
-
-```starlark
-filegroup(
-  name = "coverage",
-  srcs = ["coverage_main.py"],
-  data = ["coverage_lib1.py", ...]
-)
-```
-
-Using `filegroup` isn't required, nor are including client libraries. The
-important behaviors of the target are:
-
-*   It provides a single output file OR it provides an executable output; this
-    output is treated as the coverage entry point.
-*   If it provides runfiles, then `runfiles.files` are included into `py_test`.
diff --git a/docs/packaging.md b/docs/packaging.md
deleted file mode 100644
index ae7c473..0000000
--- a/docs/packaging.md
+++ /dev/null
@@ -1,213 +0,0 @@
-# Packaging
-
-<!-- Everything including and below this line replaced with output from Stardoc: http://skydoc.bazel.build -->
-
-Public API for for building wheels.
-
-<a id="py_package"></a>
-
-## py_package
-
-<pre>
-py_package(<a href="#py_package-name">name</a>, <a href="#py_package-deps">deps</a>, <a href="#py_package-packages">packages</a>)
-</pre>
-
-A rule to select all files in transitive dependencies of deps which
-belong to given set of Python packages.
-
-This rule is intended to be used as data dependency to py_wheel rule.
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="py_package-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="py_package-deps"></a>deps |  -   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `[]`  |
-| <a id="py_package-packages"></a>packages |  List of Python packages to include in the distribution. Sub-packages are automatically included.   | List of strings | optional |  `[]`  |
-
-
-<a id="py_wheel_dist"></a>
-
-## py_wheel_dist
-
-<pre>
-py_wheel_dist(<a href="#py_wheel_dist-name">name</a>, <a href="#py_wheel_dist-out">out</a>, <a href="#py_wheel_dist-wheel">wheel</a>)
-</pre>
-
-Prepare a dist/ folder, following Python's packaging standard practice.
-
-See https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives
-which recommends a dist/ folder containing the wheel file(s), source distributions, etc.
-
-This also has the advantage that stamping information is included in the wheel's filename.
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="py_wheel_dist-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="py_wheel_dist-out"></a>out |  name of the resulting directory   | String | required |  |
-| <a id="py_wheel_dist-wheel"></a>wheel |  a [py_wheel rule](/docs/packaging.md#py_wheel_rule)   | <a href="https://bazel.build/concepts/labels">Label</a> | optional |  `None`  |
-
-
-<a id="py_wheel_rule"></a>
-
-## py_wheel_rule
-
-<pre>
-py_wheel_rule(<a href="#py_wheel_rule-name">name</a>, <a href="#py_wheel_rule-deps">deps</a>, <a href="#py_wheel_rule-abi">abi</a>, <a href="#py_wheel_rule-author">author</a>, <a href="#py_wheel_rule-author_email">author_email</a>, <a href="#py_wheel_rule-classifiers">classifiers</a>, <a href="#py_wheel_rule-console_scripts">console_scripts</a>,
-              <a href="#py_wheel_rule-description_content_type">description_content_type</a>, <a href="#py_wheel_rule-description_file">description_file</a>, <a href="#py_wheel_rule-distribution">distribution</a>, <a href="#py_wheel_rule-entry_points">entry_points</a>,
-              <a href="#py_wheel_rule-extra_distinfo_files">extra_distinfo_files</a>, <a href="#py_wheel_rule-extra_requires">extra_requires</a>, <a href="#py_wheel_rule-homepage">homepage</a>, <a href="#py_wheel_rule-incompatible_normalize_name">incompatible_normalize_name</a>,
-              <a href="#py_wheel_rule-incompatible_normalize_version">incompatible_normalize_version</a>, <a href="#py_wheel_rule-license">license</a>, <a href="#py_wheel_rule-platform">platform</a>, <a href="#py_wheel_rule-project_urls">project_urls</a>, <a href="#py_wheel_rule-python_requires">python_requires</a>,
-              <a href="#py_wheel_rule-python_tag">python_tag</a>, <a href="#py_wheel_rule-requires">requires</a>, <a href="#py_wheel_rule-stamp">stamp</a>, <a href="#py_wheel_rule-strip_path_prefixes">strip_path_prefixes</a>, <a href="#py_wheel_rule-summary">summary</a>, <a href="#py_wheel_rule-version">version</a>)
-</pre>
-
-Internal rule used by the [py_wheel macro](/docs/packaging.md#py_wheel).
-
-These intentionally have the same name to avoid sharp edges with Bazel macros.
-For example, a `bazel query` for a user's `py_wheel` macro expands to `py_wheel` targets,
-in the way they expect.
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="py_wheel_rule-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="py_wheel_rule-deps"></a>deps |  Targets to be included in the distribution.<br><br>The targets to package are usually `py_library` rules or filesets (for packaging data files).<br><br>Note it's usually better to package `py_library` targets and use `entry_points` attribute to specify `console_scripts` than to package `py_binary` rules. `py_binary` targets would wrap a executable script that tries to locate `.runfiles` directory which is not packaged in the wheel.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `[]`  |
-| <a id="py_wheel_rule-abi"></a>abi |  Python ABI tag. 'none' for pure-Python wheels.   | String | optional |  `"none"`  |
-| <a id="py_wheel_rule-author"></a>author |  A string specifying the author of the package.   | String | optional |  `""`  |
-| <a id="py_wheel_rule-author_email"></a>author_email |  A string specifying the email address of the package author.   | String | optional |  `""`  |
-| <a id="py_wheel_rule-classifiers"></a>classifiers |  A list of strings describing the categories for the package. For valid classifiers see https://pypi.org/classifiers   | List of strings | optional |  `[]`  |
-| <a id="py_wheel_rule-console_scripts"></a>console_scripts |  Deprecated console_script entry points, e.g. `{'main': 'examples.wheel.main:main'}`.<br><br>Deprecated: prefer the `entry_points` attribute, which supports `console_scripts` as well as other entry points.   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional |  `{}`  |
-| <a id="py_wheel_rule-description_content_type"></a>description_content_type |  The type of contents in description_file. If not provided, the type will be inferred from the extension of description_file. Also see https://packaging.python.org/en/latest/specifications/core-metadata/#description-content-type   | String | optional |  `""`  |
-| <a id="py_wheel_rule-description_file"></a>description_file |  A file containing text describing the package.   | <a href="https://bazel.build/concepts/labels">Label</a> | optional |  `None`  |
-| <a id="py_wheel_rule-distribution"></a>distribution |  Name of the distribution.<br><br>This should match the project name onm PyPI. It's also the name that is used to refer to the package in other packages' dependencies.<br><br>Workspace status keys are expanded using `{NAME}` format, for example:  - `distribution = "package.{CLASSIFIER}"`  - `distribution = "{DISTRIBUTION}"`<br><br>For the available keys, see https://bazel.build/docs/user-manual#workspace-status   | String | required |  |
-| <a id="py_wheel_rule-entry_points"></a>entry_points |  entry_points, e.g. `{'console_scripts': ['main = examples.wheel.main:main']}`.   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> List of strings</a> | optional |  `{}`  |
-| <a id="py_wheel_rule-extra_distinfo_files"></a>extra_distinfo_files |  Extra files to add to distinfo directory in the archive.   | <a href="https://bazel.build/rules/lib/dict">Dictionary: Label -> String</a> | optional |  `{}`  |
-| <a id="py_wheel_rule-extra_requires"></a>extra_requires |  List of optional requirements for this package   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> List of strings</a> | optional |  `{}`  |
-| <a id="py_wheel_rule-homepage"></a>homepage |  A string specifying the URL for the package homepage.   | String | optional |  `""`  |
-| <a id="py_wheel_rule-incompatible_normalize_name"></a>incompatible_normalize_name |  Normalize the package distribution name according to latest Python packaging standards.<br><br>See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode and https://packaging.python.org/en/latest/specifications/name-normalization/.<br><br>Apart from the valid names according to the above, we also accept '{' and '}', which may be used as placeholders for stamping.   | Boolean | optional |  `False`  |
-| <a id="py_wheel_rule-incompatible_normalize_version"></a>incompatible_normalize_version |  Normalize the package version according to PEP440 standard. With this option set to True, if the user wants to pass any stamp variables, they have to be enclosed in '{}', e.g. '{BUILD_TIMESTAMP}'.   | Boolean | optional |  `False`  |
-| <a id="py_wheel_rule-license"></a>license |  A string specifying the license of the package.   | String | optional |  `""`  |
-| <a id="py_wheel_rule-platform"></a>platform |  Supported platform. Use 'any' for pure-Python wheel.<br><br>If you have included platform-specific data, such as a .pyd or .so extension module, you will need to specify the platform in standard pip format. If you support multiple platforms, you can define platform constraints, then use a select() to specify the appropriate specifier, eg:<br><br>` platform = select({     "//platforms:windows_x86_64": "win_amd64",     "//platforms:macos_x86_64": "macosx_10_7_x86_64",     "//platforms:linux_x86_64": "manylinux2014_x86_64", }) `   | String | optional |  `"any"`  |
-| <a id="py_wheel_rule-project_urls"></a>project_urls |  A string dict specifying additional browsable URLs for the project and corresponding labels, where label is the key and url is the value. e.g `{{"Bug Tracker": "http://bitbucket.org/tarek/distribute/issues/"}}`   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional |  `{}`  |
-| <a id="py_wheel_rule-python_requires"></a>python_requires |  Python versions required by this distribution, e.g. '>=3.5,<3.7'   | String | optional |  `""`  |
-| <a id="py_wheel_rule-python_tag"></a>python_tag |  Supported Python version(s), eg `py3`, `cp35.cp36`, etc   | String | optional |  `"py3"`  |
-| <a id="py_wheel_rule-requires"></a>requires |  List of requirements for this package. See the section on [Declaring required dependency](https://setuptools.readthedocs.io/en/latest/userguide/dependency_management.html#declaring-dependencies) for details and examples of the format of this argument.   | List of strings | optional |  `[]`  |
-| <a id="py_wheel_rule-stamp"></a>stamp |  Whether to encode build information into the wheel. Possible values:<br><br>- `stamp = 1`: Always stamp the build information into the wheel, even in [--nostamp](https://docs.bazel.build/versions/main/user-manual.html#flag--stamp) builds. This setting should be avoided, since it potentially kills remote caching for the target and any downstream actions that depend on it.<br><br>- `stamp = 0`: Always replace build information by constant values. This gives good build result caching.<br><br>- `stamp = -1`: Embedding of build information is controlled by the [--[no]stamp](https://docs.bazel.build/versions/main/user-manual.html#flag--stamp) flag.<br><br>Stamped targets are not rebuilt unless their dependencies change.   | Integer | optional |  `-1`  |
-| <a id="py_wheel_rule-strip_path_prefixes"></a>strip_path_prefixes |  path prefixes to strip from files added to the generated package   | List of strings | optional |  `[]`  |
-| <a id="py_wheel_rule-summary"></a>summary |  A one-line summary of what the distribution does   | String | optional |  `""`  |
-| <a id="py_wheel_rule-version"></a>version |  Version number of the package.<br><br>Note that this attribute supports stamp format strings as well as 'make variables'. For example:   - `version = "1.2.3-{BUILD_TIMESTAMP}"`   - `version = "{BUILD_EMBED_LABEL}"`   - `version = "$(VERSION)"`<br><br>Note that Bazel's output filename cannot include the stamp information, as outputs must be known during the analysis phase and the stamp data is available only during the action execution.<br><br>The [`py_wheel`](/docs/packaging.md#py_wheel) macro produces a `.dist`-suffix target which creates a `dist/` folder containing the wheel with the stamped name, suitable for publishing.<br><br>See [`py_wheel_dist`](/docs/packaging.md#py_wheel_dist) for more info.   | String | required |  |
-
-
-<a id="PyWheelInfo"></a>
-
-## PyWheelInfo
-
-<pre>
-PyWheelInfo(<a href="#PyWheelInfo-name_file">name_file</a>, <a href="#PyWheelInfo-wheel">wheel</a>)
-</pre>
-
-Information about a wheel produced by `py_wheel`
-
-**FIELDS**
-
-
-| Name  | Description |
-| :------------- | :------------- |
-| <a id="PyWheelInfo-name_file"></a>name_file |  File: A file containing the canonical name of the wheel (after stamping, if enabled).    |
-| <a id="PyWheelInfo-wheel"></a>wheel |  File: The wheel file itself.    |
-
-
-<a id="py_wheel"></a>
-
-## py_wheel
-
-<pre>
-py_wheel(<a href="#py_wheel-name">name</a>, <a href="#py_wheel-twine">twine</a>, <a href="#py_wheel-publish_args">publish_args</a>, <a href="#py_wheel-kwargs">kwargs</a>)
-</pre>
-
-Builds a Python Wheel.
-
-Wheels are Python distribution format defined in https://www.python.org/dev/peps/pep-0427/.
-
-This macro packages a set of targets into a single wheel.
-It wraps the [py_wheel rule](#py_wheel_rule).
-
-Currently only pure-python wheels are supported.
-
-Examples:
-
-```python
-# Package some specific py_library targets, without their dependencies
-py_wheel(
-    name = "minimal_with_py_library",
-    # Package data. We're building "example_minimal_library-0.0.1-py3-none-any.whl"
-    distribution = "example_minimal_library",
-    python_tag = "py3",
-    version = "0.0.1",
-    deps = [
-        "//examples/wheel/lib:module_with_data",
-        "//examples/wheel/lib:simple_module",
-    ],
-)
-
-# Use py_package to collect all transitive dependencies of a target,
-# selecting just the files within a specific python package.
-py_package(
-    name = "example_pkg",
-    # Only include these Python packages.
-    packages = ["examples.wheel"],
-    deps = [":main"],
-)
-
-py_wheel(
-    name = "minimal_with_py_package",
-    # Package data. We're building "example_minimal_package-0.0.1-py3-none-any.whl"
-    distribution = "example_minimal_package",
-    python_tag = "py3",
-    version = "0.0.1",
-    deps = [":example_pkg"],
-)
-```
-
-To publish the wheel to Pypi, the twine package is required.
-rules_python doesn't provide twine itself, see https://github.com/bazelbuild/rules_python/issues/1016
-However you can install it with pip_parse, just like we do in the WORKSPACE file in rules_python.
-
-Once you've installed twine, you can pass its label to the `twine` attribute of this macro,
-to get a "[name].publish" target.
-
-Example:
-
-```python
-py_wheel(
-    name = "my_wheel",
-    twine = "@publish_deps_twine//:pkg",
-    ...
-)
-```
-
-Now you can run a command like the following, which publishes to https://test.pypi.org/
-
-```sh
-% TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-*** \
-    bazel run --stamp --embed_label=1.2.4 -- \
-    //path/to:my_wheel.publish --repository testpypi
-```
-
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="py_wheel-name"></a>name |  A unique name for this target.   |  none |
-| <a id="py_wheel-twine"></a>twine |  A label of the external location of the py_library target for twine   |  `None` |
-| <a id="py_wheel-publish_args"></a>publish_args |  arguments passed to twine, e.g. ["--repository-url", "https://pypi.my.org/simple/"]. These are subject to make var expansion, as with the `args` attribute. Note that you can also pass additional args to the bazel run command as in the example above.   |  `[]` |
-| <a id="py_wheel-kwargs"></a>kwargs |  other named parameters passed to the underlying [py_wheel rule](#py_wheel_rule)   |  none |
-
-
diff --git a/docs/pip.md b/docs/pip.md
deleted file mode 100644
index f6b7af7..0000000
--- a/docs/pip.md
+++ /dev/null
@@ -1,267 +0,0 @@
-# pip integration
-
-This contains a set of rules that are used to support inclusion of third-party
-dependencies via fully locked `requirements.txt` files. Some of the exported
-symbols should not be used and they are either undocumented here or marked as
-for internal use only.
-
-<!-- Everything including and below this line replaced with output from Stardoc: http://skydoc.bazel.build -->
-
-Import pip requirements into Bazel.
-
-<a id="whl_library_alias"></a>
-
-## whl_library_alias
-
-<pre>
-whl_library_alias(<a href="#whl_library_alias-name">name</a>, <a href="#whl_library_alias-default_version">default_version</a>, <a href="#whl_library_alias-repo_mapping">repo_mapping</a>, <a href="#whl_library_alias-version_map">version_map</a>, <a href="#whl_library_alias-wheel_name">wheel_name</a>)
-</pre>
-
-
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="whl_library_alias-name"></a>name |  A unique name for this repository.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="whl_library_alias-default_version"></a>default_version |  Optional Python version in major.minor format, e.g. '3.10'.The Python version of the wheel to use when the versions from `version_map` don't match. This allows the default (version unaware) rules to match and select a wheel. If not specified, then the default rules won't be able to resolve a wheel and an error will occur.   | String | optional |  `""`  |
-| <a id="whl_library_alias-repo_mapping"></a>repo_mapping |  A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<p>For example, an entry `"@foo": "@bar"` declares that, for any time this repository depends on `@foo` (such as a dependency on `@foo//some:target`, it should actually resolve that dependency within globally-declared `@bar` (`@bar//some:target`).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
-| <a id="whl_library_alias-version_map"></a>version_map |  -   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
-| <a id="whl_library_alias-wheel_name"></a>wheel_name |  -   | String | required |  |
-
-
-<a id="compile_pip_requirements"></a>
-
-## compile_pip_requirements
-
-<pre>
-compile_pip_requirements(<a href="#compile_pip_requirements-name">name</a>, <a href="#compile_pip_requirements-extra_args">extra_args</a>, <a href="#compile_pip_requirements-extra_deps">extra_deps</a>, <a href="#compile_pip_requirements-generate_hashes">generate_hashes</a>, <a href="#compile_pip_requirements-py_binary">py_binary</a>, <a href="#compile_pip_requirements-py_test">py_test</a>,
-                         <a href="#compile_pip_requirements-requirements_in">requirements_in</a>, <a href="#compile_pip_requirements-requirements_txt">requirements_txt</a>, <a href="#compile_pip_requirements-requirements_darwin">requirements_darwin</a>, <a href="#compile_pip_requirements-requirements_linux">requirements_linux</a>,
-                         <a href="#compile_pip_requirements-requirements_windows">requirements_windows</a>, <a href="#compile_pip_requirements-visibility">visibility</a>, <a href="#compile_pip_requirements-tags">tags</a>, <a href="#compile_pip_requirements-kwargs">kwargs</a>)
-</pre>
-
-Generates targets for managing pip dependencies with pip-compile.
-
-By default this rules generates a filegroup named "[name]" which can be included in the data
-of some other compile_pip_requirements rule that references these requirements
-(e.g. with `-r ../other/requirements.txt`).
-
-It also generates two targets for running pip-compile:
-
-- validate with `bazel test [name]_test`
-- update with   `bazel run [name].update`
-
-If you are using a version control system, the requirements.txt generated by this rule should
-be checked into it to ensure that all developers/users have the same dependency versions.
-
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="compile_pip_requirements-name"></a>name |  base name for generated targets, typically "requirements".   |  none |
-| <a id="compile_pip_requirements-extra_args"></a>extra_args |  passed to pip-compile.   |  `[]` |
-| <a id="compile_pip_requirements-extra_deps"></a>extra_deps |  extra dependencies passed to pip-compile.   |  `[]` |
-| <a id="compile_pip_requirements-generate_hashes"></a>generate_hashes |  whether to put hashes in the requirements_txt file.   |  `True` |
-| <a id="compile_pip_requirements-py_binary"></a>py_binary |  the py_binary rule to be used.   |  `<function py_binary>` |
-| <a id="compile_pip_requirements-py_test"></a>py_test |  the py_test rule to be used.   |  `<function py_test>` |
-| <a id="compile_pip_requirements-requirements_in"></a>requirements_in |  file expressing desired dependencies.   |  `None` |
-| <a id="compile_pip_requirements-requirements_txt"></a>requirements_txt |  result of "compiling" the requirements.in file.   |  `None` |
-| <a id="compile_pip_requirements-requirements_darwin"></a>requirements_darwin |  File of darwin specific resolve output to check validate if requirement.in has changes.   |  `None` |
-| <a id="compile_pip_requirements-requirements_linux"></a>requirements_linux |  File of linux specific resolve output to check validate if requirement.in has changes.   |  `None` |
-| <a id="compile_pip_requirements-requirements_windows"></a>requirements_windows |  File of windows specific resolve output to check validate if requirement.in has changes.   |  `None` |
-| <a id="compile_pip_requirements-visibility"></a>visibility |  passed to both the _test and .update rules.   |  `["//visibility:private"]` |
-| <a id="compile_pip_requirements-tags"></a>tags |  tagging attribute common to all build rules, passed to both the _test and .update rules.   |  `None` |
-| <a id="compile_pip_requirements-kwargs"></a>kwargs |  other bazel attributes passed to the "_test" rule.   |  none |
-
-
-<a id="multi_pip_parse"></a>
-
-## multi_pip_parse
-
-<pre>
-multi_pip_parse(<a href="#multi_pip_parse-name">name</a>, <a href="#multi_pip_parse-default_version">default_version</a>, <a href="#multi_pip_parse-python_versions">python_versions</a>, <a href="#multi_pip_parse-python_interpreter_target">python_interpreter_target</a>,
-                <a href="#multi_pip_parse-requirements_lock">requirements_lock</a>, <a href="#multi_pip_parse-kwargs">kwargs</a>)
-</pre>
-
-NOT INTENDED FOR DIRECT USE!
-
-This is intended to be used by the multi_pip_parse implementation in the template of the
-multi_toolchain_aliases repository rule.
-
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="multi_pip_parse-name"></a>name |  the name of the multi_pip_parse repository.   |  none |
-| <a id="multi_pip_parse-default_version"></a>default_version |  the default Python version.   |  none |
-| <a id="multi_pip_parse-python_versions"></a>python_versions |  all Python toolchain versions currently registered.   |  none |
-| <a id="multi_pip_parse-python_interpreter_target"></a>python_interpreter_target |  a dictionary which keys are Python versions and values are resolved host interpreters.   |  none |
-| <a id="multi_pip_parse-requirements_lock"></a>requirements_lock |  a dictionary which keys are Python versions and values are locked requirements files.   |  none |
-| <a id="multi_pip_parse-kwargs"></a>kwargs |  extra arguments passed to all wrapped pip_parse.   |  none |
-
-**RETURNS**
-
-The internal implementation of multi_pip_parse repository rule.
-
-
-<a id="package_annotation"></a>
-
-## package_annotation
-
-<pre>
-package_annotation(<a href="#package_annotation-additive_build_content">additive_build_content</a>, <a href="#package_annotation-copy_files">copy_files</a>, <a href="#package_annotation-copy_executables">copy_executables</a>, <a href="#package_annotation-data">data</a>, <a href="#package_annotation-data_exclude_glob">data_exclude_glob</a>,
-                   <a href="#package_annotation-srcs_exclude_glob">srcs_exclude_glob</a>)
-</pre>
-
-Annotations to apply to the BUILD file content from package generated from a `pip_repository` rule.
-
-[cf]: https://github.com/bazelbuild/bazel-skylib/blob/main/docs/copy_file_doc.md
-
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="package_annotation-additive_build_content"></a>additive_build_content |  Raw text to add to the generated `BUILD` file of a package.   |  `None` |
-| <a id="package_annotation-copy_files"></a>copy_files |  A mapping of `src` and `out` files for [@bazel_skylib//rules:copy_file.bzl][cf]   |  `{}` |
-| <a id="package_annotation-copy_executables"></a>copy_executables |  A mapping of `src` and `out` files for [@bazel_skylib//rules:copy_file.bzl][cf]. Targets generated here will also be flagged as executable.   |  `{}` |
-| <a id="package_annotation-data"></a>data |  A list of labels to add as `data` dependencies to the generated `py_library` target.   |  `[]` |
-| <a id="package_annotation-data_exclude_glob"></a>data_exclude_glob |  A list of exclude glob patterns to add as `data` to the generated `py_library` target.   |  `[]` |
-| <a id="package_annotation-srcs_exclude_glob"></a>srcs_exclude_glob |  A list of labels to add as `srcs` to the generated `py_library` target.   |  `[]` |
-
-**RETURNS**
-
-str: A json encoded string of the provided content.
-
-
-<a id="pip_install"></a>
-
-## pip_install
-
-<pre>
-pip_install(<a href="#pip_install-requirements">requirements</a>, <a href="#pip_install-name">name</a>, <a href="#pip_install-allow_pip_install">allow_pip_install</a>, <a href="#pip_install-kwargs">kwargs</a>)
-</pre>
-
-Will be removed in 0.28.0
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="pip_install-requirements"></a>requirements |  A 'requirements.txt' pip requirements file.   |  `None` |
-| <a id="pip_install-name"></a>name |  A unique name for the created external repository (default 'pip').   |  `"pip"` |
-| <a id="pip_install-allow_pip_install"></a>allow_pip_install |  change this to keep this rule working (default False).   |  `False` |
-| <a id="pip_install-kwargs"></a>kwargs |  Additional arguments to the [`pip_repository`](./pip_repository.md) repository rule.   |  none |
-
-
-<a id="pip_parse"></a>
-
-## pip_parse
-
-<pre>
-pip_parse(<a href="#pip_parse-requirements">requirements</a>, <a href="#pip_parse-requirements_lock">requirements_lock</a>, <a href="#pip_parse-name">name</a>, <a href="#pip_parse-kwargs">kwargs</a>)
-</pre>
-
-Accepts a locked/compiled requirements file and installs the dependencies listed within.
-
-Those dependencies become available in a generated `requirements.bzl` file.
-You can instead check this `requirements.bzl` file into your repo, see the "vendoring" section below.
-
-This macro wraps the [`pip_repository`](./pip_repository.md) rule that invokes `pip`.
-In your WORKSPACE file:
-
-```python
-load("@rules_python//python:pip.bzl", "pip_parse")
-
-pip_parse(
-    name = "pip_deps",
-    requirements_lock = ":requirements.txt",
-)
-
-load("@pip_deps//:requirements.bzl", "install_deps")
-
-install_deps()
-```
-
-You can then reference installed dependencies from a `BUILD` file with:
-
-```python
-load("@pip_deps//:requirements.bzl", "requirement")
-
-py_library(
-    name = "bar",
-    ...
-    deps = [
-       "//my/other:dep",
-       requirement("requests"),
-       requirement("numpy"),
-    ],
-)
-```
-
-In addition to the `requirement` macro, which is used to access the generated `py_library`
-target generated from a package's wheel, The generated `requirements.bzl` file contains
-functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
-
-[whl_ep]: https://packaging.python.org/specifications/entry-points/
-
-```python
-load("@pip_deps//:requirements.bzl", "entry_point")
-
-alias(
-    name = "pip-compile",
-    actual = entry_point(
-        pkg = "pip-tools",
-        script = "pip-compile",
-    ),
-)
-```
-
-Note that for packages whose name and script are the same, only the name of the package
-is needed when calling the `entry_point` macro.
-
-```python
-load("@pip_deps//:requirements.bzl", "entry_point")
-
-alias(
-    name = "flake8",
-    actual = entry_point("flake8"),
-)
-```
-
-## Vendoring the requirements.bzl file
-
-In some cases you may not want to generate the requirements.bzl file as a repository rule
-while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module
-such as a ruleset, you may want to include the requirements.bzl file rather than make your users
-install the WORKSPACE setup to generate it.
-See https://github.com/bazelbuild/rules_python/issues/608
-
-This is the same workflow as Gazelle, which creates `go_repository` rules with
-[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
-
-To do this, use the "write to source file" pattern documented in
-https://blog.aspect.dev/bazel-can-write-to-the-source-folder
-to put a copy of the generated requirements.bzl into your project.
-Then load the requirements.bzl file directly rather than from the generated repository.
-See the example in rules_python/examples/pip_parse_vendored.
-
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="pip_parse-requirements"></a>requirements |  Deprecated. See requirements_lock.   |  `None` |
-| <a id="pip_parse-requirements_lock"></a>requirements_lock |  A fully resolved 'requirements.txt' pip requirement file containing the transitive set of your dependencies. If this file is passed instead of 'requirements' no resolve will take place and pip_repository will create individual repositories for each of your dependencies so that wheels are fetched/built only for the targets specified by 'build/run/test'. Note that if your lockfile is platform-dependent, you can use the `requirements_[platform]` attributes.   |  `None` |
-| <a id="pip_parse-name"></a>name |  The name of the generated repository. The generated repositories containing each requirement will be of the form `<name>_<requirement-name>`.   |  `"pip_parsed_deps"` |
-| <a id="pip_parse-kwargs"></a>kwargs |  Additional arguments to the [`pip_repository`](./pip_repository.md) repository rule.   |  none |
-
-
diff --git a/docs/py_cc_toolchain.md b/docs/py_cc_toolchain.md
deleted file mode 100644
index 49fe7ef..0000000
--- a/docs/py_cc_toolchain.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Python C/C++ toolchain rule
-
-<!-- Everything including and below this line replaced with output from Stardoc: http://skydoc.bazel.build -->
-
-Implementation of py_cc_toolchain rule.
-
-NOTE: This is a beta-quality feature. APIs subject to change until
-https://github.com/bazelbuild/rules_python/issues/824 is considered done.
-
-<a id="py_cc_toolchain"></a>
-
-## py_cc_toolchain
-
-<pre>
-py_cc_toolchain(<a href="#py_cc_toolchain-name">name</a>, <a href="#py_cc_toolchain-headers">headers</a>, <a href="#py_cc_toolchain-python_version">python_version</a>)
-</pre>
-
-A toolchain for a Python runtime's C/C++ information (e.g. headers)
-
-This rule carries information about the C/C++ side of a Python runtime, e.g.
-headers, shared libraries, etc.
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="py_cc_toolchain-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="py_cc_toolchain-headers"></a>headers |  Target that provides the Python headers. Typically this is a cc_library target.   | <a href="https://bazel.build/concepts/labels">Label</a> | required |  |
-| <a id="py_cc_toolchain-python_version"></a>python_version |  The Major.minor Python version, e.g. 3.11   | String | required |  |
-
-
diff --git a/docs/py_cc_toolchain_info.md b/docs/py_cc_toolchain_info.md
deleted file mode 100644
index 42dad95..0000000
--- a/docs/py_cc_toolchain_info.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Python C/C++ toolchain provider info.
-
-<!-- Everything including and below this line replaced with output from Stardoc: http://skydoc.bazel.build -->
-
-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.
-
-<a id="PyCcToolchainInfo"></a>
-
-## PyCcToolchainInfo
-
-<pre>
-PyCcToolchainInfo(<a href="#PyCcToolchainInfo-headers">headers</a>, <a href="#PyCcToolchainInfo-python_version">python_version</a>)
-</pre>
-
-C/C++ information about the Python runtime.
-
-**FIELDS**
-
-
-| Name  | Description |
-| :------------- | :------------- |
-| <a id="PyCcToolchainInfo-headers"></a>headers |  (struct) Information about the header files, with fields:   * providers_map: a dict of string to provider instances. The key should be     a fully qualified name (e.g. `@rules_foo//bar:baz.bzl#MyInfo`) of the     provider to uniquely identify its type.<br><br>    The following keys are always present:       * CcInfo: the CcInfo provider instance for the headers.       * DefaultInfo: the DefaultInfo provider instance for the headers.<br><br>    A map is used to allow additional providers from the originating headers     target (typically a `cc_library`) to be propagated to consumers (directly     exposing a Target object can cause memory issues and is an anti-pattern).<br><br>    When consuming this map, it's suggested to use `providers_map.values()` to     return all providers; or copy the map and filter out or replace keys as     appropriate. Note that any keys beginning with `_` (underscore) are     considered private and should be forward along as-is (this better allows     e.g. `:current_py_cc_headers` to act as the underlying headers target it     represents).    |
-| <a id="PyCcToolchainInfo-python_version"></a>python_version |  (str) The Python Major.Minor version.    |
-
-
diff --git a/docs/py_console_script_binary.md b/docs/py_console_script_binary.md
deleted file mode 100644
index e7cc9bd..0000000
--- a/docs/py_console_script_binary.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# //pytho/entrypoints:py_console_script_binary
-
-This rule is to make it easier to generate `console_script` entry points
-as per Python [specification].
-
-Generate a `py_binary` target for a particular console_script `entry_point`
-from a PyPI package, e.g. for creating an executable `pylint` target use:
-```starlark
-load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
-
-py_console_script_binary(
-    name = "pylint",
-    pkg = "@pip//pylint",
-)
-```
-
-Or for more advanced setups you can also specify extra dependencies and the
-exact script name you want to call. It is useful for tools like `flake8`, `pylint`,
-`pytest`, which have plugin discovery methods and discover dependencies from the
-PyPI packages available in the `PYTHONPATH`.
-```starlark
-load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
-
-py_console_script_binary(
-    name = "pylint_with_deps",
-    pkg = "@pip//pylint",
-    # Because `pylint` has multiple console_scripts available, we have to
-    # specify which we want if the name of the target name 'pylint_with_deps'
-    # cannot be used to guess the entry_point script.
-    script = "pylint",
-    deps = [
-        # One can add extra dependencies to the entry point.
-        # This specifically allows us to add plugins to pylint.
-        "@pip//pylint_print",
-    ],
-)
-```
-
-A specific Python version can be forced by using the generated version-aware
-wrappers, e.g. to force Python 3.9:
-```starlark
-load("@python_versions//3.9:defs.bzl", "py_console_script_binary")
-
-py_console_script_binary(
-    name = "yamllint",
-    pkg = "@pip//yamllint",
-)
-```
-
-Alternatively, the [`py_console_script_binary.binary_rule`] arg can be passed
-the version-bound `py_binary` symbol, or any other `py_binary`-compatible rule
-of your choosing:
-```starlark
-load("@python_versions//3.9:defs.bzl", "py_binary")
-load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
-
-py_console_script_binary(
-    name = "yamllint",
-    pkg = "@pip//yamllint:pkg",
-    binary_rule = py_binary,
-)
-```
-
-[specification]: https://packaging.python.org/en/latest/specifications/entry-points/
-[`py_console_script_binary.binary_rule`]: #py_console_script_binary-binary_rule
-
-
-<!-- Everything including and below this line replaced with output from Stardoc: http://skydoc.bazel.build -->
-
-Creates an executable (a non-test binary) for console_script entry points.
-
-<a id="py_console_script_binary"></a>
-
-## py_console_script_binary
-
-<pre>
-py_console_script_binary(<a href="#py_console_script_binary-name">name</a>, <a href="#py_console_script_binary-pkg">pkg</a>, <a href="#py_console_script_binary-entry_points_txt">entry_points_txt</a>, <a href="#py_console_script_binary-script">script</a>, <a href="#py_console_script_binary-binary_rule">binary_rule</a>, <a href="#py_console_script_binary-kwargs">kwargs</a>)
-</pre>
-
-Generate a py_binary for a console_script entry_point.
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="py_console_script_binary-name"></a>name |  str, The name of the resulting target.   |  none |
-| <a id="py_console_script_binary-pkg"></a>pkg |  target, the package for which to generate the script.   |  none |
-| <a id="py_console_script_binary-entry_points_txt"></a>entry_points_txt |  optional target, the entry_points.txt file to parse for available console_script values. It may be a single file, or a group of files, but must contain a file named `entry_points.txt`. If not specified, defaults to the `dist_info` target in the same package as the `pkg` Label.   |  `None` |
-| <a id="py_console_script_binary-script"></a>script |  str, The console script name that the py_binary is going to be generated for. Defaults to the normalized name attribute.   |  `None` |
-| <a id="py_console_script_binary-binary_rule"></a>binary_rule |  callable, The rule/macro to use to instantiate the target. It's expected to behave like `py_binary`. Defaults to @rules_python//python:py_binary.bzl#py_binary.   |  `<function py_binary>` |
-| <a id="py_console_script_binary-kwargs"></a>kwargs |  Extra parameters forwarded to binary_rule.   |  none |
-
-
diff --git a/docs/python.md b/docs/python.md
deleted file mode 100644
index b0f14b3..0000000
--- a/docs/python.md
+++ /dev/null
@@ -1,225 +0,0 @@
-# Core Python rules
-
-<!-- Everything including and below this line replaced with output from Stardoc: http://skydoc.bazel.build -->
-
-Core rules for building Python projects.
-
-<a id="current_py_toolchain"></a>
-
-## current_py_toolchain
-
-<pre>
-current_py_toolchain(<a href="#current_py_toolchain-name">name</a>)
-</pre>
-
-This rule exists so that the current python toolchain can be used in the `toolchains` attribute of
-other rules, such as genrule. It allows exposing a python toolchain after toolchain resolution has
-happened, to a rule which expects a concrete implementation of a toolchain, rather than a
-toolchain_type which could be resolved to that toolchain.
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="current_py_toolchain-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-
-
-<a id="py_import"></a>
-
-## py_import
-
-<pre>
-py_import(<a href="#py_import-name">name</a>, <a href="#py_import-deps">deps</a>, <a href="#py_import-srcs">srcs</a>)
-</pre>
-
-This rule allows the use of Python packages as dependencies.
-
-It imports the given `.egg` file(s), which might be checked in source files,
-fetched externally as with `http_file`, or produced as outputs of other rules.
-
-It may be used like a `py_library`, in the `deps` of other Python rules.
-
-This is similar to [java_import](https://docs.bazel.build/versions/master/be/java.html#java_import).
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="py_import-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="py_import-deps"></a>deps |  The list of other libraries to be linked in to the binary target.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `[]`  |
-| <a id="py_import-srcs"></a>srcs |  The list of Python package files provided to Python targets that depend on this target. Note that currently only the .egg format is accepted. For .whl files, try the whl_library rule. We accept contributions to extend py_import to handle .whl.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `[]`  |
-
-
-<a id="py_binary"></a>
-
-## py_binary
-
-<pre>
-py_binary(<a href="#py_binary-attrs">attrs</a>)
-</pre>
-
-See the Bazel core [py_binary](https://docs.bazel.build/versions/master/be/python.html#py_binary) documentation.
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="py_binary-attrs"></a>attrs |  Rule attributes   |  none |
-
-
-<a id="py_library"></a>
-
-## py_library
-
-<pre>
-py_library(<a href="#py_library-attrs">attrs</a>)
-</pre>
-
-See the Bazel core [py_library](https://docs.bazel.build/versions/master/be/python.html#py_library) documentation.
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="py_library-attrs"></a>attrs |  Rule attributes   |  none |
-
-
-<a id="py_runtime"></a>
-
-## py_runtime
-
-<pre>
-py_runtime(<a href="#py_runtime-attrs">attrs</a>)
-</pre>
-
-See the Bazel core [py_runtime](https://docs.bazel.build/versions/master/be/python.html#py_runtime) documentation.
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="py_runtime-attrs"></a>attrs |  Rule attributes   |  none |
-
-
-<a id="py_runtime_pair"></a>
-
-## py_runtime_pair
-
-<pre>
-py_runtime_pair(<a href="#py_runtime_pair-name">name</a>, <a href="#py_runtime_pair-py2_runtime">py2_runtime</a>, <a href="#py_runtime_pair-py3_runtime">py3_runtime</a>, <a href="#py_runtime_pair-attrs">attrs</a>)
-</pre>
-
-A toolchain rule for Python.
-
-This used to wrap up to two Python runtimes, one for Python 2 and one for Python 3.
-However, Python 2 is no longer supported, so it now only wraps a single Python 3
-runtime.
-
-Usually the wrapped runtimes are declared using the `py_runtime` rule, but any
-rule returning a `PyRuntimeInfo` provider may be used.
-
-This rule returns a `platform_common.ToolchainInfo` provider with the following
-schema:
-
-```python
-platform_common.ToolchainInfo(
-    py2_runtime = None,
-    py3_runtime = <PyRuntimeInfo or None>,
-)
-```
-
-Example usage:
-
-```python
-# In your BUILD file...
-
-load("@rules_python//python:defs.bzl", "py_runtime_pair")
-
-py_runtime(
-    name = "my_py3_runtime",
-    interpreter_path = "/system/python3",
-    python_version = "PY3",
-)
-
-py_runtime_pair(
-    name = "my_py_runtime_pair",
-    py3_runtime = ":my_py3_runtime",
-)
-
-toolchain(
-    name = "my_toolchain",
-    target_compatible_with = <...>,
-    toolchain = ":my_py_runtime_pair",
-    toolchain_type = "@rules_python//python:toolchain_type",
-)
-```
-
-```python
-# In your WORKSPACE...
-
-register_toolchains("//my_pkg:my_toolchain")
-```
-
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="py_runtime_pair-name"></a>name |  str, the name of the target   |  none |
-| <a id="py_runtime_pair-py2_runtime"></a>py2_runtime |  optional Label; must be unset or None; an error is raised otherwise.   |  `None` |
-| <a id="py_runtime_pair-py3_runtime"></a>py3_runtime |  Label; a target with `PyRuntimeInfo` for Python 3.   |  `None` |
-| <a id="py_runtime_pair-attrs"></a>attrs |  Extra attrs passed onto the native rule   |  none |
-
-
-<a id="py_test"></a>
-
-## py_test
-
-<pre>
-py_test(<a href="#py_test-attrs">attrs</a>)
-</pre>
-
-See the Bazel core [py_test](https://docs.bazel.build/versions/master/be/python.html#py_test) documentation.
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="py_test-attrs"></a>attrs |  Rule attributes   |  none |
-
-
-<a id="find_requirements"></a>
-
-## find_requirements
-
-<pre>
-find_requirements(<a href="#find_requirements-name">name</a>)
-</pre>
-
-The aspect definition. Can be invoked on the command line as
-
-bazel build //pkg:my_py_binary_target         --aspects=@rules_python//python:defs.bzl%find_requirements         --output_groups=pyversioninfo
-
-**ASPECT ATTRIBUTES**
-
-
-| Name | Type |
-| :------------- | :------------- |
-| deps| String |
-
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="find_requirements-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-
-
diff --git a/docs/sphinx/BUILD.bazel b/docs/sphinx/BUILD.bazel
index 643d716..1990269 100644
--- a/docs/sphinx/BUILD.bazel
+++ b/docs/sphinx/BUILD.bazel
@@ -15,7 +15,7 @@
 load("@docs_deps//:requirements.bzl", "requirement")
 load("@rules_python//python:pip.bzl", "compile_pip_requirements")
 load("//sphinxdocs:readthedocs.bzl", "readthedocs_install")
-load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
+load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs", "sphinx_inventory")
 load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs")
 
 # We only build for Linux and Mac because the actual doc process only runs
@@ -33,19 +33,22 @@
 sphinx_docs(
     name = "docs",
     srcs = [
+        ":bazel_inventory",
         ":bzl_api_docs",
     ] + glob(
         include = [
             "*.md",
             "**/*.md",
             "_static/**",
+            "_includes/**",
         ],
-        exclude = ["README.md"],
+        exclude = [
+            "README.md",
+            "_*",
+            "*.inv*",
+        ],
     ),
     config = "conf.py",
-    # Building produces lots of warnings right now because the docs aren't
-    # entirely ready yet. Silence these to reduce the spam in CI logs.
-    extra_opts = ["-Q"],
     formats = [
         "html",
     ],
@@ -55,12 +58,18 @@
     target_compatible_with = _TARGET_COMPATIBLE_WITH,
 )
 
+sphinx_inventory(
+    name = "bazel_inventory",
+    src = "bazel_inventory.txt",
+)
+
 sphinx_stardocs(
     name = "bzl_api_docs",
     docs = {
         "api/cc/py_cc_toolchain.md": dict(
             dep = "//python/private:py_cc_toolchain_bzl",
             input = "//python/private:py_cc_toolchain_rule.bzl",
+            public_load_path = "//python/cc:py_cc_toolchain.bzl",
         ),
         "api/cc/py_cc_toolchain_info.md": "//python/cc:py_cc_toolchain_info_bzl",
         "api/defs.md": "//python:defs_bzl",
@@ -68,6 +77,7 @@
         "api/packaging.md": "//python:packaging_bzl",
         "api/pip.md": "//python:pip_bzl",
     },
+    footer = "_stardoc_footer.md",
     tags = ["docs"],
     target_compatible_with = _TARGET_COMPATIBLE_WITH,
 )
diff --git a/docs/sphinx/_includes/py_console_script_binary.md b/docs/sphinx/_includes/py_console_script_binary.md
new file mode 100644
index 0000000..bf2fa64
--- /dev/null
+++ b/docs/sphinx/_includes/py_console_script_binary.md
@@ -0,0 +1,64 @@
+This rule is to make it easier to generate `console_script` entry points
+as per Python [specification].
+
+Generate a `py_binary` target for a particular console_script `entry_point`
+from a PyPI package, e.g. for creating an executable `pylint` target use:
+```starlark
+load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
+
+py_console_script_binary(
+    name = "pylint",
+    pkg = "@pip//pylint",
+)
+```
+
+Or for more advanced setups you can also specify extra dependencies and the
+exact script name you want to call. It is useful for tools like `flake8`, `pylint`,
+`pytest`, which have plugin discovery methods and discover dependencies from the
+PyPI packages available in the `PYTHONPATH`.
+```starlark
+load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
+
+py_console_script_binary(
+    name = "pylint_with_deps",
+    pkg = "@pip//pylint",
+    # Because `pylint` has multiple console_scripts available, we have to
+    # specify which we want if the name of the target name 'pylint_with_deps'
+    # cannot be used to guess the entry_point script.
+    script = "pylint",
+    deps = [
+        # One can add extra dependencies to the entry point.
+        # This specifically allows us to add plugins to pylint.
+        "@pip//pylint_print",
+    ],
+)
+```
+
+A specific Python version can be forced by using the generated version-aware
+wrappers, e.g. to force Python 3.9:
+```starlark
+load("@python_versions//3.9:defs.bzl", "py_console_script_binary")
+
+py_console_script_binary(
+    name = "yamllint",
+    pkg = "@pip//yamllint",
+)
+```
+
+Alternatively, the [`py_console_script_binary.binary_rule`] arg can be passed
+the version-bound `py_binary` symbol, or any other `py_binary`-compatible rule
+of your choosing:
+```starlark
+load("@python_versions//3.9:defs.bzl", "py_binary")
+load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
+
+py_console_script_binary(
+    name = "yamllint",
+    pkg = "@pip//yamllint:pkg",
+    binary_rule = py_binary,
+)
+```
+
+[specification]: https://packaging.python.org/en/latest/specifications/entry-points/
+[`py_console_script_binary.binary_rule`]: #py_console_script_binary-binary_rule
+
diff --git a/docs/sphinx/_stardoc_footer.md b/docs/sphinx/_stardoc_footer.md
new file mode 100644
index 0000000..65d74f4
--- /dev/null
+++ b/docs/sphinx/_stardoc_footer.md
@@ -0,0 +1,13 @@
+
+[`Action`]: https://bazel.build/rules/lib/Action
+[`bool`]: https://bazel.build/rules/lib/bool
+[`depset`]: https://bazel.build/rules/lib/depset
+[`dict`]: https://bazel.build/rules/lib/dict
+[`File`]: https://bazel.build/rules/lib/File
+[`Label`]: https://bazel.build/rules/lib/Label
+[`list`]: https://bazel.build/rules/lib/list
+[`str`]: https://bazel.build/rules/lib/string
+[`struct`]: https://bazel.build/rules/lib/builtins/struct
+[`Target`]: https://bazel.build/rules/lib/Target
+[target-name]: https://bazel.build/concepts/labels#target-names
+[attr-label]: https://bazel.build/concepts/labels
diff --git a/docs/sphinx/bazel_inventory.txt b/docs/sphinx/bazel_inventory.txt
new file mode 100644
index 0000000..869e66a
--- /dev/null
+++ b/docs/sphinx/bazel_inventory.txt
@@ -0,0 +1,17 @@
+# Sphinx inventory version 2
+# Project: Bazel
+# Version: 7.0.0
+# The remainder of this file is compressed using zlib
+Action bzl:obj 1 rules/lib/Action -
+File bzl:obj 1 rules/lib/File -
+Label bzl:obj 1 rules/lib/Label -
+Target bzl:obj 1 rules/lib/builtins/Target -
+bool bzl:obj 1 rules/lib/bool -
+depset bzl:obj 1 rules/lib/depset -
+dict bzl:obj 1 rules/lib/dict -
+label bzl:doc 1 concepts/labels -
+list bzl:obj: 1 rules/lib/list -
+python bzl:doc 1 reference/be/python -
+str bzl:obj 1 rules/lib/string -
+struct bzl:obj 1 rules/lib/builtins/struct -
+target-name bzl:doc 1 concepts/labels#target-names -
diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py
index cf49cfa..bfa4400 100644
--- a/docs/sphinx/conf.py
+++ b/docs/sphinx/conf.py
@@ -5,50 +5,47 @@
 copyright = "2023, The Bazel Authors"
 author = "Bazel"
 
-# Readthedocs fills these in
-release = "0.0.0"
-version = release
+# NOTE: These are overriden by -D flags via --//sphinxdocs:extra_defines
+version = "0.0.0"
+release = version
 
 # -- General configuration
+# See https://www.sphinx-doc.org/en/master/usage/configuration.html
+# for more settings
 
 # Any extensions here not built into Sphinx must also be added to
-# the dependencies of Bazel and Readthedocs.
-# * //docs:requirements.in
-# * Regenerate //docs:requirements.txt (used by readthedocs)
-# * Add the dependencies to //docs:sphinx_build
+# the dependencies of //docs/sphinx:sphinx-builder
 extensions = [
-    "sphinx.ext.duration",
-    "sphinx.ext.doctest",
     "sphinx.ext.autodoc",
-    "sphinx.ext.autosummary",
-    "sphinx.ext.intersphinx",
     "sphinx.ext.autosectionlabel",
+    "sphinx.ext.autosummary",
+    "sphinx.ext.doctest",
+    "sphinx.ext.duration",
+    "sphinx.ext.extlinks",
+    "sphinx.ext.intersphinx",
     "myst_parser",
     "sphinx_rtd_theme",  # Necessary to get jquery to make flyout work
 ]
 
-exclude_patterns = ["crossrefs.md"]
-
-intersphinx_mapping = {}
-
-intersphinx_disabled_domains = ["std"]
-
-# Prevent local refs from inadvertently linking elsewhere, per
-# https://docs.readthedocs.io/en/stable/guides/intersphinx.html#using-intersphinx
-intersphinx_disabled_reftypes = ["*"]
-
+exclude_patterns = ["_includes/*"]
 templates_path = ["_templates"]
+primary_domain = None  # The default is 'py', which we don't make much use of
+nitpicky = True
 
-# -- Options for HTML output
+# --- Intersphinx configuration
 
-html_theme = "sphinx_rtd_theme"
+intersphinx_mapping = {
+    "bazel": ("https://bazel.build/", "bazel_inventory.inv"),
+}
 
-# See https://sphinx-rtd-theme.readthedocs.io/en/stable/configuring.html
-# for options
-html_theme_options = {}
+# --- Extlinks configuration
+extlinks = {
+    "gh-path": (f"https://github.com/bazelbuild/rules_python/tree/main/%s", "%s"),
+}
 
-# Keep this in sync with the stardoc templates
-html_permalinks_icon = "¶"
+# --- MyST configuration
+# See https://myst-parser.readthedocs.io/en/latest/configuration.html
+# for more settings
 
 # See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html
 # for additional extensions.
@@ -58,8 +55,23 @@
     "attrs_inline",
     "colon_fence",
     "deflist",
+    "substitution",
 ]
 
+myst_substitutions = {}
+
+# -- Options for HTML output
+# See https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+# For additional html settings
+
+# See https://sphinx-rtd-theme.readthedocs.io/en/stable/configuring.html for
+# them-specific options
+html_theme = "sphinx_rtd_theme"
+html_theme_options = {}
+
+# Keep this in sync with the stardoc templates
+html_permalinks_icon = "¶"
+
 # These folders are copied to the documentation's HTML output
 html_static_path = ["_static"]
 
@@ -71,3 +83,12 @@
 
 # -- Options for EPUB output
 epub_show_urls = "footnote"
+
+suppress_warnings = ["myst.header", "myst.xref_missing"]
+
+
+def setup(app):
+  # Pygments says it supports starlark, but it doesn't seem to actually
+  # recognize `starlark` as a name. So just manually map it to python.
+  from sphinx.highlighting import lexer_classes
+  app.add_lexer('starlark', lexer_classes['python'])
diff --git a/docs/sphinx/coverage.md b/docs/sphinx/coverage.md
index 63f2578..3e0e673 100644
--- a/docs/sphinx/coverage.md
+++ b/docs/sphinx/coverage.md
@@ -28,12 +28,14 @@
 )
 ```
 
-NOTE: This will implicitly add the version of `coverage` bundled with
+:::{note}
+This will implicitly add the version of `coverage` bundled with
 `rules_python` to the dependencies of `py_test` rules when `bazel coverage` is
 run. If a target already transitively depends on a different version of
 `coverage`, then behavior is undefined -- it is undefined which version comes
 first in the import path. If you find yourself in this situation, then you'll
 need to manually configure coverage (see below).
+:::
 
 ## Manually configuring coverage
 
diff --git a/docs/sphinx/crossrefs.md b/docs/sphinx/crossrefs.md
deleted file mode 100644
index e69de29..0000000
--- a/docs/sphinx/crossrefs.md
+++ /dev/null
diff --git a/docs/sphinx/gazelle.md b/docs/sphinx/gazelle.md
new file mode 100644
index 0000000..89f26d6
--- /dev/null
+++ b/docs/sphinx/gazelle.md
@@ -0,0 +1,9 @@
+# Gazelle plugin
+
+[Gazelle](https://github.com/bazelbuild/bazel-gazelle)
+is a build file generator for Bazel projects. It can create new `BUILD.bazel` files for a project that follows language conventions and update existing build files to include new sources, dependencies, and options.
+
+Bazel may run Gazelle using the Gazelle rule, or it may be installed and run as a command line tool.
+
+See the documentation for Gazelle with rules_python in the {gh-path}`gazelle`
+directory.
diff --git a/docs/sphinx/getting-started.md b/docs/sphinx/getting-started.md
new file mode 100644
index 0000000..d7542fa
--- /dev/null
+++ b/docs/sphinx/getting-started.md
@@ -0,0 +1,181 @@
+# Getting started
+
+The following two sections cover using `rules_python` with bzlmod and
+the older way of configuring bazel with a `WORKSPACE` file.
+
+
+## Using bzlmod
+
+**IMPORTANT: bzlmod support is still in Beta; APIs are subject to change.**
+
+The first step to using rules_python with bzlmod is to add the dependency to
+your MODULE.bazel file:
+
+```starlark
+# Update the version "0.0.0" to the release found here:
+# https://github.com/bazelbuild/rules_python/releases.
+bazel_dep(name = "rules_python", version = "0.0.0")
+```
+
+Once added, you can load the rules and use them:
+
+```starlark
+load("@rules_python//python:py_binary.bzl", "py_binary")
+
+py_binary(...)
+```
+
+Depending on what you're doing, you likely want to do some additional
+configuration to control what Python version is used; read the following
+sections for how to do that.
+
+### Toolchain registration with bzlmod
+
+A default toolchain is automatically configured depending on
+`rules_python`. Note, however, the version used tracks the most recent Python
+release and will change often.
+
+If you want to use a specific Python version for your programs, then how
+to do so depends on if you're configuring the root module or not. The root
+module is special because it can set the *default* Python version, which
+is used by the version-unaware rules (e.g. `//python:py_binary.bzl` et al). For
+submodules, it's recommended to use the version-aware rules to pin your programs
+to a specific Python version so they don't accidentally run with a different
+version configured by the root module.
+
+#### Configuring and using the default Python version
+
+To specify what the default Python version is, set `is_default = True` when
+calling `python.toolchain()`. This can only be done by the root module; it is
+silently ignored if a submodule does it. Similarly, using the version-unaware
+rules (which always use the default Python version) should only be done by the
+root module. If submodules use them, then they may run with a different Python
+version than they expect.
+
+```starlark
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+
+python.toolchain(
+    python_version = "3.11",
+    is_default = True,
+)
+```
+
+Then use the base rules from e.g. `//python:py_binary.bzl`.
+
+#### Pinning to a Python version
+
+Pinning to a version allows targets to force that a specific Python version is
+used, even if the root module configures a different version as a default. This
+is most useful for two cases:
+
+1. For submodules to ensure they run with the appropriate Python version
+2. To allow incremental, per-target, upgrading to newer Python versions,
+   typically in a mono-repo situation.
+
+To configure a submodule with the version-aware rules, request the particular
+version you need, then use the `@python_versions` repo to use the rules that
+force specific versions:
+
+```starlark
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+
+python.toolchain(
+    python_version = "3.11",
+)
+use_repo(python, "python_versions")
+```
+
+Then use e.g. `load("@python_versions//3.11:defs.bzl", "py_binary")` to use
+the rules that force that particular version. Multiple versions can be specified
+and use within a single build.
+
+For more documentation, see the bzlmod examples under the {gh-path}`examples`
+folder.  Look for the examples that contain a `MODULE.bazel` file.
+
+#### Other toolchain details
+
+The `python.toolchain()` call makes its contents available under a repo named
+`python_X_Y`, where X and Y are the major and minor versions. For example,
+`python.toolchain(python_version="3.11")` creates the repo `@python_3_11`.
+Remember to call `use_repo()` to make repos visible to your module:
+`use_repo(python, "python_3_11")`
+
+## Using a WORKSPACE file
+
+To import rules_python in your project, you first need to add it to your
+`WORKSPACE` file, using the snippet provided in the
+[release you choose](https://github.com/bazelbuild/rules_python/releases)
+
+To depend on a particular unreleased version, you can do the following:
+
+```starlark
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+
+# Update the SHA and VERSION to the lastest version available here:
+# https://github.com/bazelbuild/rules_python/releases.
+
+SHA="84aec9e21cc56fbc7f1335035a71c850d1b9b5cc6ff497306f84cced9a769841"
+
+VERSION="0.23.1"
+
+http_archive(
+    name = "rules_python",
+    sha256 = SHA,
+    strip_prefix = "rules_python-{}".format(VERSION),
+    url = "https://github.com/bazelbuild/rules_python/releases/download/{}/rules_python-{}.tar.gz".format(VERSION,VERSION),
+)
+
+load("@rules_python//python:repositories.bzl", "py_repositories")
+
+py_repositories()
+```
+
+### Toolchain registration
+
+To register a hermetic Python toolchain rather than rely on a system-installed interpreter for runtime execution, you can add to the `WORKSPACE` file:
+
+```starlark
+load("@rules_python//python:repositories.bzl", "python_register_toolchains")
+
+python_register_toolchains(
+    name = "python_3_11",
+    # Available versions are listed in @rules_python//python:versions.bzl.
+    # We recommend using the same version your team is already standardized on.
+    python_version = "3.11",
+)
+
+load("@python_3_11//:defs.bzl", "interpreter")
+
+load("@rules_python//python:pip.bzl", "pip_parse")
+
+pip_parse(
+    ...
+    python_interpreter_target = interpreter,
+    ...
+)
+```
+
+After registration, your Python targets will use the toolchain's interpreter during execution, but a system-installed interpreter
+is still used to 'bootstrap' Python targets (see https://github.com/bazelbuild/rules_python/issues/691).
+You may also find some quirks while using this toolchain. Please refer to [python-build-standalone documentation's _Quirks_ section](https://python-build-standalone.readthedocs.io/en/latest/quirks.html).
+
+## Toolchain usage in other rules
+
+Python toolchains can be utilized in other bazel rules, such as `genrule()`, by adding the `toolchains=["@rules_python//python:current_py_toolchain"]` attribute. You can obtain the path to the Python interpreter using the `$(PYTHON2)` and `$(PYTHON3)` ["Make" Variables](https://bazel.build/reference/be/make-variables). See the
+{gh-path}`test_current_py_toolchain <tests/load_from_macro/BUILD.bazel>` target for an example.
+
+## "Hello World"
+
+Once you've imported the rule set into your `WORKSPACE` using any of these
+methods, you can then load the core rules in your `BUILD` files with the following:
+
+```starlark
+load("@rules_python//python:defs.bzl", "py_binary")
+
+py_binary(
+  name = "main",
+  srcs = ["main.py"],
+)
+```
diff --git a/docs/sphinx/glossary.md b/docs/sphinx/glossary.md
new file mode 100644
index 0000000..f54034d
--- /dev/null
+++ b/docs/sphinx/glossary.md
@@ -0,0 +1,28 @@
+# Glossary
+
+{.glossary}
+
+common attributes
+: Every rule has a set of common attributes. See Bazel's
+  [Common attributes](https://bazel.build/reference/be/common-definitions#common-attributes)
+  for a complete listing
+
+rule callable
+: A function that behaves like a rule. This includes, but is not is not
+  limited to:
+  * Accepts a `name` arg and other {term}`common attributes`.
+  * Has no return value (i.e. returns `None`).
+  * Creates at least a target named `name`
+
+  There is usually an implicit interface about what attributes and values are
+  accepted; refer to the respective API accepting this type.
+
+simple label
+: A `str` or `Label` object but not a _direct_ `select` object. These usually
+  mean a string manipulation is occuring, which can't be done on `select`
+  objects. Such attributes are usually still configurable if an alias is used,
+  and a reference to the alias is passed instead.
+
+nonconfigurable
+: A nonconfigurable value cannot use `select`. See Bazel's
+  [configurable attributes](https://bazel.build/reference/be/common-definitions#configurable-attributes) documentation.
diff --git a/docs/sphinx/index.md b/docs/sphinx/index.md
index ce54472..a84dab5 100644
--- a/docs/sphinx/index.md
+++ b/docs/sphinx/index.md
@@ -1,11 +1,66 @@
-# Bazel Python rules
+# Python Rules for Bazel
 
-Documentation for rules_python
+rules_python is the home of the core Python rules -- `py_library`,
+`py_binary`, `py_test`, `py_proto_library`, and related symbols that provide the basis for Python
+support in Bazel. It also contains package installation rules for integrating with PyPI and other indices.
+
+Documentation for rules_python lives here and in the
+[Bazel Build Encyclopedia](https://docs.bazel.build/versions/master/be/python.html).
+
+Examples are in the {gh-path}`examples` directory.
+
+Currently, the core rules build into the Bazel binary, and the symbols in this
+repository are simple aliases. However, we are migrating the rules to Starlark and removing them from the Bazel binary. Therefore, the future-proof way to depend on Python rules is via this repository. See
+{ref}`Migrating from the Bundled Rules` below.
+
+The core rules are stable. Their implementation in Bazel is subject to Bazel's
+[backward compatibility policy](https://docs.bazel.build/versions/master/backward-compatibility.html).
+Once migrated to rules_python, they may evolve at a different
+rate, but this repository will still follow [semantic versioning](https://semver.org).
+
+The Bazel community maintains this repository. Neither Google nor the Bazel team provides support for the code. However, this repository is part of the test suite used to vet new Bazel releases. See
+{gh-path}`How to contribute <CONTRIBUTING.md>` for information on our development workflow.
+
+## Bzlmod support
+
+- Status: Beta
+- Full Feature Parity: No
+
+See {gh-path}`Bzlmod support <BZLMOD_SUPPORT.md>` for more details
+
+## Migrating from the bundled rules
+
+The core rules are currently available in Bazel as built-in symbols, but this
+form is deprecated. Instead, you should depend on rules_python in your
+`WORKSPACE` file and load the Python rules from
+`@rules_python//python:defs.bzl`.
+
+A [buildifier](https://github.com/bazelbuild/buildtools/blob/master/buildifier/README.md)
+fix is available to automatically migrate `BUILD` and `.bzl` files to add the
+appropriate `load()` statements and rewrite uses of `native.py_*`.
+
+```sh
+# Also consider using the -r flag to modify an entire workspace.
+buildifier --lint=fix --warnings=native-py <files>
+```
+
+Currently, the `WORKSPACE` file needs to be updated manually as per [Getting
+started](getting-started).
+
+Note that Starlark-defined bundled symbols underneath
+`@bazel_tools//tools/python` are also deprecated. These are not yet rewritten
+by buildifier.
+
 
 ```{toctree}
-:glob:
 :hidden:
 self
-*
+getting-started
+pypi-dependencies
+pip
+coverage
+gazelle
 api/index
+glossary
+genindex
 ```
diff --git a/docs/sphinx/pip.md b/docs/sphinx/pip.md
new file mode 100644
index 0000000..180d0b4
--- /dev/null
+++ b/docs/sphinx/pip.md
@@ -0,0 +1,85 @@
+(pip-integration)=
+# Pip Integration
+
+To pull in dependencies from PyPI, the `pip_parse` macro is used.
+
+
+This macro wraps the [`pip_repository`](./pip_repository.md) rule that invokes `pip`.
+In your WORKSPACE file:
+
+```starlark
+load("@rules_python//python:pip.bzl", "pip_parse")
+
+pip_parse(
+    name = "pip_deps",
+    requirements_lock = ":requirements.txt",
+)
+
+load("@pip_deps//:requirements.bzl", "install_deps")
+
+install_deps()
+```
+
+You can then reference installed dependencies from a `BUILD` file with:
+
+```starlark
+load("@pip_deps//:requirements.bzl", "requirement")
+
+py_library(
+    name = "bar",
+    ...
+    deps = [
+        "//my/other:dep",
+        requirement("requests"),
+        requirement("numpy"),
+    ],
+)
+```
+
+In addition to the `requirement` macro, which is used to access the generated `py_library`
+target generated from a package's wheel, The generated `requirements.bzl` file contains
+functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
+
+[whl_ep]: https://packaging.python.org/specifications/entry-points/
+
+```starlark
+load("@pip_deps//:requirements.bzl", "entry_point")
+
+alias(
+    name = "pip-compile",
+    actual = entry_point(
+        pkg = "pip-tools",
+        script = "pip-compile",
+    ),
+)
+```
+
+Note that for packages whose name and script are the same, only the name of the package
+is needed when calling the `entry_point` macro.
+
+```starlark
+load("@pip_deps//:requirements.bzl", "entry_point")
+
+alias(
+    name = "flake8",
+    actual = entry_point("flake8"),
+)
+```
+
+(vendoring-requirements)=
+## Vendoring the requirements.bzl file
+
+In some cases you may not want to generate the requirements.bzl file as a repository rule
+while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module
+such as a ruleset, you may want to include the requirements.bzl file rather than make your users
+install the WORKSPACE setup to generate it.
+See https://github.com/bazelbuild/rules_python/issues/608
+
+This is the same workflow as Gazelle, which creates `go_repository` rules with
+[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
+
+To do this, use the "write to source file" pattern documented in
+https://blog.aspect.dev/bazel-can-write-to-the-source-folder
+to put a copy of the generated requirements.bzl into your project.
+Then load the requirements.bzl file directly rather than from the generated repository.
+See the example in rules_python/examples/pip_parse_vendored.
diff --git a/docs/sphinx/pypi-dependencies.md b/docs/sphinx/pypi-dependencies.md
new file mode 100644
index 0000000..ee19fbe
--- /dev/null
+++ b/docs/sphinx/pypi-dependencies.md
@@ -0,0 +1,146 @@
+# Using dependencies from PyPI
+
+Using PyPI packages (aka "pip install") involves two main steps.
+
+1. [Installing third party packages](#installing-third-party-packages)
+2. [Using third party packages as dependencies](#using-third-party-packages-as-dependencies)
+
+## Installing third party packages
+
+### Using bzlmod
+
+To add pip dependencies to your `MODULE.bazel` file, use the `pip.parse`
+extension, and call it to create the central external repo and individual wheel
+external repos. Include in the `MODULE.bazel` the toolchain extension as shown
+in the first bzlmod example above.
+
+```starlark
+pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
+pip.parse(
+    hub_name = "my_deps",
+    python_version = "3.11",
+    requirements_lock = "//:requirements_lock_3_11.txt",
+)
+use_repo(pip, "my_deps")
+```
+For more documentation, including how the rules can update/create a requirements
+file, see the bzlmod examples under the {gh-path}`examples` folder.
+
+### Using a WORKSPACE file
+
+To add pip dependencies to your `WORKSPACE`, load the `pip_parse` function and
+call it to create the central external repo and individual wheel external repos.
+
+```starlark
+load("@rules_python//python:pip.bzl", "pip_parse")
+
+# Create a central repo that knows about the dependencies needed from
+# requirements_lock.txt.
+pip_parse(
+   name = "my_deps",
+   requirements_lock = "//path/to:requirements_lock.txt",
+)
+# Load the starlark macro, which will define your dependencies.
+load("@my_deps//:requirements.bzl", "install_deps")
+# Call it to define repos for your requirements.
+install_deps()
+```
+
+### pip rules
+
+Note that since `pip_parse` is a repository rule and therefore executes pip at
+WORKSPACE-evaluation time, Bazel has no information about the Python toolchain
+and cannot enforce that the interpreter used to invoke pip matches the
+interpreter used to run `py_binary` targets. By default, `pip_parse` uses the
+system command `"python3"`. To override this, pass in the `python_interpreter`
+attribute or `python_interpreter_target` attribute to `pip_parse`.
+
+You can have multiple `pip_parse`s in the same workspace.  Or use the pip
+extension multiple times when using bzlmod. This configuration will create
+multiple external repos that have no relation to one another and may result in
+downloading the same wheels numerous times.
+
+As with any repository rule, if you would like to ensure that `pip_parse` is
+re-executed to pick up a non-hermetic change to your environment (e.g., updating
+your system `python` interpreter), you can force it to re-execute by running
+`bazel sync --only [pip_parse name]`.
+
+:::{note}
+The `pip_install` rule is deprecated. `pip_parse` offers identical
+functionality, and both `pip_install` and `pip_parse` now have the same
+implementation. The name `pip_install` may be removed in a future version of the
+rules.
+:::
+
+The maintainers have made all reasonable efforts to facilitate a smooth
+transition. Still, some users of `pip_install` will need to replace their
+existing `requirements.txt` with a fully resolved set of dependencies using a
+tool such as `pip-tools` or the `compile_pip_requirements` repository rule.
+
+## Using third party packages as dependencies
+
+Each extracted wheel repo contains a `py_library` target representing
+the wheel's contents. There are two ways to access this library. The
+first uses the `requirement()` function defined in the central
+repo's `//:requirements.bzl` file. This function maps a pip package
+name to a label:
+
+```starlark
+load("@my_deps//:requirements.bzl", "requirement")
+
+py_library(
+    name = "mylib",
+    srcs = ["mylib.py"],
+    deps = [
+        ":myotherlib",
+        requirement("some_pip_dep"),
+        requirement("another_pip_dep"),
+    ]
+)
+```
+
+The reason `requirement()` exists is to insulate from
+changes to the underlying repository and label strings. However, those
+labels have become directly used, so aren't able to easily change regardless.
+
+On the other hand, using `requirement()` has several drawbacks; see
+[this issue][requirements-drawbacks] for an enumeration. If you don't
+want to use `requirement()`, you can use the library
+labels directly instead. For `pip_parse`, the labels are of the following form:
+
+```starlark
+@{name}_{package}//:pkg
+```
+
+Here `name` is the `name` attribute that was passed to `pip_parse` and
+`package` is the pip package name with characters that are illegal in
+Bazel label names (e.g. `-`, `.`) replaced with `_`. If you need to
+update `name` from "old" to "new", then you can run the following
+buildozer command:
+
+```shell
+buildozer 'substitute deps @old_([^/]+)//:pkg @new_${1}//:pkg' //...:*
+```
+
+[requirements-drawbacks]: https://github.com/bazelbuild/rules_python/issues/414
+
+### 'Extras' dependencies
+
+Any 'extras' specified in the requirements lock file will be automatically added
+as transitive dependencies of the package. In the example above, you'd just put
+`requirement("useful_dep")`.
+
+## Consuming Wheel Dists Directly
+
+If you need to depend on the wheel dists themselves, for instance, to pass them
+to some other packaging tool, you can get a handle to them with the
+`whl_requirement` macro. For example:
+
+```starlark
+filegroup(
+    name = "whl_files",
+    data = [
+        whl_requirement("boto3"),
+    ]
+)
+```
diff --git a/python/entry_points/py_console_script_binary.bzl b/python/entry_points/py_console_script_binary.bzl
index 60fbd8c..c61d44a 100644
--- a/python/entry_points/py_console_script_binary.bzl
+++ b/python/entry_points/py_console_script_binary.bzl
@@ -14,6 +14,9 @@
 
 """
 Creates an executable (a non-test binary) for console_script entry points.
+
+```{include} /_includes/py_console_script_binary.md
+```
 """
 
 load("//python/private:py_console_script_binary.bzl", _py_console_script_binary = "py_console_script_binary")
diff --git a/python/pip.bzl b/python/pip.bzl
index 67a06f4..0d206e8 100644
--- a/python/pip.bzl
+++ b/python/pip.bzl
@@ -11,7 +11,13 @@
 # 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.
-"""Import pip requirements into Bazel."""
+"""Rules for pip integration.
+
+This contains a set of rules that are used to support inclusion of third-party
+dependencies via fully locked `requirements.txt` files. Some of the exported
+symbols should not be used and they are either undocumented here or marked as
+for internal use only.
+"""
 
 load("//python/pip_install:pip_repository.bzl", "pip_repository", _package_annotation = "package_annotation")
 load("//python/pip_install:repositories.bzl", "pip_install_dependencies")
@@ -41,87 +47,11 @@
 def pip_parse(requirements = None, requirements_lock = None, name = "pip_parsed_deps", **kwargs):
     """Accepts a locked/compiled requirements file and installs the dependencies listed within.
 
-    Those dependencies become available in a generated `requirements.bzl` file.
-    You can instead check this `requirements.bzl` file into your repo, see the "vendoring" section below.
+    Those dependencies become available as addressable targets and
+    in a generated `requirements.bzl` file. The `requirements.bzl` file can
+    be checked into source control, if desired; see {ref}`vendoring-requirements`
 
-    This macro wraps the [`pip_repository`](./pip_repository.md) rule that invokes `pip`.
-    In your WORKSPACE file:
-
-    ```python
-    load("@rules_python//python:pip.bzl", "pip_parse")
-
-    pip_parse(
-        name = "pip_deps",
-        requirements_lock = ":requirements.txt",
-    )
-
-    load("@pip_deps//:requirements.bzl", "install_deps")
-
-    install_deps()
-    ```
-
-    You can then reference installed dependencies from a `BUILD` file with:
-
-    ```python
-    load("@pip_deps//:requirements.bzl", "requirement")
-
-    py_library(
-        name = "bar",
-        ...
-        deps = [
-           "//my/other:dep",
-           requirement("requests"),
-           requirement("numpy"),
-        ],
-    )
-    ```
-
-    In addition to the `requirement` macro, which is used to access the generated `py_library`
-    target generated from a package's wheel, The generated `requirements.bzl` file contains
-    functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
-
-    [whl_ep]: https://packaging.python.org/specifications/entry-points/
-
-    ```python
-    load("@pip_deps//:requirements.bzl", "entry_point")
-
-    alias(
-        name = "pip-compile",
-        actual = entry_point(
-            pkg = "pip-tools",
-            script = "pip-compile",
-        ),
-    )
-    ```
-
-    Note that for packages whose name and script are the same, only the name of the package
-    is needed when calling the `entry_point` macro.
-
-    ```python
-    load("@pip_deps//:requirements.bzl", "entry_point")
-
-    alias(
-        name = "flake8",
-        actual = entry_point("flake8"),
-    )
-    ```
-
-    ## Vendoring the requirements.bzl file
-
-    In some cases you may not want to generate the requirements.bzl file as a repository rule
-    while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module
-    such as a ruleset, you may want to include the requirements.bzl file rather than make your users
-    install the WORKSPACE setup to generate it.
-    See https://github.com/bazelbuild/rules_python/issues/608
-
-    This is the same workflow as Gazelle, which creates `go_repository` rules with
-    [`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
-
-    To do this, use the "write to source file" pattern documented in
-    https://blog.aspect.dev/bazel-can-write-to-the-source-folder
-    to put a copy of the generated requirements.bzl into your project.
-    Then load the requirements.bzl file directly rather than from the generated repository.
-    See the example in rules_python/examples/pip_parse_vendored.
+    For more information, see {ref}`pip-integration`.
 
     Args:
         requirements_lock (Label): A fully resolved 'requirements.txt' pip requirement file
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index b8b8e51..4388594 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -203,8 +203,7 @@
     name = "util_bzl",
     srcs = ["util.bzl"],
     visibility = [
-        "//docs:__subpackages__",
-        "//python:__subpackages__",
+        "//:__subpackages__",
     ],
     deps = ["@bazel_skylib//lib:types"],
 )
diff --git a/python/private/py_console_script_binary.bzl b/python/private/py_console_script_binary.bzl
index bd992a8..deeded2 100644
--- a/python/private/py_console_script_binary.bzl
+++ b/python/private/py_console_script_binary.bzl
@@ -49,19 +49,19 @@
     """Generate a py_binary for a console_script entry_point.
 
     Args:
-        name: str, The name of the resulting target.
-        pkg: target, the package for which to generate the script.
-        entry_points_txt: optional target, the entry_points.txt file to parse
+        name: [`target-name`] The name of the resulting target.
+        pkg: {any}`simple label` the package for which to generate the script.
+        entry_points_txt: optional [`label`], the entry_points.txt file to parse
             for available console_script values. It may be a single file, or a
             group of files, but must contain a file named `entry_points.txt`.
             If not specified, defaults to the `dist_info` target in the same
             package as the `pkg` Label.
-        script: str, The console script name that the py_binary is going to be
+        script: [`str`], The console script name that the py_binary is going to be
             generated for. Defaults to the normalized name attribute.
-        binary_rule: callable, The rule/macro to use to instantiate
-            the target. It's expected to behave like `py_binary`.
-            Defaults to @rules_python//python:py_binary.bzl#py_binary.
-        **kwargs: Extra parameters forwarded to binary_rule.
+        binary_rule: {any}`rule callable`, The rule/macro to use to instantiate
+            the target. It's expected to behave like {any}`py_binary`.
+            Defaults to {any}`py_binary`.
+        **kwargs: Extra parameters forwarded to `binary_rule`.
     """
     main = "rules_python_entry_point_{}.py".format(name)
 
diff --git a/sphinxdocs/BUILD.bazel b/sphinxdocs/BUILD.bazel
index 2ff708f..a47e702 100644
--- a/sphinxdocs/BUILD.bazel
+++ b/sphinxdocs/BUILD.bazel
@@ -13,40 +13,33 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//sphinxdocs/private:sphinx.bzl", "sphinx_defines_flag")
 
 package(
     default_visibility = ["//:__subpackages__"],
 )
 
-# These are only exported because they're passed as files to the //sphinxdocs
-# macros, and thus must be visible to other packages. They should only be
-# referenced by the //sphinxdocs macros.
-exports_files(
-    [
-        "func_template.vm",
-        "header_template.vm",
-        "provider_template.vm",
-        "readthedocs_install.py",
-        "rule_template.vm",
-        "sphinx_build.py",
-        "sphinx_server.py",
-    ],
+# Additional -D values to add to every Sphinx build.
+# This is usually used to override the version when building
+sphinx_defines_flag(
+    name = "extra_defines",
+    build_setting_default = [],
 )
 
 bzl_library(
     name = "sphinx_bzl",
     srcs = ["sphinx.bzl"],
-    deps = [
-        "//python:py_binary_bzl",
-        "@bazel_skylib//lib:paths",
-        "@bazel_skylib//lib:types",
-        "@bazel_skylib//rules:build_test",
-        "@io_bazel_stardoc//stardoc:stardoc_lib",
-    ],
+    deps = ["//sphinxdocs/private:sphinx_bzl"],
+)
+
+bzl_library(
+    name = "sphinx_stardoc_bzl",
+    srcs = ["sphinx_stardoc.bzl"],
+    deps = ["//sphinxdocs/private:sphinx_stardoc_bzl"],
 )
 
 bzl_library(
     name = "readthedocs_bzl",
     srcs = ["readthedocs.bzl"],
-    deps = ["//python:py_binary_bzl"],
+    deps = ["//sphinxdocs/private:readthedocs_bzl"],
 )
diff --git a/sphinxdocs/header_template.vm b/sphinxdocs/header_template.vm
deleted file mode 100644
index fee7e2c..0000000
--- a/sphinxdocs/header_template.vm
+++ /dev/null
@@ -1 +0,0 @@
-$moduleDocstring
diff --git a/sphinxdocs/private/BUILD.bazel b/sphinxdocs/private/BUILD.bazel
new file mode 100644
index 0000000..a8701d9
--- /dev/null
+++ b/sphinxdocs/private/BUILD.bazel
@@ -0,0 +1,72 @@
+# Copyright 2023 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//:bzl_library.bzl", "bzl_library")
+load("//python:py_binary.bzl", "py_binary")
+
+package(
+    default_visibility = ["//sphinxdocs:__subpackages__"],
+)
+
+# These are only exported because they're passed as files to the //sphinxdocs
+# macros, and thus must be visible to other packages. They should only be
+# referenced by the //sphinxdocs macros.
+exports_files(
+    [
+        "func_template.vm",
+        "header_template.vm",
+        "provider_template.vm",
+        "readthedocs_install.py",
+        "rule_template.vm",
+        "sphinx_build.py",
+        "sphinx_server.py",
+    ],
+    visibility = ["//:__subpackages__"],
+)
+
+bzl_library(
+    name = "sphinx_bzl",
+    srcs = ["sphinx.bzl"],
+    deps = [
+        "//python:py_binary_bzl",
+        "@bazel_skylib//lib:paths",
+        "@bazel_skylib//lib:types",
+        "@bazel_skylib//rules:build_test",
+        "@io_bazel_stardoc//stardoc:stardoc_lib",
+    ],
+)
+
+bzl_library(
+    name = "sphinx_stardoc_bzl",
+    srcs = ["sphinx_stardoc.bzl"],
+    deps = [
+        "//python/private:util_bzl",
+        "@bazel_skylib//lib:types",
+        "@bazel_skylib//rules:build_test",
+        "@io_bazel_stardoc//stardoc:stardoc_lib",
+    ],
+)
+
+bzl_library(
+    name = "readthedocs_bzl",
+    srcs = ["readthedocs.bzl"],
+    deps = ["//python:py_binary_bzl"],
+)
+
+py_binary(
+    name = "inventory_builder",
+    srcs = ["inventory_builder.py"],
+    # Only public because it's an implicit attribute
+    visibility = ["//:__subpackages__"],
+)
diff --git a/sphinxdocs/func_template.vm b/sphinxdocs/private/func_template.vm
similarity index 100%
rename from sphinxdocs/func_template.vm
rename to sphinxdocs/private/func_template.vm
diff --git a/sphinxdocs/private/header_template.vm b/sphinxdocs/private/header_template.vm
new file mode 100644
index 0000000..81496ff
--- /dev/null
+++ b/sphinxdocs/private/header_template.vm
@@ -0,0 +1,3 @@
+# %%BZL_LOAD_PATH%%
+
+$moduleDocstring
diff --git a/sphinxdocs/private/inventory_builder.py b/sphinxdocs/private/inventory_builder.py
new file mode 100644
index 0000000..850d944
--- /dev/null
+++ b/sphinxdocs/private/inventory_builder.py
@@ -0,0 +1,24 @@
+import pathlib
+import sys
+import zlib
+
+
+def main(args):
+    in_path = pathlib.Path(args.pop(0))
+    out_path = pathlib.Path(args.pop(0))
+
+    data = in_path.read_bytes()
+    offset = 0
+    for _ in range(4):
+        offset = data.index(b"\n", offset) + 1
+
+    compressed_bytes = zlib.compress(data[offset:])
+    with out_path.open(mode="bw") as fp:
+        fp.write(data[:offset])
+        fp.write(compressed_bytes)
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/sphinxdocs/provider_template.vm b/sphinxdocs/private/provider_template.vm
similarity index 100%
rename from sphinxdocs/provider_template.vm
rename to sphinxdocs/private/provider_template.vm
diff --git a/sphinxdocs/private/readthedocs.bzl b/sphinxdocs/private/readthedocs.bzl
new file mode 100644
index 0000000..3cab75b
--- /dev/null
+++ b/sphinxdocs/private/readthedocs.bzl
@@ -0,0 +1,48 @@
+# Copyright 2023 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.
+"""Starlark rules for integrating Sphinx and Readthedocs."""
+
+load("//python:py_binary.bzl", "py_binary")
+load("//python/private:util.bzl", "add_tag")  # buildifier: disable=bzl-visibility
+
+_INSTALL_MAIN_SRC = Label("//sphinxdocs/private:readthedocs_install.py")
+
+def readthedocs_install(name, docs, **kwargs):
+    """Run a program to copy Sphinx doc files into readthedocs output directories.
+
+    This is intended to be run using `bazel run` during the readthedocs
+    build process when the build process is overridden. See
+    https://docs.readthedocs.io/en/stable/build-customization.html#override-the-build-process
+    for more information.
+
+    Args:
+        name: (str) name of the installer
+        docs: (label list) list of targets that generate directories to copy
+            into the directories readthedocs expects final output in. This
+            is typically a single `sphinx_stardocs` target.
+        **kwargs: (dict) additional kwargs to pass onto the installer
+    """
+    add_tag(kwargs, "@rules_python//sphinxdocs:readthedocs_install")
+    py_binary(
+        name = name,
+        srcs = [_INSTALL_MAIN_SRC],
+        main = _INSTALL_MAIN_SRC,
+        data = docs,
+        args = [
+            "$(rlocationpaths {})".format(d)
+            for d in docs
+        ],
+        deps = ["//python/runfiles"],
+        **kwargs
+    )
diff --git a/sphinxdocs/readthedocs_install.py b/sphinxdocs/private/readthedocs_install.py
similarity index 100%
rename from sphinxdocs/readthedocs_install.py
rename to sphinxdocs/private/readthedocs_install.py
diff --git a/sphinxdocs/rule_template.vm b/sphinxdocs/private/rule_template.vm
similarity index 100%
rename from sphinxdocs/rule_template.vm
rename to sphinxdocs/private/rule_template.vm
diff --git a/sphinxdocs/private/sphinx.bzl b/sphinxdocs/private/sphinx.bzl
new file mode 100644
index 0000000..bd082e0
--- /dev/null
+++ b/sphinxdocs/private/sphinx.bzl
@@ -0,0 +1,292 @@
+# Copyright 2023 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.
+
+"""Implementation of sphinx rules."""
+
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("//python:py_binary.bzl", "py_binary")
+load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs")  # buildifier: disable=bzl-visibility
+
+_SPHINX_BUILD_MAIN_SRC = Label("//sphinxdocs/private:sphinx_build.py")
+_SPHINX_SERVE_MAIN_SRC = Label("//sphinxdocs/private:sphinx_server.py")
+
+def sphinx_build_binary(name, py_binary_rule = py_binary, **kwargs):
+    """Create an executable with the sphinx-build command line interface.
+
+    The `deps` must contain the sphinx library and any other extensions Sphinx
+    needs at runtime.
+
+    Args:
+        name: (str) name of the target. The name "sphinx-build" is the
+            conventional name to match what Sphinx itself uses.
+        py_binary_rule: (optional callable) A `py_binary` compatible callable
+            for creating the target. If not set, the regular `py_binary`
+            rule is used. This allows using the version-aware rules, or
+            other alternative implementations.
+        **kwargs: Additional kwargs to pass onto `py_binary`. The `srcs` and
+            `main` attributes must not be specified.
+    """
+    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_build_binary")
+    py_binary_rule(
+        name = name,
+        srcs = [_SPHINX_BUILD_MAIN_SRC],
+        main = _SPHINX_BUILD_MAIN_SRC,
+        **kwargs
+    )
+
+def sphinx_docs(name, *, srcs = [], sphinx, config, formats, strip_prefix = "", extra_opts = [], **kwargs):
+    """Generate docs using Sphinx.
+
+    This generates three public targets:
+        * `<name>`: The output of this target is a directory for each
+          format Sphinx creates. This target also has a separate output
+          group for each format. e.g. `--output_group=html` will only build
+          the "html" format files.
+        * `<name>_define`: A multi-string flag to add additional `-D`
+          arguments to the Sphinx invocation. This is useful for overriding
+          the version information in the config file for builds.
+        * `<name>.serve`: A binary that locally serves the HTML output. This
+          allows previewing docs during development.
+
+    Args:
+        name: (str) name of the docs rule.
+        srcs: (label list) The source files for Sphinx to process.
+        sphinx: (label) the Sphinx tool to use for building
+            documentation. Because Sphinx supports various plugins, you must
+            construct your own binary with the necessary dependencies. The
+            `sphinx_build_binary` rule can be used to define such a binary, but
+            any executable supporting the `sphinx-build` command line interface
+            can be used (typically some `py_binary` program).
+        config: (label) the Sphinx config file (`conf.py`) to use.
+        formats: (list of str) the formats (`-b` flag) to generate documentation
+            in. Each format will become an output group.
+        strip_prefix: (str) A prefix to remove from the file paths of the
+            source files. e.g., given `//docs:foo.md`, stripping `docs/`
+            makes Sphinx see `foo.md` in its generated source directory.
+        extra_opts: (list[str]) Additional options to pass onto Sphinx building.
+        **kwargs: (dict) Common attributes to pass onto rules.
+    """
+    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs")
+    common_kwargs = copy_propagating_kwargs(kwargs)
+
+    _sphinx_docs(
+        name = name,
+        srcs = srcs,
+        sphinx = sphinx,
+        config = config,
+        formats = formats,
+        strip_prefix = strip_prefix,
+        extra_opts = extra_opts,
+        **kwargs
+    )
+
+    html_name = "_{}_html".format(name.lstrip("_"))
+    native.filegroup(
+        name = html_name,
+        srcs = [name],
+        output_group = "html",
+        **common_kwargs
+    )
+    py_binary(
+        name = name + ".serve",
+        srcs = [_SPHINX_SERVE_MAIN_SRC],
+        main = _SPHINX_SERVE_MAIN_SRC,
+        data = [html_name],
+        args = [
+            "$(execpath {})".format(html_name),
+        ],
+        **common_kwargs
+    )
+
+def _sphinx_docs_impl(ctx):
+    source_dir_path, _, inputs = _create_sphinx_source_tree(ctx)
+
+    outputs = {}
+    for format in ctx.attr.formats:
+        output_dir = _run_sphinx(
+            ctx = ctx,
+            format = format,
+            source_path = source_dir_path,
+            output_prefix = paths.join(ctx.label.name, "_build"),
+            inputs = inputs,
+        )
+        outputs[format] = output_dir
+    return [
+        DefaultInfo(files = depset(outputs.values())),
+        OutputGroupInfo(**{
+            format: depset([output])
+            for format, output in outputs.items()
+        }),
+    ]
+
+_sphinx_docs = rule(
+    implementation = _sphinx_docs_impl,
+    attrs = {
+        "config": attr.label(
+            allow_single_file = True,
+            mandatory = True,
+            doc = "Config file for Sphinx",
+        ),
+        "extra_opts": attr.string_list(
+            doc = "Additional options to pass onto Sphinx. These are added after " +
+                  "other options, but before the source/output args.",
+        ),
+        "formats": attr.string_list(doc = "Output formats for Sphinx to create."),
+        "sphinx": attr.label(
+            executable = True,
+            cfg = "exec",
+            mandatory = True,
+            doc = "Sphinx binary to generate documentation.",
+        ),
+        "srcs": attr.label_list(
+            allow_files = True,
+            doc = "Doc source files for Sphinx.",
+        ),
+        "strip_prefix": attr.string(doc = "Prefix to remove from input file paths."),
+        "_extra_defines_flag": attr.label(default = "//sphinxdocs:extra_defines"),
+    },
+)
+
+def _create_sphinx_source_tree(ctx):
+    # Sphinx only accepts a single directory to read its doc sources from.
+    # Because plain files and generated files are in different directories,
+    # we need to merge the two into a single directory.
+    source_prefix = paths.join(ctx.label.name, "_sources")
+    sphinx_source_files = []
+
+    def _symlink_source(orig):
+        source_rel_path = orig.short_path
+        if source_rel_path.startswith(ctx.attr.strip_prefix):
+            source_rel_path = source_rel_path[len(ctx.attr.strip_prefix):]
+
+        sphinx_source = ctx.actions.declare_file(paths.join(source_prefix, source_rel_path))
+        ctx.actions.symlink(
+            output = sphinx_source,
+            target_file = orig,
+            progress_message = "Symlinking Sphinx source %{input} to %{output}",
+        )
+        sphinx_source_files.append(sphinx_source)
+        return sphinx_source
+
+    # Though Sphinx has a -c flag, we move the config file into the sources
+    # directory to make the config more intuitive because some configuration
+    # options are relative to the config location, not the sources directory.
+    source_conf_file = _symlink_source(ctx.file.config)
+    sphinx_source_dir_path = paths.dirname(source_conf_file.path)
+
+    for orig_file in ctx.files.srcs:
+        _symlink_source(orig_file)
+
+    return sphinx_source_dir_path, source_conf_file, sphinx_source_files
+
+def _run_sphinx(ctx, format, source_path, inputs, output_prefix):
+    output_dir = ctx.actions.declare_directory(paths.join(output_prefix, format))
+
+    args = ctx.actions.args()
+    args.add("-T")  # Full tracebacks on error
+    args.add("-b", format)
+    args.add("-q")  # Suppress stdout informational text
+    args.add("-j", "auto")  # Build in parallel, if possible
+    args.add("-E")  # Don't try to use cache files. Bazel can't make use of them.
+    args.add("-a")  # Write all files; don't try to detect "changed" files
+    args.add_all(ctx.attr.extra_opts)
+    args.add_all(ctx.attr._extra_defines_flag[_SphinxDefinesInfo].value, before_each = "-D")
+    args.add(source_path)
+    args.add(output_dir.path)
+
+    ctx.actions.run(
+        executable = ctx.executable.sphinx,
+        arguments = [args],
+        inputs = inputs,
+        outputs = [output_dir],
+        mnemonic = "SphinxBuildDocs",
+        progress_message = "Sphinx building {} for %{{label}}".format(format),
+    )
+    return output_dir
+
+_SphinxDefinesInfo = provider(
+    doc = "Provider for the extra_defines flag value",
+    fields = ["value"],
+)
+
+def _sphinx_defines_flag_impl(ctx):
+    return _SphinxDefinesInfo(value = ctx.build_setting_value)
+
+sphinx_defines_flag = rule(
+    implementation = _sphinx_defines_flag_impl,
+    build_setting = config.string_list(flag = True, repeatable = True),
+)
+
+def sphinx_inventory(name, src, **kwargs):
+    """Creates a compressed inventory file from an uncompressed on.
+
+    The Sphinx inventory format isn't formally documented, but is understood
+    to be:
+
+    ```
+    # Sphinx inventory version 2
+    # Project: <project name>
+    # Version: <version string>
+    # The remainder of this file is compressed using zlib
+    name domain:role 1 relative-url display name
+    ```
+
+    Where:
+      * `<project name>` is a string. e.g. `Rules Python`
+      * `<version string>` is a string e.g. `1.5.3`
+
+    And there are one or more `name domain:role ...` lines
+      * `name`: the name of the symbol. It can contain special characters,
+        but not spaces.
+      * `domain:role`: The `domain` is usually a language, e.g. `py` or `bzl`.
+        The `role` is usually the type of object, e.g. `class` or `func`. There
+        is no canonical meaning to the values, they are usually domain-specific.
+      * `1` is a number. It affects search priority.
+      * `relative-url` is a URL path relative to the base url in the
+        confg.py intersphinx config.
+      * `display name` is a string. It can contain spaces, or simply be
+        the value `-` to indicate it is the same as `name`
+
+
+    Args:
+        name: [`target-name`] name of the target.
+        src: [`label`] Uncompressed inventory text file.
+        **kwargs: additional kwargs of common attributes.
+    """
+    _sphinx_inventory(name = name, src = src, **kwargs)
+
+def _sphinx_inventory_impl(ctx):
+    output = ctx.actions.declare_file(ctx.label.name + ".inv")
+    args = ctx.actions.args()
+    args.add(ctx.file.src)
+    args.add(output)
+    ctx.actions.run(
+        executable = ctx.executable._builder,
+        arguments = [args],
+        inputs = depset([ctx.file.src]),
+        outputs = [output],
+    )
+    return [DefaultInfo(files = depset([output]))]
+
+_sphinx_inventory = rule(
+    implementation = _sphinx_inventory_impl,
+    attrs = {
+        "src": attr.label(allow_single_file = True),
+        "_builder": attr.label(
+            default = "//sphinxdocs/private:inventory_builder",
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/sphinxdocs/sphinx_build.py b/sphinxdocs/private/sphinx_build.py
similarity index 100%
rename from sphinxdocs/sphinx_build.py
rename to sphinxdocs/private/sphinx_build.py
diff --git a/sphinxdocs/private/sphinx_server.py b/sphinxdocs/private/sphinx_server.py
new file mode 100644
index 0000000..e71889a
--- /dev/null
+++ b/sphinxdocs/private/sphinx_server.py
@@ -0,0 +1,49 @@
+import contextlib
+import errno
+import os
+import sys
+from http import server
+
+
+def main(argv):
+    build_workspace_directory = os.environ["BUILD_WORKSPACE_DIRECTORY"]
+    docs_directory = argv[1]
+    serve_directory = os.path.join(build_workspace_directory, docs_directory)
+
+    class DirectoryHandler(server.SimpleHTTPRequestHandler):
+        def __init__(self, *args, **kwargs):
+            super().__init__(directory=serve_directory, *args, **kwargs)
+
+    address = ("0.0.0.0", 8000)
+    # with server.ThreadingHTTPServer(address, DirectoryHandler) as (ip, port, httpd):
+    with _start_server(DirectoryHandler, "0.0.0.0", 8000) as (ip, port, httpd):
+        print(f"Serving...")
+        print(f"  Address: http://{ip}:{port}")
+        print(f"  Serving directory: {serve_directory}")
+        print(f"  CWD: {os.getcwd()}")
+        print()
+        print("*** You do not need to restart this server to see changes ***")
+        print()
+        try:
+            httpd.serve_forever()
+        except KeyboardInterrupt:
+            pass
+    return 0
+
+
+@contextlib.contextmanager
+def _start_server(handler, ip, start_port):
+    for port in range(start_port, start_port + 10):
+        try:
+            with server.ThreadingHTTPServer((ip, port), handler) as httpd:
+                yield ip, port, httpd
+        except OSError as e:
+            if e.errno == errno.EADDRINUSE:
+                pass
+            else:
+                raise
+    raise ValueError("Unable to find an available port")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/sphinxdocs/private/sphinx_stardoc.bzl b/sphinxdocs/private/sphinx_stardoc.bzl
new file mode 100644
index 0000000..1371d90
--- /dev/null
+++ b/sphinxdocs/private/sphinx_stardoc.bzl
@@ -0,0 +1,140 @@
+# Copyright 2023 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.
+
+"""Rules to generate Sphinx-compatible documentation for bzl files."""
+
+load("@bazel_skylib//lib:types.bzl", "types")
+load("@bazel_skylib//rules:build_test.bzl", "build_test")
+load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc")
+load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs")  # buildifier: disable=bzl-visibility
+
+_FUNC_TEMPLATE = Label("//sphinxdocs/private:func_template.vm")
+_HEADER_TEMPLATE = Label("//sphinxdocs/private:header_template.vm")
+_RULE_TEMPLATE = Label("//sphinxdocs/private:rule_template.vm")
+_PROVIDER_TEMPLATE = Label("//sphinxdocs/private:provider_template.vm")
+
+def sphinx_stardocs(name, docs, footer = None, **kwargs):
+    """Generate Sphinx-friendly Markdown docs using Stardoc for bzl libraries.
+
+    A `build_test` for the docs is also generated to ensure Stardoc is able
+    to process the files.
+
+    NOTE: This generates MyST-flavored Markdown.
+
+    Args:
+        name: `str`, the name of the resulting file group with the generated docs.
+        docs: `dict[str output, source]` of the bzl files to generate documentation
+            for. The `output` key is the path of the output filename, e.g.,
+            `foo/bar.md`. The `source` values can be either of:
+            * A `str` label that points to a `bzl_library` target. The target
+              name will replace `_bzl` with `.bzl` and use that as the input
+              bzl file to generate docs for. The target itself provides the
+              necessary dependencies.
+            * A `dict` with keys `input` and `dep`. The `input` key is a string
+              label to the bzl file to generate docs for. The `dep` key is a
+              string label to a `bzl_library` providing the necessary dependencies.
+        footer: optional [`label`] File to append to generated docs.
+        **kwargs: Additional kwargs to pass onto each `sphinx_stardoc` target
+    """
+    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardocs")
+    common_kwargs = copy_propagating_kwargs(kwargs)
+
+    stardocs = []
+    for out_name, entry in docs.items():
+        stardoc_kwargs = {}
+        stardoc_kwargs.update(kwargs)
+
+        if types.is_string(entry):
+            stardoc_kwargs["deps"] = [entry]
+            stardoc_kwargs["input"] = entry.replace("_bzl", ".bzl")
+        else:
+            stardoc_kwargs.update(entry)
+            stardoc_kwargs["deps"] = [stardoc_kwargs.pop("dep")]
+
+        doc_name = "_{}_{}".format(name.lstrip("_"), out_name.replace("/", "_"))
+        _sphinx_stardoc(
+            name = doc_name,
+            footer = footer,
+            out = out_name,
+            **stardoc_kwargs
+        )
+        stardocs.append(doc_name)
+
+    native.filegroup(
+        name = name,
+        srcs = stardocs,
+        **common_kwargs
+    )
+    build_test(
+        name = name + "_build_test",
+        targets = stardocs,
+        **common_kwargs
+    )
+
+def _sphinx_stardoc(*, name, out, footer = None, public_load_path = None, **kwargs):
+    if footer:
+        stardoc_name = "_{}_stardoc".format(name.lstrip("_"))
+        stardoc_out = "_{}_stardoc.out".format(name.lstrip("_"))
+    else:
+        stardoc_name = name
+        stardoc_out = out
+
+    if not public_load_path:
+        public_load_path = str(kwargs["input"])
+
+    header_name = "_{}_header".format(name.lstrip("_"))
+    _expand_stardoc_template(
+        name = header_name,
+        template = _HEADER_TEMPLATE,
+        substitutions = {
+            "%%BZL_LOAD_PATH%%": public_load_path,
+        },
+    )
+
+    stardoc(
+        name = stardoc_name,
+        func_template = _FUNC_TEMPLATE,
+        header_template = header_name,
+        rule_template = _RULE_TEMPLATE,
+        provider_template = _PROVIDER_TEMPLATE,
+        out = stardoc_out,
+        **kwargs
+    )
+
+    if footer:
+        native.genrule(
+            name = name,
+            srcs = [stardoc_out, footer],
+            outs = [out],
+            cmd = "cat $(SRCS) > $(OUTS)",
+            message = "SphinxStardoc: Adding footer to {}".format(name),
+            **copy_propagating_kwargs(kwargs)
+        )
+
+def _expand_stardoc_template_impl(ctx):
+    out = ctx.actions.declare_file(ctx.label.name + ".vm")
+    ctx.actions.expand_template(
+        template = ctx.file.template,
+        output = out,
+        substitutions = ctx.attr.substitutions,
+    )
+    return [DefaultInfo(files = depset([out]))]
+
+_expand_stardoc_template = rule(
+    implementation = _expand_stardoc_template_impl,
+    attrs = {
+        "substitutions": attr.string_dict(),
+        "template": attr.label(allow_single_file = True),
+    },
+)
diff --git a/sphinxdocs/readthedocs.bzl b/sphinxdocs/readthedocs.bzl
index 6ffc79c..4dfaf26 100644
--- a/sphinxdocs/readthedocs.bzl
+++ b/sphinxdocs/readthedocs.bzl
@@ -13,36 +13,6 @@
 # limitations under the License.
 """Starlark rules for integrating Sphinx and Readthedocs."""
 
-load("//python:py_binary.bzl", "py_binary")
-load("//python/private:util.bzl", "add_tag")  # buildifier: disable=bzl-visibility
+load("//sphinxdocs/private:readthedocs.bzl", _readthedocs_install = "readthedocs_install")
 
-_INSTALL_MAIN_SRC = Label("//sphinxdocs:readthedocs_install.py")
-
-def readthedocs_install(name, docs, **kwargs):
-    """Run a program to copy Sphinx doc files into readthedocs output directories.
-
-    This is intended to be run using `bazel run` during the readthedocs
-    build process when the build process is overridden. See
-    https://docs.readthedocs.io/en/stable/build-customization.html#override-the-build-process
-    for more information.
-
-    Args:
-        name: (str) name of the installer
-        docs: (label list) list of targets that generate directories to copy
-            into the directories readthedocs expects final output in. This
-            is typically a single `sphinx_stardocs` target.
-        **kwargs: (dict) additional kwargs to pass onto the installer
-    """
-    add_tag(kwargs, "@rules_python//sphinxdocs:readthedocs_install")
-    py_binary(
-        name = name,
-        srcs = [_INSTALL_MAIN_SRC],
-        main = _INSTALL_MAIN_SRC,
-        data = docs,
-        args = [
-            "$(rlocationpaths {})".format(d)
-            for d in docs
-        ],
-        deps = ["//python/runfiles"],
-        **kwargs
-    )
+readthedocs_install = _readthedocs_install
diff --git a/sphinxdocs/sphinx.bzl b/sphinxdocs/sphinx.bzl
index 3c8b776..a0b1a05 100644
--- a/sphinxdocs/sphinx.bzl
+++ b/sphinxdocs/sphinx.bzl
@@ -25,192 +25,13 @@
 a plugin model to support extensibility.
 """
 
-load("@bazel_skylib//lib:paths.bzl", "paths")
-load("//python:py_binary.bzl", "py_binary")
-load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs")  # buildifier: disable=bzl-visibility
-
-_SPHINX_BUILD_MAIN_SRC = Label("//sphinxdocs:sphinx_build.py")
-_SPHINX_SERVE_MAIN_SRC = Label("//sphinxdocs:sphinx_server.py")
-
-def sphinx_build_binary(name, py_binary_rule = py_binary, **kwargs):
-    """Create an executable with the sphinx-build command line interface.
-
-    The `deps` must contain the sphinx library and any other extensions Sphinx
-    needs at runtime.
-
-    Args:
-        name: (str) name of the target. The name "sphinx-build" is the
-            conventional name to match what Sphinx itself uses.
-        py_binary_rule: (optional callable) A `py_binary` compatible callable
-            for creating the target. If not set, the regular `py_binary`
-            rule is used. This allows using the version-aware rules, or
-            other alternative implementations.
-        **kwargs: Additional kwargs to pass onto `py_binary`. The `srcs` and
-            `main` attributes must not be specified.
-    """
-    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_build_binary")
-    py_binary_rule(
-        name = name,
-        srcs = [_SPHINX_BUILD_MAIN_SRC],
-        main = _SPHINX_BUILD_MAIN_SRC,
-        **kwargs
-    )
-
-def sphinx_docs(name, *, srcs = [], sphinx, config, formats, strip_prefix = "", extra_opts = [], **kwargs):
-    """Generate docs using Sphinx.
-
-    This generates two public targets:
-        * `<name>`: The output of this target is a directory for each
-          format Sphinx creates. This target also has a separate output
-          group for each format. e.g. `--output_group=html` will only build
-          the "html" format files.
-        * `<name>.serve`: A binary that locally serves the HTML output. This
-          allows previewing docs during development.
-
-    Args:
-        name: (str) name of the docs rule.
-        srcs: (label list) The source files for Sphinx to process.
-        sphinx: (label) the Sphinx tool to use for building
-            documentation. Because Sphinx supports various plugins, you must
-            construct your own binary with the necessary dependencies. The
-            `sphinx_build_binary` rule can be used to define such a binary, but
-            any executable supporting the `sphinx-build` command line interface
-            can be used (typically some `py_binary` program).
-        config: (label) the Sphinx config file (`conf.py`) to use.
-        formats: (list of str) the formats (`-b` flag) to generate documentation
-            in. Each format will become an output group.
-        strip_prefix: (str) A prefix to remove from the file paths of the
-            source files. e.g., given `//docs:foo.md`, stripping `docs/`
-            makes Sphinx see `foo.md` in its generated source directory.
-        extra_opts: (list[str]) Additional options to pass onto Sphinx building.
-        **kwargs: (dict) Common attributes to pass onto rules.
-    """
-    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs")
-    common_kwargs = copy_propagating_kwargs(kwargs)
-
-    _sphinx_docs(
-        name = name,
-        srcs = srcs,
-        sphinx = sphinx,
-        config = config,
-        formats = formats,
-        strip_prefix = strip_prefix,
-        extra_opts = extra_opts,
-        **kwargs
-    )
-
-    html_name = "_{}_html".format(name)
-    native.filegroup(
-        name = html_name,
-        srcs = [name],
-        output_group = "html",
-        **common_kwargs
-    )
-    py_binary(
-        name = name + ".serve",
-        srcs = [_SPHINX_SERVE_MAIN_SRC],
-        main = _SPHINX_SERVE_MAIN_SRC,
-        data = [html_name],
-        args = [
-            "$(execpath {})".format(html_name),
-        ],
-        **common_kwargs
-    )
-
-def _sphinx_docs_impl(ctx):
-    source_dir_path, inputs = _create_sphinx_source_tree(ctx)
-    inputs.append(ctx.file.config)
-
-    outputs = {}
-    for format in ctx.attr.formats:
-        output_dir = _run_sphinx(
-            ctx = ctx,
-            format = format,
-            source_path = source_dir_path,
-            output_prefix = paths.join(ctx.label.name, "_build"),
-            inputs = inputs,
-        )
-        outputs[format] = output_dir
-    return [
-        DefaultInfo(files = depset(outputs.values())),
-        OutputGroupInfo(**{
-            format: depset([output])
-            for format, output in outputs.items()
-        }),
-    ]
-
-_sphinx_docs = rule(
-    implementation = _sphinx_docs_impl,
-    attrs = {
-        "config": attr.label(
-            allow_single_file = True,
-            mandatory = True,
-            doc = "Config file for Sphinx",
-        ),
-        "extra_opts": attr.string_list(
-            doc = "Additional options to pass onto Sphinx. These are added after " +
-                  "other options, but before the source/output args.",
-        ),
-        "formats": attr.string_list(doc = "Output formats for Sphinx to create."),
-        "sphinx": attr.label(
-            executable = True,
-            cfg = "exec",
-            mandatory = True,
-            doc = "Sphinx binary to generate documentation.",
-        ),
-        "srcs": attr.label_list(
-            allow_files = True,
-            doc = "Doc source files for Sphinx.",
-        ),
-        "strip_prefix": attr.string(doc = "Prefix to remove from input file paths."),
-    },
+load(
+    "//sphinxdocs/private:sphinx.bzl",
+    _sphinx_build_binary = "sphinx_build_binary",
+    _sphinx_docs = "sphinx_docs",
+    _sphinx_inventory = "sphinx_inventory",
 )
 
-def _create_sphinx_source_tree(ctx):
-    # Sphinx only accepts a single directory to read its doc sources from.
-    # Because plain files and generated files are in different directories,
-    # we need to merge the two into a single directory.
-    source_prefix = paths.join(ctx.label.name, "_sources")
-    source_marker = ctx.actions.declare_file(paths.join(source_prefix, "__marker"))
-    ctx.actions.write(source_marker, "")
-    sphinx_source_dir_path = paths.dirname(source_marker.path)
-    sphinx_source_files = []
-    for orig in ctx.files.srcs:
-        source_rel_path = orig.short_path
-        if source_rel_path.startswith(ctx.attr.strip_prefix):
-            source_rel_path = source_rel_path[len(ctx.attr.strip_prefix):]
-
-        sphinx_source = ctx.actions.declare_file(paths.join(source_prefix, source_rel_path))
-        ctx.actions.symlink(
-            output = sphinx_source,
-            target_file = orig,
-            progress_message = "Symlinking Sphinx source %{input} to %{output}",
-        )
-        sphinx_source_files.append(sphinx_source)
-
-    return sphinx_source_dir_path, sphinx_source_files
-
-def _run_sphinx(ctx, format, source_path, inputs, output_prefix):
-    output_dir = ctx.actions.declare_directory(paths.join(output_prefix, format))
-
-    args = ctx.actions.args()
-    args.add("-T")  # Full tracebacks on error
-    args.add("-b", format)
-    args.add("-c", paths.dirname(ctx.file.config.path))
-    args.add("-q")  # Suppress stdout informational text
-    args.add("-j", "auto")  # Build in parallel, if possible
-    args.add("-E")  # Don't try to use cache files. Bazel can't make use of them.
-    args.add("-a")  # Write all files; don't try to detect "changed" files
-    args.add_all(ctx.attr.extra_opts)
-    args.add(source_path)
-    args.add(output_dir.path)
-
-    ctx.actions.run(
-        executable = ctx.executable.sphinx,
-        arguments = [args],
-        inputs = inputs,
-        outputs = [output_dir],
-        mnemonic = "SphinxBuildDocs",
-        progress_message = "Sphinx building {} for %{{label}}".format(format),
-    )
-    return output_dir
+sphinx_build_binary = _sphinx_build_binary
+sphinx_docs = _sphinx_docs
+sphinx_inventory = _sphinx_inventory
diff --git a/sphinxdocs/sphinx_server.py b/sphinxdocs/sphinx_server.py
deleted file mode 100644
index 55d42c0..0000000
--- a/sphinxdocs/sphinx_server.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import os
-import sys
-from http import server
-
-
-def main(argv):
-    build_workspace_directory = os.environ["BUILD_WORKSPACE_DIRECTORY"]
-    docs_directory = argv[1]
-    serve_directory = os.path.join(build_workspace_directory, docs_directory)
-
-    class DirectoryHandler(server.SimpleHTTPRequestHandler):
-        def __init__(self, *args, **kwargs):
-            super().__init__(directory=serve_directory, *args, **kwargs)
-
-    address = ("0.0.0.0", 8000)
-    with server.ThreadingHTTPServer(address, DirectoryHandler) as httpd:
-        print(f"Serving...")
-        print(f"  Address: http://{address[0]}:{address[1]}")
-        print(f"  Serving directory: {serve_directory}")
-        print(f"  CWD: {os.getcwd()}")
-        print()
-        print("*** You do not need to restart this server to see changes ***")
-        print()
-        try:
-            httpd.serve_forever()
-        except KeyboardInterrupt:
-            pass
-    return 0
-
-
-if __name__ == "__main__":
-    sys.exit(main(sys.argv))
diff --git a/sphinxdocs/sphinx_stardoc.bzl b/sphinxdocs/sphinx_stardoc.bzl
index ef610ce..623bc64 100644
--- a/sphinxdocs/sphinx_stardoc.bzl
+++ b/sphinxdocs/sphinx_stardoc.bzl
@@ -14,76 +14,6 @@
 
 """Rules to generate Sphinx-compatible documentation for bzl files."""
 
-load("@bazel_skylib//lib:types.bzl", "types")
-load("@bazel_skylib//rules:build_test.bzl", "build_test")
-load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc")
-load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs")  # buildifier: disable=bzl-visibility
+load("//sphinxdocs/private:sphinx_stardoc.bzl", _sphinx_stardocs = "sphinx_stardocs")
 
-_FUNC_TEMPLATE = Label("//sphinxdocs:func_template.vm")
-_HEADER_TEMPLATE = Label("//sphinxdocs:header_template.vm")
-_RULE_TEMPLATE = Label("//sphinxdocs:rule_template.vm")
-_PROVIDER_TEMPLATE = Label("//sphinxdocs:provider_template.vm")
-
-def sphinx_stardocs(name, docs, **kwargs):
-    """Generate Sphinx-friendly Markdown docs using Stardoc for bzl libraries.
-
-    A `build_test` for the docs is also generated to ensure Stardoc is able
-    to process the files.
-
-    NOTE: This generates MyST-flavored Markdown.
-
-    Args:
-        name: `str`, the name of the resulting file group with the generated docs.
-        docs: `dict[str output, source]` of the bzl files to generate documentation
-            for. The `output` key is the path of the output filename, e.g.,
-            `foo/bar.md`. The `source` values can be either of:
-            * A `str` label that points to a `bzl_library` target. The target
-              name will replace `_bzl` with `.bzl` and use that as the input
-              bzl file to generate docs for. The target itself provides the
-              necessary dependencies.
-            * A `dict` with keys `input` and `dep`. The `input` key is a string
-              label to the bzl file to generate docs for. The `dep` key is a
-              string label to a `bzl_library` providing the necessary dependencies.
-        **kwargs: Additional kwargs to pass onto each `sphinx_stardoc` target
-    """
-    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardocs")
-    common_kwargs = copy_propagating_kwargs(kwargs)
-
-    stardocs = []
-    for out_name, entry in docs.items():
-        if types.is_string(entry):
-            label = Label(entry)
-            input = entry.replace("_bzl", ".bzl")
-        else:
-            label = entry["dep"]
-            input = entry["input"]
-
-        doc_name = "_{}_{}".format(name, out_name.replace("/", "_"))
-        _sphinx_stardoc(
-            name = doc_name,
-            input = input,
-            deps = [label],
-            out = out_name,
-            **kwargs
-        )
-        stardocs.append(doc_name)
-
-    native.filegroup(
-        name = name,
-        srcs = stardocs,
-        **common_kwargs
-    )
-    build_test(
-        name = name + "_build_test",
-        targets = stardocs,
-        **common_kwargs
-    )
-
-def _sphinx_stardoc(**kwargs):
-    stardoc(
-        func_template = _FUNC_TEMPLATE,
-        header_template = _HEADER_TEMPLATE,
-        rule_template = _RULE_TEMPLATE,
-        provider_template = _PROVIDER_TEMPLATE,
-        **kwargs
-    )
+sphinx_stardocs = _sphinx_stardocs