blob: 2dcf44c10b8d6319c8e85c589c51c19ac62d2e62 [file] [log] [blame]
David Z. Chen55dafa92016-03-25 15:55:54 -07001# Copyright 2016 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Common functions for skydoc."""
16
17import re
David Chen709bba12016-04-13 09:18:46 +000018import textwrap
David Z. Chen55dafa92016-03-25 15:55:54 -070019from xml.sax.saxutils import escape
20
21
David Chen709bba12016-04-13 09:18:46 +000022ARGS_HEADING = "Args:"
23EXAMPLES_HEADING = "Examples:"
24EXAMPLE_HEADING = "Example:"
David Chend42081e2016-04-21 21:17:05 +000025OUTPUTS_HEADING = "Outputs:"
26
27
David Z. Chen8861b7c2016-09-14 13:35:03 -070028class InputError(Exception):
29 pass
30
31
David Chend42081e2016-04-21 21:17:05 +000032class ExtractedDocs(object):
33 """Simple class to contain the documentation extracted from a docstring."""
34
35 def __init__(self, doc, attr_docs, example_doc, output_docs):
36 self.doc = doc
37 self.attr_docs = attr_docs
38 self.example_doc = example_doc
39 self.output_docs = output_docs
David Chen709bba12016-04-13 09:18:46 +000040
41
David Z. Chen55dafa92016-03-25 15:55:54 -070042def leading_whitespace(line):
David Chen709bba12016-04-13 09:18:46 +000043 """Returns the number of leading whitespace in the line."""
David Z. Chen55dafa92016-03-25 15:55:54 -070044 return len(line) - len(line.lstrip())
45
David Chen709bba12016-04-13 09:18:46 +000046
David Z. Chen8861b7c2016-09-14 13:35:03 -070047def validate_strip_prefix(strip_prefix, bzl_files):
48 if not strip_prefix:
49 return strip_prefix
50 prefix = strip_prefix if strip_prefix.endswith('/') else strip_prefix + '/'
51 for path in bzl_files:
52 if not path.startswith(prefix):
53 raise InputError(
54 'Input file %s does not have path prefix %s. Directory prefix set '
55 'with --strip_prefix must be common to all input files.'
56 % (path, strip_prefix))
57 return prefix
58
David Chend42081e2016-04-21 21:17:05 +000059def _parse_attribute_docs(attr_docs, lines, index):
60 """Extracts documentation in the form of name: description.
61
62 This includes documentation for attributes and outputs.
David Chen709bba12016-04-13 09:18:46 +000063
64 Args:
David Chend42081e2016-04-21 21:17:05 +000065 attr_docs: A dict used to store the extracted documentation.
David Chen709bba12016-04-13 09:18:46 +000066 lines: List containing the input docstring split into lines.
David Chend42081e2016-04-21 21:17:05 +000067 index: The index in lines containing the heading that begins the
68 documentation, such as "Args:" or "Outputs:".
David Chen709bba12016-04-13 09:18:46 +000069
70 Returns:
David Chend42081e2016-04-21 21:17:05 +000071 Returns the next index after the documentation to resume processing
72 documentation in the caller.
David Chen709bba12016-04-13 09:18:46 +000073 """
74 attr = None # Current attribute name
75 desc = None # Description for current attribute
76 args_leading_ws = leading_whitespace(lines[index])
77 i = index + 1
78 while i < len(lines):
79 line = lines[i]
80 # If a blank line is encountered, we have finished parsing the "Args"
81 # section.
82 if line.strip() and leading_whitespace(line) == args_leading_ws:
83 break
84 # In practice, users sometimes add a "-" prefix, so we strip it even
85 # though it is not recommended by the style guide
David Chend42081e2016-04-21 21:17:05 +000086 match = re.search(r"^\s*-?\s*([`\{\}\%\.\w]+):\s*(.*)", line)
David Chen709bba12016-04-13 09:18:46 +000087 if match: # We have found a new attribute
88 if attr:
David Chend42081e2016-04-21 21:17:05 +000089 attr_docs[attr] = escape(desc)
David Chen709bba12016-04-13 09:18:46 +000090 attr, desc = match.group(1), match.group(2)
91 elif attr:
92 # Merge documentation when it is multiline
93 desc = desc + "\n" + line.strip()
94 i += + 1
95
96 if attr:
David Chend42081e2016-04-21 21:17:05 +000097 attr_docs[attr] = escape(desc).strip()
David Chen709bba12016-04-13 09:18:46 +000098
99 return i
100
101
102def _parse_example_docs(examples, lines, index):
103 """Extracts example documentation.
104
105 Args:
106 examples: A list to contain the lines containing the example documentation.
107 lines: List containing the input docstring split into lines.
108 index: The index in lines containing "Example[s]:", which begins the
109 example documentation.
110
111 Returns:
112 Returns the next index after the attribute documentation to resume
113 processing documentation in the caller.
114 """
115 heading_leading_ws = leading_whitespace(lines[index])
116 i = index + 1
117 while i < len(lines):
118 line = lines[i]
119 if line.strip() and leading_whitespace(line) == heading_leading_ws:
120 break
121 examples.append(line)
122 i += 1
123
124 return i
125
126
127def parse_docstring(doc):
David Z. Chen55dafa92016-03-25 15:55:54 -0700128 """Analyzes the documentation string for attributes.
129
130 This looks for the "Args:" separator to fetch documentation for each
131 attribute. The "Args" section ends at the first blank line.
132
133 Args:
134 doc: The documentation string
135
136 Returns:
137 The new documentation string and a dictionary that maps each attribute to
138 its documentation
139 """
David Chend42081e2016-04-21 21:17:05 +0000140 attr_docs = {}
141 output_docs = {}
David Chen709bba12016-04-13 09:18:46 +0000142 examples = []
David Z. Chen55dafa92016-03-25 15:55:54 -0700143 lines = doc.split("\n")
David Chen709bba12016-04-13 09:18:46 +0000144 docs = []
145 i = 0
146 while i < len(lines):
147 line = lines[i]
148 if line.strip() == ARGS_HEADING:
David Chend42081e2016-04-21 21:17:05 +0000149 i = _parse_attribute_docs(attr_docs, lines, i)
David Chen709bba12016-04-13 09:18:46 +0000150 continue
151 elif line.strip() == EXAMPLES_HEADING or line.strip() == EXAMPLE_HEADING:
152 i = _parse_example_docs(examples, lines, i)
153 continue
David Chend42081e2016-04-21 21:17:05 +0000154 elif line.strip() == OUTPUTS_HEADING:
155 i = _parse_attribute_docs(output_docs, lines, i)
156 continue
David Z. Chen55dafa92016-03-25 15:55:54 -0700157
David Chen709bba12016-04-13 09:18:46 +0000158 docs.append(line)
159 i += 1
David Z. Chen55dafa92016-03-25 15:55:54 -0700160
David Chen709bba12016-04-13 09:18:46 +0000161 doc = "\n".join(docs).strip()
162 examples_doc = textwrap.dedent("\n".join(examples)).strip()
David Chend42081e2016-04-21 21:17:05 +0000163 return ExtractedDocs(doc, attr_docs, examples_doc, output_docs)