diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 08626bf..f62c53e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 `: (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 ^^^^^^^^^ diff --git a/autoapi/extension.py b/autoapi/extension.py index 8027821..fe6f2cd 100644 --- a/autoapi/extension.py +++ b/autoapi/extension.py @@ -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) diff --git a/autoapi/inheritance_diagrams.py b/autoapi/inheritance_diagrams.py new file mode 100644 index 0000000..0fe7687 --- /dev/null +++ b/autoapi/inheritance_diagrams.py @@ -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 diff --git a/autoapi/mappers/base.py b/autoapi/mappers/base.py index 61767a1..999703f 100644 --- a/autoapi/mappers/base.py +++ b/autoapi/mappers/base.py @@ -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 diff --git a/autoapi/templates/python/class.rst b/autoapi/templates/python/class.rst index 1c082dd..513157b 100644 --- a/autoapi/templates/python/class.rst +++ b/autoapi/templates/python/class.rst @@ -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) }} diff --git a/docs/reference/config.rst b/docs/reference/config.rst index e6fe42e..56580c2 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -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 `_ to be installed. + .. confval:: autoapi_include_summaries Default: ``False`` diff --git a/docs/reference/directives.rst b/docs/reference/directives.rst index f225e8d..69ea793 100644 --- a/docs/reference/directives.rst +++ b/docs/reference/directives.rst @@ -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 `_ to be installed. + + The directive can be configured using the same options as + :mod:`sphinx.ext.inheritance_diagram`. diff --git a/pylintrc b/pylintrc index 03bb764..5414f3f 100644 --- a/pylintrc +++ b/pylintrc @@ -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