blob: 18d4e1e045e082965c41f8f68d8ad653722e84d1 [file] [log] [blame]
# 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:]))