No Description

doxy2swig.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. #!/usr/bin/env python
  2. """Doxygen XML to SWIG docstring converter.
  3. Usage:
  4. doxy2swig.py [options] input.xml output.i
  5. Converts Doxygen generated XML files into a file containing docstrings
  6. that can be used by SWIG-1.3.x. Note that you need to get SWIG
  7. version > 1.3.23 or use Robin Dunn's docstring patch to be able to use
  8. the resulting output.
  9. input.xml is your doxygen generated XML file and output.i is where the
  10. output will be written (the file will be clobbered).
  11. """
  12. #
  13. #
  14. # This code is implemented using Mark Pilgrim's code as a guideline:
  15. # http://www.faqs.org/docs/diveintopython/kgp_divein.html
  16. #
  17. # Author: Prabhu Ramachandran
  18. # License: BSD style
  19. #
  20. # Thanks:
  21. # Johan Hake: the include_function_definition feature
  22. # Bill Spotz: bug reports and testing.
  23. # Sebastian Henschel: Misc. enhancements.
  24. #
  25. #
  26. from xml.dom import minidom
  27. import re
  28. import textwrap
  29. import sys
  30. import os.path
  31. import optparse
  32. def my_open_read(source):
  33. if hasattr(source, "read"):
  34. return source
  35. else:
  36. return open(source)
  37. def my_open_write(dest):
  38. if hasattr(dest, "write"):
  39. return dest
  40. else:
  41. return open(dest, 'w')
  42. class Doxy2SWIG:
  43. """Converts Doxygen generated XML files into a file containing
  44. docstrings that can be used by SWIG-1.3.x that have support for
  45. feature("docstring"). Once the data is parsed it is stored in
  46. self.pieces.
  47. """
  48. def __init__(self, src, include_function_definition=True, quiet=False):
  49. """Initialize the instance given a source object. `src` can
  50. be a file or filename. If you do not want to include function
  51. definitions from doxygen then set
  52. `include_function_definition` to `False`. This is handy since
  53. this allows you to use the swig generated function definition
  54. using %feature("autodoc", [0,1]).
  55. """
  56. f = my_open_read(src)
  57. self.my_dir = os.path.dirname(f.name)
  58. self.xmldoc = minidom.parse(f).documentElement
  59. f.close()
  60. self.pieces = []
  61. self.pieces.append('\n// File: %s\n' %
  62. os.path.basename(f.name))
  63. self.space_re = re.compile(r'\s+')
  64. self.lead_spc = re.compile(r'^(%feature\S+\s+\S+\s*?)"\s+(\S)')
  65. self.multi = 0
  66. self.ignores = ['inheritancegraph', 'param', 'listofallmembers',
  67. 'innerclass', 'name', 'declname', 'incdepgraph',
  68. 'invincdepgraph', 'programlisting', 'type',
  69. 'references', 'referencedby', 'location',
  70. 'collaborationgraph', 'reimplements',
  71. 'reimplementedby', 'derivedcompoundref',
  72. 'basecompoundref']
  73. #self.generics = []
  74. self.include_function_definition = include_function_definition
  75. if not include_function_definition:
  76. self.ignores.append('argsstring')
  77. self.quiet = quiet
  78. def generate(self):
  79. """Parses the file set in the initialization. The resulting
  80. data is stored in `self.pieces`.
  81. """
  82. self.parse(self.xmldoc)
  83. def parse(self, node):
  84. """Parse a given node. This function in turn calls the
  85. `parse_<nodeType>` functions which handle the respective
  86. nodes.
  87. """
  88. pm = getattr(self, "parse_%s" % node.__class__.__name__)
  89. pm(node)
  90. def parse_Document(self, node):
  91. self.parse(node.documentElement)
  92. def parse_Text(self, node):
  93. txt = node.data
  94. txt = txt.replace('\\', r'\\\\')
  95. txt = txt.replace('"', r'\"')
  96. # ignore pure whitespace
  97. m = self.space_re.match(txt)
  98. if m and len(m.group()) == len(txt):
  99. pass
  100. else:
  101. self.add_text(textwrap.fill(txt, break_long_words=False))
  102. def parse_Element(self, node):
  103. """Parse an `ELEMENT_NODE`. This calls specific
  104. `do_<tagName>` handers for different elements. If no handler
  105. is available the `generic_parse` method is called. All
  106. tagNames specified in `self.ignores` are simply ignored.
  107. """
  108. name = node.tagName
  109. ignores = self.ignores
  110. if name in ignores:
  111. return
  112. attr = "do_%s" % name
  113. if hasattr(self, attr):
  114. handlerMethod = getattr(self, attr)
  115. handlerMethod(node)
  116. else:
  117. self.generic_parse(node)
  118. #if name not in self.generics: self.generics.append(name)
  119. def parse_Comment(self, node):
  120. """Parse a `COMMENT_NODE`. This does nothing for now."""
  121. return
  122. def add_text(self, value):
  123. """Adds text corresponding to `value` into `self.pieces`."""
  124. if isinstance(value, (list, tuple)):
  125. self.pieces.extend(value)
  126. else:
  127. self.pieces.append(value)
  128. def get_specific_nodes(self, node, names):
  129. """Given a node and a sequence of strings in `names`, return a
  130. dictionary containing the names as keys and child
  131. `ELEMENT_NODEs`, that have a `tagName` equal to the name.
  132. """
  133. nodes = [(x.tagName, x) for x in node.childNodes
  134. if x.nodeType == x.ELEMENT_NODE and
  135. x.tagName in names]
  136. return dict(nodes)
  137. def generic_parse(self, node, pad=0):
  138. """A Generic parser for arbitrary tags in a node.
  139. Parameters:
  140. - node: A node in the DOM.
  141. - pad: `int` (default: 0)
  142. If 0 the node data is not padded with newlines. If 1 it
  143. appends a newline after parsing the childNodes. If 2 it
  144. pads before and after the nodes are processed. Defaults to
  145. 0.
  146. """
  147. npiece = 0
  148. if pad:
  149. npiece = len(self.pieces)
  150. if pad == 2:
  151. self.add_text('\n')
  152. for n in node.childNodes:
  153. self.parse(n)
  154. if pad:
  155. if len(self.pieces) > npiece:
  156. self.add_text('\n')
  157. def space_parse(self, node):
  158. self.add_text(' ')
  159. self.generic_parse(node)
  160. do_ref = space_parse
  161. do_emphasis = space_parse
  162. do_bold = space_parse
  163. do_computeroutput = space_parse
  164. do_formula = space_parse
  165. def do_compoundname(self, node):
  166. self.add_text('\n\n')
  167. data = node.firstChild.data
  168. self.add_text('%%feature("docstring") %s "\n' % data)
  169. def do_compounddef(self, node):
  170. kind = node.attributes['kind'].value
  171. if kind in ('class', 'struct'):
  172. prot = node.attributes['prot'].value
  173. if prot != 'public':
  174. return
  175. names = ('compoundname', 'briefdescription',
  176. 'detaileddescription', 'includes')
  177. first = self.get_specific_nodes(node, names)
  178. for n in names:
  179. if first.has_key(n):
  180. self.parse(first[n])
  181. self.add_text(['";', '\n'])
  182. for n in node.childNodes:
  183. if n not in first.values():
  184. self.parse(n)
  185. elif kind in ('file', 'namespace'):
  186. nodes = node.getElementsByTagName('sectiondef')
  187. for n in nodes:
  188. self.parse(n)
  189. def do_includes(self, node):
  190. self.add_text('C++ includes: ')
  191. self.generic_parse(node, pad=1)
  192. def do_parameterlist(self, node):
  193. text = 'unknown'
  194. for key, val in node.attributes.items():
  195. if key == 'kind':
  196. if val == 'param':
  197. text = 'Parameters'
  198. elif val == 'exception':
  199. text = 'Exceptions'
  200. elif val == 'retval':
  201. text = 'Returns'
  202. else:
  203. text = val
  204. break
  205. self.add_text(['\n', '\n', text, ':', '\n'])
  206. self.generic_parse(node, pad=1)
  207. def do_para(self, node):
  208. self.add_text('\n')
  209. self.generic_parse(node, pad=1)
  210. def do_parametername(self, node):
  211. self.add_text('\n')
  212. try:
  213. data = node.firstChild.data
  214. except AttributeError: # perhaps a <ref> tag in it
  215. data = node.firstChild.firstChild.data
  216. if data.find('Exception') != -1:
  217. self.add_text(data)
  218. else:
  219. self.add_text("%s: " % data)
  220. def do_parameterdefinition(self, node):
  221. self.generic_parse(node, pad=1)
  222. def do_detaileddescription(self, node):
  223. self.generic_parse(node, pad=1)
  224. def do_briefdescription(self, node):
  225. self.generic_parse(node, pad=1)
  226. def do_memberdef(self, node):
  227. prot = node.attributes['prot'].value
  228. id = node.attributes['id'].value
  229. kind = node.attributes['kind'].value
  230. tmp = node.parentNode.parentNode.parentNode
  231. compdef = tmp.getElementsByTagName('compounddef')[0]
  232. cdef_kind = compdef.attributes['kind'].value
  233. if prot == 'public':
  234. first = self.get_specific_nodes(node, ('definition', 'name'))
  235. name = first['name'].firstChild.data
  236. if name[:8] == 'operator': # Don't handle operators yet.
  237. return
  238. if not 'definition' in first or \
  239. kind in ['variable', 'typedef']:
  240. return
  241. if self.include_function_definition:
  242. defn = first['definition'].firstChild.data
  243. else:
  244. defn = ""
  245. self.add_text('\n')
  246. self.add_text('%feature("docstring") ')
  247. anc = node.parentNode.parentNode
  248. if cdef_kind in ('file', 'namespace'):
  249. ns_node = anc.getElementsByTagName('innernamespace')
  250. if not ns_node and cdef_kind == 'namespace':
  251. ns_node = anc.getElementsByTagName('compoundname')
  252. if ns_node:
  253. ns = ns_node[0].firstChild.data
  254. self.add_text(' %s::%s "\n%s' % (ns, name, defn))
  255. else:
  256. self.add_text(' %s "\n%s' % (name, defn))
  257. elif cdef_kind in ('class', 'struct'):
  258. # Get the full function name.
  259. anc_node = anc.getElementsByTagName('compoundname')
  260. cname = anc_node[0].firstChild.data
  261. self.add_text(' %s::%s "\n%s' % (cname, name, defn))
  262. for n in node.childNodes:
  263. if n not in first.values():
  264. self.parse(n)
  265. self.add_text(['";', '\n'])
  266. def do_definition(self, node):
  267. data = node.firstChild.data
  268. self.add_text('%s "\n%s' % (data, data))
  269. def do_sectiondef(self, node):
  270. kind = node.attributes['kind'].value
  271. if kind in ('public-func', 'func', 'user-defined', ''):
  272. self.generic_parse(node)
  273. def do_header(self, node):
  274. """For a user defined section def a header field is present
  275. which should not be printed as such, so we comment it in the
  276. output."""
  277. data = node.firstChild.data
  278. self.add_text('\n/*\n %s \n*/\n' % data)
  279. # If our immediate sibling is a 'description' node then we
  280. # should comment that out also and remove it from the parent
  281. # node's children.
  282. parent = node.parentNode
  283. idx = parent.childNodes.index(node)
  284. if len(parent.childNodes) >= idx + 2:
  285. nd = parent.childNodes[idx + 2]
  286. if nd.nodeName == 'description':
  287. nd = parent.removeChild(nd)
  288. self.add_text('\n/*')
  289. self.generic_parse(nd)
  290. self.add_text('\n*/\n')
  291. def do_simplesect(self, node):
  292. kind = node.attributes['kind'].value
  293. if kind in ('date', 'rcs', 'version'):
  294. pass
  295. elif kind == 'warning':
  296. self.add_text(['\n', 'WARNING: '])
  297. self.generic_parse(node)
  298. elif kind == 'see':
  299. self.add_text('\n')
  300. self.add_text('See: ')
  301. self.generic_parse(node)
  302. else:
  303. self.generic_parse(node)
  304. def do_argsstring(self, node):
  305. self.generic_parse(node, pad=1)
  306. def do_member(self, node):
  307. kind = node.attributes['kind'].value
  308. refid = node.attributes['refid'].value
  309. if kind == 'function' and refid[:9] == 'namespace':
  310. self.generic_parse(node)
  311. def do_doxygenindex(self, node):
  312. self.multi = 1
  313. comps = node.getElementsByTagName('compound')
  314. for c in comps:
  315. refid = c.attributes['refid'].value
  316. fname = refid + '.xml'
  317. if not os.path.exists(fname):
  318. fname = os.path.join(self.my_dir, fname)
  319. if not self.quiet:
  320. print("parsing file: %s" % fname)
  321. p = Doxy2SWIG(fname, self.include_function_definition, self.quiet)
  322. p.generate()
  323. self.pieces.extend(self.clean_pieces(p.pieces))
  324. def write(self, fname):
  325. o = my_open_write(fname)
  326. if self.multi:
  327. o.write("".join(x.encode('utf-8') for x in self.pieces))
  328. else:
  329. o.write("".join(self.clean_pieces(self.pieces)))
  330. o.close()
  331. def clean_pieces(self, pieces):
  332. """Cleans the list of strings given as `pieces`. It replaces
  333. multiple newlines by a maximum of 2 and returns a new list.
  334. It also wraps the paragraphs nicely.
  335. """
  336. ret = []
  337. count = 0
  338. for i in pieces:
  339. if i == '\n':
  340. count = count + 1
  341. else:
  342. if i == '";':
  343. if count:
  344. ret.append('\n')
  345. elif count > 2:
  346. ret.append('\n\n')
  347. elif count:
  348. ret.append('\n' * count)
  349. count = 0
  350. ret.append(i)
  351. _data = "".join(ret)
  352. ret = []
  353. for i in _data.split('\n\n'):
  354. if i == 'Parameters:' or i == 'Exceptions:' or i == 'Returns:':
  355. ret.extend([i, '\n' + '-' * len(i), '\n\n'])
  356. elif i.find('// File:') > -1: # leave comments alone.
  357. ret.extend([i, '\n'])
  358. else:
  359. _tmp = textwrap.fill(i.strip(), break_long_words=False)
  360. _tmp = self.lead_spc.sub(r'\1"\2', _tmp)
  361. ret.extend([_tmp, '\n\n'])
  362. return ret
  363. def convert(input, output, include_function_definition=True, quiet=False):
  364. p = Doxy2SWIG(input, include_function_definition, quiet)
  365. p.generate()
  366. p.write(output)
  367. def main():
  368. usage = __doc__
  369. parser = optparse.OptionParser(usage)
  370. parser.add_option("-n", '--no-function-definition',
  371. action='store_true',
  372. default=False,
  373. dest='func_def',
  374. help='do not include doxygen function definitions')
  375. parser.add_option("-q", '--quiet',
  376. action='store_true',
  377. default=False,
  378. dest='quiet',
  379. help='be quiet and minimize output')
  380. options, args = parser.parse_args()
  381. if len(args) != 2:
  382. parser.error("error: no input and output specified")
  383. convert(args[0], args[1], not options.func_def, options.quiet)
  384. if __name__ == '__main__':
  385. main()