Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
autodoxy: don't complain if the provided prototype is missing 'const'
[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 != '']
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'', my_id = None):
195         super(DoxygenDocumenter, self).__init__(directive, name, indent)
196         if my_id is not None:
197             self.parse_id(my_id)
198
199     def parse_id(self, id_to_parse):
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.klassname = parts[-2]
228             self.objname = parts[-1]
229         else:
230             self.objname = self.name
231             self.klassname = ""
232
233         return True
234
235     def add_directive_header(self, sig):
236         """Add the directive header and options to the generated content."""
237         domain = getattr(self, 'domain', 'cpp')
238         directive = getattr(self, 'directivetype', self.objtype)
239         name = self.format_name()
240         sourcename = self.get_sourcename()
241         #print('.. %s:%s:: %s%s' % (domain, directive, name, sig))
242         self.add_line(u'.. %s:%s:: %s%s' % (domain, directive, name, sig),
243                       sourcename)
244
245     def document_members(self, all_members=False):
246         """Generate reST for member documentation.
247         If *all_members* is True, do all members, else those given by
248         *self.options.members*.
249         """
250         want_all = all_members or self.options.inherited_members or \
251             self.options.members is ALL
252         # find out which members are documentable
253         members_check_module, members = self.get_object_members(want_all)
254
255         # remove members given by exclude-members
256         if self.options.exclude_members:
257             members = [(membername, member) for (membername, member) in members
258                        if membername not in self.options.exclude_members]
259
260         # document non-skipped members
261         memberdocumenters = []
262         for (mname, member, isattr) in self.filter_members(members, want_all):
263             classes = [cls for cls in itervalues(AutoDirective._registry)
264                        if cls.can_document_member(member, mname, isattr, self)]
265             if not classes:
266                 # don't know how to document this member
267                 continue
268
269             # prefer the documenter with the highest priority
270             classes.sort(key=lambda cls: cls.priority)
271
272             documenter = classes[-1](self.directive, mname, indent=self.indent, id=member.get('id'))
273             memberdocumenters.append((documenter, isattr))
274
275         for documenter, isattr in memberdocumenters:
276             documenter.generate(
277                 all_members=True, real_modname=self.real_modname,
278                 check_module=members_check_module and not isattr)
279
280         # reset current objects
281         self.env.temp_data['autodoc:module'] = None
282         self.env.temp_data['autodoc:class'] = None
283
284
285 class DoxygenClassDocumenter(DoxygenDocumenter):
286     objtype = 'doxyclass'
287     directivetype = 'class'
288     domain = 'cpp'
289     priority = 100
290
291     option_spec = {
292         'members': members_option,
293     }
294
295     @classmethod
296     def can_document_member(cls, member, membername, isattr, parent):
297         # this method is only called from Documenter.document_members
298         # when a higher level documenter (module or namespace) is trying
299         # to choose the appropriate documenter for each of its lower-level
300         # members. Currently not implemented since we don't have a higher-level
301         # doumenter like a DoxygenNamespaceDocumenter.
302         return False
303
304     def import_object(self):
305         """Import the object and set it as *self.object*.  In the call sequence, this
306         is executed right after parse_name(), so it can use *self.fullname*, *self.objname*,
307         and *self.modname*.
308
309         Returns True if successful, False if an error occurred.
310         """
311         xpath_query = './/compoundname[text()="%s"]/..' % self.fullname
312         match = get_doxygen_root().xpath(xpath_query)
313         if len(match) != 1:
314             raise ExtensionError('[autodoxy] could not find class (fullname="%s"). I tried'
315                                  'the following xpath: "%s"' % (self.fullname, xpath_query))
316
317         self.object = match[0]
318         return True
319
320     def format_signature(self):
321         return ''
322
323     def format_name(self):
324         return self.fullname
325
326     def get_doc(self, encoding):
327         detaileddescription = self.object.find('detaileddescription')
328         doc = [format_xml_paragraph(detaileddescription)]
329         return doc
330
331     def get_object_members(self, want_all):
332         all_members = self.object.xpath('.//sectiondef[@kind="public-func" '
333             'or @kind="public-static-func"]/memberdef[@kind="function"]')
334
335         if want_all:
336             return False, ((m.find('name').text, m) for m in all_members)
337         if not self.options.members:
338             return False, []
339         return False, ((m.find('name').text, m) for m in all_members 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_to_parse):
365         xp = './/*[@id="%s"]' % id_to_parse
366         match = get_doxygen_root().xpath(xp)
367         if match:
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         if '::' in self.fullname:
383             (obj, meth) = self.fullname.rsplit('::', 1)
384             # 'public-func' and 'public-static-func' are for classes while 'func' alone is for namespaces
385             prefix = './/compoundname[text()="{:s}"]/../sectiondef[@kind="public-func" or @kind="public-static-func" or @kind="func"]'.format(obj)
386             obj = "{:s}::".format(obj)
387         else:
388             meth = self.fullname
389             prefix = './'
390             obj = ''
391
392         xpath_query_noparam = ('{:s}/memberdef[@kind="function"]/name[text()="{:s}"]/..').format(prefix, meth)
393         xpath_query = ""
394         if self.argsstring != None:
395             xpath_query = ('{:s}/memberdef[@kind="function" and argsstring/text()="{:s}"]/name[text()="{:s}"]/..').format(prefix,self.argsstring,meth)
396         else:
397             xpath_query = xpath_query_noparam
398         match = get_doxygen_root().xpath(xpath_query)
399         if not match:
400             logger = logging.getLogger(__name__)
401
402             if self.argsstring != None:
403                 candidates = get_doxygen_root().xpath(xpath_query_noparam)
404                 if len(candidates) == 1:
405                     if "{}{}{}".format(obj, meth, candidates[0].find('argsstring').text) == "{}{}{} const".format(obj, meth, self.argsstring):
406                         # ignore discrepencies due to the missing 'const' method quantifyier
407                         pass
408                     else:
409                         logger.warning("[autodoxy] Using method '{}{}{}' instead of '{}{}{}'. You may want to drop your specification of the signature, or to fix it."
410                                        .format(obj, meth, candidates[0].find('argsstring').text, obj, meth, self.argsstring))
411                     self.object = candidates[0]
412                     return True
413                 logger.warning("[autodoxy] WARNING: Could not find method {}{}{}".format(obj, meth, self.argsstring))
414                 if not candidates:
415                     logger.warning("[autodoxy] WARNING:  (no candidate found)")
416                 for cand in candidates:
417                     logger.warning("[autodoxy] WARNING:   Existing candidate: {}{}{}".format(obj, meth, cand.find('argsstring').text))
418             else:
419                 logger.warning("[autodoxy] WARNING: Could not find method {}{} in Doxygen files\nQuery: {}".format(obj, meth, xpath_query))
420             return False
421         self.object = match[0]
422         return True
423
424     def get_doc(self, encoding):
425         detaileddescription = self.object.find('detaileddescription')
426         doc = [format_xml_paragraph(detaileddescription)]
427         return doc
428
429     def format_name(self):
430         def text(el):
431             if el.text is not None:
432                 return el.text
433             return ''
434
435         def tail(el):
436             if el.tail is not None:
437                 return el.tail
438             return ''
439
440         rtype_el = self.object.find('type')
441         rtype_el_ref = rtype_el.find('ref')
442         if rtype_el_ref is not None:
443             rtype = text(rtype_el) + text(rtype_el_ref) + tail(rtype_el_ref)
444         else:
445             rtype = rtype_el.text
446
447  #       print("rtype: {}".format(rtype))
448         signame = (rtype and (rtype + ' ') or '') + self.klassname + "::"+ self.objname
449         return self.format_template_name() + signame
450
451     def format_template_name(self):
452         types = [e.text for e in self.object.findall('templateparamlist/param/type')]
453         if not types:
454             return ''
455         ret = 'template <%s>' % ','.join(types)
456 #        print ("template: {}".format(ret))
457         return ret
458
459     def format_signature(self):
460         args = self.object.find('argsstring').text
461         return args
462
463     def document_members(self, all_members=False):
464         pass
465
466 class DoxygenVariableDocumenter(DoxygenDocumenter):
467     objtype = 'doxyvar'
468     directivetype = 'var'
469     domain = 'cpp'
470     priority = 100
471
472     @classmethod
473     def can_document_member(cls, member, membername, isattr, parent):
474         if ET.iselement(member) and member.tag == 'memberdef' and member.get('kind') == 'variable':
475             return True
476         return False
477
478     def import_object(self):
479         if ET.iselement(self.object):
480             # self.object already set from DoxygenDocumenter.parse_name(),
481             # caused by passing in the `id` of the node instead of just a
482             # classname or method name
483             return True
484
485         (obj, var) = self.fullname.rsplit('::', 1)
486
487         xpath_query = ('.//compoundname[text()="{:s}"]/../sectiondef[@kind="public-attrib" or @kind="public-static-attrib"]'
488                        '/memberdef[@kind="variable"]/name[text()="{:s}"]/..').format(obj, var)
489 #        print("fullname {}".format(self.fullname))
490         match = get_doxygen_root().xpath(xpath_query)
491         if not match:
492             logger = logging.getLogger(__name__)
493
494             logger.warning("[autodoxy] WARNING: could not find variable {}::{} in Doxygen files".format(obj, var))
495             return False
496         self.object = match[0]
497         return True
498
499     def parse_id(self, id_to_parse):
500         xp = './/*[@id="%s"]' % id_to_parse
501         match = get_doxygen_root().xpath(xp)
502         if match:
503             match = match[0]
504             self.fullname = match.find('./definition').text.split()[-1]
505             self.modname = self.fullname
506             self.objname = match.find('./name').text
507             self.object = match
508         return False
509
510     def format_name(self):
511         def text(el):
512             if el.text is not None:
513                 return el.text
514             return ''
515
516         def tail(el):
517             if el.tail is not None:
518                 return el.tail
519             return ''
520
521         rtype_el = self.object.find('type')
522         rtype_el_ref = rtype_el.find('ref')
523         if rtype_el_ref is not None:
524             rtype = text(rtype_el) + text(rtype_el_ref) + tail(rtype_el_ref)
525         else:
526             rtype = rtype_el.text
527
528  #       print("rtype: {}".format(rtype))
529         signame = (rtype and (rtype + ' ') or '') + self.klassname + "::" + self.objname
530         return self.format_template_name() + signame
531
532     def get_doc(self, encoding):
533         detaileddescription = self.object.find('detaileddescription')
534         doc = [format_xml_paragraph(detaileddescription)]
535         return doc
536
537     def format_template_name(self):
538         types = [e.text for e in self.object.findall('templateparamlist/param/type')]
539         if not types:
540             return ''
541         ret = 'template <%s>' % ','.join(types)
542 #        print ("template: {}".format(ret))
543         return ret
544
545     def document_members(self, all_members=False):
546         pass
547
548
549 ##########################################################################
550 # Setup the extension
551 ##########################################################################
552 def set_doxygen_xml(app):
553     """Load all doxygen XML files from the app config variable
554     `app.config.doxygen_xml` which should be a path to a directory
555     containing doxygen xml output
556     """
557     err = ExtensionError(
558         '[autodoxy] No doxygen xml output found in doxygen_xml="%s"' % app.config.doxygen_xml)
559
560     if not os.path.isdir(app.config.doxygen_xml):
561         raise err
562
563     files = [os.path.join(app.config.doxygen_xml, f)
564              for f in os.listdir(app.config.doxygen_xml)
565              if f.lower().endswith('.xml') and not f.startswith('._')]
566     if not files:
567         raise err
568
569     setup.DOXYGEN_ROOT = ET.ElementTree(ET.Element('root')).getroot()
570     for current_file in files:
571         root = ET.parse(current_file).getroot()
572         for node in root:
573             setup.DOXYGEN_ROOT.append(node)
574
575
576 def get_doxygen_root():
577     """Get the root element of the doxygen XML document.
578     """
579     if not hasattr(setup, 'DOXYGEN_ROOT'):
580         setup.DOXYGEN_ROOT = ET.Element("root")  # dummy
581     return setup.DOXYGEN_ROOT
582
583
584 def setup(app):
585     import sphinx.ext.autosummary
586
587     app.connect("builder-inited", set_doxygen_xml)
588 #    app.connect("builder-inited", process_generate_options)
589
590     app.setup_extension('sphinx.ext.autodoc')
591 #    app.setup_extension('sphinx.ext.autosummary')
592
593     app.add_autodocumenter(DoxygenClassDocumenter)
594     app.add_autodocumenter(DoxygenMethodDocumenter)
595     app.add_autodocumenter(DoxygenVariableDocumenter)
596     app.add_config_value("doxygen_xml", "", True)
597
598 #    app.add_directive('autodoxysummary', DoxygenAutosummary)
599 #    app.add_directive('autodoxyenum', DoxygenAutoEnum)
600
601     return {'version': sphinx.__display_version__, 'parallel_read_safe': True}