aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Levasseur <rlevasseur@google.com>2023-12-19 16:51:44 -0800
committerGitHub <noreply@github.com>2023-12-20 00:51:44 +0000
commit509b02f8c719decb40f4731b735e65b6545ceea0 (patch)
treed9f29f6f039c859f7cface9ab7d027e306d867d2
parent87a3a54cd937b037f531fedac243350933dd1eb7 (diff)
downloadbazelbuild-rules_python-509b02f8c719decb40f4731b735e65b6545ceea0.tar.gz
docs: use stardoc proto output to generate markdown docs (#1629)
The template language Stardoc uses (Velocity) is niche and fairly esoteric, and requires a lot of experimenting to understand how to make it produce the desired output. In particular, it largely assumes whitespace doesn't matter, which makes it a poor fit for generating Markdown, where whitespace often does matter. Instead, a small Python program is used to consume the binary proto output of Stardoc, which converts it to Markdown. This also makes it easier to customize the overall output and re-use code for the different types of objects rendered. The visible changes to the docs are: * Module extensions are now documented * Repository rules follow the style of the other generated docs * Fixed the rendering of pip_repository docs -- it had an h2 section which broke the section grouping of the API objects. * Puts some padding between the border and content for text in params/attrs/fields listings. Other notable changes: * Make RTD builds use bzlmod. This is necessary so that the pip extension can be documented. It loads `@pythons_hub//:interpreters.bzl`, but that repo is only created when bzlmod is enabled) --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
-rw-r--r--.bazelrc5
-rw-r--r--CHANGELOG.md4
-rw-r--r--MODULE.bazel6
-rw-r--r--WORKSPACE10
-rw-r--r--docs/sphinx/BUILD.bazel14
-rw-r--r--docs/sphinx/_stardoc_footer.md2
-rw-r--r--docs/sphinx/_static/css/custom.css11
-rw-r--r--docs/sphinx/pyproject.toml1
-rwxr-xr-xdocs/sphinx/readthedocs_build.sh1
-rw-r--r--docs/sphinx/requirements_linux.txt4
-rw-r--r--python/BUILD.bazel1
-rw-r--r--python/extensions/BUILD.bazel16
-rw-r--r--python/pip_install/pip_repository.bzl2
-rw-r--r--python/private/bzlmod/BUILD.bazel45
-rw-r--r--python/private/bzlmod/pythons_hub.bzl24
-rw-r--r--sphinxdocs/private/BUILD.bazel27
-rw-r--r--sphinxdocs/private/func_template.vm57
-rw-r--r--sphinxdocs/private/header_template.vm3
-rw-r--r--sphinxdocs/private/proto_to_markdown.py488
-rw-r--r--sphinxdocs/private/provider_template.vm30
-rw-r--r--sphinxdocs/private/rule_template.vm48
-rw-r--r--sphinxdocs/private/sphinx_stardoc.bzl89
-rw-r--r--sphinxdocs/tests/BUILD.bazel13
-rw-r--r--sphinxdocs/tests/proto_to_markdown/BUILD.bazel24
-rw-r--r--sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py203
25 files changed, 928 insertions, 200 deletions
diff --git a/.bazelrc b/.bazelrc
index 52251b1..fd2e442 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -25,5 +25,8 @@ startup --windows_enable_symlinks
common --noexperimental_enable_bzlmod
# Additional config to use for readthedocs builds.
-# See .readthedocs.yml for additional flags
+# See .readthedocs.yml for additional flags that can only be determined from
+# the runtime environment.
build:rtd --stamp
+# Some bzl files contain repos only available under bzlmod
+build:rtd --enable_bzlmod
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0b4a72..b032f4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,6 +47,10 @@ A brief description of the categories of changes:
* (bzlmod pip.parse) Requirements files with duplicate entries for the same
package (e.g. one for the package, one for an extra) now work.
+### Added
+
+* (docs) bzlmod extensions are now documented on rules-python.readthedocs.io
+
[0.XX.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.XX.0
## [0.27.0] - 2023-11-16
diff --git a/MODULE.bazel b/MODULE.bazel
index 9aaeaf6..f53815c 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -56,12 +56,12 @@ use_repo(python, "pythons_hub")
register_toolchains("@pythons_hub//:all")
# ===== DEV ONLY SETUP =====
-docs_pip = use_extension(
+dev_pip = use_extension(
"//python/extensions:pip.bzl",
"pip",
dev_dependency = True,
)
-docs_pip.parse(
+dev_pip.parse(
experimental_requirement_cycles = {
"sphinx": [
"sphinx",
@@ -72,7 +72,7 @@ docs_pip.parse(
"sphinxcontrib-applehelp",
],
},
- hub_name = "docs_deps",
+ hub_name = "dev_pip",
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 074a7b9..b8e778e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -109,7 +109,7 @@ install_deps()
# Install sphinx for doc generation.
pip_parse(
- name = "docs_deps",
+ name = "dev_pip",
experimental_requirement_cycles = {
"sphinx": [
"sphinx",
@@ -126,7 +126,7 @@ pip_parse(
requirements_lock = "//docs/sphinx:requirements_linux.txt",
)
-load("@docs_deps//:requirements.bzl", docs_install_deps = "install_deps")
+load("@dev_pip//:requirements.bzl", docs_install_deps = "install_deps")
docs_install_deps()
@@ -140,3 +140,9 @@ http_file(
"https://files.pythonhosted.org/packages/50/67/3e966d99a07d60a21a21d7ec016e9e4c2642a86fea251ec68677daf71d4d/numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
],
)
+
+# rules_proto expects //external:python_headers to point at the python headers.
+bind(
+ name = "python_headers",
+ actual = "//python/cc:current_py_cc_headers",
+)
diff --git a/docs/sphinx/BUILD.bazel b/docs/sphinx/BUILD.bazel
index 95b3cfa..4c14aee 100644
--- a/docs/sphinx/BUILD.bazel
+++ b/docs/sphinx/BUILD.bazel
@@ -12,8 +12,10 @@
# 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("@dev_pip//:requirements.bzl", "requirement")
+load("//python:pip.bzl", "compile_pip_requirements")
+load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
+load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
load("//sphinxdocs:readthedocs.bzl", "readthedocs_install")
load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs", "sphinx_inventory")
load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs")
@@ -83,7 +85,13 @@ sphinx_stardocs(
"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",
- },
+ } | ({
+ # Bazel 6 + Stardoc isn't able to parse something about the python bzlmod extension
+ "api/extensions/python.md": "//python/extensions:python_bzl",
+ } if IS_BAZEL_7_OR_HIGHER else {}) | ({
+ # This depends on @pythons_hub, which is only created under bzlmod
+ "api/extensions/pip.md": "//python/extensions:pip_bzl",
+ } if BZLMOD_ENABLED else {}),
footer = "_stardoc_footer.md",
tags = ["docs"],
target_compatible_with = _TARGET_COMPATIBLE_WITH,
diff --git a/docs/sphinx/_stardoc_footer.md b/docs/sphinx/_stardoc_footer.md
index 65d74f4..7aa33f7 100644
--- a/docs/sphinx/_stardoc_footer.md
+++ b/docs/sphinx/_stardoc_footer.md
@@ -7,6 +7,8 @@
[`Label`]: https://bazel.build/rules/lib/Label
[`list`]: https://bazel.build/rules/lib/list
[`str`]: https://bazel.build/rules/lib/string
+[str]: https://bazel.build/rules/lib/string
+[`int`]: https://bazel.build/rules/lib/int
[`struct`]: https://bazel.build/rules/lib/builtins/struct
[`Target`]: https://bazel.build/rules/lib/Target
[target-name]: https://bazel.build/concepts/labels#target-names
diff --git a/docs/sphinx/_static/css/custom.css b/docs/sphinx/_static/css/custom.css
index c97d2f5..4b073d4 100644
--- a/docs/sphinx/_static/css/custom.css
+++ b/docs/sphinx/_static/css/custom.css
@@ -12,8 +12,17 @@
border-bottom: thin solid grey;
padding-left: 0.5ex;
}
+.starlark-object h3 {
+ background-color: #e7f2fa;
+ padding-left: 0.5ex;
+}
+
+.starlark-module-extension-tag-class h3 {
+ background-color: #add8e6;
+ padding-left: 0.5ex;
+}
-.starlark-object>p, .starlark-object>dl {
+.starlark-object>p, .starlark-object>dl, .starlark-object>section>* {
/* Prevent the words from touching the border line */
padding-left: 0.5ex;
}
diff --git a/docs/sphinx/pyproject.toml b/docs/sphinx/pyproject.toml
index 02e0f36..d36c9f2 100644
--- a/docs/sphinx/pyproject.toml
+++ b/docs/sphinx/pyproject.toml
@@ -9,4 +9,5 @@ dependencies = [
"myst-parser",
"sphinx_rtd_theme",
"readthedocs-sphinx-ext",
+ "absl-py",
]
diff --git a/docs/sphinx/readthedocs_build.sh b/docs/sphinx/readthedocs_build.sh
index e6908a3..c611b7c 100755
--- a/docs/sphinx/readthedocs_build.sh
+++ b/docs/sphinx/readthedocs_build.sh
@@ -14,6 +14,7 @@ extra_env+=("--//sphinxdocs:extra_env=HOSTNAME=$HOSTNAME")
set -x
bazel run \
+ --config=rtd \
"--//sphinxdocs:extra_defines=version=$READTHEDOCS_VERSION" \
"${extra_env[@]}" \
//docs/sphinx:readthedocs_install
diff --git a/docs/sphinx/requirements_linux.txt b/docs/sphinx/requirements_linux.txt
index 429ddd4..85c61f3 100644
--- a/docs/sphinx/requirements_linux.txt
+++ b/docs/sphinx/requirements_linux.txt
@@ -4,6 +4,10 @@
#
# bazel run //docs/sphinx:requirements.update
#
+absl-py==2.0.0 \
+ --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3 \
+ --hash=sha256:d9690211c5fcfefcdd1a45470ac2b5c5acd45241c3af71eed96bc5441746c0d5
+ # via rules-python-docs (docs/sphinx/pyproject.toml)
alabaster==0.7.13 \
--hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
--hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index 480b193..1ab59d5 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -196,6 +196,7 @@ bzl_library(
srcs = ["repositories.bzl"],
deps = [
":versions_bzl",
+ "//python/pip_install:repositories_bzl",
"//python/private:auth_bzl",
"//python/private:bazel_tools_bzl",
"//python/private:bzlmod_enabled_bzl",
diff --git a/python/extensions/BUILD.bazel b/python/extensions/BUILD.bazel
index 4be3e37..88e3984 100644
--- a/python/extensions/BUILD.bazel
+++ b/python/extensions/BUILD.bazel
@@ -12,6 +12,8 @@
# 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 = ["//visibility:public"])
licenses(["notice"])
@@ -21,3 +23,17 @@ filegroup(
srcs = glob(["**"]),
visibility = ["//python:__pkg__"],
)
+
+bzl_library(
+ name = "pip_bzl",
+ srcs = ["pip.bzl"],
+ visibility = ["//:__subpackages__"],
+ deps = ["//python/private/bzlmod:pip_bzl"],
+)
+
+bzl_library(
+ name = "python_bzl",
+ srcs = ["python.bzl"],
+ visibility = ["//:__subpackages__"],
+ deps = ["//python/private/bzlmod:python_bzl"],
+)
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index a02aecc..dca36ce 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -678,7 +678,7 @@ alias(
)
```
-## Vendoring the requirements.bzl file
+### 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
diff --git a/python/private/bzlmod/BUILD.bazel b/python/private/bzlmod/BUILD.bazel
index fc8449e..a312922 100644
--- a/python/private/bzlmod/BUILD.bazel
+++ b/python/private/bzlmod/BUILD.bazel
@@ -13,8 +13,9 @@
# limitations under the License.
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
-package(default_visibility = ["//visibility:private"])
+package(default_visibility = ["//:__subpackages__"])
licenses(["notice"])
@@ -25,6 +26,28 @@ filegroup(
)
bzl_library(
+ name = "pip_bzl",
+ srcs = ["pip.bzl"],
+ deps = [
+ ":pip_repository_bzl",
+ "//python/pip_install:pip_repository_bzl",
+ "//python/pip_install:requirements_parser_bzl",
+ "//python/private:full_version_bzl",
+ "//python/private:normalize_name_bzl",
+ "//python/private:parse_whl_name_bzl",
+ "//python/private:version_label_bzl",
+ ":bazel_features_bzl",
+ ] + [
+ "@pythons_hub//:interpreters_bzl",
+ ] if BZLMOD_ENABLED else [],
+)
+
+bzl_library(
+ name = "bazel_features_bzl",
+ srcs = ["@bazel_features//:bzl_files"] if BZLMOD_ENABLED else [],
+)
+
+bzl_library(
name = "pip_repository_bzl",
srcs = ["pip_repository.bzl"],
visibility = ["//:__subpackages__"],
@@ -33,3 +56,23 @@ bzl_library(
"//python/private:text_util_bzl",
],
)
+
+bzl_library(
+ name = "python_bzl",
+ srcs = ["python.bzl"],
+ deps = [
+ ":pythons_hub_bzl",
+ "//python:repositories_bzl",
+ "//python/private:toolchains_repo_bzl",
+ ],
+)
+
+bzl_library(
+ name = "pythons_hub_bzl",
+ srcs = ["pythons_hub.bzl"],
+ deps = [
+ "//python:versions_bzl",
+ "//python/private:full_version_bzl",
+ "//python/private:toolchains_repo_bzl",
+ ],
+)
diff --git a/python/private/bzlmod/pythons_hub.bzl b/python/private/bzlmod/pythons_hub.bzl
index f36ce45..5f536f3 100644
--- a/python/private/bzlmod/pythons_hub.bzl
+++ b/python/private/bzlmod/pythons_hub.bzl
@@ -29,7 +29,19 @@ def _have_same_length(*lists):
fail("expected at least one list")
return len({len(length): None for length in lists}) == 1
-def _python_toolchain_build_file_content(
+_HUB_BUILD_FILE_TEMPLATE = """\
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+bzl_library(
+ name = "interpreters_bzl",
+ srcs = ["interpreters.bzl"],
+ visibility = ["@rules_python//:__subpackages__"],
+)
+
+{toolchains}
+"""
+
+def _hub_build_file_content(
prefixes,
python_versions,
set_python_version_constraints,
@@ -48,7 +60,7 @@ def _python_toolchain_build_file_content(
# Iterate over the length of python_versions and call
# build the toolchain content by calling python_toolchain_build_file_content
- return "\n".join([python_toolchain_build_file_content(
+ toolchains = "\n".join([python_toolchain_build_file_content(
prefix = prefixes[i],
python_version = full_version(python_versions[i]),
set_python_version_constraint = set_python_version_constraints[i],
@@ -56,7 +68,9 @@ def _python_toolchain_build_file_content(
rules_python = rules_python,
) for i in range(len(python_versions))])
-_build_file_for_hub_template = """
+ return _HUB_BUILD_FILE_TEMPLATE.format(toolchains = toolchains)
+
+_interpreters_bzl_template = """
INTERPRETER_LABELS = {{
{interpreter_labels}
}}
@@ -72,7 +86,7 @@ def _hub_repo_impl(rctx):
# write them to the BUILD file.
rctx.file(
"BUILD.bazel",
- _python_toolchain_build_file_content(
+ _hub_build_file_content(
rctx.attr.toolchain_prefixes,
rctx.attr.toolchain_python_versions,
rctx.attr.toolchain_set_python_version_constraints,
@@ -97,7 +111,7 @@ def _hub_repo_impl(rctx):
rctx.file(
"interpreters.bzl",
- _build_file_for_hub_template.format(
+ _interpreters_bzl_template.format(
interpreter_labels = interpreter_labels,
default_python_version = rctx.attr.default_python_version,
),
diff --git a/sphinxdocs/private/BUILD.bazel b/sphinxdocs/private/BUILD.bazel
index a8701d9..01758b3 100644
--- a/sphinxdocs/private/BUILD.bazel
+++ b/sphinxdocs/private/BUILD.bazel
@@ -13,7 +13,9 @@
# limitations under the License.
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python:proto.bzl", "py_proto_library")
load("//python:py_binary.bzl", "py_binary")
+load("//python:py_library.bzl", "py_library")
package(
default_visibility = ["//sphinxdocs:__subpackages__"],
@@ -70,3 +72,28 @@ py_binary(
# Only public because it's an implicit attribute
visibility = ["//:__subpackages__"],
)
+
+py_binary(
+ name = "proto_to_markdown",
+ srcs = ["proto_to_markdown.py"],
+ # Only public because it's an implicit attribute
+ visibility = ["//:__subpackages__"],
+ deps = [":proto_to_markdown_lib"],
+)
+
+py_library(
+ name = "proto_to_markdown_lib",
+ srcs = ["proto_to_markdown.py"],
+ # Only public because it's an implicit attribute
+ visibility = ["//:__subpackages__"],
+ deps = [
+ ":stardoc_output_proto_py_pb2",
+ ],
+)
+
+py_proto_library(
+ name = "stardoc_output_proto_py_pb2",
+ deps = [
+ "@io_bazel_stardoc//stardoc/proto:stardoc_output_proto",
+ ],
+)
diff --git a/sphinxdocs/private/func_template.vm b/sphinxdocs/private/func_template.vm
deleted file mode 100644
index 81dd203..0000000
--- a/sphinxdocs/private/func_template.vm
+++ /dev/null
@@ -1,57 +0,0 @@
-#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/private/header_template.vm b/sphinxdocs/private/header_template.vm
deleted file mode 100644
index 81496ff..0000000
--- a/sphinxdocs/private/header_template.vm
+++ /dev/null
@@ -1,3 +0,0 @@
-# %%BZL_LOAD_PATH%%
-
-$moduleDocstring
diff --git a/sphinxdocs/private/proto_to_markdown.py b/sphinxdocs/private/proto_to_markdown.py
new file mode 100644
index 0000000..18d4e1e
--- /dev/null
+++ b/sphinxdocs/private/proto_to_markdown.py
@@ -0,0 +1,488 @@
+# 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.
+
+import argparse
+import io
+import itertools
+import pathlib
+import sys
+import textwrap
+from typing import Callable, TextIO, TypeVar
+
+from stardoc.proto import stardoc_output_pb2
+
+_AttributeType = stardoc_output_pb2.AttributeType
+
+_T = TypeVar("_T")
+
+
+def _anchor_id(text: str) -> str:
+ # MyST/Sphinx's markdown processing doesn't like dots in anchor ids.
+ return "#" + text.replace(".", "_").lower()
+
+
+# Create block attribute line.
+# See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#block-attributes
+def _block_attrs(*attrs: str) -> str:
+ return "{" + " ".join(attrs) + "}\n"
+
+
+def _link(display: str, link: str = "", *, ref: str = "", classes: str = "") -> str:
+ if ref:
+ ref = f"[{ref}]"
+ if link:
+ link = f"({link})"
+ if classes:
+ classes = "{" + classes + "}"
+ return f"[{display}]{ref}{link}{classes}"
+
+
+def _span(display: str, classes: str = ".span") -> str:
+ return f"[{display}]{{" + classes + "}"
+
+
+def _link_here_icon(anchor: str) -> str:
+ # The headerlink class activates some special logic to show/hide
+ # text upon mouse-over; it's how headings show a clickable link.
+ return _link("¶", anchor, classes=".headerlink")
+
+
+def _inline_anchor(anchor: str) -> str:
+ return _span("", anchor)
+
+
+def _indent_block_text(text: str) -> str:
+ return text.strip().replace("\n", "\n ")
+
+
+def _join_csv_and(values: list[str]) -> str:
+ if len(values) == 1:
+ return values[0]
+
+ values = list(values)
+ values[-1] = "and " + values[-1]
+ return ", ".join(values)
+
+
+def _position_iter(values: list[_T]) -> tuple[bool, bool, _T]:
+ for i, value in enumerate(values):
+ yield i == 0, i == len(values) - 1, value
+
+
+class _MySTRenderer:
+ def __init__(
+ self,
+ module: stardoc_output_pb2.ModuleInfo,
+ out_stream: TextIO,
+ public_load_path: str,
+ ):
+ self._module = module
+ self._out_stream = out_stream
+ self._public_load_path = public_load_path
+
+ def render(self):
+ self._render_module(self._module)
+
+ def _render_module(self, module: stardoc_output_pb2.ModuleInfo):
+ if self._public_load_path:
+ bzl_path = self._public_load_path
+ else:
+ bzl_path = "//" + self._module.file.split("//")[1]
+ self._write(
+ f"# {bzl_path}\n",
+ "\n",
+ module.module_docstring.strip(),
+ "\n\n",
+ )
+
+ # Sort the objects by name
+ objects = itertools.chain(
+ ((r.rule_name, r, self._render_rule) for r in module.rule_info),
+ ((p.provider_name, p, self._render_provider) for p in module.provider_info),
+ ((f.function_name, f, self._render_func) for f in module.func_info),
+ ((a.aspect_name, a, self._render_aspect) for a in module.aspect_info),
+ (
+ (m.extension_name, m, self._render_module_extension)
+ for m in module.module_extension_info
+ ),
+ (
+ (r.rule_name, r, self._render_repository_rule)
+ for r in module.repository_rule_info
+ ),
+ )
+
+ objects = sorted(objects, key=lambda v: v[0].lower())
+
+ for _, obj, func in objects:
+ func(obj)
+ self._write("\n")
+
+ def _render_aspect(self, aspect: stardoc_output_pb2.AspectInfo):
+ aspect_anchor = _anchor_id(aspect.aspect_name)
+ self._write(
+ _block_attrs(".starlark-object"),
+ f"## {aspect.aspect_name}\n\n",
+ "_Propagates on attributes:_ ", # todo add link here
+ ", ".join(sorted(f"`{attr}`" for attr in aspect.aspect_attribute)),
+ "\n\n",
+ aspect.doc_string.strip(),
+ "\n\n",
+ )
+
+ if aspect.attribute:
+ self._render_attributes(aspect_anchor, aspect.attribute)
+ self._write("\n")
+
+ def _render_module_extension(self, mod_ext: stardoc_output_pb2.ModuleExtensionInfo):
+ self._write(
+ _block_attrs(".starlark-object"),
+ f"## {mod_ext.extension_name}\n\n",
+ )
+
+ self._write(mod_ext.doc_string.strip(), "\n\n")
+
+ mod_ext_anchor = _anchor_id(mod_ext.extension_name)
+ for tag in mod_ext.tag_class:
+ tag_name = f"{mod_ext.extension_name}.{tag.tag_name}"
+ tag_anchor = f"{mod_ext_anchor}_{tag.tag_name}"
+ self._write(
+ _block_attrs(".starlark-module-extension-tag-class"),
+ f"### {tag_name}\n\n",
+ )
+ self._render_signature(
+ tag_name,
+ tag_anchor,
+ tag.attribute,
+ get_name=lambda a: a.name,
+ get_default=lambda a: a.default_value,
+ )
+
+ self._write(tag.doc_string.strip(), "\n\n")
+ self._render_attributes(tag_anchor, tag.attribute)
+ self._write("\n")
+
+ def _render_repository_rule(self, repo_rule: stardoc_output_pb2.RepositoryRuleInfo):
+ self._write(
+ _block_attrs(".starlark-object"),
+ f"## {repo_rule.rule_name}\n\n",
+ )
+ repo_anchor = _anchor_id(repo_rule.rule_name)
+ self._render_signature(
+ repo_rule.rule_name,
+ repo_anchor,
+ repo_rule.attribute,
+ get_name=lambda a: a.name,
+ get_default=lambda a: a.default_value,
+ )
+ self._write(repo_rule.doc_string.strip(), "\n\n")
+ if repo_rule.attribute:
+ self._render_attributes(repo_anchor, repo_rule.attribute)
+ if repo_rule.environ:
+ self._write(
+ "**ENVIRONMENT VARIABLES** ",
+ _link_here_icon(repo_anchor + "_env"),
+ "\n",
+ )
+ for name in sorted(repo_rule.environ):
+ self._write(f"* `{name}`\n")
+ self._write("\n")
+
+ def _render_rule(self, rule: stardoc_output_pb2.RuleInfo):
+ rule_name = rule.rule_name
+ rule_anchor = _anchor_id(rule_name)
+ self._write(
+ _block_attrs(".starlark-object"),
+ f"## {rule_name}\n\n",
+ )
+
+ self._render_signature(
+ rule_name,
+ rule_anchor,
+ rule.attribute,
+ get_name=lambda r: r.name,
+ get_default=lambda r: r.default_value,
+ )
+
+ self._write(rule.doc_string.strip(), "\n\n")
+
+ if len(rule.advertised_providers.provider_name) == 0:
+ self._write("_Provides_: no providers advertised.")
+ else:
+ self._write(
+ "_Provides_: ",
+ ", ".join(rule.advertised_providers.provider_name),
+ )
+ self._write("\n\n")
+
+ if rule.attribute:
+ self._render_attributes(rule_anchor, rule.attribute)
+
+ def _rule_attr_type_string(self, attr: stardoc_output_pb2.AttributeInfo) -> str:
+ if attr.type == _AttributeType.NAME:
+ return _link("Name", ref="target-name")
+ elif attr.type == _AttributeType.INT:
+ return _link("int", ref="int")
+ elif attr.type == _AttributeType.LABEL:
+ return _link("label", ref="attr-label")
+ elif attr.type == _AttributeType.STRING:
+ return _link("string", ref="str")
+ elif attr.type == _AttributeType.STRING_LIST:
+ return "list of " + _link("string", ref="str")
+ elif attr.type == _AttributeType.INT_LIST:
+ return "list of " + _link("int", ref="int")
+ elif attr.type == _AttributeType.LABEL_LIST:
+ return "list of " + _link("label", ref="attr-label") + "s"
+ elif attr.type == _AttributeType.BOOLEAN:
+ return _link("bool", ref="bool")
+ elif attr.type == _AttributeType.LABEL_STRING_DICT:
+ return "dict of {key} to {value}".format(
+ key=_link("label", ref="attr-label"), value=_link("string", ref="str")
+ )
+ elif attr.type == _AttributeType.STRING_DICT:
+ return "dict of {key} to {value}".format(
+ key=_link("string", ref="str"), value=_link("string", ref="str")
+ )
+ elif attr.type == _AttributeType.STRING_LIST_DICT:
+ return "dict of {key} to list of {value}".format(
+ key=_link("string", ref="str"), value=_link("string", ref="str")
+ )
+ elif attr.type == _AttributeType.OUTPUT:
+ return _link("label", ref="attr-label")
+ elif attr.type == _AttributeType.OUTPUT_LIST:
+ return "list of " + _link("label", ref="attr-label")
+ else:
+ # If we get here, it means the value was unknown for some reason.
+ # Rather than error, give some somewhat understandable value.
+ return _AttributeType.Name(attr.type)
+
+ def _render_func(self, func: stardoc_output_pb2.StarlarkFunctionInfo):
+ func_name = func.function_name
+ func_anchor = _anchor_id(func_name)
+ self._write(
+ _block_attrs(".starlark-object"),
+ f"## {func_name}\n\n",
+ )
+
+ parameters = [param for param in func.parameter if param.name != "self"]
+
+ self._render_signature(
+ func_name,
+ func_anchor,
+ parameters,
+ get_name=lambda p: p.name,
+ get_default=lambda p: p.default_value,
+ )
+
+ self._write(func.doc_string.strip(), "\n\n")
+
+ if parameters:
+ self._write(
+ _block_attrs(f"{func_anchor}_parameters"),
+ "**PARAMETERS** ",
+ _link_here_icon(f"{func_anchor}_parameters"),
+ "\n\n",
+ )
+ entries = []
+ for param in parameters:
+ entries.append(
+ [
+ f"{func_anchor}_{param.name}",
+ param.name,
+ f"(_default `{param.default_value}`_) "
+ if param.default_value
+ else "",
+ param.doc_string if param.doc_string else "_undocumented_",
+ ]
+ )
+ self._render_field_list(entries)
+
+ if getattr(func, "return").doc_string:
+ return_doc = _indent_block_text(getattr(func, "return").doc_string)
+ self._write(
+ _block_attrs(f"{func_anchor}_returns"),
+ "RETURNS",
+ _link_here_icon(func_anchor + "_returns"),
+ "\n",
+ ": ",
+ return_doc,
+ "\n",
+ )
+ if func.deprecated.doc_string:
+ self._write(
+ "\n\n**DEPRECATED**\n\n", func.deprecated.doc_string.strip(), "\n"
+ )
+
+ def _render_provider(self, provider: stardoc_output_pb2.ProviderInfo):
+ self._write(
+ _block_attrs(".starlark-object"),
+ f"## {provider.provider_name}\n\n",
+ )
+
+ provider_anchor = _anchor_id(provider.provider_name)
+ self._render_signature(
+ provider.provider_name,
+ provider_anchor,
+ provider.field_info,
+ get_name=lambda f: f.name,
+ )
+
+ self._write(provider.doc_string.strip(), "\n\n")
+
+ if provider.field_info:
+ self._write(
+ _block_attrs(provider_anchor),
+ "**FIELDS** ",
+ _link_here_icon(provider_anchor + "_fields"),
+ "\n",
+ "\n",
+ )
+ entries = []
+ for field in provider.field_info:
+ entries.append(
+ [
+ f"{provider_anchor}_{field.name}",
+ field.name,
+ field.doc_string,
+ ]
+ )
+ self._render_field_list(entries)
+
+ def _render_attributes(
+ self, base_anchor: str, attributes: list[stardoc_output_pb2.AttributeInfo]
+ ):
+ self._write(
+ _block_attrs(f"{base_anchor}_attributes"),
+ "**ATTRIBUTES** ",
+ _link_here_icon(f"{base_anchor}_attributes"),
+ "\n",
+ )
+ entries = []
+ for attr in attributes:
+ anchor = f"{base_anchor}_{attr.name}"
+ required = "required" if attr.mandatory else "optional"
+ attr_type = self._rule_attr_type_string(attr)
+ default = f", default `{attr.default_value}`" if attr.default_value else ""
+ providers_parts = []
+ if attr.provider_name_group:
+ providers_parts.append("\n\n_Required providers_: ")
+ if len(attr.provider_name_group) == 1:
+ provider_group = attr.provider_name_group[0]
+ if len(provider_group.provider_name) == 1:
+ providers_parts.append(provider_group.provider_name[0])
+ else:
+ providers_parts.extend(
+ ["all of ", _join_csv_and(provider_group.provider_name)]
+ )
+ elif len(attr.provider_name_group) > 1:
+ providers_parts.append("any of \n")
+ for group in attr.provider_name_group:
+ providers_parts.extend(["* ", _join_csv_and(group.provider_name)])
+ if providers_parts:
+ providers_parts.append("\n")
+
+ entries.append(
+ [
+ anchor,
+ attr.name,
+ f"_({required} {attr_type}{default})_\n",
+ attr.doc_string,
+ *providers_parts,
+ ]
+ )
+ self._render_field_list(entries)
+
+ def _render_signature(
+ self,
+ name: str,
+ base_anchor: str,
+ parameters: list[_T],
+ *,
+ get_name: Callable[_T, str],
+ get_default: Callable[_T, str] = lambda v: None,
+ ):
+ self._write(_block_attrs(".starlark-signature"), name, "(")
+ for _, is_last, param in _position_iter(parameters):
+ param_name = get_name(param)
+ self._write(_link(param_name, f"{base_anchor}_{param_name}"))
+ default_value = get_default(param)
+ if default_value:
+ self._write(f"={default_value}")
+ if not is_last:
+ self._write(",\n")
+ self._write(")\n\n")
+
+ def _render_field_list(self, entries: list[list[str]]):
+ """Render a list of field lists.
+
+ Args:
+ entries: list of field list entries. Each element is 3
+ pieces: an anchor, field description, and one or more
+ text strings for the body of the field list entry.
+ """
+ for anchor, description, *body_pieces in entries:
+ body_pieces = [_block_attrs(anchor), *body_pieces]
+ self._write(
+ ":",
+ _span(description + _link_here_icon(anchor)),
+ ":\n ",
+ # The text has to be indented to be associated with the block correctly.
+ "".join(body_pieces).strip().replace("\n", "\n "),
+ "\n",
+ )
+ # Ensure there is an empty line after the field list, otherwise
+ # the next line of content will fold into the field list
+ self._write("\n")
+
+ def _write(self, *lines: str):
+ self._out_stream.writelines(lines)
+
+
+def _convert(
+ *,
+ proto: pathlib.Path,
+ output: pathlib.Path,
+ footer: pathlib.Path,
+ public_load_path: str,
+):
+ if footer:
+ footer_content = footer.read_text()
+
+ module = stardoc_output_pb2.ModuleInfo.FromString(proto.read_bytes())
+ with output.open("wt", encoding="utf8") as out_stream:
+ _MySTRenderer(module, out_stream, public_load_path).render()
+ out_stream.write(footer_content)
+
+
+def _create_parser():
+ parser = argparse.ArgumentParser(fromfile_prefix_chars="@")
+ parser.add_argument("--footer", dest="footer", type=pathlib.Path)
+ parser.add_argument("--proto", dest="proto", type=pathlib.Path)
+ parser.add_argument("--output", dest="output", type=pathlib.Path)
+ parser.add_argument("--public-load-path", dest="public_load_path")
+ return parser
+
+
+def main(args):
+ options = _create_parser().parse_args(args)
+ _convert(
+ proto=options.proto,
+ output=options.output,
+ footer=options.footer,
+ public_load_path=options.public_load_path,
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/sphinxdocs/private/provider_template.vm b/sphinxdocs/private/provider_template.vm
deleted file mode 100644
index 49ae894..0000000
--- a/sphinxdocs/private/provider_template.vm
+++ /dev/null
@@ -1,30 +0,0 @@
-#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)
-{#${pnl}_fields}
-**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/private/rule_template.vm b/sphinxdocs/private/rule_template.vm
deleted file mode 100644
index d91bad2..0000000
--- a/sphinxdocs/private/rule_template.vm
+++ /dev/null
@@ -1,48 +0,0 @@
-#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/private/sphinx_stardoc.bzl b/sphinxdocs/private/sphinx_stardoc.bzl
index 1371d90..810dca3 100644
--- a/sphinxdocs/private/sphinx_stardoc.bzl
+++ b/sphinxdocs/private/sphinx_stardoc.bzl
@@ -19,11 +19,6 @@ 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.
@@ -83,58 +78,62 @@ def sphinx_stardocs(name, docs, footer = None, **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
+ stardoc_name = "_{}_stardoc".format(name.lstrip("_"))
+ stardoc_pb = stardoc_name + ".binaryproto"
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,
+ out = stardoc_pb,
+ format = "proto",
**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,
+ _stardoc_proto_to_markdown(
+ name = name,
+ src = stardoc_pb,
output = out,
- substitutions = ctx.attr.substitutions,
+ footer = footer,
+ public_load_path = public_load_path,
+ )
+
+def _stardoc_proto_to_markdown_impl(ctx):
+ args = ctx.actions.args()
+ args.use_param_file("@%s")
+ args.set_param_file_format("multiline")
+
+ inputs = [ctx.file.src]
+ args.add("--proto", ctx.file.src)
+ args.add("--output", ctx.outputs.output)
+
+ if ctx.file.footer:
+ args.add("--footer", ctx.file.footer)
+ inputs.append(ctx.file.footer)
+ if ctx.attr.public_load_path:
+ args.add("--public-load-path={}".format(ctx.attr.public_load_path))
+
+ ctx.actions.run(
+ executable = ctx.executable._proto_to_markdown,
+ arguments = [args],
+ inputs = inputs,
+ outputs = [ctx.outputs.output],
+ mnemonic = "SphinxStardocProtoToMd",
+ progress_message = "SphinxStardoc: converting proto to markdown: %{input} -> %{output}",
)
- return [DefaultInfo(files = depset([out]))]
-_expand_stardoc_template = rule(
- implementation = _expand_stardoc_template_impl,
+_stardoc_proto_to_markdown = rule(
+ implementation = _stardoc_proto_to_markdown_impl,
attrs = {
- "substitutions": attr.string_dict(),
- "template": attr.label(allow_single_file = True),
+ "footer": attr.label(allow_single_file = True),
+ "output": attr.output(mandatory = True),
+ "public_load_path": attr.string(),
+ "src": attr.label(allow_single_file = True, mandatory = True),
+ "_proto_to_markdown": attr.label(
+ default = "//sphinxdocs/private:proto_to_markdown",
+ executable = True,
+ cfg = "exec",
+ ),
},
)
diff --git a/sphinxdocs/tests/BUILD.bazel b/sphinxdocs/tests/BUILD.bazel
new file mode 100644
index 0000000..4101095
--- /dev/null
+++ b/sphinxdocs/tests/BUILD.bazel
@@ -0,0 +1,13 @@
+# 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.
diff --git a/sphinxdocs/tests/proto_to_markdown/BUILD.bazel b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel
new file mode 100644
index 0000000..2964785
--- /dev/null
+++ b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel
@@ -0,0 +1,24 @@
+# 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("//python:py_test.bzl", "py_test")
+
+py_test(
+ name = "proto_to_markdown_test",
+ srcs = ["proto_to_markdown_test.py"],
+ deps = [
+ "//sphinxdocs/private:proto_to_markdown_lib",
+ "@dev_pip//absl_py",
+ ],
+)
diff --git a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
new file mode 100644
index 0000000..2f5b22e
--- /dev/null
+++ b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
@@ -0,0 +1,203 @@
+# 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.
+
+import io
+import re
+
+from absl.testing import absltest
+from google.protobuf import text_format
+from stardoc.proto import stardoc_output_pb2
+
+from sphinxdocs.private import proto_to_markdown
+
+_EVERYTHING_MODULE = """\
+module_docstring: "MODULE_DOC_STRING"
+file: "@repo//pkg:foo.bzl"
+
+rule_info: {
+ rule_name: "rule_1"
+ doc_string: "RULE_1_DOC_STRING"
+ attribute: {
+ name: "rule_1_attr_1",
+ doc_string: "RULE_1_ATTR_1_DOC_STRING"
+ type: STRING
+ default_value: "RULE_1_ATTR_1_DEFAULT_VALUE"
+ }
+}
+provider_info: {
+ provider_name: "ProviderAlpha"
+ doc_string: "PROVIDER_ALPHA_DOC_STRING"
+ field_info: {
+ name: "ProviderAlpha_field_a"
+ doc_string: "PROVIDER_ALPHA_FIELD_A_DOC_STRING"
+ }
+}
+func_info: {
+ function_name: "function_1"
+ doc_string: "FUNCTION_1_DOC_STRING"
+ parameter: {
+ name: "function_1_param_a"
+ doc_string: "FUNCTION_1_PARAM_A_DOC_STRING"
+ default_value: "FUNCTION_1_PARAM_A_DEFAULT_VALUE"
+ }
+ return: {
+ doc_string: "FUNCTION_1_RETURN_DOC_STRING"
+ }
+ deprecated: {
+ doc_string: "FUNCTION_1_DEPRECATED_DOC_STRING"
+ }
+}
+aspect_info: {
+ aspect_name: "aspect_1"
+ doc_string: "ASPECT_1_DOC_STRING"
+ aspect_attribute: "aspect_1_aspect_attribute_a"
+ attribute: {
+ name: "aspect_1_attribute_a",
+ doc_string: "ASPECT_1_ATTRIBUTE_A_DOC_STRING"
+ type: INT
+ default_value: "694638"
+ }
+}
+module_extension_info: {
+ extension_name: "bzlmod_ext"
+ doc_string: "BZLMOD_EXT_DOC_STRING"
+ tag_class: {
+ tag_name: "bzlmod_ext_tag_a"
+ doc_string: "BZLMOD_EXT_TAG_A_DOC_STRING"
+ attribute: {
+ name: "bzlmod_ext_tag_a_attribute_1",
+ doc_string: "BZLMOD_EXT_TAG_A_ATTRIBUTE_1_DOC_STRING"
+ type: STRING_LIST
+ default_value: "[BZLMOD_EXT_TAG_A_ATTRIBUTE_1_DEFAULT_VALUE]"
+ }
+ }
+}
+repository_rule_info: {
+ rule_name: "repository_rule",
+ doc_string: "REPOSITORY_RULE_DOC_STRING"
+ attribute: {
+ name: "repository_rule_attribute_a",
+ doc_string: "REPOSITORY_RULE_ATTRIBUTE_A_DOC_STRING"
+ type: BOOLEAN
+ default_value: "True"
+ }
+ environ: "ENV_VAR_A"
+}
+"""
+
+
+class ProtoToMarkdownTest(absltest.TestCase):
+ def setUp(self):
+ super().setUp()
+ self.stream = io.StringIO()
+
+ def _render(self, module_text):
+ renderer = proto_to_markdown._MySTRenderer(
+ module=text_format.Parse(module_text, stardoc_output_pb2.ModuleInfo()),
+ out_stream=self.stream,
+ public_load_path="",
+ )
+ renderer.render()
+ return self.stream.getvalue()
+
+ def test_basic_rendering_everything(self):
+ actual = self._render(_EVERYTHING_MODULE)
+
+ self.assertRegex(actual, "# //pkg:foo.bzl")
+ self.assertRegex(actual, "MODULE_DOC_STRING")
+
+ self.assertRegex(actual, "## rule_1.*")
+ self.assertRegex(actual, "RULE_1_DOC_STRING")
+ self.assertRegex(actual, "rule_1_attr_1")
+ self.assertRegex(actual, "RULE_1_ATTR_1_DOC_STRING")
+ self.assertRegex(actual, "RULE_1_ATTR_1_DEFAULT_VALUE")
+
+ self.assertRegex(actual, "## ProviderAlpha")
+ self.assertRegex(actual, "PROVIDER_ALPHA_DOC_STRING")
+ self.assertRegex(actual, "ProviderAlpha_field_a")
+ self.assertRegex(actual, "PROVIDER_ALPHA_FIELD_A_DOC_STRING")
+
+ self.assertRegex(actual, "## function_1")
+ self.assertRegex(actual, "FUNCTION_1_DOC_STRING")
+ self.assertRegex(actual, "function_1_param_a")
+ self.assertRegex(actual, "FUNCTION_1_PARAM_A_DOC_STRING")
+ self.assertRegex(actual, "FUNCTION_1_PARAM_A_DEFAULT_VALUE")
+ self.assertRegex(actual, "FUNCTION_1_RETURN_DOC_STRING")
+ self.assertRegex(actual, "FUNCTION_1_DEPRECATED_DOC_STRING")
+
+ self.assertRegex(actual, "## aspect_1")
+ self.assertRegex(actual, "ASPECT_1_DOC_STRING")
+ self.assertRegex(actual, "aspect_1_aspect_attribute_a")
+ self.assertRegex(actual, "aspect_1_attribute_a")
+ self.assertRegex(actual, "ASPECT_1_ATTRIBUTE_A_DOC_STRING")
+ self.assertRegex(actual, "694638")
+
+ self.assertRegex(actual, "## bzlmod_ext")
+ self.assertRegex(actual, "BZLMOD_EXT_DOC_STRING")
+ self.assertRegex(actual, "### bzlmod_ext.bzlmod_ext_tag_a")
+ self.assertRegex(actual, "BZLMOD_EXT_TAG_A_DOC_STRING")
+ self.assertRegex(actual, "bzlmod_ext_tag_a_attribute_1")
+ self.assertRegex(actual, "BZLMOD_EXT_TAG_A_ATTRIBUTE_1_DOC_STRING")
+ self.assertRegex(actual, "BZLMOD_EXT_TAG_A_ATTRIBUTE_1_DEFAULT_VALUE")
+
+ self.assertRegex(actual, "## repository_rule")
+ self.assertRegex(actual, "REPOSITORY_RULE_DOC_STRING")
+ self.assertRegex(actual, "repository_rule_attribute_a")
+ self.assertRegex(actual, "REPOSITORY_RULE_ATTRIBUTE_A_DOC_STRING")
+ self.assertRegex(actual, "repository_rule_attribute_a.*=.*True")
+ self.assertRegex(actual, "ENV_VAR_A")
+
+ def test_render_signature(self):
+ actual = self._render(
+ """\
+file: "@repo//pkg:foo.bzl"
+func_info: {
+ function_name: "func"
+ parameter: {
+ name: "param_with_default"
+ default_value: "DEFAULT"
+ }
+ parameter: {
+ name: "param_without_default"
+ }
+ parameter: {
+ name: "last_param"
+ }
+}
+ """
+ )
+ self.assertIn("[param_with_default](#func_param_with_default)=DEFAULT,", actual)
+ self.assertIn("[param_without_default](#func_param_without_default),", actual)
+
+ def test_render_field_list(self):
+ actual = self._render(
+ """\
+file: "@repo//pkg:foo.bzl"
+func_info: {
+ function_name: "func"
+ parameter: {
+ name: "param"
+ default_value: "DEFAULT"
+ }
+}
+"""
+ )
+ self.assertRegex(
+ actual, re.compile("^:.*param.*¶.*headerlink.*:\n", re.MULTILINE)
+ )
+ self.assertRegex(actual, re.compile("^ .*#func_param", re.MULTILINE))
+
+
+if __name__ == "__main__":
+ absltest.main()