""" Generate documentation for pyboard API from C files. """ import os import argparse import re import markdown # given a list of (name,regex) pairs, find the first one that matches the given line def re_match_first(regexs, line): for name, regex in regexs: match = re.match(regex, line) if match: return name, match return None, None def makedirs(d): if not os.path.isdir(d): os.makedirs(d) class Lexer: class LexerError(Exception): pass class EOF(Exception): pass class Break(Exception): pass def __init__(self, file): self.filename = file with open(file, "rt") as f: line_num = 0 lines = [] for line in f: line_num += 1 line = line.strip() if line == "///": lines.append((line_num, "")) elif line.startswith("/// "): lines.append((line_num, line[4:])) elif len(lines) > 0 and lines[-1][1] is not None: lines.append((line_num, None)) if len(lines) > 0 and lines[-1][1] is not None: lines.append((line_num, None)) self.cur_line = 0 self.lines = lines def opt_break(self): if len(self.lines) > 0 and self.lines[0][1] is None: self.lines.pop(0) def next(self): if len(self.lines) == 0: raise Lexer.EOF else: l = self.lines.pop(0) self.cur_line = l[0] if l[1] is None: raise Lexer.Break else: return l[1] def error(self, msg): print("({}:{}) {}".format(self.filename, self.cur_line, msg)) raise Lexer.LexerError class MarkdownWriter: def __init__(self): pass def start(self): self.lines = [] def end(self): return "\n".join(self.lines) def heading(self, level, text): if len(self.lines) > 0: self.lines.append("") self.lines.append(level * "#" + " " + text) self.lines.append("") def para(self, text): if len(self.lines) > 0 and self.lines[-1] != "": self.lines.append("") if isinstance(text, list): self.lines.extend(text) elif isinstance(text, str): self.lines.append(text) else: assert False self.lines.append("") def single_line(self, text): self.lines.append(text) def module(self, name, short_descr, descr): self.heading(1, "module {}".format(name)) self.para(descr) def function(self, ctx, name, args, descr): proto = "{}.{}{}".format(ctx, self.name, self.args) self.heading(3, "`" + proto + "`") self.para(descr) def method(self, ctx, name, args, descr): if name == "\\constructor": proto = "{}{}".format(ctx, args) elif name == "\\call": proto = "{}{}".format(ctx, args) else: proto = "{}.{}{}".format(ctx, name, args) self.heading(3, "`" + proto + "`") self.para(descr) def constant(self, ctx, name, descr): self.single_line("`{}.{}` - {}".format(ctx, name, descr)) class ReStructuredTextWriter: head_chars = {1: "=", 2: "-", 3: "."} def __init__(self): pass def start(self): self.lines = [] def end(self): return "\n".join(self.lines) def _convert(self, text): return text.replace("`", "``").replace("*", "\\*") def heading(self, level, text, convert=True): if len(self.lines) > 0: self.lines.append("") if convert: text = self._convert(text) self.lines.append(text) self.lines.append(len(text) * self.head_chars[level]) self.lines.append("") def para(self, text, indent=""): if len(self.lines) > 0 and self.lines[-1] != "": self.lines.append("") if isinstance(text, list): for t in text: self.lines.append(indent + self._convert(t)) elif isinstance(text, str): self.lines.append(indent + self._convert(text)) else: assert False self.lines.append("") def single_line(self, text): self.lines.append(self._convert(text)) def module(self, name, short_descr, descr): self.heading(1, ":mod:`{}` --- {}".format(name, self._convert(short_descr)), convert=False) self.lines.append(".. module:: {}".format(name)) self.lines.append(" :synopsis: {}".format(short_descr)) self.para(descr) def function(self, ctx, name, args, descr): args = self._convert(args) self.lines.append(".. function:: " + name + args) self.para(descr, indent=" ") def method(self, ctx, name, args, descr): args = self._convert(args) if name == "\\constructor": self.lines.append(".. class:: " + ctx + args) elif name == "\\call": self.lines.append(".. method:: " + ctx + args) else: self.lines.append(".. method:: " + ctx + "." + name + args) self.para(descr, indent=" ") def constant(self, ctx, name, descr): self.lines.append(".. data:: " + name) self.para(descr, indent=" ") class DocValidateError(Exception): pass class DocItem: def __init__(self): self.doc = [] def add_doc(self, lex): try: while True: line = lex.next() if len(line) > 0 or len(self.doc) > 0: self.doc.append(line) except Lexer.Break: pass def dump(self, writer): writer.para(self.doc) class DocConstant(DocItem): def __init__(self, name, descr): super().__init__() self.name = name self.descr = descr def dump(self, ctx, writer): writer.constant(ctx, self.name, self.descr) class DocFunction(DocItem): def __init__(self, name, args): super().__init__() self.name = name self.args = args def dump(self, ctx, writer): writer.function(ctx, self.name, self.args, self.doc) class DocMethod(DocItem): def __init__(self, name, args): super().__init__() self.name = name self.args = args def dump(self, ctx, writer): writer.method(ctx, self.name, self.args, self.doc) class DocClass(DocItem): def __init__(self, name, descr): super().__init__() self.name = name self.descr = descr self.constructors = {} self.classmethods = {} self.methods = {} self.constants = {} def process_classmethod(self, lex, d): name = d["id"] if name == "\\constructor": dict_ = self.constructors else: dict_ = self.classmethods if name in dict_: lex.error("multiple definition of method '{}'".format(name)) method = dict_[name] = DocMethod(name, d["args"]) method.add_doc(lex) def process_method(self, lex, d): name = d["id"] dict_ = self.methods if name in dict_: lex.error("multiple definition of method '{}'".format(name)) method = dict_[name] = DocMethod(name, d["args"]) method.add_doc(lex) def process_constant(self, lex, d): name = d["id"] if name in self.constants: lex.error("multiple definition of constant '{}'".format(name)) self.constants[name] = DocConstant(name, d["descr"]) lex.opt_break() def dump(self, writer): writer.heading(1, "class {}".format(self.name)) super().dump(writer) if len(self.constructors) > 0: writer.heading(2, "Constructors") for f in sorted(self.constructors.values(), key=lambda x: x.name): f.dump(self.name, writer) if len(self.classmethods) > 0: writer.heading(2, "Class methods") for f in sorted(self.classmethods.values(), key=lambda x: x.name): f.dump(self.name, writer) if len(self.methods) > 0: writer.heading(2, "Methods") for f in sorted(self.methods.values(), key=lambda x: x.name): f.dump(self.name.lower(), writer) if len(self.constants) > 0: writer.heading(2, "Constants") for c in sorted(self.constants.values(), key=lambda x: x.name): c.dump(self.name, writer) class DocModule(DocItem): def __init__(self, name, descr): super().__init__() self.name = name self.descr = descr self.functions = {} self.constants = {} self.classes = {} self.cur_class = None def new_file(self): self.cur_class = None def process_function(self, lex, d): name = d["id"] if name in self.functions: lex.error("multiple definition of function '{}'".format(name)) function = self.functions[name] = DocFunction(name, d["args"]) function.add_doc(lex) # def process_classref(self, lex, d): # name = d['id'] # self.classes[name] = name # lex.opt_break() def process_class(self, lex, d): name = d["id"] if name in self.classes: lex.error("multiple definition of class '{}'".format(name)) self.cur_class = self.classes[name] = DocClass(name, d["descr"]) self.cur_class.add_doc(lex) def process_classmethod(self, lex, d): self.cur_class.process_classmethod(lex, d) def process_method(self, lex, d): self.cur_class.process_method(lex, d) def process_constant(self, lex, d): if self.cur_class is None: # a module-level constant name = d["id"] if name in self.constants: lex.error("multiple definition of constant '{}'".format(name)) self.constants[name] = DocConstant(name, d["descr"]) lex.opt_break() else: # a class-level constant self.cur_class.process_constant(lex, d) def validate(self): if self.descr is None: raise DocValidateError("module {} referenced but never defined".format(self.name)) def dump(self, writer): writer.module(self.name, self.descr, self.doc) if self.functions: writer.heading(2, "Functions") for f in sorted(self.functions.values(), key=lambda x: x.name): f.dump(self.name, writer) if self.constants: writer.heading(2, "Constants") for c in sorted(self.constants.values(), key=lambda x: x.name): c.dump(self.name, writer) if self.classes: writer.heading(2, "Classes") for c in sorted(self.classes.values(), key=lambda x: x.name): writer.para("[`{}.{}`]({}) - {}".format(self.name, c.name, c.name, c.descr)) def write_html(self, dir): md_writer = MarkdownWriter() md_writer.start() self.dump(md_writer) with open(os.path.join(dir, "index.html"), "wt") as f: f.write(markdown.markdown(md_writer.end())) for c in self.classes.values(): class_dir = os.path.join(dir, c.name) makedirs(class_dir) md_writer.start() md_writer.para("part of the [{} module](./)".format(self.name)) c.dump(md_writer) with open(os.path.join(class_dir, "index.html"), "wt") as f: f.write(markdown.markdown(md_writer.end())) def write_rst(self, dir): rst_writer = ReStructuredTextWriter() rst_writer.start() self.dump(rst_writer) with open(dir + "/" + self.name + ".rst", "wt") as f: f.write(rst_writer.end()) for c in self.classes.values(): rst_writer.start() c.dump(rst_writer) with open(dir + "/" + self.name + "." + c.name + ".rst", "wt") as f: f.write(rst_writer.end()) class Doc: def __init__(self): self.modules = {} self.cur_module = None def new_file(self): self.cur_module = None for m in self.modules.values(): m.new_file() def check_module(self, lex): if self.cur_module is None: lex.error("module not defined") def process_module(self, lex, d): name = d["id"] if name not in self.modules: self.modules[name] = DocModule(name, None) self.cur_module = self.modules[name] if self.cur_module.descr is not None: lex.error("multiple definition of module '{}'".format(name)) self.cur_module.descr = d["descr"] self.cur_module.add_doc(lex) def process_moduleref(self, lex, d): name = d["id"] if name not in self.modules: self.modules[name] = DocModule(name, None) self.cur_module = self.modules[name] lex.opt_break() def process_class(self, lex, d): self.check_module(lex) self.cur_module.process_class(lex, d) def process_function(self, lex, d): self.check_module(lex) self.cur_module.process_function(lex, d) def process_classmethod(self, lex, d): self.check_module(lex) self.cur_module.process_classmethod(lex, d) def process_method(self, lex, d): self.check_module(lex) self.cur_module.process_method(lex, d) def process_constant(self, lex, d): self.check_module(lex) self.cur_module.process_constant(lex, d) def validate(self): for m in self.modules.values(): m.validate() def dump(self, writer): writer.heading(1, "Modules") writer.para("These are the Python modules that are implemented.") for m in sorted(self.modules.values(), key=lambda x: x.name): writer.para("[`{}`]({}/) - {}".format(m.name, m.name, m.descr)) def write_html(self, dir): md_writer = MarkdownWriter() with open(os.path.join(dir, "module", "index.html"), "wt") as f: md_writer.start() self.dump(md_writer) f.write(markdown.markdown(md_writer.end())) for m in self.modules.values(): mod_dir = os.path.join(dir, "module", m.name) makedirs(mod_dir) m.write_html(mod_dir) def write_rst(self, dir): # with open(os.path.join(dir, 'module', 'index.html'), 'wt') as f: # f.write(markdown.markdown(self.dump())) for m in self.modules.values(): m.write_rst(dir) regex_descr = r"(?P.*)" doc_regexs = ( (Doc.process_module, re.compile(r"\\module (?P[a-z][a-z0-9]*) - " + regex_descr + r"$")), (Doc.process_moduleref, re.compile(r"\\moduleref (?P[a-z]+)$")), (Doc.process_function, re.compile(r"\\function (?P[a-z0-9_]+)(?P\(.*\))$")), (Doc.process_classmethod, re.compile(r"\\classmethod (?P\\?[a-z0-9_]+)(?P\(.*\))$")), (Doc.process_method, re.compile(r"\\method (?P\\?[a-z0-9_]+)(?P\(.*\))$")), ( Doc.process_constant, re.compile(r"\\constant (?P[A-Za-z0-9_]+) - " + regex_descr + r"$"), ), # (Doc.process_classref, re.compile(r'\\classref (?P[A-Za-z0-9_]+)$')), (Doc.process_class, re.compile(r"\\class (?P[A-Za-z0-9_]+) - " + regex_descr + r"$")), ) def process_file(file, doc): lex = Lexer(file) doc.new_file() try: try: while True: line = lex.next() fun, match = re_match_first(doc_regexs, line) if fun == None: lex.error("unknown line format: {}".format(line)) fun(doc, lex, match.groupdict()) except Lexer.Break: lex.error("unexpected break") except Lexer.EOF: pass except Lexer.LexerError: return False return True def main(): cmd_parser = argparse.ArgumentParser( description="Generate documentation for pyboard API from C files." ) cmd_parser.add_argument( "--outdir", metavar="", default="gendoc-out", help="ouput directory" ) cmd_parser.add_argument("--format", default="html", help="output format: html or rst") cmd_parser.add_argument("files", nargs="+", help="input files") args = cmd_parser.parse_args() doc = Doc() for file in args.files: print("processing", file) if not process_file(file, doc): return try: doc.validate() except DocValidateError as e: print(e) makedirs(args.outdir) if args.format == "html": doc.write_html(args.outdir) elif args.format == "rst": doc.write_rst(args.outdir) else: print("unknown format:", args.format) return print("written to", args.outdir) if __name__ == "__main__": main()