Added inheritance diagrams support

Close #140
This commit is contained in:
Ashley Whetter 2020-01-11 17:20:50 -08:00
parent 3747596021
commit 076427e6ae
8 changed files with 191 additions and 3 deletions

View file

@ -12,6 +12,11 @@ Features
AutoAPI to search for implicit namespace packages.
* Added support for Sphinx 2.2 and 2.3.
* Added support for Python 3.8.
* `#140 <https://github.com/readthedocs/sphinx-autoapi/issues/140>`: (Python)
Added the ``autoapi-inheritance-diagram`` directive to create
inheritance diagrams without importing modules.
Enable the ``autoapi_include_inheritance_diagrams`` option to
turn the diagrams on in generated documentation.
Bug Fixes
^^^^^^^^^

View file

@ -24,6 +24,7 @@ from .backends import (
LANGUAGE_REQUIREMENTS,
)
from .directives import AutoapiSummary, NestedParse
from .inheritance_diagrams import AutoapiInheritanceDiagram
from .settings import API_ROOT
from .toctree import add_domain_to_toctree
@ -260,6 +261,7 @@ def setup(app):
app.add_config_value("autoapi_keep_files", False, "html")
app.add_config_value("autoapi_add_toctree_entry", True, "html")
app.add_config_value("autoapi_template_dir", None, "html")
app.add_config_value("autoapi_include_inheritance_graphs", False, "html")
app.add_config_value("autoapi_include_summaries", False, "html")
app.add_config_value("autoapi_python_use_implicit_namespaces", False, "html")
app.add_config_value("autoapi_python_class_content", "class", "html")
@ -277,3 +279,5 @@ def setup(app):
directives.register_directive("autoapisummary", AutoapiSummary)
app.setup_extension("sphinx.ext.autosummary")
app.add_event("autoapi-skip-member")
app.setup_extension("sphinx.ext.inheritance_diagram")
app.add_directive("autoapi-inheritance-diagram", AutoapiInheritanceDiagram)

View file

@ -0,0 +1,129 @@
import sys
import astroid
import sphinx.ext.inheritance_diagram
if sys.version_info >= (3,):
_BUILTINS = "builtins"
else:
_BUILTINS = "__builtins__"
def _import_class(name, currmodule):
path_stack = list(reversed(name.split(".")))
target = None
if currmodule:
try:
target = astroid.MANAGER.ast_from_module_name(currmodule)
while target and path_stack:
target = (target.getattr(path_stack.pop()) or (None,))[0]
except astroid.AstroidError:
target = None
if target is None:
path_stack = list(reversed(name.split(".")))
try:
target = astroid.MANAGER.ast_from_module_name(path_stack.pop())
while target and path_stack:
target = (target.getattr(path_stack.pop()) or (None,))[0]
except astroid.AstroidError:
target = None
if not target:
raise sphinx.ext.inheritance_diagram.InheritanceException(
"Could not import class or module {} specified for inheritance diagram".format(
name
)
)
if isinstance(target, astroid.ClassDef):
return [target]
if isinstance(target, astroid.Module):
classes = []
for child in target.children:
if isinstance(child, astroid.ClassDef):
classes.append(child)
return classes
raise sphinx.ext.inheritance_diagram.InheritanceException(
"{} specified for inheritance diagram is not a class or module".format(name)
)
class _AutoapiInheritanceGraph(sphinx.ext.inheritance_diagram.InheritanceGraph):
@staticmethod
def _import_classes(class_names, currmodule):
classes = []
for name in class_names:
classes.extend(_import_class(name, currmodule))
return classes
def _class_info(
self, classes, show_builtins, private_bases, parts, aliases, top_classes
): # pylint: disable=too-many-arguments
all_classes = {}
def recurse(cls):
if cls in all_classes:
return
if not show_builtins and cls.root().name == _BUILTINS:
return
if not private_bases and cls.name.startswith("_"):
return
nodename = self.class_name(cls, parts, aliases)
fullname = self.class_name(cls, 0, aliases)
tooltip = None
if cls.doc:
doc = cls.doc.strip().split("\n")[0]
if doc:
tooltip = '"%s"' % doc.replace('"', '\\"')
baselist = []
all_classes[cls] = (nodename, fullname, baselist, tooltip)
if fullname in top_classes:
return
for base in cls.ancestors(recurs=False):
if not show_builtins and base.root().name == _BUILTINS:
continue
if not private_bases and base.name.startswith("_"):
continue
baselist.append(self.class_name(base, parts, aliases))
if base not in all_classes:
recurse(base)
for cls in classes:
recurse(cls)
return list(all_classes.values())
@staticmethod
def class_name(node, parts=0, aliases=None):
fullname = node.qname()
if fullname.startswith(("__builtin__.", "builtins")):
fullname = fullname.split(".", 1)[-1]
if parts == 0:
result = fullname
else:
name_parts = fullname.split(".")
result = ".".join(name_parts[-parts:])
if aliases is not None and result in aliases:
return aliases[result]
return result
class AutoapiInheritanceDiagram(sphinx.ext.inheritance_diagram.InheritanceDiagram):
def run(self):
# Yucky! Monkeypatch InheritanceGraph to use our own
old_graph = sphinx.ext.inheritance_diagram.InheritanceGraph
sphinx.ext.inheritance_diagram.InheritanceGraph = _AutoapiInheritanceGraph
try:
return super(AutoapiInheritanceDiagram, self).run()
finally:
sphinx.ext.inheritance_diagram.InheritanceGraph = old_graph

View file

@ -85,7 +85,13 @@ class PythonMapperBase(object):
@property
def rendered(self):
"""Shortcut to render an object in templates."""
return self.render()
return self.render(
include_inheritance_graphs=self.app.config.autoapi_include_inheritance_graphs,
include_private_inheritance=(
"private-members" in self.app.config.autoapi_options
),
include_summaries=self.app.config.autoapi_include_summaries,
)
def get_context_data(self):
return {"obj": self, "sphinx_version": sphinx.version_info}
@ -291,7 +297,11 @@ class SphinxMapperBase(object):
stringify_func=(lambda x: x[0]),
):
rst = obj.render(
include_summaries=self.app.config.autoapi_include_summaries
include_inheritance_graphs=self.app.config.autoapi_include_inheritance_graphs,
include_private_inheritance=(
"private-members" in self.app.config.autoapi_options
),
include_summaries=self.app.config.autoapi_include_summaries,
)
if not rst:
continue

View file

@ -6,6 +6,12 @@
Bases: {% for base in obj.bases %}:class:`{{ base }}`{% if not loop.last %}, {% endif %}{% endfor %}
{% if include_inheritance_graphs and obj.bases != ["object"] %}
.. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }}
:parts: 1
{% if include_private_inheritance %}:private-bases:{% endif %}
{% endif %}
{% endif %}
{% if obj.docstring %}
{{ obj.docstring|prepare_docstring|indent(3) }}

View file

@ -113,6 +113,15 @@ Customisation Options
and you will need to include the generated documentation
in a TOC tree entry yourself.
.. confval:: autoapi_include_inheritance_graphs
Defalut: ``False``
Whether to include inheritance diagrams in generated class documentation.
This is a shortcut for needing to edit the templates yourself.
It makes use of the :mod:`sphinx.ext.inheritance_diagram` extension,
and requires `Graphviz <https://graphviz.org/>`_ to be installed.
.. confval:: autoapi_include_summaries
Default: ``False``

View file

@ -1,7 +1,10 @@
Directives
==========
.. _autodoc-directives:
Autodoc-Style Directives
========================
------------------------
You can opt to write API documentation yourself using autodoc style directives.
These directives work similarly to autodoc,
@ -40,3 +43,24 @@ The following directives are available:
Equivalent to :rst:dir:`autofunction`, :rst:dir:`autodata`,
:rst:dir:`automethod`, and :rst:dir:`autoattribute` respectively.
Inheritance Diagrams
--------------------
.. rst:directive:: autoapi-inheritance-diagram
This directive uses the :mod:`sphinx.ext.inheritance_diagram` extension
to create inheritance diagrams for classes.
For example:
.. autoapi-inheritance-diagram:: autoapi.mappers.python.objects.PythonModule autoapi.mappers.python.objects.PythonPackage
:parts: 1
:mod:`sphinx.ext.inheritance_diagram` makes use of the
:mod:`sphinx.ext.graphviz` extension,
and therefore it requires `Graphviz <https://graphviz.org/>`_ to be installed.
The directive can be configured using the same options as
:mod:`sphinx.ext.inheritance_diagram`.

View file

@ -11,6 +11,7 @@ disable=bad-continuation,
missing-class-docstring,
missing-function-docstring,
missing-module-docstring,
too-few-public-methods,
too-many-locals,
too-many-instance-attributes,
useless-object-inheritance