Skip to content

Instantly share code, notes, and snippets.

@clintharrison
Created August 2, 2019 15:27
Show Gist options
  • Save clintharrison/2c51a1ad1b739e793721fe7c1b4b0fda to your computer and use it in GitHub Desktop.
Save clintharrison/2c51a1ad1b739e793721fe7c1b4b0fda to your computer and use it in GitHub Desktop.
"""Generate reStructuredText from Stardoc protobuf input.
The default Stardoc documentation renderer creates Markdown files with a significant amount of embedded HTML.
This raw HTML can't be published to Confluence with the Sphinx Confluence builder, but Markdown table support
is very primitive, and it doesn't support multi-line content in cells.
As a result, we instead build up a reStructuredText document, which has better table support, from the "source" protobuf
messages output by Stardoc. This is the "native" format of Sphinx, but is slightly less pleasant for humans to write.
"""
from __future__ import print_function
import argparse
import jinja2
import stardoc_output_pb2
import tabulate
H1_UNDERLINE = "="
H2_UNDERLINE = "-"
H3_UNDERLINE = "^"
PAGE_HEADER = """\
.. GENERATED BY STARDOC
..
.. role:: param(code)
.. role:: value(code)
.. role:: type(emphasis)
"""
PAGE_CHUNK_TEMPLATE = jinja2.Template("""{{ header }}
{{ summary }}
{% if doc_string %}
{{ doc_string }}
{% endif %}
{% if table %}
{{ table_header }}
{{ table }}
{% endif %}
""")
def header(s, header_char):
return "{}\n{}\n".format(s, header_char*len(s))
def codeblock(code):
codeblock = "::\n\n "
codeblock += code.replace("\n", "\n ") + "\n\n"
return codeblock
def summary(name, params):
param_names = [param.name for param in params]
return codeblock("{}({})".format(name, ", ".join([name for name in param_names])))
def attr_type_desc(attr_type):
"""Convert a protobuf AttributeType to a human-readable string.
See @io_bazel//src/main/java/com/google/devtools/build/skydoc/rendering:MarkdownUtil.java
for the source of these.
"""
at = stardoc_output_pb2.AttributeType
types = {
at.NAME: "Name",
at.INT: "Integer",
at.LABEL: "Label",
at.STRING: "String",
at.STRING_LIST: "List of strings",
at.INT_LIST: "List of integers",
at.LABEL_LIST: "List of labels",
at.BOOLEAN: "Boolean",
at.LABEL_STRING_DICT: "Dictionary: Label -> String",
at.STRING_DICT: "Dictionary: String -> String",
at.STRING_LIST_DICT: "Dictionary: String -> List of strings",
at.OUTPUT: "Label",
at.OUTPUT_LIST: "List of labels",
}
if attr_type not in types:
raise ValueError("Unhandled type: {}".format(at.Name(attr_type)))
return types[attr_type]
def attr_type_url(attr_type):
"""Convert a protobuf AttributeType to a URL in the Bazel documentation.
See @io_bazel//src/main/java/com/google/devtools/build/skydoc/rendering:MarkdownUtil.java
for the source of these.
"""
at = stardoc_output_pb2.AttributeType
if attr_type in (at.LABEL, at.LABEL_LIST, at.OUTPUT):
return "https://bazel.build/docs/build-ref.html#labels"
elif attr_type in (at.STRING_DICT, at.STRING_LIST_DICT, at.LABEL_STRING_DICT):
return "https://bazel.build/docs/skylark/lib/dict.html"
elif attr_type == at.NAME:
return "https://bazel.build/docs/build-ref.html#name"
return None
def formatted_attr_type_link(info):
"""Generate the RST link markup for an attribute type, if possible."""
link_title = attr_type_desc(info.type)
type_url = attr_type_url(info.type)
if type_url is not None:
return "`{} <{}>`_".format(link_title, type_url)
return link_title
def attr_mandatory_str(info):
if info.mandatory:
return "**mandatory**"
if hasattr(info, "default"):
return "*optional*, default :code:`{}`".format(info.default)
return "*optional*"
def rule_attr_table(rule_info):
"""Create the attribute table for a build rule"""
table = []
for info in rule_info.attribute:
name = ":param:`{}`".format(info.name)
desc = "{type}; {mandatory}".format(
type=formatted_attr_type_link(info),
mandatory=attr_mandatory_str(info),
)
if info.doc_string:
desc += "\n\n"
desc += info.doc_string
table.append([name, desc])
return tabulate.tabulate(
table,
headers=("**Name**", "**Description**"),
tablefmt="grid"
)
def func_param_table(func_info):
"""Create the parameter table for a function (or Starlark macro)"""
table = []
for info in func_info.parameter:
name = ":param:`{}`".format(info.name)
desc = attr_mandatory_str(info)
if info.doc_string:
desc += "\n\n"
desc += info.doc_string
table.append([name, desc])
return tabulate.tabulate(
table,
headers=("**Name**", "**Description**"),
tablefmt="grid"
)
def field_table(provider_info):
"""Create the fields table for a Provider"""
table = []
for field_info in provider_info.field_info:
table.append([
":param:`{}`".format(field_info.name),
field_info.doc_string,
])
return tabulate.tabulate(
table,
headers=("**Name**", "**Type**"),
tablefmt="grid"
)
def write_rule_infos(output, rule_infos):
for rule_info in rule_infos:
output.write(PAGE_CHUNK_TEMPLATE.render(
header=header(rule_info.rule_name, H1_UNDERLINE),
summary=summary(rule_info.rule_name, rule_info.attribute),
doc_string=rule_info.doc_string,
table_header=header("Attributes", H2_UNDERLINE),
table=rule_attr_table(rule_info),
))
def write_provider_infos(output, provider_infos):
for provider_info in provider_infos:
output.write(PAGE_CHUNK_TEMPLATE.render(
header=header(provider_info.provider_name, H1_UNDERLINE),
summary=summary(provider_info.provider_name, provider_info.field_info),
doc_string=provider_info.doc_string,
table_header=header("Fields", H2_UNDERLINE),
table=field_table(provider_info),
))
def write_func_info(output, func_infos):
for func_info in func_infos:
output.write(PAGE_CHUNK_TEMPLATE.render(
header=header(func_info.function_name, H1_UNDERLINE),
summary=summary(func_info.function_name, func_info.parameter),
doc_string=func_info.doc_string,
table_header=header("Parameters", H2_UNDERLINE),
table=func_param_table(func_info),
))
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, type=argparse.FileType("rb"))
parser.add_argument("--output", required=True, type=argparse.FileType("w"))
return parser.parse_args()
def main(args):
module_info = stardoc_output_pb2.ModuleInfo()
module_info.ParseFromString(args.input.read())
args.output.write(PAGE_HEADER)
write_rule_infos(args.output, module_info.rule_info)
write_provider_infos(args.output, module_info.provider_info)
write_func_info(args.output, module_info.func_info)
if __name__ == "__main__":
main(parse_args())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment