Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
Add autodoxy, a sphinx extension heavily inspired from autodoc_doxygen
authorMartin Quinson <martin.quinson@ens-rennes.fr>
Tue, 5 Nov 2019 15:12:02 +0000 (16:12 +0100)
committerMartin Quinson <martin.quinson@ens-rennes.fr>
Sun, 10 Nov 2019 18:09:06 +0000 (19:09 +0100)
This version is really really close to the now abandonned
https://github.com/rmcgibbo/sphinxcontrib-autodoc_doxygen
but I plan to improve it in the next commits.

The only difference to the original code is what is needed to make it
compile here (change imports, add a new setup.py, don't name the
template as '*.rst' since sphinx spits an error on them) plus the
rename autodoc_doxygen -> autodoxy.

Used version:
https://github.com/rmcgibbo/sphinxcontrib-autodoc_doxygen/commit/ad70f62805affdeb0e6cc638344c15a213394e0d

docs/source/_ext/autodoxy/README [new file with mode: 0644]
docs/source/_ext/autodoxy/autodoxy/__init__.py [new file with mode: 0644]
docs/source/_ext/autodoxy/autodoxy/autodoxy.py [new file with mode: 0644]
docs/source/_ext/autodoxy/autodoxy/autosummary/__init__.py [new file with mode: 0644]
docs/source/_ext/autodoxy/autodoxy/autosummary/generate.py [new file with mode: 0644]
docs/source/_ext/autodoxy/autodoxy/autosummary/templates/doxyclass.rst.in [new file with mode: 0644]
docs/source/_ext/autodoxy/autodoxy/xmlutils.py [new file with mode: 0644]
docs/source/_ext/autodoxy/setup.py [new file with mode: 0644]
docs/source/conf.py

diff --git a/docs/source/_ext/autodoxy/README b/docs/source/_ext/autodoxy/README
new file mode 100644 (file)
index 0000000..6e4e936
--- /dev/null
@@ -0,0 +1,31 @@
+This is autodoxy, a sphinx extension providing autodoc-like directives
+that are feed with Doxygen files.
+
+It is highly inspired from the autodoc_doxygen sphinx extension, but
+adapted to my own needs in SimGrid.
+https://github.com/rmcgibbo/sphinxcontrib-autodoc_doxygen
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Robert T. McGibbon
+Copytight (c) 2019 Martin Quinson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/docs/source/_ext/autodoxy/autodoxy/__init__.py b/docs/source/_ext/autodoxy/autodoxy/__init__.py
new file mode 100644 (file)
index 0000000..3e06d7b
--- /dev/null
@@ -0,0 +1,57 @@
+import os.path
+from lxml import etree as ET
+from sphinx.errors import ExtensionError
+
+
+def set_doxygen_xml(app):
+    """Load all doxygen XML files from the app config variable
+    `app.config.doxygen_xml` which should be a path to a directory
+    containing doxygen xml output
+    """
+    err = ExtensionError(
+        '[autodoxy] No doxygen xml output found in doxygen_xml="%s"' % app.config.doxygen_xml)
+
+    if not os.path.isdir(app.config.doxygen_xml):
+        raise err
+
+    files = [os.path.join(app.config.doxygen_xml, f)
+             for f in os.listdir(app.config.doxygen_xml)
+             if f.lower().endswith('.xml') and not f.startswith('._')]
+    if len(files) == 0:
+        raise err
+
+    setup.DOXYGEN_ROOT = ET.ElementTree(ET.Element('root')).getroot()
+    for file in files:
+        root = ET.parse(file).getroot()
+        for node in root:
+            setup.DOXYGEN_ROOT.append(node)
+
+
+def get_doxygen_root():
+    """Get the root element of the doxygen XML document.
+    """
+    if not hasattr(setup, 'DOXYGEN_ROOT'):
+        setup.DOXYGEN_ROOT = ET.Element("root")  # dummy
+    return setup.DOXYGEN_ROOT
+
+
+def setup(app):
+    import sphinx.ext.autosummary
+    from autodoxy.autodoxy import DoxygenClassDocumenter, DoxygenMethodDocumenter
+    from autodoxy.autosummary import DoxygenAutosummary, DoxygenAutoEnum
+    from autodoxy.autosummary.generate import process_generate_options
+
+    app.connect("builder-inited", set_doxygen_xml)
+    app.connect("builder-inited", process_generate_options)
+
+    app.setup_extension('sphinx.ext.autodoc')
+    app.setup_extension('sphinx.ext.autosummary')
+
+    app.add_autodocumenter(DoxygenClassDocumenter)
+    app.add_autodocumenter(DoxygenMethodDocumenter)
+    app.add_config_value("doxygen_xml", "", True)
+
+    app.add_directive('autodoxysummary', DoxygenAutosummary)
+    app.add_directive('autodoxyenum', DoxygenAutoEnum)
+
+    return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
diff --git a/docs/source/_ext/autodoxy/autodoxy/autodoxy.py b/docs/source/_ext/autodoxy/autodoxy/autodoxy.py
new file mode 100644 (file)
index 0000000..15602da
--- /dev/null
@@ -0,0 +1,255 @@
+from __future__ import print_function, absolute_import, division
+
+from six import itervalues
+from lxml import etree as ET
+from sphinx.ext.autodoc import Documenter, AutoDirective, members_option, ALL
+from sphinx.errors import ExtensionError
+
+from . import get_doxygen_root
+from .xmlutils import format_xml_paragraph
+
+
+class DoxygenDocumenter(Documenter):
+    # Variables to store the names of the object being documented. modname and fullname are redundant,
+    # and objpath is always the empty list. This is inelegant, but we need to work with the superclass.
+
+    fullname = None  # example: "OpenMM::NonbondedForce" or "OpenMM::NonbondedForce::methodName""
+    modname = None   # example: "OpenMM::NonbondedForce" or "OpenMM::NonbondedForce::methodName""
+    objname = None   # example: "NonbondedForce"  or "methodName"
+    objpath = []     # always the empty list
+    object = None    # the xml node for the object
+
+    option_spec = {
+        'members': members_option,
+    }
+
+    def __init__(self, directive, name, indent=u'', id=None):
+        super(DoxygenDocumenter, self).__init__(directive, name, indent)
+        if id is not None:
+            self.parse_id(id)
+
+    def parse_id(self, id):
+        return False
+
+    def parse_name(self):
+        """Determine what module to import and what attribute to document.
+        Returns True and sets *self.modname*, *self.objname*, *self.fullname*,
+        if parsing and resolving was successful.
+        """
+        # To view the context and order in which all of these methods get called,
+        # See, Documenter.generate(). That's the main "entry point" that first
+        # calls parse_name(), follwed by import_object(), format_signature(),
+        # add_directive_header(), and then add_content() (which calls get_doc())
+
+        # methods in the superclass sometimes use '.' to join namespace/class
+        # names with method names, and we don't want that.
+        self.name = self.name.replace('.', '::')
+        self.fullname = self.name
+        self.modname = self.fullname
+        self.objpath = []
+
+        if '::' in self.name:
+            parts = self.name.split('::')
+            self.objname = parts[-1]
+        else:
+            self.objname = self.name
+
+        return True
+
+    def add_directive_header(self, sig):
+        """Add the directive header and options to the generated content."""
+        domain = getattr(self, 'domain', 'cpp')
+        directive = getattr(self, 'directivetype', self.objtype)
+        name = self.format_name()
+        sourcename = self.get_sourcename()
+        self.add_line(u'.. %s:%s:: %s%s' % (domain, directive, name, sig),
+                      sourcename)
+
+    def document_members(self, all_members=False):
+        """Generate reST for member documentation.
+        If *all_members* is True, do all members, else those given by
+        *self.options.members*.
+        """
+        want_all = all_members or self.options.inherited_members or \
+            self.options.members is ALL
+        # find out which members are documentable
+        members_check_module, members = self.get_object_members(want_all)
+
+        # remove members given by exclude-members
+        if self.options.exclude_members:
+            members = [(membername, member) for (membername, member) in members
+                       if membername not in self.options.exclude_members]
+
+        # document non-skipped members
+        memberdocumenters = []
+        for (mname, member, isattr) in self.filter_members(members, want_all):
+            classes = [cls for cls in itervalues(AutoDirective._registry)
+                       if cls.can_document_member(member, mname, isattr, self)]
+            if not classes:
+                # don't know how to document this member
+                continue
+
+            # prefer the documenter with the highest priority
+            classes.sort(key=lambda cls: cls.priority)
+
+            documenter = classes[-1](self.directive, mname, indent=self.indent, id=member.get('id'))
+            memberdocumenters.append((documenter, isattr))
+
+        for documenter, isattr in memberdocumenters:
+            documenter.generate(
+                all_members=True, real_modname=self.real_modname,
+                check_module=members_check_module and not isattr)
+
+        # reset current objects
+        self.env.temp_data['autodoc:module'] = None
+        self.env.temp_data['autodoc:class'] = None
+
+
+class DoxygenClassDocumenter(DoxygenDocumenter):
+    objtype = 'doxyclass'
+    directivetype = 'class'
+    domain = 'cpp'
+    priority = 100
+
+    option_spec = {
+        'members': members_option,
+    }
+
+    @classmethod
+    def can_document_member(cls, member, membername, isattr, parent):
+        # this method is only called from Documenter.document_members
+        # when a higher level documenter (module or namespace) is trying
+        # to choose the appropriate documenter for each of its lower-level
+        # members. Currently not implemented since we don't have a higher-level
+        # doumenter like a DoxygenNamespaceDocumenter.
+        return False
+
+    def import_object(self):
+        """Import the object and set it as *self.object*.  In the call sequence, this
+        is executed right after parse_name(), so it can use *self.fullname*, *self.objname*,
+        and *self.modname*.
+
+        Returns True if successful, False if an error occurred.
+        """
+        xpath_query = './/compoundname[text()="%s"]/..' % self.fullname
+        match = get_doxygen_root().xpath(xpath_query)
+        if len(match) != 1:
+            raise ExtensionError('[autodoxy] could not find class (fullname="%s"). I tried'
+                                 'the following xpath: "%s"' % (self.fullname, xpath_query))
+
+        self.object = match[0]
+        return True
+
+    def format_signaure(self):
+        return ''
+
+    def format_name(self):
+        return self.fullname
+
+    def get_doc(self, encoding):
+        detaileddescription = self.object.find('detaileddescription')
+        doc = [format_xml_paragraph(detaileddescription)]
+        return doc
+
+    def get_object_members(self, want_all):
+        all_members = self.object.xpath('.//sectiondef[@kind="public-func" '
+            'or @kind="public-static-func"]/memberdef[@kind="function"]')
+
+        if want_all:
+            return False, ((m.find('name').text, m) for m in all_members)
+        else:
+            if not self.options.members:
+                return False, []
+            else:
+                return False, ((m.find('name').text, m) for m in all_members
+                               if m.find('name').text in self.options.members)
+
+    def filter_members(self, members, want_all):
+        ret = []
+        for (membername, member) in members:
+            ret.append((membername, member, False))
+        return ret
+
+    def document_members(self, all_members=False):
+        super(DoxygenClassDocumenter, self).document_members(all_members=all_members)
+        # Uncomment to view the generated rst for the class.
+        # print('\n'.join(self.directive.result))
+
+
+class DoxygenMethodDocumenter(DoxygenDocumenter):
+    objtype = 'doxymethod'
+    directivetype = 'function'
+    domain = 'cpp'
+    priority = 100
+
+    @classmethod
+    def can_document_member(cls, member, membername, isattr, parent):
+        if ET.iselement(member) and member.tag == 'memberdef' and member.get('kind') == 'function':
+            return True
+        return False
+
+    def parse_id(self, id):
+        xp = './/*[@id="%s"]' % id
+        match = get_doxygen_root().xpath(xp)
+        if len(match) > 0:
+            match = match[0]
+            self.fullname = match.find('./definition').text.split()[-1]
+            self.modname = self.fullname
+            self.objname = match.find('./name').text
+            self.object = match
+        return False
+
+    def import_object(self):
+        if ET.iselement(self.object):
+            # self.object already set from DoxygenDocumenter.parse_name(),
+            # caused by passing in the `id` of the node instead of just a
+            # classname or method name
+            return True
+
+        xpath_query = ('.//compoundname[text()="%s"]/../sectiondef[@kind="public-func"]'
+                       '/memberdef[@kind="function"]/name[text()="%s"]/..') % tuple(self.fullname.rsplit('::', 1))
+        match = get_doxygen_root().xpath(xpath_query)
+        if len(match) == 0:
+            raise ExtensionError('[autodoxy] could not find method (modname="%s", objname="%s"). I tried '
+                                 'the following xpath: "%s"' % (tuple(self.fullname.rsplit('::', 1)) + (xpath_query,)))
+        self.object = match[0]
+        return True
+
+    def get_doc(self, encoding):
+        detaileddescription = self.object.find('detaileddescription')
+        doc = [format_xml_paragraph(detaileddescription)]
+        return doc
+
+    def format_name(self):
+        def text(el):
+            if el.text is not None:
+                return el.text
+            return ''
+
+        def tail(el):
+            if el.tail is not None:
+                return el.tail
+            return ''
+
+        rtype_el = self.object.find('type')
+        rtype_el_ref = rtype_el.find('ref')
+        if rtype_el_ref is not None:
+            rtype = text(rtype_el) + text(rtype_el_ref) + tail(rtype_el_ref)
+        else:
+            rtype = rtype_el.text
+
+        signame = (rtype and (rtype + ' ') or '') + self.objname
+        return self.format_template_name() + signame
+
+    def format_template_name(self):
+        types = [e.text for e in self.object.findall('templateparamlist/param/type')]
+        if len(types) == 0:
+            return ''
+        return 'template <%s>\n' % ','.join(types)
+
+    def format_signature(self):
+        args = self.object.find('argsstring').text
+        return args
+
+    def document_members(self, all_members=False):
+        pass
diff --git a/docs/source/_ext/autodoxy/autodoxy/autosummary/__init__.py b/docs/source/_ext/autodoxy/autodoxy/autosummary/__init__.py
new file mode 100644 (file)
index 0000000..0f2b423
--- /dev/null
@@ -0,0 +1,224 @@
+from __future__ import print_function, absolute_import, division
+
+import re
+import operator
+from functools import reduce
+from itertools import count, groupby
+
+from docutils import nodes
+from docutils.statemachine import ViewList
+from sphinx import addnodes
+from sphinx.ext.autosummary import Autosummary, autosummary_table
+
+from autodoxy import get_doxygen_root
+from autodoxy.autodoxy import DoxygenMethodDocumenter, DoxygenClassDocumenter
+from autodoxy.xmlutils import format_xml_paragraph
+
+
+def import_by_name(name, env=None, prefixes=None, i=0):
+    """Get xml documentation for a class/method with a given name.
+    If there are multiple classes or methods with that name, you
+    can use the `i` kwarg to pick which one.
+    """
+    if prefixes is None:
+        prefixes = [None]
+
+    if env is not None:
+        parent = env.ref_context.get('cpp:parent_symbol')
+        parent_symbols = []
+        while parent is not None and parent.identifier is not None:
+            parent_symbols.insert(0, str(parent.identifier))
+            parent = parent.parent
+        prefixes.append('::'.join(parent_symbols))
+
+    tried = []
+    for prefix in prefixes:
+        try:
+            if prefix:
+                prefixed_name = '::'.join([prefix, name])
+            else:
+                prefixed_name = name
+            return _import_by_name(prefixed_name, i=i)
+        except ImportError:
+            tried.append(prefixed_name)
+    raise ImportError('no module named %s' % ' or '.join(tried))
+
+
+def _import_by_name(name, i=0):
+    root = get_doxygen_root()
+    name = name.replace('.', '::')
+
+    if '::' in name:
+        xpath_query = (
+            './/compoundname[text()="%s"]/../'
+            'sectiondef[@kind="public-func"]/memberdef[@kind="function"]/'
+            'name[text()="%s"]/..') % tuple(name.rsplit('::', 1))
+        m = root.xpath(xpath_query)
+        if len(m) > 0:
+            obj = m[i]
+            full_name = '.'.join(name.rsplit('::', 1))
+            return full_name, obj, full_name, ''
+
+        xpath_query = (
+            './/compoundname[text()="%s"]/../'
+            'sectiondef[@kind="public-type"]/memberdef[@kind="enum"]/'
+            'name[text()="%s"]/..') % tuple(name.rsplit('::', 1))
+        m = root.xpath(xpath_query)
+        if len(m) > 0:
+            obj = m[i]
+            full_name = '.'.join(name.rsplit('::', 1))
+            return full_name, obj, full_name, ''
+
+    xpath_query = ('.//compoundname[text()="%s"]/..' % name)
+    m = root.xpath(xpath_query)
+    if len(m) > 0:
+        obj = m[i]
+        return (name, obj, name, '')
+
+    raise ImportError()
+
+
+def get_documenter(obj, full_name):
+    if obj.tag == 'memberdef' and obj.get('kind') == 'function':
+        return DoxygenMethodDocumenter
+    elif obj.tag == 'compounddef':
+        return DoxygenClassDocumenter
+
+    raise NotImplementedError(obj.tag)
+
+
+class DoxygenAutosummary(Autosummary):
+    def get_items(self, names):
+        """Try to import the given names, and return a list of
+        ``[(name, signature, summary_string, real_name), ...]``.
+        """
+        env = self.state.document.settings.env
+        items = []
+
+        names_and_counts = reduce(operator.add,
+            [tuple(zip(g, count())) for _, g in groupby(names)]) # type: List[(Str, Int)]
+
+        for name, i in names_and_counts:
+            display_name = name
+            if name.startswith('~'):
+                name = name[1:]
+                display_name = name.split('::')[-1]
+
+            try:
+                real_name, obj, parent, modname = import_by_name(name, env=env, i=i)
+            except ImportError:
+                self.warn('failed to import %s' % name)
+                items.append((name, '', '', name))
+                continue
+
+            self.result = ViewList()  # initialize for each documenter
+            documenter = get_documenter(obj, parent)(self, real_name, id=obj.get('id'))
+            if not documenter.parse_name():
+                self.warn('failed to parse name %s' % real_name)
+                items.append((display_name, '', '', real_name))
+                continue
+            if not documenter.import_object():
+                self.warn('failed to import object %s' % real_name)
+                items.append((display_name, '', '', real_name))
+                continue
+            if documenter.options.members and not documenter.check_module():
+                continue
+            # -- Grab the signature
+            sig = documenter.format_signature()
+
+            # -- Grab the summary
+            documenter.add_content(None)
+            doc = list(documenter.process_doc([self.result.data]))
+
+            while doc and not doc[0].strip():
+                doc.pop(0)
+
+            # If there's a blank line, then we can assume the first sentence /
+            # paragraph has ended, so anything after shouldn't be part of the
+            # summary
+            for i, piece in enumerate(doc):
+                if not piece.strip():
+                    doc = doc[:i]
+                    break
+
+            # Try to find the "first sentence", which may span multiple lines
+            m = re.search(r"^([A-Z].*?\.)(?:\s|$)", " ".join(doc).strip())
+            if m:
+                summary = m.group(1).strip()
+            elif doc:
+                summary = doc[0].strip()
+            else:
+                summary = ''
+
+            items.append((display_name, sig, summary, real_name))
+
+        return items
+
+    def get_tablespec(self):
+        table_spec = addnodes.tabular_col_spec()
+        table_spec['spec'] = 'll'
+
+        table = autosummary_table('')
+        real_table = nodes.table('', classes=['longtable'])
+        table.append(real_table)
+        group = nodes.tgroup('', cols=2)
+        real_table.append(group)
+        group.append(nodes.colspec('', colwidth=10))
+        group.append(nodes.colspec('', colwidth=90))
+        body = nodes.tbody('')
+        group.append(body)
+
+        def append_row(*column_texts):
+            row = nodes.row('')
+            for text in column_texts:
+                node = nodes.paragraph('')
+                vl = ViewList()
+                vl.append(text, '<autosummary>')
+                self.state.nested_parse(vl, 0, node)
+                try:
+                    if isinstance(node[0], nodes.paragraph):
+                        node = node[0]
+                except IndexError:
+                    pass
+                row.append(nodes.entry('', node))
+            body.append(row)
+        return table, table_spec, append_row
+
+    def get_table(self, items):
+        """Generate a proper list of table nodes for autosummary:: directive.
+
+        *items* is a list produced by :meth:`get_items`.
+        """
+        table, table_spec, append_row = self.get_tablespec()
+        for name, sig, summary, real_name in items:
+            qualifier = 'cpp:any'
+            # required for cpp autolink
+            full_name = real_name.replace('.', '::')
+            col1 = ':%s:`%s <%s>`' % (qualifier, name, full_name)
+            col2 = summary
+            append_row(col1, col2)
+
+        self.result.append('   .. rubric: sdsf', 0)
+        return [table_spec, table]
+
+
+class DoxygenAutoEnum(DoxygenAutosummary):
+
+    def get_items(self, names):
+        env = self.state.document.settings.env
+        self.name = names[0]
+
+        real_name, obj, parent, modname = import_by_name(self.name, env=env)
+        names = [n.text for n in obj.findall('./enumvalue/name')]
+        descriptions = [format_xml_paragraph(d) for d in obj.findall('./enumvalue/detaileddescription')]
+        return zip(names, descriptions)
+
+    def get_table(self, items):
+        table, table_spec, append_row = self.get_tablespec()
+        for name, description in items:
+            col1 = ':strong:`' + name + '`'
+            while description and not description[0].strip():
+                description.pop(0)
+            col2 = ' '.join(description)
+            append_row(col1, col2)
+        return [nodes.rubric('', 'Enum: %s' % self.name), table]
diff --git a/docs/source/_ext/autodoxy/autodoxy/autosummary/generate.py b/docs/source/_ext/autodoxy/autodoxy/autosummary/generate.py
new file mode 100644 (file)
index 0000000..0b8d66a
--- /dev/null
@@ -0,0 +1,204 @@
+from __future__ import print_function, absolute_import, division
+
+import codecs
+import os
+import re
+import sys
+
+from jinja2 import FileSystemLoader
+from jinja2.sandbox import SandboxedEnvironment
+from sphinx.jinja2glue import BuiltinTemplateLoader
+from sphinx.util.osutil import ensuredir
+
+from . import import_by_name
+
+
+def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
+                              base_path=None, builder=None, template_dir=None):
+
+    showed_sources = list(sorted(sources))
+    if len(showed_sources) > 20:
+        showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:]
+    print('[autosummary] generating autosummary for: %s' %
+          ', '.join(showed_sources))
+
+    if output_dir:
+        print('[autosummary] writing to %s' % output_dir)
+
+    if base_path is not None:
+        sources = [os.path.join(base_path, filename) for filename in sources]
+
+    # create our own templating environment
+    template_dirs = [os.path.join(os.path.dirname(__file__), 'templates')]
+
+    if builder is not None:
+        # allow the user to override the templates
+        template_loader = BuiltinTemplateLoader()
+        template_loader.init(builder, dirs=template_dirs)
+    else:
+        if template_dir:
+            template_dirs.insert(0, template_dir)
+        template_loader = FileSystemLoader(template_dirs)
+    template_env = SandboxedEnvironment(loader=template_loader)
+
+    # read
+    items = find_autosummary_in_files(sources)
+
+    # keep track of new files
+    new_files = []
+
+    for name, path, template_name in sorted(set(items), key=str):
+        if path is None:
+            # The corresponding autosummary:: directive did not have
+            # a :toctree: option
+            continue
+
+        path = output_dir or os.path.abspath(path)
+        ensuredir(path)
+
+        try:
+            name, obj, parent, mod_name = import_by_name(name)
+        except ImportError as e:
+            print('WARNING [autosummary] failed to import %r: %s' % (name, e), file=sys.stderr)
+            continue
+
+        fn = os.path.join(path, name + suffix).replace('::', '.')
+
+        # skip it if it exists
+        if os.path.isfile(fn):
+            continue
+
+        new_files.append(fn)
+
+        if template_name is None:
+            if obj.tag == 'compounddef' and obj.get('kind') == 'class':
+                template_name = 'doxyclass.rst.in'
+            else:
+                raise NotImplementedError('No template for %s' % obj)
+
+        with open(fn, 'w') as f:
+            template = template_env.get_template(template_name)
+            ns = {}
+            if obj.tag == 'compounddef' and obj.get('kind') == 'class':
+                ns['methods'] = [e.text for e in obj.findall('.//sectiondef[@kind="public-func"]/memberdef[@kind="function"]/name')]
+                ns['enums'] = [e.text for e in obj.findall('.//sectiondef[@kind="public-type"]/memberdef[@kind="enum"]/name')]
+                ns['objtype'] = 'class'
+            else:
+                raise NotImplementedError(obj)
+
+            parts = name.split('::')
+            mod_name, obj_name = '::'.join(parts[:-1]), parts[-1]
+
+            ns['fullname'] = name
+            ns['module'] = mod_name
+            ns['objname'] = obj_name
+            ns['name'] = parts[-1]
+            ns['underline'] = len(name) * '='
+
+            rendered = template.render(**ns)
+            f.write(rendered)
+
+    # descend recursively to new files
+    if new_files:
+        generate_autosummary_docs(new_files, output_dir=output_dir,
+                                  suffix=suffix, base_path=base_path, builder=builder,
+                                  template_dir=template_dir)
+
+
+def find_autosummary_in_files(filenames):
+    """Find out what items are documented in source/*.rst.
+
+    See `find_autosummary_in_lines`.
+    """
+    documented = []
+    for filename in filenames:
+        with codecs.open(filename, 'r', encoding='utf-8',
+                         errors='ignore') as f:
+            lines = f.read().splitlines()
+            documented.extend(find_autosummary_in_lines(lines,
+                                                        filename=filename))
+    return documented
+
+
+def find_autosummary_in_lines(lines, module=None, filename=None):
+    """Find out what items appear in autosummary:: directives in the
+    given lines.
+
+    Returns a list of (name, toctree, template) where *name* is a name
+    of an object and *toctree* the :toctree: path of the corresponding
+    autosummary directive (relative to the root of the file name), and
+    *template* the value of the :template: option. *toctree* and
+    *template* ``None`` if the directive does not have the
+    corresponding options set.
+    """
+    autosummary_re = re.compile(r'^(\s*)\.\.\s+autodoxysummary::\s*')
+    autosummary_item_re = re.compile(r'^\s+(~?[_a-zA-Z][a-zA-Z0-9_.:]*)\s*.*?')
+    toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$')
+    template_arg_re = re.compile(r'^\s+:template:\s*(.*?)\s*$')
+
+    documented = []
+
+    toctree = None
+    template = None
+    in_autosummary = False
+    base_indent = ""
+
+    for line in lines:
+        if in_autosummary:
+            m = toctree_arg_re.match(line)
+            if m:
+                toctree = m.group(1)
+                if filename:
+                    toctree = os.path.join(os.path.dirname(filename),
+                                           toctree)
+                continue
+
+            m = template_arg_re.match(line)
+            if m:
+                template = m.group(1).strip()
+                continue
+
+            if line.strip().startswith(':'):
+                continue  # skip options
+
+            m = autosummary_item_re.match(line)
+            if m:
+                name = m.group(1).strip()
+                if name.startswith('~'):
+                    name = name[1:]
+                documented.append((name, toctree, template))
+                continue
+
+            if not line.strip() or line.startswith(base_indent + " "):
+                continue
+
+            in_autosummary = False
+
+        m = autosummary_re.match(line)
+        if m:
+            in_autosummary = True
+            base_indent = m.group(1)
+            toctree = None
+            template = None
+            continue
+
+    return documented
+
+
+def process_generate_options(app):
+    genfiles = app.config.autosummary_generate
+
+    if genfiles and not hasattr(genfiles, '__len__'):
+        env = app.builder.env
+        genfiles = [env.doc2path(x, base=None) for x in env.found_docs
+                    if os.path.isfile(env.doc2path(x))]
+
+    if not genfiles:
+        return
+
+    ext = app.config.source_suffix[0]
+    genfiles = [genfile + (not genfile.endswith(ext) and ext or '')
+                for genfile in genfiles]
+
+    generate_autosummary_docs(genfiles, builder=app.builder,
+                              suffix=ext, base_path=app.srcdir)
diff --git a/docs/source/_ext/autodoxy/autodoxy/autosummary/templates/doxyclass.rst.in b/docs/source/_ext/autodoxy/autodoxy/autosummary/templates/doxyclass.rst.in
new file mode 100644 (file)
index 0000000..2559677
--- /dev/null
@@ -0,0 +1,20 @@
+{{ name }}
+{{ underline }}
+
+.. autodoxyclass:: {{ fullname }}
+   :members:
+
+   {% if methods %}
+   .. rubric:: Methods
+
+   .. autodoxysummary::
+   {% for item in methods %}
+      ~{{ fullname }}::{{ item }}
+   {%- endfor %}
+   {% endif %}
+
+   {% if enums %}
+   {% for enum in enums %}
+   .. autodoxyenum:: {{ enum }}
+   {% endfor %}
+   {% endif %}
\ No newline at end of file
diff --git a/docs/source/_ext/autodoxy/autodoxy/xmlutils.py b/docs/source/_ext/autodoxy/autodoxy/xmlutils.py
new file mode 100644 (file)
index 0000000..7aa507f
--- /dev/null
@@ -0,0 +1,128 @@
+from __future__ import print_function, absolute_import, division
+from . import get_doxygen_root
+
+
+def format_xml_paragraph(xmlnode):
+    """Format an Doxygen XML segment (principally a detaileddescription)
+    as a paragraph for inclusion in the rst document
+
+    Parameters
+    ----------
+    xmlnode
+
+    Returns
+    -------
+    lines
+        A list of lines.
+    """
+    return [l.rstrip() for l in _DoxygenXmlParagraphFormatter().generic_visit(xmlnode).lines]
+
+
+class _DoxygenXmlParagraphFormatter(object):
+    # This class follows the model of the stdlib's ast.NodeVisitor for tree traversal
+    # where you dispatch on the element type to a different method for each node
+    # during the traverse.
+
+    # It's supposed to handle paragraphs, references, preformatted text (code blocks), and lists.
+
+    def __init__(self):
+        self.lines = ['']
+        self.continue_line = False
+
+    def visit(self, node):
+        method = 'visit_' + node.tag
+        visitor = getattr(self, method, self.generic_visit)
+        return visitor(node)
+
+    def generic_visit(self, node):
+        for child in node.getchildren():
+            self.visit(child)
+        return self
+
+    def visit_ref(self, node):
+        ref = get_doxygen_root().findall('.//*[@id="%s"]' % node.get('refid'))
+        if ref:
+            ref = ref[0]
+            if ref.tag == 'memberdef':
+                parent = ref.xpath('./ancestor::compounddef/compoundname')[0].text
+                name = ref.find('./name').text
+                real_name = parent + '::' + name
+            elif ref.tag in ('compounddef', 'enumvalue'):
+                name_node = ref.find('./name')
+                real_name = name_node.text if name_node is not None else ''
+            else:
+                raise NotImplementedError(ref.tag)
+        else:
+            real_name = None
+
+        val = [':cpp:any:`', node.text]
+        if real_name:
+            val.extend((' <', real_name, '>`'))
+        else:
+            val.append('`')
+        if node.tail is not None:
+            val.append(node.tail)
+
+        self.lines[-1] += ''.join(val)
+
+    def visit_para(self, node):
+        if node.text is not None:
+            if self.continue_line:
+                self.lines[-1] += node.text
+            else:
+                self.lines.append(node.text)
+        self.generic_visit(node)
+        self.lines.append('')
+        self.continue_line = False
+
+    def visit_parametername(self, node):
+        if 'direction' in node.attrib:
+            direction = '[%s] ' % node.get('direction')
+        else:
+            direction = ''
+
+        self.lines.append('**%s** -- %s' % (
+            node.text, direction))
+        self.continue_line = True
+
+    def visit_parameterlist(self, node):
+        lines = [l for l in type(self)().generic_visit(node).lines if l is not '']
+        self.lines.extend([':parameters:', ''] + ['* %s' % l for l in lines] + [''])
+
+    def visit_simplesect(self, node):
+        if node.get('kind') == 'return':
+            self.lines.append(':returns: ')
+            self.continue_line = True
+        self.generic_visit(node)
+
+    def visit_listitem(self, node):
+        self.lines.append('   - ')
+        self.continue_line = True
+        self.generic_visit(node)
+
+    def visit_preformatted(self, node):
+        segment = [node.text if node.text is not None else '']
+        for n in node.getchildren():
+            segment.append(n.text)
+            if n.tail is not None:
+                segment.append(n.tail)
+
+        lines = ''.join(segment).split('\n')
+        self.lines.extend(('.. code-block:: C++', ''))
+        self.lines.extend(['  ' + l for l in lines])
+
+    def visit_computeroutput(self, node):
+        c = node.find('preformatted')
+        if c is not None:
+            return self.visit_preformatted(c)
+        return self.visit_preformatted(node)
+
+    def visit_xrefsect(self, node):
+        if node.find('xreftitle').text == 'Deprecated':
+            sublines = type(self)().generic_visit(node).lines
+            self.lines.extend(['.. admonition:: Deprecated'] + ['   ' + s for s in sublines])
+        else:
+            raise ValueError(node)
+
+    def visit_subscript(self, node):
+        self.lines[-1] += '\ :sub:`%s` %s' % (node.text, node.tail)
diff --git a/docs/source/_ext/autodoxy/setup.py b/docs/source/_ext/autodoxy/setup.py
new file mode 100644 (file)
index 0000000..63f5171
--- /dev/null
@@ -0,0 +1,22 @@
+from setuptools import setup, Extension
+
+with open("README", "r") as fh:
+    long_description = fh.read()
+    
+setup(
+    name="autodoxy",
+    version="0.0.1",
+    author="Martin Quinson",
+#    author_email="author@example.com",
+    description="A bridge between the autodoc of Python and Doxygen of C/C++",
+    long_description=long_description,
+    long_description_content_type="text/plain",
+    url="https://framagit.org/simgrid/simgrid/docs/source/_ext/autodoxy",
+    packages=setuptools.find_packages(),
+    classifiers=[
+      "Programming Language :: Python :: 3",
+      "License :: OSI Approved :: MIT License",
+      "Operating System :: OS Independent",
+    ],
+    python_requires='>=3.6',
+)
index eb4687f..fe63c21 100644 (file)
@@ -50,41 +50,28 @@ version = u'3.24.1'
 extensions = [
     'sphinx.ext.todo',
     'breathe',
-    #    'exhale',
     'sphinx.ext.autodoc',
     'sphinx.ext.intersphinx',
-    #    'sphinx.ext.napoleon',
     'sphinx.ext.autosummary',
     'sphinx_tabs.tabs',
     'sphinxcontrib.contentui',
     'javasphinx',
     'showfile',
+    'autodoxy',
 ]
 
 todo_include_todos = True
 
+# Setup the breath extension
 breathe_projects = {'simgrid': '../build/xml'}
 breathe_default_project = "simgrid"
 
-# Setup the exhale extension
-
-exhale_args = {
-    # These arguments are required
-    "containmentFolder": "./api",
-    "rootFileName": "library_root.rst",
-    "rootFileTitle": "SimGrid Full API",
-    "doxygenStripFromPath": "..",
-    # Suggested optional arguments
-    "createTreeView": True,
-    "exhaleExecutesDoxygen": False,
-    # "exhaleUseDoxyfile":     True,
-}
-
+# Setup the autodoxy extension
+doxygen_xml = os.path.join(os.path.dirname(__file__), "..", "build", "xml")
 
 # For cross-ref generation
 primary_domain = 'cpp'
 
-
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']