blob: 8ec4bd5982fe3959ee8310b3090de30ecc64259e [file] [log] [blame]
# Copyright 2016 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.
"""Common functions for skydoc."""
import collections
import re
import textwrap
from xml.sax.saxutils import escape
ARGS_HEADING = "Args:"
EXAMPLES_HEADING = "Examples:"
EXAMPLE_HEADING = "Example:"
OUTPUTS_HEADING = "Outputs:"
class InputError(Exception):
pass
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 validate_strip_prefix(strip_prefix, bzl_files):
if not strip_prefix:
return strip_prefix
prefix = strip_prefix if strip_prefix.endswith('/') else strip_prefix + '/'
for path in bzl_files:
if not path.startswith(prefix):
raise InputError(
'Input file %s does not have path prefix %s. Directory prefix set '
'with --strip_prefix must be common to all input files.'
% (path, strip_prefix))
return prefix
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_docs: A dict used to store the extracted documentation.
lines: List containing the input docstring split into lines.
index: The index in lines containing the heading that begins the
documentation, such as "Args:" or "Outputs:".
Returns:
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
args_leading_ws = leading_whitespace(lines[index])
i = index + 1
while i < len(lines):
line = lines[i]
# If a blank line is encountered, we have finished parsing the "Args"
# section.
if line.strip() and leading_whitespace(line) == args_leading_ws:
break
# In practice, users sometimes add a "-" prefix, so we strip it even
# though it is not recommended by the style guide
pattern = re.compile(r"""
# Any amount of leading whitespace, plus an optional "-" prefix.
^\s*-?\s*
# The attribute name, plus an optional "**" prefix for a **kwargs
# attribute.
((?:\*\*)?[`\{\}\%\.\w\*]+)
# A colon plus any amount of whitespace to separate the attribute name
# from the description text.
:\s*
# The attribute description text.
(.*)
""", re.VERBOSE)
match = re.search(pattern, line)
if match: # We have found a new attribute
if attr:
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_docs[attr] = escape(desc).strip()
return i
def _parse_example_docs(examples, lines, index):
"""Extracts example documentation.
Args:
examples: A list to contain the lines containing the example documentation.
lines: List containing the input docstring split into lines.
index: The index in lines containing "Example[s]:", which begins the
example documentation.
Returns:
Returns the next index after the attribute documentation to resume
processing documentation in the caller.
"""
heading_leading_ws = leading_whitespace(lines[index])
i = index + 1
while i < len(lines):
line = lines[i]
if line.strip() and leading_whitespace(line) == heading_leading_ws:
break
examples.append(line)
i += 1
return i
def parse_docstring(doc):
"""Analyzes the documentation string for attributes.
This looks for the "Args:" separator to fetch documentation for each
attribute. The "Args" section ends at the first blank line.
Args:
doc: The documentation string
Returns:
The new documentation string and a dictionary that maps each attribute to
its documentation
"""
attr_docs = collections.OrderedDict() if hasattr(collections, 'OrderedDict') else {}
output_docs = collections.OrderedDict() if hasattr(collections, 'OrderedDict') else {}
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_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 ExtractedDocs(doc, attr_docs, examples_doc, output_docs)