From 958fe10103d30342625122c1f5d89c29bd7ee548 Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Thu, 31 Aug 2017 16:42:47 -0700 Subject: [PATCH] Added autoapisummary directive --- autoapi/directives.py | 128 +++++++++++++++++++++++++- autoapi/extension.py | 12 ++- autoapi/mappers/base.py | 9 ++ autoapi/mappers/python.py | 11 +++ autoapi/templates/python/function.rst | 2 +- 5 files changed, 157 insertions(+), 5 deletions(-) diff --git a/autoapi/directives.py b/autoapi/directives.py index e4736ea..0ff9282 100644 --- a/autoapi/directives.py +++ b/autoapi/directives.py @@ -1,9 +1,135 @@ """AutoAPI directives""" -from docutils.parsers.rst import Directive +import posixpath +import re + +from docutils.parsers.rst import Directive, directives +from docutils.statemachine import ViewList from docutils import nodes +from sphinx import addnodes +import sphinx.ext.autosummary from sphinx.util.nodes import nested_parse_with_titles +try: + from sphinx.util.rst import escape +except ImportError: + # sphinx.util.rst is available in sphinx >=1.4.7 only. + # This implementation is taken from sphinx 1.6.5. + def escape(text): + return re.compile(r'([!-/:-@\[-`{-~])').sub(r'\\\1', text) + + +class AutoapiSummary(Directive): + """A version of autosummary that uses static analysis.""" + + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + has_content = True + option_spec = { + 'toctree': directives.unchanged, + 'nosignatures': directives.flag, + 'template': directives.unchanged, + } + + def warn(self, msg): + """Add a warning message. + + :param msg: The warning message to add. + :type msg: str + """ + self.warnings.append( + self.state.document.reporter.warning(msg, line=self.lineno) + ) + + def _get_names(self): + """Get the names of the objects to include in the table. + + :returns: The names of the objects to include. + :rtype: generator(str) + """ + for line in self.content: + line = line.strip() + if line and re.search('^[a-zA-Z0-9]', line): + yield line + + def run(self): + self.warnings = [] + + env = self.state.document.settings.env + mapper = env.autoapi_mapper + + objects = [mapper.all_objects[name] for name in self._get_names()] + nodes_ = self._get_table(objects) + + if 'toctree' in self.options: + dirname = posixpath.dirname(env.docname) + + tree_prefix = self.options['toctree'].strip() + docnames = [] + for obj in objects: + docname = posixpath.join(tree_prefix, obj.name) + docname = posixpath.normpath(posixpath.join(dirname, docname)) + if docname not in env.found_docs: + self.warn( + 'toctree references unknown document {}'.format(docname) + ) + docnames.append(docname) + + tocnode = addnodes.toctree() + tocnode['includefiles'] = docnames + tocnode['entries'] = [(None, docn) for docn in docnames] + tocnode['maxdepth'] = -1 + tocnode['glob'] = None + + tocnode = sphinx.ext.autosummary.autosummary_toc('', '', tocnode) + nodes_.append(tocnode) + + return self.warnings + nodes_ + + def _get_row(self, obj): + template = ':{}:`{} <{}>`\\ {}' + if 'nosignatures' in self.options: + template = ':{}:`{} <{}>`' + + col1 = template.format( + 'obj', obj.short_name, obj.name, escape(obj.signature), + ) + col2 = obj.summary + + row = nodes.row('') + for text in (col1, col2): + node = nodes.paragraph('') + view_list = ViewList() + view_list.append(text, '') + self.state.nested_parse(view_list, 0, node) + try: + if isinstance(node[0], nodes.paragraph): + node = node[0] + except IndexError: + pass + row.append(nodes.entry('', node)) + + return row + + def _get_table(self, objects): + table_spec = addnodes.tabular_col_spec() + table_spec['spec'] = r'p{0.5\linewidth}p{0.5\linewidth}' + + table = sphinx.ext.autosummary.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) + + for obj in objects: + body.append(self._get_row(obj)) + + return [table_spec, table] class NestedParse(Directive): diff --git a/autoapi/extension.py b/autoapi/extension.py index a864330..aaa4ccb 100644 --- a/autoapi/extension.py +++ b/autoapi/extension.py @@ -14,7 +14,7 @@ from sphinx.errors import ExtensionError from docutils.parsers.rst import directives from .backends import default_file_mapping, default_ignore_patterns, default_backend_mapping -from .directives import NestedParse +from .directives import AutoapiSummary, NestedParse from .settings import API_ROOT from .toctree import add_domain_to_toctree @@ -54,11 +54,10 @@ def run_autoapi(app): normalized_root = os.path.normpath(os.path.join(app.confdir, app.config.autoapi_root)) url_root = os.path.join('/', app.config.autoapi_root) - app.env.autoapi_data = [] - sphinx_mapper = default_backend_mapping[app.config.autoapi_type] sphinx_mapper_obj = sphinx_mapper(app, template_dir=app.config.autoapi_template_dir, url_root=url_root) + app.env.autoapi_mapper = sphinx_mapper_obj if app.config.autoapi_file_patterns: file_patterns = app.config.autoapi_file_patterns @@ -147,11 +146,17 @@ def doctree_read(app, doctree): ) +def clear_env(app, env): + """Clears the environment of the unpicklable objects that we left behind.""" + env.autoapi_mapper = None + + def setup(app): app.connect('builder-inited', run_autoapi) app.connect('doctree-read', doctree_read) app.connect('doctree-resolved', add_domain_to_toctree) app.connect('build-finished', build_finished) + app.connect('env-updated', clear_env) app.add_config_value('autoapi_type', 'python', 'html') app.add_config_value('autoapi_root', API_ROOT, 'html') app.add_config_value('autoapi_ignore', [], 'html') @@ -164,3 +169,4 @@ def setup(app): app.add_config_value('autoapi_template_dir', [], 'html') app.add_stylesheet('autoapi.css') directives.register_directive('autoapi-nested-parse', NestedParse) + directives.register_directive('autoapisummary', AutoapiSummary) diff --git a/autoapi/mappers/base.py b/autoapi/mappers/base.py index ef273bc..15b0339 100644 --- a/autoapi/mappers/base.py +++ b/autoapi/mappers/base.py @@ -154,6 +154,10 @@ class PythonMapperBase(object): if pieces: return '.'.join(pieces) + @property + def signature(self): + return '({})'.format(','.join(self.args)) + class SphinxMapperBase(object): @@ -191,6 +195,8 @@ class SphinxMapperBase(object): self.paths = OrderedDict() # Mapping of {object id -> Python Object} self.objects = OrderedDict() + # Mapping of {object id -> Python Object} + self.all_objects = OrderedDict() # Mapping of {namespace id -> Python Object} self.namespaces = OrderedDict() # Mapping of {namespace id -> Python Object} @@ -263,6 +269,9 @@ class SphinxMapperBase(object): :param obj: Instance of a AutoAPI object ''' self.objects[obj.id] = obj + self.all_objects[obj.id] = obj + for child in obj.children: + self.all_objects[child.id] = child def map(self, options=None): '''Trigger find of serialized sources and build objects''' diff --git a/autoapi/mappers/python.py b/autoapi/mappers/python.py index 518706d..9a27a49 100644 --- a/autoapi/mappers/python.py +++ b/autoapi/mappers/python.py @@ -243,15 +243,26 @@ class PythonPythonMapper(PythonMapperBase): return arguments + @property + def summary(self): + for line in self.docstring.splitlines(): + line = line.strip() + if line: + return line + + return '' + class PythonFunction(PythonPythonMapper): type = 'function' is_callable = True + ref_directive = 'func' class PythonMethod(PythonPythonMapper): type = 'method' is_callable = True + ref_directive = 'meth' class PythonModule(PythonPythonMapper): diff --git a/autoapi/templates/python/function.rst b/autoapi/templates/python/function.rst index 08aef29..b20c8cc 100644 --- a/autoapi/templates/python/function.rst +++ b/autoapi/templates/python/function.rst @@ -6,4 +6,4 @@ {{ obj.docstring|prepare_docstring|indent(3) }} {% endif %} -{% endif %} \ No newline at end of file +{% endif %}