Skip to content
This repository has been archived by the owner on Sep 15, 2021. It is now read-only.

Commit

Permalink
Add support for documenting implicit output targets.
Browse files Browse the repository at this point in the history
Fixes #6

RELNOTES: Support documenting implicit output targets.

--
MOS_MIGRATED_REVID=120483792
  • Loading branch information
davidzchen committed Apr 21, 2016
1 parent 709bba1 commit d42081e
Show file tree
Hide file tree
Showing 15 changed files with 434 additions and 63 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ sass_repositories()
git_repository(
name = "io_bazel_skydoc",
remote = "https://github.com/bazelbuild/skydoc.git",
tag = "0.0.2",
tag = "0.0.3",
)
load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
skydoc_repositories()
Expand Down
10 changes: 9 additions & 1 deletion skydoc/build.proto
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,16 @@ message AllowedRuleClassInfo {
repeated string allowed_rule_class = 2;
}

// Information about an output target for a rule.
message OutputTarget {
// The template name of the output target (e.g. %{name}.jar.
optional string template = 1;
// The documentation for the output target.
optional string documentation = 2;
}

// This message represents a single attribute of a single rule.
message AttributeDefinition {

// Rule name, e.g. "cc_library"
required string name = 1;
required Attribute.Discriminator type = 2;
Expand All @@ -81,6 +88,7 @@ message RuleDefinition {

optional string example_documentation = 4;

repeated OutputTarget output = 5;
}

message BuildLanguage {
Expand Down
43 changes: 30 additions & 13 deletions skydoc/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,38 @@
ARGS_HEADING = "Args:"
EXAMPLES_HEADING = "Examples:"
EXAMPLE_HEADING = "Example:"
OUTPUTS_HEADING = "Outputs:"


class ExtractedDocs(object):
"""Simple class to contain the documentation extracted from a docstring."""

def __init__(self, doc, attr_docs, example_doc, output_docs):
self.doc = doc
self.attr_docs = attr_docs
self.example_doc = example_doc
self.output_docs = output_docs


def leading_whitespace(line):
"""Returns the number of leading whitespace in the line."""
return len(line) - len(line.lstrip())


def _parse_attribute_docs(attr_doc, lines, index):
"""Extracts attribute documentation.
def _parse_attribute_docs(attr_docs, lines, index):
"""Extracts documentation in the form of name: description.
This includes documentation for attributes and outputs.
Args:
attr_doc: A dict used to store the extracted attribute documentation.
attr_docs: A dict used to store the extracted documentation.
lines: List containing the input docstring split into lines.
index: The index in lines containing "Args:", which begins the argument
documentation.
index: The index in lines containing the heading that begins the
documentation, such as "Args:" or "Outputs:".
Returns:
Returns the next index after the attribute documentation to resume
processing documentation in the caller.
Returns the next index after the documentation to resume processing
documentation in the caller.
"""
attr = None # Current attribute name
desc = None # Description for current attribute
Expand All @@ -54,18 +67,18 @@ def _parse_attribute_docs(attr_doc, lines, index):
break
# In practice, users sometimes add a "-" prefix, so we strip it even
# though it is not recommended by the style guide
match = re.search(r"^\s*-?\s*(\w+):\s*(.*)", line)
match = re.search(r"^\s*-?\s*([`\{\}\%\.\w]+):\s*(.*)", line)
if match: # We have found a new attribute
if attr:
attr_doc[attr] = escape(desc)
attr_docs[attr] = escape(desc)
attr, desc = match.group(1), match.group(2)
elif attr:
# Merge documentation when it is multiline
desc = desc + "\n" + line.strip()
i += + 1

if attr:
attr_doc[attr] = escape(desc).strip()
attr_docs[attr] = escape(desc).strip()

return i

Expand Down Expand Up @@ -108,23 +121,27 @@ def parse_docstring(doc):
The new documentation string and a dictionary that maps each attribute to
its documentation
"""
attr_doc = {}
attr_docs = {}
output_docs = {}
examples = []
lines = doc.split("\n")
docs = []
i = 0
while i < len(lines):
line = lines[i]
if line.strip() == ARGS_HEADING:
i = _parse_attribute_docs(attr_doc, lines, i)
i = _parse_attribute_docs(attr_docs, lines, i)
continue
elif line.strip() == EXAMPLES_HEADING or line.strip() == EXAMPLE_HEADING:
i = _parse_example_docs(examples, lines, i)
continue
elif line.strip() == OUTPUTS_HEADING:
i = _parse_attribute_docs(output_docs, lines, i)
continue

docs.append(line)
i += 1

doc = "\n".join(docs).strip()
examples_doc = textwrap.dedent("\n".join(examples)).strip()
return doc, attr_doc, examples_doc
return ExtractedDocs(doc, attr_docs, examples_doc, output_docs)
84 changes: 60 additions & 24 deletions skydoc/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ class CommonTest(unittest.TestCase):

def test_rule_doc_only(self):
docstring = 'Rule documentation only docstring.'
doc, attr_doc, example_doc = common.parse_docstring(docstring)
self.assertEqual('Rule documentation only docstring.', doc)
self.assertDictEqual({}, attr_doc)
self.assertEqual('', example_doc)
extracted_docs = common.parse_docstring(docstring)
self.assertEqual('Rule documentation only docstring.', extracted_docs.doc)
self.assertDictEqual({}, extracted_docs.attr_docs)
self.assertEqual('', extracted_docs.example_doc)
self.assertDictEqual({}, extracted_docs.output_docs)

def test_rule_and_attribute_doc(self):
docstring = (
Expand All @@ -39,10 +40,11 @@ def test_rule_and_attribute_doc(self):
'visibility': 'The visibility of this rule.'
}

doc, attr_doc, example_doc = common.parse_docstring(docstring)
self.assertEqual('Rule and attribute documentation.', doc)
self.assertDictEqual(expected_attrs, attr_doc)
self.assertEqual('', example_doc)
extracted_docs = common.parse_docstring(docstring)
self.assertEqual('Rule and attribute documentation.', extracted_docs.doc)
self.assertDictEqual(expected_attrs, extracted_docs.attr_docs)
self.assertEqual('', extracted_docs.example_doc)
self.assertDictEqual({}, extracted_docs.output_docs)

def test_multi_line_doc(self):
docstring = (
Expand All @@ -68,10 +70,11 @@ def test_multi_line_doc(self):
'Documentation for visibility continued here.')
}

doc, attr_doc, example_doc = common.parse_docstring(docstring)
self.assertEqual(expected_doc, doc)
self.assertDictEqual(expected_attrs, attr_doc)
self.assertEqual('', example_doc)
extracted_docs = common.parse_docstring(docstring)
self.assertEqual(expected_doc, extracted_docs.doc)
self.assertDictEqual(expected_attrs, extracted_docs.attr_docs)
self.assertEqual('', extracted_docs.example_doc)
self.assertDictEqual({}, extracted_docs.output_docs)

def test_invalid_args(self):
docstring = (
Expand All @@ -81,10 +84,11 @@ def test_invalid_args(self):
' name: A unique name for this rule.\n'
' visibility: The visibility of this rule.\n')

doc, attr_doc, example_doc = common.parse_docstring(docstring)
self.assertEqual(docstring.strip(), doc)
self.assertDictEqual({}, attr_doc)
self.assertEqual('', example_doc)
extracted_docs = common.parse_docstring(docstring)
self.assertEqual(docstring.strip(), extracted_docs.doc)
self.assertDictEqual({}, extracted_docs.attr_docs)
self.assertEqual('', extracted_docs.example_doc)
self.assertDictEqual({}, extracted_docs.output_docs)

def test_example(self):
docstring = (
Expand All @@ -111,10 +115,11 @@ def test_example(self):
'\n'
'Note about this example.')

doc, attr_doc, example_doc = common.parse_docstring(docstring)
self.assertEqual('Documentation with example', doc)
self.assertDictEqual(expected_attrs, attr_doc)
self.assertEqual(expected_example_doc, example_doc)
extracted_docs = common.parse_docstring(docstring)
self.assertEqual('Documentation with example', extracted_docs.doc)
self.assertDictEqual(expected_attrs, extracted_docs.attr_docs)
self.assertEqual(expected_example_doc, extracted_docs.example_doc)
self.assertDictEqual({}, extracted_docs.output_docs)

def test_example_after_attrs(self):
docstring = (
Expand All @@ -141,10 +146,41 @@ def test_example_after_attrs(self):
'\n'
'Note about this example.')

doc, attr_doc, example_doc = common.parse_docstring(docstring)
self.assertEqual('Documentation with example', doc)
self.assertDictEqual(expected_attrs, attr_doc)
self.assertEqual(expected_example_doc, example_doc)
extracted_docs = common.parse_docstring(docstring)
self.assertEqual('Documentation with example', extracted_docs.doc)
self.assertDictEqual(expected_attrs, extracted_docs.attr_docs)
self.assertEqual(expected_example_doc, extracted_docs.example_doc)
self.assertDictEqual({}, extracted_docs.output_docs)

def test_outputs(self):
docstring = (
'Documentation with outputs\n'
'\n'
'Args:\n'
' name: A unique name for this rule.\n'
' visibility: The visibility of this rule.\n'
'\n'
'Outputs:\n'
' %{name}.jar: A Java archive.\n'
' %{name}_deploy.jar: A Java archive suitable for deployment.\n'
'\n'
' Only built if explicitly requested.\n')
expected_attrs = {
'name': 'A unique name for this rule.',
'visibility': 'The visibility of this rule.'
}
expected_outputs = {
'%{name}.jar': 'A Java archive.',
'%{name}_deploy.jar': ('A Java archive suitable for deployment.\n\n'
'Only built if explicitly requested.'),
}

extracted_docs = common.parse_docstring(docstring)
self.assertEqual('Documentation with outputs', extracted_docs.doc)
self.assertDictEqual(expected_attrs, extracted_docs.attr_docs)
self.assertEqual('', extracted_docs.example_doc)
self.assertDictEqual(expected_outputs, extracted_docs.output_docs)


if __name__ == '__main__':
unittest.main()
20 changes: 13 additions & 7 deletions skydoc/macro_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,21 @@ def _add_macro_doc(self, stmt):

doc = ast.get_docstring(stmt)
if doc:
doc, attr_doc, example_doc = common.parse_docstring(doc)
rule.documentation = doc
rule.example_documentation = example_doc
extracted_docs = common.parse_docstring(doc)
rule.documentation = extracted_docs.doc
if extracted_docs.example_doc:
rule.example_documentation = extracted_docs.example_doc
else:
doc = ""
attr_doc = {}
extracted_docs = common.ExtractedDocs(
doc="", attr_docs={}, example_doc="", output_docs={})

for i in range(len(stmt.args.args)):
attr = rule.attribute.add()
attr_name = stmt.args.args[i].id
attr.name = attr_name

if attr_name in attr_doc:
attr.documentation = attr_doc[attr_name]
if attr_name in extracted_docs.attr_docs:
attr.documentation = extracted_docs.attr_docs[attr_name]

if i < shift: # The first arguments are mandatory
attr.mandatory = True
Expand All @@ -94,6 +95,11 @@ def _add_macro_doc(self, stmt):
attr.mandatory = False
attr.type = get_type(stmt.args.defaults[i - shift])

for template, doc in extracted_docs.output_docs.iteritems():
output = rule.output.add()
output.template = template
output.documentation = doc

def parse_bzl(self, bzl_file):
"""Extracts documentation for all public macros from the given .bzl file.
Expand Down
Loading

0 comments on commit d42081e

Please sign in to comment.