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
41 def my_open_read(source):
42 if hasattr(source, "read"):
47 def my_open_write(dest):
48 if hasattr(dest, "write"):
51 return open(dest, 'w')
55 """Converts Doxygen generated XML files into a file containing
56 docstrings that can be used by SWIG-1.3.x that have support for
57 feature("docstring"). Once the data is parsed it is stored in
62 def __init__(self, src, include_function_definition=True, quiet=False):
63 """Initialize the instance given a source object. `src` can
64 be a file or filename. If you do not want to include function
65 definitions from doxygen then set
66 `include_function_definition` to `False`. This is handy since
67 this allows you to use the swig generated function definition
68 using %feature("autodoc", [0,1]).
72 self.my_dir = os.path.dirname(f.name)
73 self.xmldoc = minidom.parse(f).documentElement
77 self.pieces.append('\n// File: %s\n'%\
78 os.path.basename(f.name))
80 self.space_re = re.compile(r'\s+')
81 self.lead_spc = re.compile(r'^(%feature\S+\s+\S+\s*?)"\s+(\S)')
83 self.ignores = ['inheritancegraph', 'param', 'listofallmembers',
84 'innerclass', 'name', 'declname', 'incdepgraph',
85 'invincdepgraph', 'programlisting', 'type',
86 'references', 'referencedby', 'location',
87 'collaborationgraph', 'reimplements',
88 'reimplementedby', 'derivedcompoundref',
91 self.include_function_definition = include_function_definition
92 if not include_function_definition:
93 self.ignores.append('argsstring')
99 """Parses the file set in the initialization. The resulting
100 data is stored in `self.pieces`.
103 self.parse(self.xmldoc)
105 def parse(self, node):
106 """Parse a given node. This function in turn calls the
107 `parse_<nodeType>` functions which handle the respective
111 pm = getattr(self, "parse_%s"%node.__class__.__name__)
114 def parse_Document(self, node):
115 self.parse(node.documentElement)
117 def parse_Text(self, node):
119 txt = txt.replace('\\', r'\\\\')
120 txt = txt.replace('"', r'\"')
121 # ignore pure whitespace
122 m = self.space_re.match(txt)
123 if m and len(m.group()) == len(txt):
126 self.add_text(textwrap.fill(txt, break_long_words=False))
128 def parse_Element(self, node):
129 """Parse an `ELEMENT_NODE`. This calls specific
130 `do_<tagName>` handers for different elements. If no handler
131 is available the `generic_parse` method is called. All
132 tagNames specified in `self.ignores` are simply ignored.
136 ignores = self.ignores
139 attr = "do_%s" % name
140 if hasattr(self, attr):
141 handlerMethod = getattr(self, attr)
144 self.generic_parse(node)
145 #if name not in self.generics: self.generics.append(name)
147 def parse_Comment(self, node):
148 """Parse a `COMMENT_NODE`. This does nothing for now."""
151 def add_text(self, value):
152 """Adds text corresponding to `value` into `self.pieces`."""
153 if type(value) in (types.ListType, types.TupleType):
154 self.pieces.extend(value)
156 self.pieces.append(value)
158 def get_specific_nodes(self, node, names):
159 """Given a node and a sequence of strings in `names`, return a
160 dictionary containing the names as keys and child
161 `ELEMENT_NODEs`, that have a `tagName` equal to the name.
164 nodes = [(x.tagName, x) for x in node.childNodes \
165 if x.nodeType == x.ELEMENT_NODE and \
169 def generic_parse(self, node, pad=0):
170 """A Generic parser for arbitrary tags in a node.
174 - node: A node in the DOM.
175 - pad: `int` (default: 0)
177 If 0 the node data is not padded with newlines. If 1 it
178 appends a newline after parsing the childNodes. If 2 it
179 pads before and after the nodes are processed. Defaults to
185 npiece = len(self.pieces)
188 for n in node.childNodes:
191 if len(self.pieces) > npiece:
194 def space_parse(self, node):
196 self.generic_parse(node)
199 do_emphasis = space_parse
200 do_bold = space_parse
201 do_computeroutput = space_parse
202 do_formula = space_parse
204 def do_compoundname(self, node):
205 self.add_text('\n\n')
206 data = node.firstChild.data
207 self.add_text('%%feature("docstring") %s "\n'%data)
209 def do_compounddef(self, node):
210 kind = node.attributes['kind'].value
211 if kind in ('class', 'struct'):
212 prot = node.attributes['prot'].value
215 names = ('compoundname', 'briefdescription',
216 'detaileddescription', 'includes')
217 first = self.get_specific_nodes(node, names)
221 self.add_text(['";','\n'])
222 for n in node.childNodes:
223 if n not in first.values():
225 elif kind in ('file', 'namespace'):
226 nodes = node.getElementsByTagName('sectiondef')
230 def do_includes(self, node):
231 self.add_text('C++ includes: ')
232 self.generic_parse(node, pad=1)
234 def do_parameterlist(self, node):
236 for key, val in node.attributes.items():
238 if val == 'param': text = 'Parameters'
239 elif val == 'exception': text = 'Exceptions'
240 elif val == 'retval': text = 'Returns'
243 self.add_text(['\n', '\n', text, ':', '\n'])
244 self.generic_parse(node, pad=1)
246 def do_para(self, node):
248 self.generic_parse(node, pad=1)
250 def do_parametername(self, node):
253 data=node.firstChild.data
254 except AttributeError: # perhaps a <ref> tag in it
255 data=node.firstChild.firstChild.data
256 if data.find('Exception') != -1:
259 self.add_text("%s: "%data)
261 def do_parameterdefinition(self, node):
262 self.generic_parse(node, pad=1)
264 def do_detaileddescription(self, node):
265 self.generic_parse(node, pad=1)
267 def do_briefdescription(self, node):
268 self.generic_parse(node, pad=1)
270 def do_memberdef(self, node):
271 prot = node.attributes['prot'].value
272 id = node.attributes['id'].value
273 kind = node.attributes['kind'].value
274 tmp = node.parentNode.parentNode.parentNode
275 compdef = tmp.getElementsByTagName('compounddef')[0]
276 cdef_kind = compdef.attributes['kind'].value
279 first = self.get_specific_nodes(node, ('definition', 'name'))
280 name = first['name'].firstChild.data
281 if name[:8] == 'operator': # Don't handle operators yet.
284 if not first.has_key('definition') or \
285 kind in ['variable', 'typedef']:
288 if self.include_function_definition:
289 defn = first['definition'].firstChild.data
293 self.add_text('%feature("docstring") ')
295 anc = node.parentNode.parentNode
296 if cdef_kind in ('file', 'namespace'):
297 ns_node = anc.getElementsByTagName('innernamespace')
298 if not ns_node and cdef_kind == 'namespace':
299 ns_node = anc.getElementsByTagName('compoundname')
301 ns = ns_node[0].firstChild.data
302 self.add_text(' %s::%s "\n%s'%(ns, name, defn))
304 self.add_text(' %s "\n%s'%(name, defn))
305 elif cdef_kind in ('class', 'struct'):
306 # Get the full function name.
307 anc_node = anc.getElementsByTagName('compoundname')
308 cname = anc_node[0].firstChild.data
309 self.add_text(' %s::%s "\n%s'%(cname, name, defn))
311 for n in node.childNodes:
312 if n not in first.values():
314 self.add_text(['";', '\n'])
316 def do_definition(self, node):
317 data = node.firstChild.data
318 self.add_text('%s "\n%s'%(data, data))
320 def do_sectiondef(self, node):
321 kind = node.attributes['kind'].value
322 if kind in ('public-func', 'func', 'user-defined', ''):
323 self.generic_parse(node)
325 def do_header(self, node):
326 """For a user defined section def a header field is present
327 which should not be printed as such, so we comment it in the
329 data = node.firstChild.data
330 self.add_text('\n/*\n %s \n*/\n'%data)
331 # If our immediate sibling is a 'description' node then we
332 # should comment that out also and remove it from the parent
334 parent = node.parentNode
335 idx = parent.childNodes.index(node)
336 if len(parent.childNodes) >= idx + 2:
337 nd = parent.childNodes[idx+2]
338 if nd.nodeName == 'description':
339 nd = parent.removeChild(nd)
340 self.add_text('\n/*')
341 self.generic_parse(nd)
342 self.add_text('\n*/\n')
344 def do_simplesect(self, node):
345 kind = node.attributes['kind'].value
346 if kind in ('date', 'rcs', 'version'):
348 elif kind == 'warning':
349 self.add_text(['\n', 'WARNING: '])
350 self.generic_parse(node)
353 self.add_text('See: ')
354 self.generic_parse(node)
356 self.generic_parse(node)
358 def do_argsstring(self, node):
359 self.generic_parse(node, pad=1)
361 def do_member(self, node):
362 kind = node.attributes['kind'].value
363 refid = node.attributes['refid'].value
364 if kind == 'function' and refid[:9] == 'namespace':
365 self.generic_parse(node)
367 def do_doxygenindex(self, node):
369 comps = node.getElementsByTagName('compound')
371 refid = c.attributes['refid'].value
372 fname = refid + '.xml'
373 if not os.path.exists(fname):
374 fname = os.path.join(self.my_dir, fname)
376 print "parsing file: %s"%fname
377 p = Doxy2SWIG(fname, self.include_function_definition, self.quiet)
379 self.pieces.extend(self.clean_pieces(p.pieces))
381 def write(self, fname):
382 o = my_open_write(fname)
384 o.write("".join(self.pieces))
386 o.write("".join(self.clean_pieces(self.pieces)))
389 def clean_pieces(self, pieces):
390 """Cleans the list of strings given as `pieces`. It replaces
391 multiple newlines by a maximum of 2 and returns a new list.
392 It also wraps the paragraphs nicely.
407 ret.append('\n'*count)
413 for i in _data.split('\n\n'):
414 if i == 'Parameters:' or i == 'Exceptions:' or i == 'Returns:':
415 ret.extend([i, '\n'+'-'*len(i), '\n\n'])
416 elif i.find('// File:') > -1: # leave comments alone.
417 ret.extend([i, '\n'])
419 _tmp = textwrap.fill(i.strip(), break_long_words=False)
420 _tmp = self.lead_spc.sub(r'\1"\2', _tmp)
421 ret.extend([_tmp, '\n\n'])
425 def convert(input, output, include_function_definition=True, quiet=False):
426 p = Doxy2SWIG(input, include_function_definition, quiet)
432 parser = optparse.OptionParser(usage)
433 parser.add_option("-n", '--no-function-definition',
437 help='do not include doxygen function definitions')
438 parser.add_option("-q", '--quiet',
442 help='be quiet and minimize output')
444 options, args = parser.parse_args()
446 parser.error("error: no input and output specified")
448 convert(args[0], args[1], not options.func_def, options.quiet)
451 if __name__ == '__main__':