gendoc.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. """
  2. Generate documentation for pyboard API from C files.
  3. """
  4. import os
  5. import argparse
  6. import re
  7. import markdown
  8. # given a list of (name,regex) pairs, find the first one that matches the given line
  9. def re_match_first(regexs, line):
  10. for name, regex in regexs:
  11. match = re.match(regex, line)
  12. if match:
  13. return name, match
  14. return None, None
  15. def makedirs(d):
  16. if not os.path.isdir(d):
  17. os.makedirs(d)
  18. class Lexer:
  19. class LexerError(Exception):
  20. pass
  21. class EOF(Exception):
  22. pass
  23. class Break(Exception):
  24. pass
  25. def __init__(self, file):
  26. self.filename = file
  27. with open(file, 'rt') as f:
  28. line_num = 0
  29. lines = []
  30. for line in f:
  31. line_num += 1
  32. line = line.strip()
  33. if line == '///':
  34. lines.append((line_num, ''))
  35. elif line.startswith('/// '):
  36. lines.append((line_num, line[4:]))
  37. elif len(lines) > 0 and lines[-1][1] is not None:
  38. lines.append((line_num, None))
  39. if len(lines) > 0 and lines[-1][1] is not None:
  40. lines.append((line_num, None))
  41. self.cur_line = 0
  42. self.lines = lines
  43. def opt_break(self):
  44. if len(self.lines) > 0 and self.lines[0][1] is None:
  45. self.lines.pop(0)
  46. def next(self):
  47. if len(self.lines) == 0:
  48. raise Lexer.EOF
  49. else:
  50. l = self.lines.pop(0)
  51. self.cur_line = l[0]
  52. if l[1] is None:
  53. raise Lexer.Break
  54. else:
  55. return l[1]
  56. def error(self, msg):
  57. print('({}:{}) {}'.format(self.filename, self.cur_line, msg))
  58. raise Lexer.LexerError
  59. class MarkdownWriter:
  60. def __init__(self):
  61. pass
  62. def start(self):
  63. self.lines = []
  64. def end(self):
  65. return '\n'.join(self.lines)
  66. def heading(self, level, text):
  67. if len(self.lines) > 0:
  68. self.lines.append('')
  69. self.lines.append(level * '#' + ' ' + text)
  70. self.lines.append('')
  71. def para(self, text):
  72. if len(self.lines) > 0 and self.lines[-1] != '':
  73. self.lines.append('')
  74. if isinstance(text, list):
  75. self.lines.extend(text)
  76. elif isinstance(text, str):
  77. self.lines.append(text)
  78. else:
  79. assert False
  80. self.lines.append('')
  81. def single_line(self, text):
  82. self.lines.append(text)
  83. def module(self, name, short_descr, descr):
  84. self.heading(1, 'module {}'.format(name))
  85. self.para(descr)
  86. def function(self, ctx, name, args, descr):
  87. proto = '{}.{}{}'.format(ctx, self.name, self.args)
  88. self.heading(3, '`' + proto + '`')
  89. self.para(descr)
  90. def method(self, ctx, name, args, descr):
  91. if name == '\\constructor':
  92. proto = '{}{}'.format(ctx, args)
  93. elif name == '\\call':
  94. proto = '{}{}'.format(ctx, args)
  95. else:
  96. proto = '{}.{}{}'.format(ctx, name, args)
  97. self.heading(3, '`' + proto + '`')
  98. self.para(descr)
  99. def constant(self, ctx, name, descr):
  100. self.single_line('`{}.{}` - {}'.format(ctx, name, descr))
  101. class ReStructuredTextWriter:
  102. head_chars = {1:'=', 2:'-', 3:'.'}
  103. def __init__(self):
  104. pass
  105. def start(self):
  106. self.lines = []
  107. def end(self):
  108. return '\n'.join(self.lines)
  109. def _convert(self, text):
  110. return text.replace('`', '``').replace('*', '\\*')
  111. def heading(self, level, text, convert=True):
  112. if len(self.lines) > 0:
  113. self.lines.append('')
  114. if convert:
  115. text = self._convert(text)
  116. self.lines.append(text)
  117. self.lines.append(len(text) * self.head_chars[level])
  118. self.lines.append('')
  119. def para(self, text, indent=''):
  120. if len(self.lines) > 0 and self.lines[-1] != '':
  121. self.lines.append('')
  122. if isinstance(text, list):
  123. for t in text:
  124. self.lines.append(indent + self._convert(t))
  125. elif isinstance(text, str):
  126. self.lines.append(indent + self._convert(text))
  127. else:
  128. assert False
  129. self.lines.append('')
  130. def single_line(self, text):
  131. self.lines.append(self._convert(text))
  132. def module(self, name, short_descr, descr):
  133. self.heading(1, ':mod:`{}` --- {}'.format(name, self._convert(short_descr)), convert=False)
  134. self.lines.append('.. module:: {}'.format(name))
  135. self.lines.append(' :synopsis: {}'.format(short_descr))
  136. self.para(descr)
  137. def function(self, ctx, name, args, descr):
  138. args = self._convert(args)
  139. self.lines.append('.. function:: ' + name + args)
  140. self.para(descr, indent=' ')
  141. def method(self, ctx, name, args, descr):
  142. args = self._convert(args)
  143. if name == '\\constructor':
  144. self.lines.append('.. class:: ' + ctx + args)
  145. elif name == '\\call':
  146. self.lines.append('.. method:: ' + ctx + args)
  147. else:
  148. self.lines.append('.. method:: ' + ctx + '.' + name + args)
  149. self.para(descr, indent=' ')
  150. def constant(self, ctx, name, descr):
  151. self.lines.append('.. data:: ' + name)
  152. self.para(descr, indent=' ')
  153. class DocValidateError(Exception):
  154. pass
  155. class DocItem:
  156. def __init__(self):
  157. self.doc = []
  158. def add_doc(self, lex):
  159. try:
  160. while True:
  161. line = lex.next()
  162. if len(line) > 0 or len(self.doc) > 0:
  163. self.doc.append(line)
  164. except Lexer.Break:
  165. pass
  166. def dump(self, writer):
  167. writer.para(self.doc)
  168. class DocConstant(DocItem):
  169. def __init__(self, name, descr):
  170. super().__init__()
  171. self.name = name
  172. self.descr = descr
  173. def dump(self, ctx, writer):
  174. writer.constant(ctx, self.name, self.descr)
  175. class DocFunction(DocItem):
  176. def __init__(self, name, args):
  177. super().__init__()
  178. self.name = name
  179. self.args = args
  180. def dump(self, ctx, writer):
  181. writer.function(ctx, self.name, self.args, self.doc)
  182. class DocMethod(DocItem):
  183. def __init__(self, name, args):
  184. super().__init__()
  185. self.name = name
  186. self.args = args
  187. def dump(self, ctx, writer):
  188. writer.method(ctx, self.name, self.args, self.doc)
  189. class DocClass(DocItem):
  190. def __init__(self, name, descr):
  191. super().__init__()
  192. self.name = name
  193. self.descr = descr
  194. self.constructors = {}
  195. self.classmethods = {}
  196. self.methods = {}
  197. self.constants = {}
  198. def process_classmethod(self, lex, d):
  199. name = d['id']
  200. if name == '\\constructor':
  201. dict_ = self.constructors
  202. else:
  203. dict_ = self.classmethods
  204. if name in dict_:
  205. lex.error("multiple definition of method '{}'".format(name))
  206. method = dict_[name] = DocMethod(name, d['args'])
  207. method.add_doc(lex)
  208. def process_method(self, lex, d):
  209. name = d['id']
  210. dict_ = self.methods
  211. if name in dict_:
  212. lex.error("multiple definition of method '{}'".format(name))
  213. method = dict_[name] = DocMethod(name, d['args'])
  214. method.add_doc(lex)
  215. def process_constant(self, lex, d):
  216. name = d['id']
  217. if name in self.constants:
  218. lex.error("multiple definition of constant '{}'".format(name))
  219. self.constants[name] = DocConstant(name, d['descr'])
  220. lex.opt_break()
  221. def dump(self, writer):
  222. writer.heading(1, 'class {}'.format(self.name))
  223. super().dump(writer)
  224. if len(self.constructors) > 0:
  225. writer.heading(2, 'Constructors')
  226. for f in sorted(self.constructors.values(), key=lambda x:x.name):
  227. f.dump(self.name, writer)
  228. if len(self.classmethods) > 0:
  229. writer.heading(2, 'Class methods')
  230. for f in sorted(self.classmethods.values(), key=lambda x:x.name):
  231. f.dump(self.name, writer)
  232. if len(self.methods) > 0:
  233. writer.heading(2, 'Methods')
  234. for f in sorted(self.methods.values(), key=lambda x:x.name):
  235. f.dump(self.name.lower(), writer)
  236. if len(self.constants) > 0:
  237. writer.heading(2, 'Constants')
  238. for c in sorted(self.constants.values(), key=lambda x:x.name):
  239. c.dump(self.name, writer)
  240. class DocModule(DocItem):
  241. def __init__(self, name, descr):
  242. super().__init__()
  243. self.name = name
  244. self.descr = descr
  245. self.functions = {}
  246. self.constants = {}
  247. self.classes = {}
  248. self.cur_class = None
  249. def new_file(self):
  250. self.cur_class = None
  251. def process_function(self, lex, d):
  252. name = d['id']
  253. if name in self.functions:
  254. lex.error("multiple definition of function '{}'".format(name))
  255. function = self.functions[name] = DocFunction(name, d['args'])
  256. function.add_doc(lex)
  257. #def process_classref(self, lex, d):
  258. # name = d['id']
  259. # self.classes[name] = name
  260. # lex.opt_break()
  261. def process_class(self, lex, d):
  262. name = d['id']
  263. if name in self.classes:
  264. lex.error("multiple definition of class '{}'".format(name))
  265. self.cur_class = self.classes[name] = DocClass(name, d['descr'])
  266. self.cur_class.add_doc(lex)
  267. def process_classmethod(self, lex, d):
  268. self.cur_class.process_classmethod(lex, d)
  269. def process_method(self, lex, d):
  270. self.cur_class.process_method(lex, d)
  271. def process_constant(self, lex, d):
  272. if self.cur_class is None:
  273. # a module-level constant
  274. name = d['id']
  275. if name in self.constants:
  276. lex.error("multiple definition of constant '{}'".format(name))
  277. self.constants[name] = DocConstant(name, d['descr'])
  278. lex.opt_break()
  279. else:
  280. # a class-level constant
  281. self.cur_class.process_constant(lex, d)
  282. def validate(self):
  283. if self.descr is None:
  284. raise DocValidateError('module {} referenced but never defined'.format(self.name))
  285. def dump(self, writer):
  286. writer.module(self.name, self.descr, self.doc)
  287. if self.functions:
  288. writer.heading(2, 'Functions')
  289. for f in sorted(self.functions.values(), key=lambda x:x.name):
  290. f.dump(self.name, writer)
  291. if self.constants:
  292. writer.heading(2, 'Constants')
  293. for c in sorted(self.constants.values(), key=lambda x:x.name):
  294. c.dump(self.name, writer)
  295. if self.classes:
  296. writer.heading(2, 'Classes')
  297. for c in sorted(self.classes.values(), key=lambda x:x.name):
  298. writer.para('[`{}.{}`]({}) - {}'.format(self.name, c.name, c.name, c.descr))
  299. def write_html(self, dir):
  300. md_writer = MarkdownWriter()
  301. md_writer.start()
  302. self.dump(md_writer)
  303. with open(os.path.join(dir, 'index.html'), 'wt') as f:
  304. f.write(markdown.markdown(md_writer.end()))
  305. for c in self.classes.values():
  306. class_dir = os.path.join(dir, c.name)
  307. makedirs(class_dir)
  308. md_writer.start()
  309. md_writer.para('part of the [{} module](./)'.format(self.name))
  310. c.dump(md_writer)
  311. with open(os.path.join(class_dir, 'index.html'), 'wt') as f:
  312. f.write(markdown.markdown(md_writer.end()))
  313. def write_rst(self, dir):
  314. rst_writer = ReStructuredTextWriter()
  315. rst_writer.start()
  316. self.dump(rst_writer)
  317. with open(dir + '/' + self.name + '.rst', 'wt') as f:
  318. f.write(rst_writer.end())
  319. for c in self.classes.values():
  320. rst_writer.start()
  321. c.dump(rst_writer)
  322. with open(dir + '/' + self.name + '.' + c.name + '.rst', 'wt') as f:
  323. f.write(rst_writer.end())
  324. class Doc:
  325. def __init__(self):
  326. self.modules = {}
  327. self.cur_module = None
  328. def new_file(self):
  329. self.cur_module = None
  330. for m in self.modules.values():
  331. m.new_file()
  332. def check_module(self, lex):
  333. if self.cur_module is None:
  334. lex.error('module not defined')
  335. def process_module(self, lex, d):
  336. name = d['id']
  337. if name not in self.modules:
  338. self.modules[name] = DocModule(name, None)
  339. self.cur_module = self.modules[name]
  340. if self.cur_module.descr is not None:
  341. lex.error("multiple definition of module '{}'".format(name))
  342. self.cur_module.descr = d['descr']
  343. self.cur_module.add_doc(lex)
  344. def process_moduleref(self, lex, d):
  345. name = d['id']
  346. if name not in self.modules:
  347. self.modules[name] = DocModule(name, None)
  348. self.cur_module = self.modules[name]
  349. lex.opt_break()
  350. def process_class(self, lex, d):
  351. self.check_module(lex)
  352. self.cur_module.process_class(lex, d)
  353. def process_function(self, lex, d):
  354. self.check_module(lex)
  355. self.cur_module.process_function(lex, d)
  356. def process_classmethod(self, lex, d):
  357. self.check_module(lex)
  358. self.cur_module.process_classmethod(lex, d)
  359. def process_method(self, lex, d):
  360. self.check_module(lex)
  361. self.cur_module.process_method(lex, d)
  362. def process_constant(self, lex, d):
  363. self.check_module(lex)
  364. self.cur_module.process_constant(lex, d)
  365. def validate(self):
  366. for m in self.modules.values():
  367. m.validate()
  368. def dump(self, writer):
  369. writer.heading(1, 'Modules')
  370. writer.para('These are the Python modules that are implemented.')
  371. for m in sorted(self.modules.values(), key=lambda x:x.name):
  372. writer.para('[`{}`]({}/) - {}'.format(m.name, m.name, m.descr))
  373. def write_html(self, dir):
  374. md_writer = MarkdownWriter()
  375. with open(os.path.join(dir, 'module', 'index.html'), 'wt') as f:
  376. md_writer.start()
  377. self.dump(md_writer)
  378. f.write(markdown.markdown(md_writer.end()))
  379. for m in self.modules.values():
  380. mod_dir = os.path.join(dir, 'module', m.name)
  381. makedirs(mod_dir)
  382. m.write_html(mod_dir)
  383. def write_rst(self, dir):
  384. #with open(os.path.join(dir, 'module', 'index.html'), 'wt') as f:
  385. # f.write(markdown.markdown(self.dump()))
  386. for m in self.modules.values():
  387. m.write_rst(dir)
  388. regex_descr = r'(?P<descr>.*)'
  389. doc_regexs = (
  390. (Doc.process_module, re.compile(r'\\module (?P<id>[a-z][a-z0-9]*) - ' + regex_descr + r'$')),
  391. (Doc.process_moduleref, re.compile(r'\\moduleref (?P<id>[a-z]+)$')),
  392. (Doc.process_function, re.compile(r'\\function (?P<id>[a-z0-9_]+)(?P<args>\(.*\))$')),
  393. (Doc.process_classmethod, re.compile(r'\\classmethod (?P<id>\\?[a-z0-9_]+)(?P<args>\(.*\))$')),
  394. (Doc.process_method, re.compile(r'\\method (?P<id>\\?[a-z0-9_]+)(?P<args>\(.*\))$')),
  395. (Doc.process_constant, re.compile(r'\\constant (?P<id>[A-Za-z0-9_]+) - ' + regex_descr + r'$')),
  396. #(Doc.process_classref, re.compile(r'\\classref (?P<id>[A-Za-z0-9_]+)$')),
  397. (Doc.process_class, re.compile(r'\\class (?P<id>[A-Za-z0-9_]+) - ' + regex_descr + r'$')),
  398. )
  399. def process_file(file, doc):
  400. lex = Lexer(file)
  401. doc.new_file()
  402. try:
  403. try:
  404. while True:
  405. line = lex.next()
  406. fun, match = re_match_first(doc_regexs, line)
  407. if fun == None:
  408. lex.error('unknown line format: {}'.format(line))
  409. fun(doc, lex, match.groupdict())
  410. except Lexer.Break:
  411. lex.error('unexpected break')
  412. except Lexer.EOF:
  413. pass
  414. except Lexer.LexerError:
  415. return False
  416. return True
  417. def main():
  418. cmd_parser = argparse.ArgumentParser(description='Generate documentation for pyboard API from C files.')
  419. cmd_parser.add_argument('--outdir', metavar='<output dir>', default='gendoc-out', help='ouput directory')
  420. cmd_parser.add_argument('--format', default='html', help='output format: html or rst')
  421. cmd_parser.add_argument('files', nargs='+', help='input files')
  422. args = cmd_parser.parse_args()
  423. doc = Doc()
  424. for file in args.files:
  425. print('processing', file)
  426. if not process_file(file, doc):
  427. return
  428. try:
  429. doc.validate()
  430. except DocValidateError as e:
  431. print(e)
  432. makedirs(args.outdir)
  433. if args.format == 'html':
  434. doc.write_html(args.outdir)
  435. elif args.format == 'rst':
  436. doc.write_rst(args.outdir)
  437. else:
  438. print('unknown format:', args.format)
  439. return
  440. print('written to', args.outdir)
  441. if __name__ == "__main__":
  442. main()