Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
Meld autodoxy into a single file for sake of import simplicity
[simgrid.git] / docs / source / _ext / autodoxy.py
1 """
2 This is autodoxy, a sphinx extension providing autodoc-like directives
3 that are feed with Doxygen files.
4
5 It is highly inspired from the autodoc_doxygen sphinx extension, but
6 adapted to my own needs in SimGrid.
7 https://github.com/rmcgibbo/sphinxcontrib-autodoc_doxygen
8
9 Licence: MIT
10 Copyright (c) 2015 Robert T. McGibbon
11 Copyright (c) 2019 Martin Quinson
12 """
13 from __future__ import print_function, absolute_import, division
14
15 import os.path
16 import re
17 import sys
18
19 from six import itervalues
20 from lxml import etree as ET
21 from sphinx.ext.autodoc import Documenter, AutoDirective, members_option, ALL
22 from sphinx.errors import ExtensionError
23 from sphinx.util import logging
24
25
26 ##########################################################################
27 # XML utils
28 ##########################################################################
29 def format_xml_paragraph(xmlnode):
30     """Format an Doxygen XML segment (principally a detaileddescription)
31     as a paragraph for inclusion in the rst document
32
33     Parameters
34     ----------
35     xmlnode
36
37     Returns
38     -------
39     lines
40         A list of lines.
41     """
42     return [l.rstrip() for l in _DoxygenXmlParagraphFormatter().generic_visit(xmlnode).lines]
43
44
45 class _DoxygenXmlParagraphFormatter(object):
46     # This class follows the model of the stdlib's ast.NodeVisitor for tree traversal
47     # where you dispatch on the element type to a different method for each node
48     # during the traverse.
49
50     # It's supposed to handle paragraphs, references, preformatted text (code blocks), and lists.
51
52     def __init__(self):
53         self.lines = ['']
54         self.continue_line = False
55
56     def visit(self, node):
57         method = 'visit_' + node.tag
58         visitor = getattr(self, method, self.generic_visit)
59         return visitor(node)
60
61     def generic_visit(self, node):
62         for child in node.getchildren():
63             self.visit(child)
64         return self
65
66     def visit_ref(self, node):
67         ref = get_doxygen_root().findall('.//*[@id="%s"]' % node.get('refid'))
68         if ref:
69             ref = ref[0]
70             if ref.tag == 'memberdef':
71                 parent = ref.xpath('./ancestor::compounddef/compoundname')[0].text
72                 name = ref.find('./name').text
73                 real_name = parent + '::' + name
74             elif ref.tag in ('compounddef', 'enumvalue'):
75                 name_node = ref.find('./name')
76                 real_name = name_node.text if name_node is not None else ''
77             else:
78                 raise NotImplementedError(ref.tag)
79         else:
80             real_name = None
81
82         val = [':cpp:any:`', node.text]
83         if real_name:
84             val.extend((' <', real_name, '>`'))
85         else:
86             val.append('`')
87         if node.tail is not None:
88             val.append(node.tail)
89
90         self.lines[-1] += ''.join(val)
91
92     def visit_para(self, node):
93         if node.text is not None:
94             if self.continue_line:
95                 self.lines[-1] += node.text
96             else:
97                 self.lines.append(node.text)
98         self.generic_visit(node)
99         self.lines.append('')
100         self.continue_line = False
101
102     def visit_verbatim(self, node):
103         if node.text is not None:
104             # remove the leading ' *' of any lines
105             lines = [re.sub('^\s*\*','', l) for l in node.text.split('\n')]
106             # Merge each paragraph together
107             text = re.sub("\n\n", "PaRaGrraphSplit", '\n'.join(lines))
108             text = re.sub('\n', '', text)
109             lines = text.split('PaRaGrraphSplit')
110
111             # merge content to the built doc
112             if self.continue_line:
113                 self.lines[-1] += lines[0]
114                 lines = lines[1:]
115             for l in lines:
116                 self.lines.append('')
117                 self.lines.append(l)
118         self.generic_visit(node)
119         self.lines.append('')
120         self.continue_line = False
121
122     def visit_parametername(self, node):
123         if 'direction' in node.attrib:
124             direction = '[%s] ' % node.get('direction')
125         else:
126             direction = ''
127
128         self.lines.append('**%s** -- %s' % (
129             node.text, direction))
130         self.continue_line = True
131
132     def visit_parameterlist(self, node):
133         lines = [l for l in type(self)().generic_visit(node).lines if l is not '']
134         self.lines.extend([':parameters:', ''] + ['* %s' % l for l in lines] + [''])
135
136     def visit_simplesect(self, node):
137         if node.get('kind') == 'return':
138             self.lines.append(':returns: ')
139             self.continue_line = True
140         self.generic_visit(node)
141
142     def visit_listitem(self, node):
143         self.lines.append('   - ')
144         self.continue_line = True
145         self.generic_visit(node)
146
147     def visit_preformatted(self, node):
148         segment = [node.text if node.text is not None else '']
149         for n in node.getchildren():
150             segment.append(n.text)
151             if n.tail is not None:
152                 segment.append(n.tail)
153
154         lines = ''.join(segment).split('\n')
155         self.lines.extend(('.. code-block:: C++', ''))
156         self.lines.extend(['  ' + l for l in lines])
157
158     def visit_computeroutput(self, node):
159         c = node.find('preformatted')
160         if c is not None:
161             return self.visit_preformatted(c)
162         return self.visit_preformatted(node)
163
164     def visit_xrefsect(self, node):
165         if node.find('xreftitle').text == 'Deprecated':
166             sublines = type(self)().generic_visit(node).lines
167             self.lines.extend(['.. admonition:: Deprecated'] + ['   ' + s for s in sublines])
168         else:
169             raise ValueError(node)
170
171     def visit_subscript(self, node):
172         self.lines[-1] += '\ :sub:`%s` %s' % (node.text, node.tail)
173
174
175 ##########################################################################
176 # Directives
177 ##########################################################################
178
179
180 class DoxygenDocumenter(Documenter):
181     # Variables to store the names of the object being documented. modname and fullname are redundant,
182     # and objpath is always the empty list. This is inelegant, but we need to work with the superclass.
183
184     fullname = None  # example: "OpenMM::NonbondedForce" or "OpenMM::NonbondedForce::methodName""
185     modname = None   # example: "OpenMM::NonbondedForce" or "OpenMM::NonbondedForce::methodName""
186     objname = None   # example: "NonbondedForce"  or "methodName"
187     objpath = []     # always the empty list
188     object = None    # the xml node for the object
189
190     option_spec = {
191         'members': members_option,
192     }
193
194     def __init__(self, directive, name, indent=u'', id=None):
195         super(DoxygenDocumenter, self).__init__(directive, name, indent)
196         if id is not None:
197             self.parse_id(id)
198
199     def parse_id(self, id):
200         return False
201
202     def parse_name(self):
203         """Determine what module to import and what attribute to document.
204         Returns True and sets *self.modname*, *self.objname*, *self.fullname*,
205         if parsing and resolving was successful.
206         """
207         # To view the context and order in which all of these methods get called,
208         # See, Documenter.generate(). That's the main "entry point" that first
209         # calls parse_name(), follwed by import_object(), format_signature(),
210         # add_directive_header(), and then add_content() (which calls get_doc())
211
212         # If we were provided a prototype, that must be an overloaded function. Save it.
213         self.argsstring = None
214         if "(" in self.name:
215             (self.name, self.argsstring) = self.name.split('(', 1)
216             self.argsstring = "({}".format(self.argsstring)
217
218         # methods in the superclass sometimes use '.' to join namespace/class
219         # names with method names, and we don't want that.
220         self.name = self.name.replace('.', '::')
221         self.fullname = self.name
222         self.modname = self.fullname
223         self.objpath = []
224
225         if '::' in self.name:
226             parts = self.name.split('::')
227             self.objname = parts[-1]
228         else:
229             self.objname = self.name
230
231         return True
232
233     def add_directive_header(self, sig):
234         """Add the directive header and options to the generated content."""
235         domain = getattr(self, 'domain', 'cpp')
236         directive = getattr(self, 'directivetype', self.objtype)
237         name = self.format_name()
238         sourcename = self.get_sourcename()
239         self.add_line(u'.. %s:%s:: %s%s' % (domain, directive, name, sig),
240                       sourcename)
241
242     def document_members(self, all_members=False):
243         """Generate reST for member documentation.
244         If *all_members* is True, do all members, else those given by
245         *self.options.members*.
246         """
247         want_all = all_members or self.options.inherited_members or \
248             self.options.members is ALL
249         # find out which members are documentable
250         members_check_module, members = self.get_object_members(want_all)
251
252         # remove members given by exclude-members
253         if self.options.exclude_members:
254             members = [(membername, member) for (membername, member) in members
255                        if membername not in self.options.exclude_members]
256
257         # document non-skipped members
258         memberdocumenters = []
259         for (mname, member, isattr) in self.filter_members(members, want_all):
260             classes = [cls for cls in itervalues(AutoDirective._registry)
261                        if cls.can_document_member(member, mname, isattr, self)]
262             if not classes:
263                 # don't know how to document this member
264                 continue
265
266             # prefer the documenter with the highest priority
267             classes.sort(key=lambda cls: cls.priority)
268
269             documenter = classes[-1](self.directive, mname, indent=self.indent, id=member.get('id'))
270             memberdocumenters.append((documenter, isattr))
271
272         for documenter, isattr in memberdocumenters:
273             documenter.generate(
274                 all_members=True, real_modname=self.real_modname,
275                 check_module=members_check_module and not isattr)
276
277         # reset current objects
278         self.env.temp_data['autodoc:module'] = None
279         self.env.temp_data['autodoc:class'] = None
280
281
282 class DoxygenClassDocumenter(DoxygenDocumenter):
283     objtype = 'doxyclass'
284     directivetype = 'class'
285     domain = 'cpp'
286     priority = 100
287
288     option_spec = {
289         'members': members_option,
290     }
291
292     @classmethod
293     def can_document_member(cls, member, membername, isattr, parent):
294         # this method is only called from Documenter.document_members
295         # when a higher level documenter (module or namespace) is trying
296         # to choose the appropriate documenter for each of its lower-level
297         # members. Currently not implemented since we don't have a higher-level
298         # doumenter like a DoxygenNamespaceDocumenter.
299         return False
300
301     def import_object(self):
302         """Import the object and set it as *self.object*.  In the call sequence, this
303         is executed right after parse_name(), so it can use *self.fullname*, *self.objname*,
304         and *self.modname*.
305
306         Returns True if successful, False if an error occurred.
307         """
308         xpath_query = './/compoundname[text()="%s"]/..' % self.fullname
309         match = get_doxygen_root().xpath(xpath_query)
310         if len(match) != 1:
311             raise ExtensionError('[autodoxy] could not find class (fullname="%s"). I tried'
312                                  'the following xpath: "%s"' % (self.fullname, xpath_query))
313
314         self.object = match[0]
315         return True
316
317     def format_signature(self):
318         return ''
319
320     def format_name(self):
321         return self.fullname
322
323     def get_doc(self, encoding):
324         detaileddescription = self.object.find('detaileddescription')
325         doc = [format_xml_paragraph(detaileddescription)]
326         return doc
327
328     def get_object_members(self, want_all):
329         all_members = self.object.xpath('.//sectiondef[@kind="public-func" '
330             'or @kind="public-static-func"]/memberdef[@kind="function"]')
331
332         if want_all:
333             return False, ((m.find('name').text, m) for m in all_members)
334         else:
335             if not self.options.members:
336                 return False, []
337             else:
338                 return False, ((m.find('name').text, m) for m in all_members
339                                if m.find('name').text in self.options.members)
340
341     def filter_members(self, members, want_all):
342         ret = []
343         for (membername, member) in members:
344             ret.append((membername, member, False))
345         return ret
346
347     def document_members(self, all_members=False):
348         super(DoxygenClassDocumenter, self).document_members(all_members=all_members)
349         # Uncomment to view the generated rst for the class.
350         # print('\n'.join(self.directive.result))
351
352 class DoxygenMethodDocumenter(DoxygenDocumenter):
353     objtype = 'doxymethod'
354     directivetype = 'function'
355     domain = 'cpp'
356     priority = 100
357
358     @classmethod
359     def can_document_member(cls, member, membername, isattr, parent):
360         if ET.iselement(member) and member.tag == 'memberdef' and member.get('kind') == 'function':
361             return True
362         return False
363
364     def parse_id(self, id):
365         xp = './/*[@id="%s"]' % id
366         match = get_doxygen_root().xpath(xp)
367         if len(match) > 0:
368             match = match[0]
369             self.fullname = match.find('./definition').text.split()[-1]
370             self.modname = self.fullname
371             self.objname = match.find('./name').text
372             self.object = match
373         return False
374
375     def import_object(self):
376         if ET.iselement(self.object):
377             # self.object already set from DoxygenDocumenter.parse_name(),
378             # caused by passing in the `id` of the node instead of just a
379             # classname or method name
380             return True
381
382         (obj, meth) = self.fullname.rsplit('::', 1)
383
384         xpath_query_noparam = ('.//compoundname[text()="{:s}"]/../sectiondef[@kind="public-func" or @kind="public-static-func"]'
385                                '/memberdef[@kind="function"]/name[text()="{:s}"]/..').format(obj, meth)
386         xpath_query = ""
387 #        print("fullname {}".format(self.fullname))
388         if self.argsstring != None:
389             xpath_query = ('.//compoundname[text()="{:s}"]/../sectiondef[@kind="public-func" or @kind="public-static-func"]'
390                            '/memberdef[@kind="function" and argsstring/text()="{:s}"]/name[text()="{:s}"]/..').format(obj,self.argsstring,meth)
391         else:
392             xpath_query = xpath_query_noparam
393         match = get_doxygen_root().xpath(xpath_query)
394         if len(match) == 0:
395             logger = logging.getLogger(__name__)
396
397             if self.argsstring != None:
398                 candidates = get_doxygen_root().xpath(xpath_query_noparam)
399                 if len(candidates) == 1:
400                     logger.warning("[autodoxy] Using method '{}::{}{}' instead of '{}::{}{}'. You may want to drop your specification of the signature, or to fix it."
401                                    .format(obj, meth, candidates[0].find('argsstring').text, obj, meth, self.argsstring))
402                     self.object = candidates[0]
403                     return True
404                 logger.warning("[autodoxy] WARNING: Could not find method {}::{}{}".format(obj, meth, self.argsstring))
405                 for cand in candidates:
406                     logger.warning("[autodoxy] WARNING:   Existing candidate: {}::{}{}".format(obj, meth, cand.find('argsstring').text))
407             else:
408                 logger.warning("[autodoxy] WARNING: could not find method {}::{} in Doxygen files".format(obj, meth))
409             return False
410         self.object = match[0]
411         return True
412
413     def get_doc(self, encoding):
414         detaileddescription = self.object.find('detaileddescription')
415         doc = [format_xml_paragraph(detaileddescription)]
416         return doc
417
418     def format_name(self):
419         def text(el):
420             if el.text is not None:
421                 return el.text
422             return ''
423
424         def tail(el):
425             if el.tail is not None:
426                 return el.tail
427             return ''
428
429         rtype_el = self.object.find('type')
430         rtype_el_ref = rtype_el.find('ref')
431         if rtype_el_ref is not None:
432             rtype = text(rtype_el) + text(rtype_el_ref) + tail(rtype_el_ref)
433         else:
434             rtype = rtype_el.text
435
436  #       print("rtype: {}".format(rtype))
437         signame = (rtype and (rtype + ' ') or '') + self.objname
438         return self.format_template_name() + signame
439
440     def format_template_name(self):
441         types = [e.text for e in self.object.findall('templateparamlist/param/type')]
442         if len(types) == 0:
443             return ''
444         ret = 'template <%s>' % ','.join(types)
445 #        print ("template: {}".format(ret))
446         return ret
447
448     def format_signature(self):
449         args = self.object.find('argsstring').text
450         return args
451
452     def document_members(self, all_members=False):
453         pass
454
455 class DoxygenVariableDocumenter(DoxygenDocumenter):
456     objtype = 'doxyvar'
457     directivetype = 'var'
458     domain = 'cpp'
459     priority = 100
460
461     @classmethod
462     def can_document_member(cls, member, membername, isattr, parent):
463         if ET.iselement(member) and member.tag == 'memberdef' and member.get('kind') == 'variable':
464             return True
465         return False
466
467     def import_object(self):
468         if ET.iselement(self.object):
469             # self.object already set from DoxygenDocumenter.parse_name(),
470             # caused by passing in the `id` of the node instead of just a
471             # classname or method name
472             return True
473
474         (obj, var) = self.fullname.rsplit('::', 1)
475
476         xpath_query = ('.//compoundname[text()="{:s}"]/../sectiondef[@kind="public-attrib" or @kind="public-static-attrib"]'
477                        '/memberdef[@kind="variable"]/name[text()="{:s}"]/..').format(obj, var)
478 #        print("fullname {}".format(self.fullname))
479         match = get_doxygen_root().xpath(xpath_query)
480         if len(match) == 0:
481             logger = logging.getLogger(__name__)
482
483             logger.warning("[autodoxy] WARNING: could not find variable {}::{} in Doxygen files".format(obj, var))
484             return False
485         self.object = match[0]
486         return True
487
488     def parse_id(self, id):
489         xp = './/*[@id="%s"]' % id
490         match = get_doxygen_root().xpath(xp)
491         if len(match) > 0:
492             match = match[0]
493             self.fullname = match.find('./definition').text.split()[-1]
494             self.modname = self.fullname
495             self.objname = match.find('./name').text
496             self.object = match
497         return False
498
499     def format_name(self):
500         def text(el):
501             if el.text is not None:
502                 return el.text
503             return ''
504
505         def tail(el):
506             if el.tail is not None:
507                 return el.tail
508             return ''
509
510         rtype_el = self.object.find('type')
511         rtype_el_ref = rtype_el.find('ref')
512         if rtype_el_ref is not None:
513             rtype = text(rtype_el) + text(rtype_el_ref) + tail(rtype_el_ref)
514         else:
515             rtype = rtype_el.text
516
517  #       print("rtype: {}".format(rtype))
518         signame = (rtype and (rtype + ' ') or '') + self.objname
519         return self.format_template_name() + signame
520
521     def get_doc(self, encoding):
522         detaileddescription = self.object.find('detaileddescription')
523         doc = [format_xml_paragraph(detaileddescription)]
524         return doc
525
526     def format_template_name(self):
527         types = [e.text for e in self.object.findall('templateparamlist/param/type')]
528         if len(types) == 0:
529             return ''
530         ret = 'template <%s>' % ','.join(types)
531 #        print ("template: {}".format(ret))
532         return ret
533
534     def document_members(self, all_members=False):
535         pass
536
537
538 ##########################################################################
539 # Setup the extension
540 ##########################################################################
541 def set_doxygen_xml(app):
542     """Load all doxygen XML files from the app config variable
543     `app.config.doxygen_xml` which should be a path to a directory
544     containing doxygen xml output
545     """
546     err = ExtensionError(
547         '[autodoxy] No doxygen xml output found in doxygen_xml="%s"' % app.config.doxygen_xml)
548
549     if not os.path.isdir(app.config.doxygen_xml):
550         raise err
551
552     files = [os.path.join(app.config.doxygen_xml, f)
553              for f in os.listdir(app.config.doxygen_xml)
554              if f.lower().endswith('.xml') and not f.startswith('._')]
555     if len(files) == 0:
556         raise err
557
558     setup.DOXYGEN_ROOT = ET.ElementTree(ET.Element('root')).getroot()
559     for file in files:
560         root = ET.parse(file).getroot()
561         for node in root:
562             setup.DOXYGEN_ROOT.append(node)
563
564
565 def get_doxygen_root():
566     """Get the root element of the doxygen XML document.
567     """
568     if not hasattr(setup, 'DOXYGEN_ROOT'):
569         setup.DOXYGEN_ROOT = ET.Element("root")  # dummy
570     return setup.DOXYGEN_ROOT
571
572
573 def setup(app):
574     import sphinx.ext.autosummary
575
576     app.connect("builder-inited", set_doxygen_xml)
577 #    app.connect("builder-inited", process_generate_options)
578
579     app.setup_extension('sphinx.ext.autodoc')
580 #    app.setup_extension('sphinx.ext.autosummary')
581
582     app.add_autodocumenter(DoxygenClassDocumenter)
583     app.add_autodocumenter(DoxygenMethodDocumenter)
584     app.add_autodocumenter(DoxygenVariableDocumenter)
585     app.add_config_value("doxygen_xml", "", True)
586
587 #    app.add_directive('autodoxysummary', DoxygenAutosummary)
588 #    app.add_directive('autodoxyenum', DoxygenAutoEnum)
589
590     return {'version': sphinx.__display_version__, 'parallel_read_safe': True}