245 lines
6.5 KiB
Python
Executable File
245 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright Amazon.com, Inc. or its affiliates. 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.
|
|
# A copy of the License is located at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# or in the "license" file accompanying this file. This file 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.
|
|
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
import jinja2
|
|
import yaml
|
|
|
|
|
|
_DESCRIPTION = "Convert a series of commented voluptuous schemas into groff"
|
|
_EPILOG = """
|
|
This program is used to automatically generate documentation for Litani's
|
|
voluptuous schemata. The codebase contains schemata in methods that are preceded
|
|
by the following magic comment:
|
|
|
|
# doc-gen
|
|
# {
|
|
# "page": "PAGE_NAME",
|
|
# "order": N,
|
|
# "title": "TITLE",
|
|
# }
|
|
|
|
When this script is run with --page-name PAGE_NAME, it reads all the python
|
|
files in the codebase looking for such blocks with an equal PAGE_NAME. It then
|
|
converts the schema and associated documentation into scdoc format, ready to be
|
|
converted into groff and read in a manual pager.
|
|
"""
|
|
|
|
|
|
|
|
class Comment:
|
|
def __init__(self, lines):
|
|
indent_pat = re.compile(r"(?P<indentation>\s*)")
|
|
match = indent_pat.match(lines[0])
|
|
self.indent = int(len(match["indentation"]) / 4) - 1
|
|
self.lines = lines
|
|
|
|
|
|
def print_full(self):
|
|
return "\n{indentation}{comment}".format(
|
|
indentation=("\t" * self.indent),
|
|
comment=" ".join([string.strip()[2:] for string in self.lines]))
|
|
|
|
|
|
def print_compact(self):
|
|
return ""
|
|
|
|
|
|
|
|
class Code:
|
|
def __init__(self, line):
|
|
indent_pat = re.compile(r"^(?P<indentation>\s*)")
|
|
match = indent_pat.match(line)
|
|
self.indent = int(len(match["indentation"]) / 4) - 1
|
|
self.line = self.format_line(line)
|
|
|
|
|
|
def format_line(self, line):
|
|
line = line.strip()
|
|
line = re.sub(r"voluptuous\.", "", line)
|
|
line = re.sub(r"^return\s+", "", line)
|
|
pat = re.compile(r"(?P<key>.+?):\s+(?P<value>.*?)(?P<end>[^\w]+)$")
|
|
m = pat.match(line)
|
|
if m:
|
|
return "{indentation}{key}: {value} {end}".format(
|
|
indentation="\t" * self.indent,
|
|
key=f"*{m['key']}*",
|
|
value=f"_{m['value']}_" if m['value'] else '',
|
|
end=m['end'])
|
|
return "{indentation}{line}".format(
|
|
indentation="\t" * self.indent,
|
|
line=line)
|
|
|
|
|
|
def print_compact(self):
|
|
return self.line
|
|
|
|
|
|
def print_full(self):
|
|
return self.line
|
|
|
|
|
|
|
|
class Blank:
|
|
def print_compact(self):
|
|
return ""
|
|
|
|
|
|
def print_full(self):
|
|
return "\n"
|
|
|
|
|
|
|
|
def next_line(iterator):
|
|
try:
|
|
return next(iterator).rstrip()
|
|
except StopIteration:
|
|
return None
|
|
|
|
|
|
def get_args():
|
|
pars = argparse.ArgumentParser(description=_DESCRIPTION, epilog=_EPILOG)
|
|
for arg in [{
|
|
"flags": ["--page-name"],
|
|
"required": True,
|
|
}, {
|
|
"flags": ["--project-root"],
|
|
"required": True,
|
|
}, {
|
|
"flags": ["--data-path"],
|
|
"required": True,
|
|
}, {
|
|
"flags": ["--template"],
|
|
"required": True,
|
|
"type": pathlib.Path,
|
|
}]:
|
|
flags = arg.pop("flags")
|
|
pars.add_argument(*flags, **arg)
|
|
return pars.parse_args()
|
|
|
|
|
|
def parse_schema(iterator):
|
|
comment_buf = []
|
|
ret = []
|
|
|
|
line = next_line(iterator)
|
|
while not (
|
|
line is None or \
|
|
line.startswith("def") or \
|
|
line.startswith("# end-doc-gen")):
|
|
if line.strip().startswith("# "):
|
|
comment_buf.append(line)
|
|
else:
|
|
if comment_buf:
|
|
ret.append(Comment(comment_buf))
|
|
comment_buf = []
|
|
if not line:
|
|
ret.append(Blank())
|
|
else:
|
|
ret.append(Code(line))
|
|
line = next_line(iterator)
|
|
return ret
|
|
|
|
|
|
def parse_fun_def(iterator):
|
|
"""Skip the first few lines of a schema function and return the schema"""
|
|
line = next_line(iterator)
|
|
while line:
|
|
line = next_line(iterator)
|
|
return parse_schema(iterator)
|
|
|
|
|
|
def parse_header(iterator, page_name):
|
|
"""Parse the JSON at the top of a single documentation fragment"""
|
|
|
|
line = next_line(iterator)
|
|
buf = []
|
|
while line is not None:
|
|
buf.append(line)
|
|
line = next_line(iterator)
|
|
|
|
# { <----- to avoid confusing syntax highlighting
|
|
if line != "# }":
|
|
continue
|
|
buf.append(line)
|
|
header_str = "\n".join([string[2:] for string in buf])
|
|
try:
|
|
header_d = json.loads(header_str)
|
|
except json.decoder.JSONDecodeError:
|
|
print(
|
|
"Could not parse JSON fragment %s" % header_str,
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
if header_d["page"] != page_name:
|
|
return []
|
|
return [{
|
|
**header_d,
|
|
"body": parse_fun_def(iterator)
|
|
}]
|
|
return []
|
|
|
|
|
|
def parse_file(iterator, page_name):
|
|
"""Return a list of all documentation fragments in a single file"""
|
|
|
|
ret = []
|
|
line = next_line(iterator)
|
|
while line is not None:
|
|
if line == "# doc-gen":
|
|
ret.extend(parse_header(iterator, page_name))
|
|
line = next_line(iterator)
|
|
return ret
|
|
|
|
|
|
def get_page_fragments(project_root, page_name):
|
|
ret = []
|
|
for root, _, fyles in os.walk(project_root):
|
|
root = pathlib.Path(root)
|
|
for fyle in fyles:
|
|
if not fyle.endswith(".py"):
|
|
continue
|
|
with open(root / fyle) as handle:
|
|
ret.extend(parse_file(iter(handle), page_name))
|
|
return ret
|
|
|
|
|
|
def main():
|
|
args = get_args()
|
|
fragments = get_page_fragments(args.project_root, args.page_name)
|
|
fragments.sort(key=lambda x: x["order"])
|
|
|
|
with open(args.data_path) as handle:
|
|
page = yaml.safe_load(handle)
|
|
|
|
env = jinja2.Environment(
|
|
loader=jinja2.FileSystemLoader(str(args.template.parent)),
|
|
autoescape=jinja2.select_autoescape(
|
|
enabled_extensions=('html'),
|
|
default_for_string=True))
|
|
templ = env.get_template(args.template.name)
|
|
out = templ.render(page=page, fragments=fragments, page_name=args.page_name)
|
|
print(out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|