From 704e6bf842c07a5762c2494015c2e8e25f230e6f Mon Sep 17 00:00:00 2001 From: Martin Quinson Date: Tue, 5 Nov 2019 16:12:02 +0100 Subject: [PATCH] Add autodoxy, a sphinx extension heavily inspired from autodoc_doxygen 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 | 31 +++ .../source/_ext/autodoxy/autodoxy/__init__.py | 57 ++++ .../source/_ext/autodoxy/autodoxy/autodoxy.py | 255 ++++++++++++++++++ .../autodoxy/autodoxy/autosummary/__init__.py | 224 +++++++++++++++ .../autodoxy/autodoxy/autosummary/generate.py | 204 ++++++++++++++ .../autosummary/templates/doxyclass.rst.in | 20 ++ .../source/_ext/autodoxy/autodoxy/xmlutils.py | 128 +++++++++ docs/source/_ext/autodoxy/setup.py | 22 ++ docs/source/conf.py | 21 +- 9 files changed, 945 insertions(+), 17 deletions(-) create mode 100644 docs/source/_ext/autodoxy/README create mode 100644 docs/source/_ext/autodoxy/autodoxy/__init__.py create mode 100644 docs/source/_ext/autodoxy/autodoxy/autodoxy.py create mode 100644 docs/source/_ext/autodoxy/autodoxy/autosummary/__init__.py create mode 100644 docs/source/_ext/autodoxy/autodoxy/autosummary/generate.py create mode 100644 docs/source/_ext/autodoxy/autodoxy/autosummary/templates/doxyclass.rst.in create mode 100644 docs/source/_ext/autodoxy/autodoxy/xmlutils.py create mode 100644 docs/source/_ext/autodoxy/setup.py diff --git a/docs/source/_ext/autodoxy/README b/docs/source/_ext/autodoxy/README new file mode 100644 index 0000000000..6e4e936a2c --- /dev/null +++ b/docs/source/_ext/autodoxy/README @@ -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 index 0000000000..3e06d7bfbf --- /dev/null +++ b/docs/source/_ext/autodoxy/autodoxy/__init__.py @@ -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 index 0000000000..15602daa0a --- /dev/null +++ b/docs/source/_ext/autodoxy/autodoxy/autodoxy.py @@ -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 index 0000000000..0f2b423dc6 --- /dev/null +++ b/docs/source/_ext/autodoxy/autodoxy/autosummary/__init__.py @@ -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, '') + 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 index 0000000000..0b8d66ac2d --- /dev/null +++ b/docs/source/_ext/autodoxy/autodoxy/autosummary/generate.py @@ -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 index 0000000000..2559677eb3 --- /dev/null +++ b/docs/source/_ext/autodoxy/autodoxy/autosummary/templates/doxyclass.rst.in @@ -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 index 0000000000..7aa507fa98 --- /dev/null +++ b/docs/source/_ext/autodoxy/autodoxy/xmlutils.py @@ -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 index 0000000000..63f517109e --- /dev/null +++ b/docs/source/_ext/autodoxy/setup.py @@ -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', +) diff --git a/docs/source/conf.py b/docs/source/conf.py index eb4687f619..fe63c21a97 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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'] -- 2.20.1