Logo AND Algorithmique Numérique Distribuée

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