3 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5 # Licensed under the Apache License, Version 2.0 (the "License").
6 # You may not use this file except in compliance with the License.
7 # A copy of the License is located at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # or in the "license" file accompanying this file. This file is distributed
12 # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13 # express or implied. See the License for the specific language governing
14 # permissions and limitations under the License.
28 _DESCRIPTION = "Convert a series of commented voluptuous schemas into groff"
30 This program is used to automatically generate documentation for Litani's
31 voluptuous schemata. The codebase contains schemata in methods that are preceded
32 by the following magic comment:
36 # "page": "PAGE_NAME",
41 When this script is run with --page-name PAGE_NAME, it reads all the python
42 files in the codebase looking for such blocks with an equal PAGE_NAME. It then
43 converts the schema and associated documentation into scdoc format, ready to be
44 converted into groff and read in a manual pager.
50 def __init__(self, lines):
51 indent_pat = re.compile(r"(?P<indentation>\s*)")
52 match = indent_pat.match(lines[0])
53 self.indent = int(len(match["indentation"]) / 4) - 1
58 return "\n{indentation}{comment}".format(
59 indentation=("\t" * self.indent),
60 comment=" ".join([string.strip()[2:] for string in self.lines]))
63 def print_compact(self):
69 def __init__(self, line):
70 indent_pat = re.compile(r"^(?P<indentation>\s*)")
71 match = indent_pat.match(line)
72 self.indent = int(len(match["indentation"]) / 4) - 1
73 self.line = self.format_line(line)
76 def format_line(self, line):
78 line = re.sub(r"voluptuous\.", "", line)
79 line = re.sub(r"^return\s+", "", line)
80 pat = re.compile(r"(?P<key>.+?):\s+(?P<value>.*?)(?P<end>[^\w]+)$")
83 return "{indentation}{key}: {value} {end}".format(
84 indentation="\t" * self.indent,
86 value=f"_{m['value']}_" if m['value'] else '',
88 return "{indentation}{line}".format(
89 indentation="\t" * self.indent,
93 def print_compact(self):
103 def print_compact(self):
107 def print_full(self):
112 def next_line(iterator):
114 return next(iterator).rstrip()
115 except StopIteration:
120 pars = argparse.ArgumentParser(description=_DESCRIPTION, epilog=_EPILOG)
122 "flags": ["--page-name"],
125 "flags": ["--project-root"],
128 "flags": ["--data-path"],
131 "flags": ["--template"],
133 "type": pathlib.Path,
135 flags = arg.pop("flags")
136 pars.add_argument(*flags, **arg)
137 return pars.parse_args()
140 def parse_schema(iterator):
144 line = next_line(iterator)
147 line.startswith("def") or \
148 line.startswith("# end-doc-gen")):
149 if line.strip().startswith("# "):
150 comment_buf.append(line)
153 ret.append(Comment(comment_buf))
158 ret.append(Code(line))
159 line = next_line(iterator)
163 def parse_fun_def(iterator):
164 """Skip the first few lines of a schema function and return the schema"""
165 line = next_line(iterator)
167 line = next_line(iterator)
168 return parse_schema(iterator)
171 def parse_header(iterator, page_name):
172 """Parse the JSON at the top of a single documentation fragment"""
174 line = next_line(iterator)
176 while line is not None:
178 line = next_line(iterator)
180 # { <----- to avoid confusing syntax highlighting
184 header_str = "\n".join([string[2:] for string in buf])
186 header_d = json.loads(header_str)
187 except json.decoder.JSONDecodeError:
189 "Could not parse JSON fragment %s" % header_str,
192 if header_d["page"] != page_name:
196 "body": parse_fun_def(iterator)
201 def parse_file(iterator, page_name):
202 """Return a list of all documentation fragments in a single file"""
205 line = next_line(iterator)
206 while line is not None:
207 if line == "# doc-gen":
208 ret.extend(parse_header(iterator, page_name))
209 line = next_line(iterator)
213 def get_page_fragments(project_root, page_name):
215 for root, _, fyles in os.walk(project_root):
216 root = pathlib.Path(root)
218 if not fyle.endswith(".py"):
220 with open(root / fyle) as handle:
221 ret.extend(parse_file(iter(handle), page_name))
227 fragments = get_page_fragments(args.project_root, args.page_name)
228 fragments.sort(key=lambda x: x["order"])
230 with open(args.data_path) as handle:
231 page = yaml.safe_load(handle)
233 env = jinja2.Environment(
234 loader=jinja2.FileSystemLoader(str(args.template.parent)),
235 autoescape=jinja2.select_autoescape(
236 enabled_extensions=('html'),
237 default_for_string=True))
238 templ = env.get_template(args.template.name)
239 out = templ.render(page=page, fragments=fragments, page_name=args.page_name)
243 if __name__ == "__main__":