2 """Doxygen XML to SWIG docstring converter.
6 doxy2swig.py [options] input.xml output.i
8 Converts Doxygen generated XML files into a file containing docstrings
9 that can be used by SWIG-1.3.x. Note that you need to get SWIG
10 version > 1.3.23 or use Robin Dunn's docstring patch to be able to use
13 input.xml is your doxygen generated XML file and output.i is where the
14 output will be written (the file will be clobbered).
17 ######################################################################
19 # This code is implemented using Mark Pilgrim's code as a guideline:
20 # http://www.faqs.org/docs/diveintopython/kgp_divein.html
22 # Author: Prabhu Ramachandran
26 # Johan Hake: the include_function_definition feature
27 # Bill Spotz: bug reports and testing.
28 # Sebastian Henschel: Misc. enhancements.
30 ######################################################################
32 from xml.dom import minidom
40 def my_open_read(source):
41 if hasattr(source, "read"):
46 def my_open_write(dest):
47 if hasattr(dest, "write"):
50 return open(dest, 'w')
54 """Converts Doxygen generated XML files into a file containing
55 docstrings that can be used by SWIG-1.3.x that have support for
56 feature("docstring"). Once the data is parsed it is stored in
61 def __init__(self, src, include_function_definition=True, quiet=False):
62 """Initialize the instance given a source object. `src` can
63 be a file or filename. If you do not want to include function
64 definitions from doxygen then set
65 `include_function_definition` to `False`. This is handy since
66 this allows you to use the swig generated function definition
67 using %feature("autodoc", [0,1]).
71 self.my_dir = os.path.dirname(f.name)
72 self.xmldoc = minidom.parse(f).documentElement
76 self.pieces.append('\n// File: %s\n'%\
77 os.path.basename(f.name))
79 self.space_re = re.compile(r'\s+')
80 self.lead_spc = re.compile(r'^(%feature\S+\s+\S+\s*?)"\s+(\S)')
82 self.ignores = ['inheritancegraph', 'param', 'listofallmembers',
83 'innerclass', 'name', 'declname', 'incdepgraph',
84 'invincdepgraph', 'programlisting', 'type',
85 'references', 'referencedby', 'location',
86 'collaborationgraph', 'reimplements',
87 'reimplementedby', 'derivedcompoundref',
90 self.include_function_definition = include_function_definition
91 if not include_function_definition:
92 self.ignores.append('argsstring')
98 """Parses the file set in the initialization. The resulting
99 data is stored in `self.pieces`.
102 self.parse(self.xmldoc)
104 def parse(self, node):
105 """Parse a given node. This function in turn calls the
106 `parse_<nodeType>` functions which handle the respective
110 pm = getattr(self, "parse_%s"%node.__class__.__name__)
113 def parse_Document(self, node):
114 self.parse(node.documentElement)
116 def parse_Text(self, node):
118 txt = txt.replace('\\', r'\\\\')
119 txt = txt.replace('"', r'\"')
120 # ignore pure whitespace
121 m = self.space_re.match(txt)
122 if m and len(m.group()) == len(txt):
125 self.add_text(textwrap.fill(txt, break_long_words=False))
127 def parse_Element(self, node):
128 """Parse an `ELEMENT_NODE`. This calls specific
129 `do_<tagName>` handers for different elements. If no handler
130 is available the `generic_parse` method is called. All
131 tagNames specified in `self.ignores` are simply ignored.
135 ignores = self.ignores
138 attr = "do_%s" % name
139 if hasattr(self, attr):
140 handlerMethod = getattr(self, attr)
143 self.generic_parse(node)
144 #if name not in self.generics: self.generics.append(name)
146 def parse_Comment(self, node):
147 """Parse a `COMMENT_NODE`. This does nothing for now."""
150 def add_text(self, value):
151 """Adds text corresponding to `value` into `self.pieces`."""
152 if isinstance(value, (list, tuple)):
153 self.pieces.extend(value)
155 self.pieces.append(value)
157 def get_specific_nodes(self, node, names):
158 """Given a node and a sequence of strings in `names`, return a
159 dictionary containing the names as keys and child
160 `ELEMENT_NODEs`, that have a `tagName` equal to the name.
163 nodes = [(x.tagName, x) for x in node.childNodes \
164 if x.nodeType == x.ELEMENT_NODE and \
168 def generic_parse(self, node, pad=0):
169 """A Generic parser for arbitrary tags in a node.
173 - node: A node in the DOM.
174 - pad: `int` (default: 0)
176 If 0 the node data is not padded with newlines. If 1 it
177 appends a newline after parsing the childNodes. If 2 it
178 pads before and after the nodes are processed. Defaults to
184 npiece = len(self.pieces)
187 for n in node.childNodes:
190 if len(self.pieces) > npiece:
193 def space_parse(self, node):
195 self.generic_parse(node)
198 do_emphasis = space_parse
199 do_bold = space_parse
200 do_computeroutput = space_parse
201 do_formula = space_parse
203 def do_compoundname(self, node):
204 self.add_text('\n\n')
205 data = node.firstChild.data
206 self.add_text('%%feature("docstring") %s "\n'%data)
208 def do_compounddef(self, node):
209 kind = node.attributes['kind'].value
210 if kind in ('class', 'struct'):
211 prot = node.attributes['prot'].value
214 names = ('compoundname', 'briefdescription',
215 'detaileddescription', 'includes')
216 first = self.get_specific_nodes(node, names)
220 self.add_text(['";','\n'])
221 for n in node.childNodes:
222 if n not in first.values():
224 elif kind in ('file', 'namespace'):
225 nodes = node.getElementsByTagName('sectiondef')
229 def do_includes(self, node):
230 self.add_text('C++ includes: ')
231 self.generic_parse(node, pad=1)
233 def do_parameterlist(self, node):
235 for key, val in node.attributes.items():
237 if val == 'param': text = 'Parameters'
238 elif val == 'exception': text = 'Exceptions'
239 elif val == 'retval': text = 'Returns'
242 self.add_text(['\n', '\n', text, ':', '\n'])
243 self.generic_parse(node, pad=1)
245 def do_para(self, node):
247 self.generic_parse(node, pad=1)
249 def do_parametername(self, node):
252 data=node.firstChild.data
253 except AttributeError: # perhaps a <ref> tag in it
254 data=node.firstChild.firstChild.data
255 if data.find('Exception') != -1:
258 self.add_text("%s: "%data)
260 def do_parameterdefinition(self, node):
261 self.generic_parse(node, pad=1)
263 def do_detaileddescription(self, node):
264 self.generic_parse(node, pad=1)
266 def do_briefdescription(self, node):
267 self.generic_parse(node, pad=1)
269 def do_memberdef(self, node):
270 prot = node.attributes['prot'].value
271 id = node.attributes['id'].value
272 kind = node.attributes['kind'].value
273 tmp = node.parentNode.parentNode.parentNode
274 compdef = tmp.getElementsByTagName('compounddef')[0]
275 cdef_kind = compdef.attributes['kind'].value
278 first = self.get_specific_nodes(node, ('definition', 'name'))
279 name = first['name'].firstChild.data
280 if name[:8] == 'operator': # Don't handle operators yet.
283 if not 'definition' in first or \
284 kind in ['variable', 'typedef']:
287 if self.include_function_definition:
288 defn = first['definition'].firstChild.data
292 self.add_text('%feature("docstring") ')
294 anc = node.parentNode.parentNode
295 if cdef_kind in ('file', 'namespace'):
296 ns_node = anc.getElementsByTagName('innernamespace')
297 if not ns_node and cdef_kind == 'namespace':
298 ns_node = anc.getElementsByTagName('compoundname')
300 ns = ns_node[0].firstChild.data
301 self.add_text(' %s::%s "\n%s'%(ns, name, defn))
303 self.add_text(' %s "\n%s'%(name, defn))
304 elif cdef_kind in ('class', 'struct'):
305 # Get the full function name.
306 anc_node = anc.getElementsByTagName('compoundname')
307 cname = anc_node[0].firstChild.data
308 self.add_text(' %s::%s "\n%s'%(cname, name, defn))
310 for n in node.childNodes:
311 if n not in first.values():
313 self.add_text(['";', '\n'])
315 def do_definition(self, node):
316 data = node.firstChild.data
317 self.add_text('%s "\n%s'%(data, data))
319 def do_sectiondef(self, node):
320 kind = node.attributes['kind'].value
321 if kind in ('public-func', 'func', 'user-defined', ''):
322 self.generic_parse(node)
324 def do_header(self, node):
325 """For a user defined section def a header field is present
326 which should not be printed as such, so we comment it in the
328 data = node.firstChild.data
329 self.add_text('\n/*\n %s \n*/\n'%data)
330 # If our immediate sibling is a 'description' node then we
331 # should comment that out also and remove it from the parent
333 parent = node.parentNode
334 idx = parent.childNodes.index(node)
335 if len(parent.childNodes) >= idx + 2:
336 nd = parent.childNodes[idx+2]
337 if nd.nodeName == 'description':
338 nd = parent.removeChild(nd)
339 self.add_text('\n/*')
340 self.generic_parse(nd)
341 self.add_text('\n*/\n')
343 def do_simplesect(self, node):
344 kind = node.attributes['kind'].value
345 if kind in ('date', 'rcs', 'version'):
347 elif kind == 'warning':
348 self.add_text(['\n', 'WARNING: '])
349 self.generic_parse(node)
352 self.add_text('See: ')
353 self.generic_parse(node)
355 self.generic_parse(node)
357 def do_argsstring(self, node):
358 self.generic_parse(node, pad=1)
360 def do_member(self, node):
361 kind = node.attributes['kind'].value
362 refid = node.attributes['refid'].value
363 if kind == 'function' and refid[:9] == 'namespace':
364 self.generic_parse(node)
366 def do_doxygenindex(self, node):
368 comps = node.getElementsByTagName('compound')
370 refid = c.attributes['refid'].value
371 fname = refid + '.xml'
372 if not os.path.exists(fname):
373 fname = os.path.join(self.my_dir, fname)
375 print( "parsing file: %s"%fname )
376 p = Doxy2SWIG(fname, self.include_function_definition, self.quiet)
378 self.pieces.extend(self.clean_pieces(p.pieces))
380 def write(self, fname):
381 o = my_open_write(fname)
383 o.write("".join(self.pieces))
385 o.write("".join(self.clean_pieces(self.pieces)))
388 def clean_pieces(self, pieces):
389 """Cleans the list of strings given as `pieces`. It replaces
390 multiple newlines by a maximum of 2 and returns a new list.
391 It also wraps the paragraphs nicely.
406 ret.append('\n'*count)
412 for i in _data.split('\n\n'):
413 if i == 'Parameters:' or i == 'Exceptions:' or i == 'Returns:':
414 ret.extend([i, '\n'+'-'*len(i), '\n\n'])
415 elif i.find('// File:') > -1: # leave comments alone.
416 ret.extend([i, '\n'])
418 _tmp = textwrap.fill(i.strip(), break_long_words=False)
419 _tmp = self.lead_spc.sub(r'\1"\2', _tmp)
420 ret.extend([_tmp, '\n\n'])
424 def convert(input, output, include_function_definition=True, quiet=False):
425 p = Doxy2SWIG(input, include_function_definition, quiet)
431 parser = optparse.OptionParser(usage)
432 parser.add_option("-n", '--no-function-definition',
436 help='do not include doxygen function definitions')
437 parser.add_option("-q", '--quiet',
441 help='be quiet and minimize output')
443 options, args = parser.parse_args()
445 parser.error("error: no input and output specified")
447 convert(args[0], args[1], not options.func_def, options.quiet)
450 if __name__ == '__main__':