diff options
author | Richard Levasseur <rlevasseur@google.com> | 2023-12-19 16:51:44 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-20 00:51:44 +0000 |
commit | 509b02f8c719decb40f4731b735e65b6545ceea0 (patch) | |
tree | d9f29f6f039c859f7cface9ab7d027e306d867d2 | |
parent | 87a3a54cd937b037f531fedac243350933dd1eb7 (diff) | |
download | bazelbuild-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>
25 files changed, 928 insertions, 200 deletions
@@ -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", @@ -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() |