Created
August 2, 2019 15:27
-
-
Save clintharrison/2c51a1ad1b739e793721fe7c1b4b0fda to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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