diff options
author | Damien George <damien.p.george@gmail.com> | 2014-05-10 19:12:47 +0100 |
---|---|---|
committer | Damien George <damien.p.george@gmail.com> | 2014-05-10 19:12:47 +0100 |
commit | 3793830ed997baf0bd4dbafc13827f2615edea10 (patch) | |
tree | 5068cb8ef9c159fd75c8b813843c6ff9990e685a /tools/gendoc.py | |
parent | 09bbe7215a70dbe9105a48865a1545e6c095baef (diff) | |
download | micropython-3793830ed997baf0bd4dbafc13827f2615edea10.tar.gz micropython-3793830ed997baf0bd4dbafc13827f2615edea10.zip |
tools: Move gendoc.py to tools, and make it a little more generic.
Diffstat (limited to 'tools/gendoc.py')
-rw-r--r-- | tools/gendoc.py | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/tools/gendoc.py b/tools/gendoc.py new file mode 100644 index 0000000000..5a33a8195d --- /dev/null +++ b/tools/gendoc.py @@ -0,0 +1,372 @@ +""" +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 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): + return '\n'.join(self.doc) + +class DocConstant(DocItem): + def __init__(self, name, descr): + super().__init__() + self.name = name + self.descr = descr + + def dump(self, ctx): + return '{}.{} - {}'.format(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): + if self.name == '\\constructor': + s = '### `{}{}`'.format(ctx, self.args) + elif self.name == '\\call': + s = '### `{}{}`'.format(ctx, self.args) + else: + s = '### `{}.{}{}`'.format(ctx, self.name, self.args) + return s + '\n' + super().dump() + +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] = DocFunction(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] = DocFunction(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): + s = [] + s.append('') + s.append('# class {}'.format(self.name)) + s.append('') + s.append(super().dump()) + if len(self.constructors) > 0: + s.append('') + s.append("## Constructors") + for f in sorted(self.constructors.values(), key=lambda x:x.name): + s.append('') + s.append(f.dump(self.name)) + if len(self.classmethods) > 0: + s.append('') + s.append("## Class methods") + for f in sorted(self.classmethods.values(), key=lambda x:x.name): + s.append('') + s.append(f.dump(self.name)) + if len(self.methods) > 0: + s.append('') + s.append("## Methods") + for f in sorted(self.methods.values(), key=lambda x:x.name): + s.append('') + s.append(f.dump(self.name.lower())) + if len(self.constants) > 0: + s.append('') + s.append("## Constants") + for c in sorted(self.constants.values(), key=lambda x:x.name): + s.append('') + s.append('`{}`'.format(c.dump(self.name))) + return '\n'.join(s) + +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): + 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): + s = [] + s.append('# module {}'.format(self.name)) + s.append('') + s.append(super().dump()) + s.append('') + s.append('## Functions') + for f in sorted(self.functions.values(), key=lambda x:x.name): + s.append('') + s.append(f.dump(self.name)) + s.append('') + s.append('## Classes') + for c in sorted(self.classes.values(), key=lambda x:x.name): + s.append('') + s.append('[`{}.{}`]({}) - {}'.format(self.name, c.name, c.name, c.descr)) + return '\n'.join(s) + + def write(self, dir): + index = markdown.markdown(self.dump()) + with open(os.path.join(dir, 'index.html'), 'wt') as f: + f.write(index) + for c in self.classes.values(): + class_dir = os.path.join(dir, c.name) + makedirs(class_dir) + class_dump = c.dump() + class_dump = 'part of the [{} module](./)'.format(self.name) + '\n' + class_dump + index = markdown.markdown(class_dump) + with open(os.path.join(class_dir, 'index.html'), 'wt') as f: + f.write(index) + +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 write(self, dir): + for m in self.modules.values(): + mod_dir = os.path.join(dir, 'module', m.name) + makedirs(mod_dir) + m.write(mod_dir) + +regex_descr = r'(?P<descr>.*)' + +doc_regexs = ( + (Doc.process_module, re.compile(r'\\module (?P<id>[a-z]+) - ' + regex_descr + r'$')), + (Doc.process_moduleref, re.compile(r'\\moduleref (?P<id>[a-z]+)$')), + (Doc.process_function, re.compile(r'\\function (?P<id>[a-z0-9_]+)(?P<args>\(.*\))$')), + (Doc.process_classmethod, re.compile(r'\\classmethod (?P<id>\\?[a-z0-9_]+)(?P<args>\(.*\))$')), + (Doc.process_method, re.compile(r'\\method (?P<id>\\?[a-z0-9_]+)(?P<args>\(.*\))$')), + (Doc.process_constant, re.compile(r'\\constant (?P<id>[A-Z0-9_]+) - ' + regex_descr + r'$')), + #(Doc.process_classref, re.compile(r'\\classref (?P<id>[A-Za-z0-9_]+)$')), + (Doc.process_class, re.compile(r'\\class (?P<id>[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='<output dir>', default='gendoc-out', help='ouput directory') + 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) + doc.write(args.outdir) + print('written to', args.outdir) + +if __name__ == "__main__": + main() |