docs: initial doc generation using Sphinx (#1489)

This lays the groundwork for using Sphinx to generate user-facing
documentation and
having it published on readthedocs. It integrates with Bazel Stardoc to
generate
MyST-flavored Markdown that Sphinx can process.

There are 4 basic pieces that are glued together:
1. `sphinx_docs`: This rule invokes Sphinx to generate e.g. html, latex,
etc
2. `sphinx_stardoc`: This rule invokes Stardoc to generate MyST-flavored
Markdown
     that Sphinx can process
3. `sphinx_build_binary`: This rule defines the Sphinx executable with
any necessary
dependencies (e.g. Sphinx extensions, like MyST) to process the docs in
(1)
4. `readthedocs_install`: This rule does the necessary steps to build
the docs and
put them into the location the readthedocs build process expects. This
is basically
just `cp -r`, but its cleaner to hide it behind a `bazel run` command
than have
     to put various shell in the readthedocs yaml config.

* Bump Bazel 6 requirement: 6.0.0 -> 6.20. This is necessary to support
  bzlmod and Stardoc.

Work towards #1332, #1484
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index b231834..a8ef70c 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -23,7 +23,7 @@
   # NOTE: Keep in sync with //:version.bzl
   bazel: 5.4.0
 .minimum_supported_bzlmod_version: &minimum_supported_bzlmod_version
-  bazel: 6.0.0 # test minimum supported version of bazel for bzlmod tests
+  bazel: 6.2.0 # test minimum supported version of bazel for bzlmod tests
 .reusable_config: &reusable_config
   build_targets:
     - "--"
@@ -154,8 +154,9 @@
       # on Bazel 5.4 and earlier. To workaround this, manually specify the
       # build kite cc toolchain.
       - "--extra_toolchains=@buildkite_config//config:cc-toolchain"
+      - "--build_tag_filters=-docs"
     test_flags:
-      - "--test_tag_filters=-integration-test,-acceptance-test"
+      - "--test_tag_filters=-integration-test,-acceptance-test,-docs"
       # BazelCI sets --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1,
       # which prevents cc toolchain autodetection from working correctly
       # on Bazel 5.4 and earlier. To workaround this, manually specify the
diff --git a/.bazelversion b/.bazelversion
index 09b254e..6abaeb2 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-6.0.0
+6.2.0
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..d1cdbc0
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,10 @@
+
+version: 2
+
+build:
+  os: "ubuntu-22.04"
+  tools:
+    nodejs: "19"
+  commands:
+    - npm install -g @bazel/bazelisk
+    - bazel run //docs/sphinx:readthedocs_install
diff --git a/MODULE.bazel b/MODULE.bazel
index efff733..9eae5e7 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -54,3 +54,16 @@
 
 # This call registers the Python toolchains.
 register_toolchains("@pythons_hub//:all")
+
+# ===== DEV ONLY SETUP =====
+docs_pip = use_extension(
+    "//python/extensions:pip.bzl",
+    "pip",
+    dev_dependency = True,
+)
+docs_pip.parse(
+    hub_name = "docs_deps",
+    python_version = "3.11",
+    requirements_darwin = "//docs/sphinx:requirements_darwin.txt",
+    requirements_lock = "//docs/sphinx:requirements_linux.txt",
+)
diff --git a/WORKSPACE b/WORKSPACE
index 9f4fd82..be12334 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -84,11 +84,13 @@
 # for python requirements.
 _py_gazelle_deps()
 
+# This interpreter is used for various rules_python dev-time tools
+load("@python//3.11.6:defs.bzl", "interpreter")
+
 #####################
 # Install twine for our own runfiles wheel publishing.
 # Eventually we might want to install twine automatically for users too, see:
 # https://github.com/bazelbuild/rules_python/issues/1016.
-load("@python//3.11.6:defs.bzl", "interpreter")
 load("@rules_python//python:pip.bzl", "pip_parse")
 
 pip_parse(
@@ -103,6 +105,21 @@
 
 install_deps()
 
+#####################
+# Install sphinx for doc generation.
+
+pip_parse(
+    name = "docs_deps",
+    incompatible_generate_aliases = True,
+    python_interpreter_target = interpreter,
+    requirements_darwin = "//docs/sphinx:requirements_darwin.txt",
+    requirements_lock = "//docs/sphinx:requirements_linux.txt",
+)
+
+load("@docs_deps//:requirements.bzl", docs_install_deps = "install_deps")
+
+docs_install_deps()
+
 # This wheel is purely here to validate the wheel extraction code. It's not
 # intended for anything else.
 http_file(
diff --git a/docs/sphinx/BUILD.bazel b/docs/sphinx/BUILD.bazel
new file mode 100644
index 0000000..643d716
--- /dev/null
+++ b/docs/sphinx/BUILD.bazel
@@ -0,0 +1,99 @@
+# 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("@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_stardoc.bzl", "sphinx_stardocs")
+
+# We only build for Linux and Mac because the actual doc process only runs
+# on Linux. Mac is close enough. Making CI happy under Windows is too much
+# of a headache, though, so we don't bother with that.
+_TARGET_COMPATIBLE_WITH = select({
+    "@platforms//os:linux": [],
+    "@platforms//os:macos": [],
+    "//conditions:default": ["@platforms//:incompatible"],
+})
+
+# See README.md for instructions. Short version:
+# * `bazel run //docs/sphinx:docs.serve` in a separate terminal
+# * `ibazel build //docs/sphinx:docs` to automatically rebuild docs
+sphinx_docs(
+    name = "docs",
+    srcs = [
+        ":bzl_api_docs",
+    ] + glob(
+        include = [
+            "*.md",
+            "**/*.md",
+            "_static/**",
+        ],
+        exclude = ["README.md"],
+    ),
+    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",
+    ],
+    sphinx = ":sphinx-build",
+    strip_prefix = package_name() + "/",
+    tags = ["docs"],
+    target_compatible_with = _TARGET_COMPATIBLE_WITH,
+)
+
+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",
+        ),
+        "api/cc/py_cc_toolchain_info.md": "//python/cc:py_cc_toolchain_info_bzl",
+        "api/defs.md": "//python:defs_bzl",
+        "api/entry_points/py_console_script_binary.md": "//python/entry_points:py_console_script_binary_bzl",
+        "api/packaging.md": "//python:packaging_bzl",
+        "api/pip.md": "//python:pip_bzl",
+    },
+    tags = ["docs"],
+    target_compatible_with = _TARGET_COMPATIBLE_WITH,
+)
+
+readthedocs_install(
+    name = "readthedocs_install",
+    docs = [":docs"],
+    target_compatible_with = _TARGET_COMPATIBLE_WITH,
+)
+
+sphinx_build_binary(
+    name = "sphinx-build",
+    target_compatible_with = _TARGET_COMPATIBLE_WITH,
+    deps = [
+        requirement("sphinx"),
+        requirement("sphinx_rtd_theme"),
+        requirement("myst_parser"),
+        requirement("readthedocs_sphinx_ext"),
+    ],
+)
+
+# Run bazel run //docs/sphinx:requirements.update
+compile_pip_requirements(
+    name = "requirements",
+    requirements_darwin = "requirements_darwin.txt",
+    requirements_in = "requirements.in",
+    requirements_txt = "requirements_linux.txt",
+    target_compatible_with = _TARGET_COMPATIBLE_WITH,
+)
diff --git a/docs/sphinx/README.md b/docs/sphinx/README.md
new file mode 100644
index 0000000..98420e4
--- /dev/null
+++ b/docs/sphinx/README.md
@@ -0,0 +1,72 @@
+# rules_python Sphinx docs generation
+
+The docs for rules_python are generated using a combination of Sphinx, Bazel,
+and Readthedocs.org. The Markdown files in source control are unlikely to render
+properly without the Sphinx processing step because they rely on Sphinx and
+MyST-specific Markdown functionality.
+
+The actual sources that Sphinx consumes are in this directory, with Stardoc
+generating additional sources or Sphinx.
+
+Manually building the docs isn't necessary -- readthedocs.org will
+automatically build and deploy them when commits are pushed to the repo.
+
+## Generating docs for development
+
+Generating docs for development is a two-part process: starting a local HTTP
+server to serve the generated HTML, and re-generating the HTML when sources
+change. The quick start is:
+
+```
+bazel run //docs/sphinx:docs.serve  # Run in separate terminal
+ibazel build //docs/sphinx:docs  # Automatically rebuilds docs
+```
+
+This will build the docs and start a local webserver at http://localhost:8000
+where you can view the output. As you edit files, ibazel will detect the file
+changes and re-run the build process, and you can simply refresh your browser to
+see the changes. Using ibazel is not required; you can manually run the
+equivalent bazel command if desired.
+
+### Installing ibazel
+
+The `ibazel` tool can be used to automatically rebuild the docs as you
+development them. See the [ibazel docs](https://github.com/bazelbuild/bazel-watcher) for
+how to install it. The quick start for linux is:
+
+```
+sudo apt install npm
+sudo npm install -g @bazel/ibazel
+```
+
+## MyST Markdown flavor
+
+Sphinx is configured to parse Markdown files using MyST, which is a more
+advanced flavor of Markdown that supports most features of restructured text and
+integrates with Sphinx functionality such as automatic cross references,
+creating indexes, and using concise markup to generate rich documentation.
+
+MyST features and behaviors are controlled by the Sphinx configuration file,
+`docs/sphinx/conf.py`. For more info, see https://myst-parser.readthedocs.io.
+
+## Sphinx configuration
+
+The Sphinx-specific configuration files and input doc files live in
+docs/sphinx.
+
+The Sphinx configuration is `docs/sphinx/conf.py`. See
+https://www.sphinx-doc.org/ for details about the configuration file.
+
+## Readthedocs configuration
+
+There's two basic parts to the readthedocs configuration:
+
+*   `.readthedocs.yaml`: This configuration file controls most settings, such as
+    the OS version used to build, Python version, dependencies, what Bazel
+    commands to run, etc.
+*   https://readthedocs.org/projects/rules-python: This is the project
+    administration page. While most settings come from the config file, this
+    controls additional settings such as permissions, what versions are
+    published, when to publish changes, etc.
+
+For more readthedocs configuration details, see docs.readthedocs.io.
diff --git a/docs/sphinx/_static/css/custom.css b/docs/sphinx/_static/css/custom.css
new file mode 100644
index 0000000..c97d2f5
--- /dev/null
+++ b/docs/sphinx/_static/css/custom.css
@@ -0,0 +1,34 @@
+.wy-nav-content {
+  max-width: 70%;
+}
+
+.starlark-object {
+  border: thin solid grey;
+  margin-bottom: 1em;
+}
+
+.starlark-object h2 {
+  background-color: #e7f2fa;
+  border-bottom: thin solid grey;
+  padding-left: 0.5ex;
+}
+
+.starlark-object>p, .starlark-object>dl {
+  /* Prevent the words from touching the border line */
+  padding-left: 0.5ex;
+}
+
+.starlark-signature {
+  font-family: monospace;
+}
+
+/* Fixup the headerlinks in param names */
+.starlark-object dt a {
+  /* Offset the link icon to be outside the colon */
+  position: relative;
+  right: -1ex;
+  /* Remove the empty space between the param name and colon */
+  width: 0;
+  /* Override the .headerlink margin */
+  margin-left: 0 !important;
+}
diff --git a/docs/sphinx/api/index.md b/docs/sphinx/api/index.md
new file mode 100644
index 0000000..028fab7
--- /dev/null
+++ b/docs/sphinx/api/index.md
@@ -0,0 +1,6 @@
+# API Reference
+
+```{toctree}
+:glob:
+**
+```
diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py
new file mode 100644
index 0000000..cf49cfa
--- /dev/null
+++ b/docs/sphinx/conf.py
@@ -0,0 +1,73 @@
+# Configuration file for the Sphinx documentation builder.
+
+# -- Project information
+project = "rules_python"
+copyright = "2023, The Bazel Authors"
+author = "Bazel"
+
+# Readthedocs fills these in
+release = "0.0.0"
+version = release
+
+# -- General configuration
+
+# 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
+extensions = [
+    "sphinx.ext.duration",
+    "sphinx.ext.doctest",
+    "sphinx.ext.autodoc",
+    "sphinx.ext.autosummary",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.autosectionlabel",
+    "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 = ["*"]
+
+templates_path = ["_templates"]
+
+# -- Options for HTML output
+
+html_theme = "sphinx_rtd_theme"
+
+# See https://sphinx-rtd-theme.readthedocs.io/en/stable/configuring.html
+# for options
+html_theme_options = {}
+
+# Keep this in sync with the stardoc templates
+html_permalinks_icon = "¶"
+
+# See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html
+# for additional extensions.
+myst_enable_extensions = [
+    "fieldlist",
+    "attrs_block",
+    "attrs_inline",
+    "colon_fence",
+    "deflist",
+]
+
+# These folders are copied to the documentation's HTML output
+html_static_path = ["_static"]
+
+# These paths are either relative to html_static_path
+# or fully qualified paths (eg. https://...)
+html_css_files = [
+    "css/custom.css",
+]
+
+# -- Options for EPUB output
+epub_show_urls = "footnote"
diff --git a/docs/sphinx/coverage.md b/docs/sphinx/coverage.md
new file mode 100644
index 0000000..63f2578
--- /dev/null
+++ b/docs/sphinx/coverage.md
@@ -0,0 +1,58 @@
+# 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/sphinx/crossrefs.md b/docs/sphinx/crossrefs.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/sphinx/crossrefs.md
diff --git a/docs/sphinx/index.md b/docs/sphinx/index.md
new file mode 100644
index 0000000..ce54472
--- /dev/null
+++ b/docs/sphinx/index.md
@@ -0,0 +1,11 @@
+# Bazel Python rules
+
+Documentation for rules_python
+
+```{toctree}
+:glob:
+:hidden:
+self
+*
+api/index
+```
diff --git a/docs/sphinx/requirements.in b/docs/sphinx/requirements.in
new file mode 100644
index 0000000..c403778
--- /dev/null
+++ b/docs/sphinx/requirements.in
@@ -0,0 +1,6 @@
+# NOTE: This is only used as input to create the resolved requirements.txt file,
+# which is what builds, both Bazel and Readthedocs, both use.
+sphinx
+myst-parser
+sphinx_rtd_theme
+readthedocs-sphinx-ext
diff --git a/docs/sphinx/requirements_darwin.txt b/docs/sphinx/requirements_darwin.txt
new file mode 100644
index 0000000..3e65ad8
--- /dev/null
+++ b/docs/sphinx/requirements_darwin.txt
@@ -0,0 +1,297 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    bazel run //docs/sphinx:requirements.update
+#
+alabaster==0.7.13 \
+    --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+    --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+    # via sphinx
+babel==2.12.1 \
+    --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+    --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+    # via sphinx
+certifi==2022.12.7 \
+    --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \
+    --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18
+    # via requests
+charset-normalizer==3.1.0 \
+    --hash=sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6 \
+    --hash=sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1 \
+    --hash=sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e \
+    --hash=sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373 \
+    --hash=sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62 \
+    --hash=sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230 \
+    --hash=sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be \
+    --hash=sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c \
+    --hash=sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0 \
+    --hash=sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448 \
+    --hash=sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f \
+    --hash=sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649 \
+    --hash=sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d \
+    --hash=sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0 \
+    --hash=sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706 \
+    --hash=sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a \
+    --hash=sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59 \
+    --hash=sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23 \
+    --hash=sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5 \
+    --hash=sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb \
+    --hash=sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e \
+    --hash=sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e \
+    --hash=sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c \
+    --hash=sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28 \
+    --hash=sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d \
+    --hash=sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41 \
+    --hash=sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974 \
+    --hash=sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce \
+    --hash=sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f \
+    --hash=sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1 \
+    --hash=sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d \
+    --hash=sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8 \
+    --hash=sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017 \
+    --hash=sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31 \
+    --hash=sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7 \
+    --hash=sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8 \
+    --hash=sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e \
+    --hash=sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14 \
+    --hash=sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd \
+    --hash=sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d \
+    --hash=sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795 \
+    --hash=sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b \
+    --hash=sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b \
+    --hash=sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b \
+    --hash=sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203 \
+    --hash=sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f \
+    --hash=sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19 \
+    --hash=sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1 \
+    --hash=sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a \
+    --hash=sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac \
+    --hash=sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9 \
+    --hash=sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0 \
+    --hash=sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137 \
+    --hash=sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f \
+    --hash=sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6 \
+    --hash=sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5 \
+    --hash=sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909 \
+    --hash=sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f \
+    --hash=sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0 \
+    --hash=sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324 \
+    --hash=sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755 \
+    --hash=sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb \
+    --hash=sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854 \
+    --hash=sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c \
+    --hash=sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60 \
+    --hash=sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84 \
+    --hash=sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0 \
+    --hash=sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b \
+    --hash=sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1 \
+    --hash=sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531 \
+    --hash=sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1 \
+    --hash=sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11 \
+    --hash=sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326 \
+    --hash=sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df \
+    --hash=sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab
+    # via requests
+docutils==0.18.1 \
+    --hash=sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c \
+    --hash=sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06
+    # via
+    #   myst-parser
+    #   sphinx
+    #   sphinx-rtd-theme
+idna==3.4 \
+    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+    # via requests
+imagesize==1.4.1 \
+    --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+    --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+    # via sphinx
+jinja2==3.1.2 \
+    --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+    --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+    # via
+    #   myst-parser
+    #   readthedocs-sphinx-ext
+    #   sphinx
+markdown-it-py==2.2.0 \
+    --hash=sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30 \
+    --hash=sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1
+    # via
+    #   mdit-py-plugins
+    #   myst-parser
+markupsafe==2.1.2 \
+    --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
+    --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
+    --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \
+    --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \
+    --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \
+    --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \
+    --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \
+    --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \
+    --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \
+    --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \
+    --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \
+    --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \
+    --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \
+    --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \
+    --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \
+    --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \
+    --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \
+    --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \
+    --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \
+    --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \
+    --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \
+    --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \
+    --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \
+    --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \
+    --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \
+    --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \
+    --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \
+    --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \
+    --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \
+    --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \
+    --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \
+    --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \
+    --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \
+    --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \
+    --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \
+    --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \
+    --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \
+    --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \
+    --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \
+    --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \
+    --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \
+    --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \
+    --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \
+    --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \
+    --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \
+    --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \
+    --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \
+    --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \
+    --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \
+    --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58
+    # via jinja2
+mdit-py-plugins==0.3.5 \
+    --hash=sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e \
+    --hash=sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a
+    # via myst-parser
+mdurl==0.1.2 \
+    --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
+    --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
+    # via markdown-it-py
+myst-parser==1.0.0 \
+    --hash=sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae \
+    --hash=sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c
+    # via -r docs/sphinx/requirements.in
+packaging==23.0 \
+    --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+    --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+    # via
+    #   readthedocs-sphinx-ext
+    #   sphinx
+pygments==2.15.0 \
+    --hash=sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094 \
+    --hash=sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500
+    # via sphinx
+pyyaml==6.0 \
+    --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \
+    --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
+    --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
+    --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
+    --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
+    --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
+    --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
+    --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
+    --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
+    --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
+    --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
+    --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
+    --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \
+    --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
+    --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
+    --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
+    --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
+    --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
+    --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \
+    --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
+    --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
+    --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
+    --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
+    --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
+    --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
+    --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \
+    --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
+    --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
+    --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \
+    --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
+    --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
+    --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
+    --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \
+    --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
+    --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
+    --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
+    --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
+    --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \
+    --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
+    --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
+    # via myst-parser
+readthedocs-sphinx-ext==2.2.3 \
+    --hash=sha256:6583c26791a5853ee9e57ce9db864e2fb06808ba470f805d74d53fc50811e012 \
+    --hash=sha256:e9d911792789b88ae12e2be94d88c619f89a4fa1fe9e42c1505c9930a07163d8
+    # via -r docs/sphinx/requirements.in
+requests==2.28.2 \
+    --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \
+    --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf
+    # via
+    #   readthedocs-sphinx-ext
+    #   sphinx
+snowballstemmer==2.2.0 \
+    --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+    --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+    # via sphinx
+sphinx==6.1.3 \
+    --hash=sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2 \
+    --hash=sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc
+    # via
+    #   -r docs/sphinx/requirements.in
+    #   myst-parser
+    #   sphinx-rtd-theme
+    #   sphinxcontrib-jquery
+sphinx-rtd-theme==1.2.0 \
+    --hash=sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8 \
+    --hash=sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2
+    # via -r docs/sphinx/requirements.in
+sphinxcontrib-applehelp==1.0.4 \
+    --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+    --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+    # via sphinx
+sphinxcontrib-devhelp==1.0.2 \
+    --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+    --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+    # via sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+    --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+    --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+    # via sphinx
+sphinxcontrib-jquery==4.1 \
+    --hash=sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a \
+    --hash=sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae
+    # via sphinx-rtd-theme
+sphinxcontrib-jsmath==1.0.1 \
+    --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+    --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+    # via sphinx
+sphinxcontrib-qthelp==1.0.3 \
+    --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+    --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+    --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+    --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+    # via sphinx
+urllib3==1.26.15 \
+    --hash=sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305 \
+    --hash=sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42
+    # via requests
diff --git a/docs/sphinx/requirements_linux.txt b/docs/sphinx/requirements_linux.txt
new file mode 100644
index 0000000..3e65ad8
--- /dev/null
+++ b/docs/sphinx/requirements_linux.txt
@@ -0,0 +1,297 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    bazel run //docs/sphinx:requirements.update
+#
+alabaster==0.7.13 \
+    --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+    --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+    # via sphinx
+babel==2.12.1 \
+    --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+    --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+    # via sphinx
+certifi==2022.12.7 \
+    --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \
+    --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18
+    # via requests
+charset-normalizer==3.1.0 \
+    --hash=sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6 \
+    --hash=sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1 \
+    --hash=sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e \
+    --hash=sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373 \
+    --hash=sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62 \
+    --hash=sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230 \
+    --hash=sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be \
+    --hash=sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c \
+    --hash=sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0 \
+    --hash=sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448 \
+    --hash=sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f \
+    --hash=sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649 \
+    --hash=sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d \
+    --hash=sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0 \
+    --hash=sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706 \
+    --hash=sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a \
+    --hash=sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59 \
+    --hash=sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23 \
+    --hash=sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5 \
+    --hash=sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb \
+    --hash=sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e \
+    --hash=sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e \
+    --hash=sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c \
+    --hash=sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28 \
+    --hash=sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d \
+    --hash=sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41 \
+    --hash=sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974 \
+    --hash=sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce \
+    --hash=sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f \
+    --hash=sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1 \
+    --hash=sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d \
+    --hash=sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8 \
+    --hash=sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017 \
+    --hash=sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31 \
+    --hash=sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7 \
+    --hash=sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8 \
+    --hash=sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e \
+    --hash=sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14 \
+    --hash=sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd \
+    --hash=sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d \
+    --hash=sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795 \
+    --hash=sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b \
+    --hash=sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b \
+    --hash=sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b \
+    --hash=sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203 \
+    --hash=sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f \
+    --hash=sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19 \
+    --hash=sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1 \
+    --hash=sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a \
+    --hash=sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac \
+    --hash=sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9 \
+    --hash=sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0 \
+    --hash=sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137 \
+    --hash=sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f \
+    --hash=sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6 \
+    --hash=sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5 \
+    --hash=sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909 \
+    --hash=sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f \
+    --hash=sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0 \
+    --hash=sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324 \
+    --hash=sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755 \
+    --hash=sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb \
+    --hash=sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854 \
+    --hash=sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c \
+    --hash=sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60 \
+    --hash=sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84 \
+    --hash=sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0 \
+    --hash=sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b \
+    --hash=sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1 \
+    --hash=sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531 \
+    --hash=sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1 \
+    --hash=sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11 \
+    --hash=sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326 \
+    --hash=sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df \
+    --hash=sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab
+    # via requests
+docutils==0.18.1 \
+    --hash=sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c \
+    --hash=sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06
+    # via
+    #   myst-parser
+    #   sphinx
+    #   sphinx-rtd-theme
+idna==3.4 \
+    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+    # via requests
+imagesize==1.4.1 \
+    --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+    --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+    # via sphinx
+jinja2==3.1.2 \
+    --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+    --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+    # via
+    #   myst-parser
+    #   readthedocs-sphinx-ext
+    #   sphinx
+markdown-it-py==2.2.0 \
+    --hash=sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30 \
+    --hash=sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1
+    # via
+    #   mdit-py-plugins
+    #   myst-parser
+markupsafe==2.1.2 \
+    --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
+    --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
+    --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \
+    --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \
+    --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \
+    --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \
+    --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \
+    --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \
+    --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \
+    --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \
+    --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \
+    --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \
+    --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \
+    --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \
+    --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \
+    --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \
+    --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \
+    --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \
+    --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \
+    --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \
+    --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \
+    --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \
+    --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \
+    --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \
+    --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \
+    --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \
+    --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \
+    --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \
+    --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \
+    --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \
+    --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \
+    --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \
+    --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \
+    --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \
+    --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \
+    --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \
+    --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \
+    --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \
+    --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \
+    --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \
+    --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \
+    --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \
+    --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \
+    --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \
+    --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \
+    --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \
+    --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \
+    --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \
+    --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \
+    --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58
+    # via jinja2
+mdit-py-plugins==0.3.5 \
+    --hash=sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e \
+    --hash=sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a
+    # via myst-parser
+mdurl==0.1.2 \
+    --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
+    --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
+    # via markdown-it-py
+myst-parser==1.0.0 \
+    --hash=sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae \
+    --hash=sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c
+    # via -r docs/sphinx/requirements.in
+packaging==23.0 \
+    --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+    --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+    # via
+    #   readthedocs-sphinx-ext
+    #   sphinx
+pygments==2.15.0 \
+    --hash=sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094 \
+    --hash=sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500
+    # via sphinx
+pyyaml==6.0 \
+    --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \
+    --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
+    --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
+    --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
+    --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
+    --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
+    --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
+    --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
+    --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
+    --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
+    --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
+    --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
+    --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \
+    --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
+    --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
+    --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
+    --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
+    --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
+    --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \
+    --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
+    --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
+    --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
+    --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
+    --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
+    --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
+    --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \
+    --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
+    --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
+    --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \
+    --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
+    --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
+    --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
+    --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \
+    --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
+    --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
+    --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
+    --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
+    --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \
+    --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
+    --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
+    # via myst-parser
+readthedocs-sphinx-ext==2.2.3 \
+    --hash=sha256:6583c26791a5853ee9e57ce9db864e2fb06808ba470f805d74d53fc50811e012 \
+    --hash=sha256:e9d911792789b88ae12e2be94d88c619f89a4fa1fe9e42c1505c9930a07163d8
+    # via -r docs/sphinx/requirements.in
+requests==2.28.2 \
+    --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \
+    --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf
+    # via
+    #   readthedocs-sphinx-ext
+    #   sphinx
+snowballstemmer==2.2.0 \
+    --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+    --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+    # via sphinx
+sphinx==6.1.3 \
+    --hash=sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2 \
+    --hash=sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc
+    # via
+    #   -r docs/sphinx/requirements.in
+    #   myst-parser
+    #   sphinx-rtd-theme
+    #   sphinxcontrib-jquery
+sphinx-rtd-theme==1.2.0 \
+    --hash=sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8 \
+    --hash=sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2
+    # via -r docs/sphinx/requirements.in
+sphinxcontrib-applehelp==1.0.4 \
+    --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+    --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+    # via sphinx
+sphinxcontrib-devhelp==1.0.2 \
+    --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+    --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+    # via sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+    --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+    --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+    # via sphinx
+sphinxcontrib-jquery==4.1 \
+    --hash=sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a \
+    --hash=sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae
+    # via sphinx-rtd-theme
+sphinxcontrib-jsmath==1.0.1 \
+    --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+    --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+    # via sphinx
+sphinxcontrib-qthelp==1.0.3 \
+    --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+    --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+    --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+    --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+    # via sphinx
+urllib3==1.26.15 \
+    --hash=sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305 \
+    --hash=sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42
+    # via requests
diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel
index feb1cfb..f8d0ebe 100644
--- a/examples/BUILD.bazel
+++ b/examples/BUILD.bazel
@@ -68,5 +68,5 @@
 bazel_integration_test(
     name = "bzlmod_example",
     bzlmod = True,
-    override_bazel_version = "6.0.0",
+    override_bazel_version = "6.2.0",
 )
diff --git a/sphinxdocs/BUILD.bazel b/sphinxdocs/BUILD.bazel
new file mode 100644
index 0000000..2ff708f
--- /dev/null
+++ b/sphinxdocs/BUILD.bazel
@@ -0,0 +1,52 @@
+# 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")
+
+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",
+    ],
+)
+
+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 = "readthedocs_bzl",
+    srcs = ["readthedocs.bzl"],
+    deps = ["//python:py_binary_bzl"],
+)
diff --git a/sphinxdocs/func_template.vm b/sphinxdocs/func_template.vm
new file mode 100644
index 0000000..ee6a2bf
--- /dev/null
+++ b/sphinxdocs/func_template.vm
@@ -0,0 +1,56 @@
+#set( $nl = "
+" )
+#set( $fn = $funcInfo.functionName)
+#set( $fnl = $fn.replaceAll("[.]", "_").toLowerCase())
+{.starlark-object}
+#[[##]]# $fn
+
+#set( $hasParams = false)
+{.starlark-signature}
+${funcInfo.functionName}(## Comment to consume newline
+#foreach ($param in $funcInfo.getParameterList())
+#if($param.name != "self")
+#set( $hasParams = true)
+[${param.name}](#${fnl}_${param.name})## Comment to consume newline
+#if(!$param.getDefaultValue().isEmpty())
+=$param.getDefaultValue()#end#if($foreach.hasNext),
+#end
+#end
+#end
+)
+
+${funcInfo.docString}
+
+#if ($hasParams)
+{#${fnl}_parameters}
+**PARAMETERS** [¶](#${fnl}_parameters){.headerlink}
+
+#foreach ($param in $funcInfo.getParameterList())
+#if($param.name != "self")
+#set($link = $fnl + "_" + $param.name)
+#if($foreach.first)
+{.params-box}
+#end
+## The .span wrapper is necessary so the trailing colon doesn't wrap
+:[${param.name}[¶](#$link){.headerlink}]{.span}: []{#$link}
+#if(!$param.getDefaultValue().isEmpty())(_default `${param.getDefaultValue()}`_) #end
+#if(!$param.docString.isEmpty())
+  $param.docString.replaceAll("$nl", "$nl  ")
+#else
+  _undocumented_
+#end
+#end
+#end
+#end
+#if (!$funcInfo.getReturn().docString.isEmpty())
+
+{#${fnl}_returns}
+RETURNS [¶](#${fnl}_returns){.headerlink}
+: ${funcInfo.getReturn().docString.replaceAll("$nl", "$nl  ")}
+#end
+#if (!$funcInfo.getDeprecated().docString.isEmpty())
+
+**DEPRECATED**
+
+${funcInfo.getDeprecated().docString}
+#end
diff --git a/sphinxdocs/header_template.vm b/sphinxdocs/header_template.vm
new file mode 100644
index 0000000..fee7e2c
--- /dev/null
+++ b/sphinxdocs/header_template.vm
@@ -0,0 +1 @@
+$moduleDocstring
diff --git a/sphinxdocs/provider_template.vm b/sphinxdocs/provider_template.vm
new file mode 100644
index 0000000..55e6871
--- /dev/null
+++ b/sphinxdocs/provider_template.vm
@@ -0,0 +1,29 @@
+#set( $nl = "
+" )
+#set( $pn = $providerInfo.providerName)
+#set( $pnl = $pn.replaceAll("[.]", "_").toLowerCase())
+{.starlark-object}
+#[[##]]# ${providerName}
+
+#set( $hasFields = false)
+{.starlark-signature}
+${providerInfo.providerName}(## Comment to consume newline
+#foreach ($field in $providerInfo.getFieldInfoList())
+#set( $hasFields = true)
+[${field.name}](#${pnl}_${field.name})## Comment to consume newline
+#if($foreach.hasNext),
+#end
+#end
+)
+
+$providerInfo.docString
+
+#if ($hasFields)
+**FIELDS** [¶](#${pnl}_fields){.headerlink}
+
+#foreach ($field in $providerInfo.getFieldInfoList())
+#set($link = $pnl + "_" + $field.name)
+:[${field.name}[¶](#$link){.headerlink}]{.span}: []{#$link}
+  $field.docString.replaceAll("$nl", "$nl  ")
+#end
+#end
diff --git a/sphinxdocs/readthedocs.bzl b/sphinxdocs/readthedocs.bzl
new file mode 100644
index 0000000..6ffc79c
--- /dev/null
+++ b/sphinxdocs/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: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/readthedocs_install.py
new file mode 100644
index 0000000..9b1f2a8
--- /dev/null
+++ b/sphinxdocs/readthedocs_install.py
@@ -0,0 +1,27 @@
+import os
+import pathlib
+import shutil
+import sys
+
+from python import runfiles
+
+
+def main(args):
+    if not args:
+        raise ValueError("Empty args: expected paths to copy")
+
+    if not (install_to := os.environ.get("READTHEDOCS_OUTPUT")):
+        raise ValueError("READTHEDOCS_OUTPUT environment variable not set")
+
+    install_to = pathlib.Path(install_to)
+
+    rf = runfiles.Create()
+    for doc_dir_runfiles_path in args:
+        doc_dir_path = pathlib.Path(rf.Rlocation(doc_dir_runfiles_path))
+        dest = install_to / doc_dir_path.name
+        print(f"Copying {doc_dir_path} to {dest}")
+        shutil.copytree(src=doc_dir_path, dst=dest, dirs_exist_ok=True)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/sphinxdocs/rule_template.vm b/sphinxdocs/rule_template.vm
new file mode 100644
index 0000000..d91bad2
--- /dev/null
+++ b/sphinxdocs/rule_template.vm
@@ -0,0 +1,48 @@
+#set( $nl = "
+" )
+#set( $rn = $ruleInfo.ruleName)
+#set( $rnl = $rn.replaceAll("[.]", "_").toLowerCase())
+{.starlark-object}
+#[[##]]# $ruleName
+
+#set( $hasAttrs = false)
+{.starlark-signature}
+${ruleInfo.ruleName}(## Comment to consume newline
+#foreach ($attr in $ruleInfo.getAttributeList())
+#set( $hasAttrs = true)
+[${attr.name}](#${rnl}_${attr.name})## Comment to consume newline
+#if(!$attr.getDefaultValue().isEmpty())
+=$attr.getDefaultValue()#end#if($foreach.hasNext),
+#end
+#end
+)
+
+$ruleInfo.docString
+
+#if ($hasAttrs)
+{#${rnl}_attributes}
+**ATTRIBUTES** [¶](#${rnl}_attributes){.headerlink}
+
+#foreach ($attr in $ruleInfo.getAttributeList())
+#set($link = $rnl + "_" + $attr.name)
+#if($attr.mandatory)
+#set($opt = "required")
+#else
+#set($opt = "optional")
+#end
+#if($attr.type == "NAME")
+#set($type = "[Name][target-name]")
+#elseif($attr.type == "LABEL_LIST")
+#set($type = "list of [label][attr-label]s")
+#end
+#if(!$attr.getDefaultValue().isEmpty())
+#set($default = ", default `" + $attr.getDefaultValue() + "`")
+#else
+#set($default = "")
+#end
+:[${attr.name}[¶](#$link){.headerlink}]{.span}: []{#$link}
+  _($opt $type$default)_
+  $attr.docString.replaceAll("$nl", "$nl  ")
+
+#end
+#end
diff --git a/sphinxdocs/sphinx.bzl b/sphinxdocs/sphinx.bzl
new file mode 100644
index 0000000..3c8b776
--- /dev/null
+++ b/sphinxdocs/sphinx.bzl
@@ -0,0 +1,216 @@
+# 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 documentation.
+
+The general usage of the Sphinx rules requires two pieces:
+
+1. Using `sphinx_docs` to define the docs to build and options for building.
+2. Defining a `sphinx-build` binary to run Sphinx with the necessary
+   dependencies to be used by (1); the `sphinx_build_binary` rule helps with
+   this.
+
+Defining your own `sphinx-build` binary is necessary because Sphinx uses
+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."),
+    },
+)
+
+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
diff --git a/sphinxdocs/sphinx_build.py b/sphinxdocs/sphinx_build.py
new file mode 100644
index 0000000..3b7b32e
--- /dev/null
+++ b/sphinxdocs/sphinx_build.py
@@ -0,0 +1,8 @@
+import os
+import pathlib
+import sys
+
+from sphinx.cmd.build import main
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/sphinxdocs/sphinx_server.py b/sphinxdocs/sphinx_server.py
new file mode 100644
index 0000000..55d42c0
--- /dev/null
+++ b/sphinxdocs/sphinx_server.py
@@ -0,0 +1,32 @@
+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
new file mode 100644
index 0000000..ef610ce
--- /dev/null
+++ b/sphinxdocs/sphinx_stardoc.bzl
@@ -0,0 +1,89 @@
+# 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: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
+    )
diff --git a/version.bzl b/version.bzl
index 8c7f01c..bf6f822 100644
--- a/version.bzl
+++ b/version.bzl
@@ -17,7 +17,7 @@
 # against.
 # This version should be updated together with the version of Bazel
 # in .bazelversion.
-BAZEL_VERSION = "6.0.0"
+BAZEL_VERSION = "6.2.0"
 
 # NOTE: Keep in sync with .bazelci/presubmit.yml
 # This is the minimum supported bazel version, that we have some tests for.