Logo AND Algorithmique Numérique Distribuée

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