mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
New `inheritance_diagram
` extension to embed... inheritance diagrams!
This commit is contained in:
parent
75637a5d6a
commit
c5d0ba9913
1
AUTHORS
1
AUTHORS
@ -6,6 +6,7 @@ Substantial parts of the templates were written by Armin Ronacher
|
|||||||
Other contributors, listed alphabetically, are:
|
Other contributors, listed alphabetically, are:
|
||||||
|
|
||||||
* Daniel Bültmann -- todo extension
|
* Daniel Bültmann -- todo extension
|
||||||
|
* Michael Droettboom -- inheritance_diagram extension
|
||||||
* Charles Duffy -- original graphviz extension
|
* Charles Duffy -- original graphviz extension
|
||||||
* Josip Dzolonga -- coverage builder
|
* Josip Dzolonga -- coverage builder
|
||||||
* Horst Gutmann -- internationalization support
|
* Horst Gutmann -- internationalization support
|
||||||
|
3
CHANGES
3
CHANGES
@ -139,6 +139,9 @@ New features added
|
|||||||
|
|
||||||
- New ``graphviz`` extension to embed graphviz graphs.
|
- New ``graphviz`` extension to embed graphviz graphs.
|
||||||
|
|
||||||
|
- New ``inheritance_diagram`` extension to embed... inheritance
|
||||||
|
diagrams!
|
||||||
|
|
||||||
- Autodoc now has a reusable Python API, which can be used to
|
- Autodoc now has a reusable Python API, which can be used to
|
||||||
create custom types of objects to auto-document (e.g. Zope
|
create custom types of objects to auto-document (e.g. Zope
|
||||||
interfaces). See also ``Sphinx.add_autodocumenter()``.
|
interfaces). See also ``Sphinx.add_autodocumenter()``.
|
||||||
|
46
doc/ext/inheritance.rst
Normal file
46
doc/ext/inheritance.rst
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
.. highlight:: rest
|
||||||
|
|
||||||
|
The inheritance diagram extension
|
||||||
|
=================================
|
||||||
|
|
||||||
|
.. module:: sphinx.ext.inheritance_diagram
|
||||||
|
:synopsis: Support for displaying inheritance diagrams via graphviz.
|
||||||
|
|
||||||
|
.. versionadded:: 0.6
|
||||||
|
|
||||||
|
This extension allows you to include inheritance diagrams, rendered via the
|
||||||
|
:mod:`Graphviz extension <sphinx.ext.graphviz>`.
|
||||||
|
|
||||||
|
It adds this directive:
|
||||||
|
|
||||||
|
.. directive:: inheritance-diagram
|
||||||
|
|
||||||
|
This directive has one or more arguments, each giving a module or class
|
||||||
|
name. Class names can be unqualified; in that case they are taken to exist
|
||||||
|
in the currently described module (see :dir:`module`).
|
||||||
|
|
||||||
|
For each given class, and each class in each given module, the base classes
|
||||||
|
are determined. Then, from all classes and their base classes, a graph is
|
||||||
|
generated which is then rendered via the graphviz extension to a directed
|
||||||
|
graph.
|
||||||
|
|
||||||
|
This directive supports an option called ``parts`` that, if given, must be an
|
||||||
|
integer, advising the directive to remove that many parts of module names
|
||||||
|
from the displayed names. (For example, if all your class names start with
|
||||||
|
``lib.``, you can give ``:parts: 1`` to remove that prefix from the displayed
|
||||||
|
node names.)
|
||||||
|
|
||||||
|
|
||||||
|
New config values are:
|
||||||
|
|
||||||
|
.. confval:: inheritance_graph_attrs
|
||||||
|
|
||||||
|
A dictionary of graphviz graph attributes for inheritance diagrams.
|
||||||
|
|
||||||
|
.. confval:: inheritance_node_attrs
|
||||||
|
|
||||||
|
A dictionary of graphviz node attributes for inheritance diagrams.
|
||||||
|
|
||||||
|
.. confval:: inheritance_edge_attrs
|
||||||
|
|
||||||
|
A dictionary of graphviz edge attributes for inheritance diagrams.
|
367
sphinx/ext/inheritance_diagram.py
Normal file
367
sphinx/ext/inheritance_diagram.py
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
"""
|
||||||
|
sphinx.ext.inheritance_diagram
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Defines a docutils directive for inserting inheritance diagrams.
|
||||||
|
|
||||||
|
Provide the directive with one or more classes or modules (separated
|
||||||
|
by whitespace). For modules, all of the classes in that module will
|
||||||
|
be used.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
Given the following classes:
|
||||||
|
|
||||||
|
class A: pass
|
||||||
|
class B(A): pass
|
||||||
|
class C(A): pass
|
||||||
|
class D(B, C): pass
|
||||||
|
class E(B): pass
|
||||||
|
|
||||||
|
.. inheritance-diagram: D E
|
||||||
|
|
||||||
|
Produces a graph like the following:
|
||||||
|
|
||||||
|
A
|
||||||
|
/ \
|
||||||
|
B C
|
||||||
|
/ \ /
|
||||||
|
E D
|
||||||
|
|
||||||
|
The graph is inserted as a PNG+image map into HTML and a PDF in
|
||||||
|
LaTeX.
|
||||||
|
|
||||||
|
:copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
|
||||||
|
:license: BSD, see LICENSE for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
from hashlib import md5
|
||||||
|
except ImportError:
|
||||||
|
from md5 import md5
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.parsers.rst import directives
|
||||||
|
|
||||||
|
from sphinx.roles import xfileref_role
|
||||||
|
from sphinx.ext.graphviz import render_dot_html, render_dot_latex
|
||||||
|
from sphinx.util.compat import Directive
|
||||||
|
|
||||||
|
|
||||||
|
class_sig_re = re.compile(r'''^([\w.]*\.)? # module names
|
||||||
|
(\w+) \s* $ # class/final module name
|
||||||
|
''', re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
|
class InheritanceException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InheritanceGraph(object):
|
||||||
|
"""
|
||||||
|
Given a list of classes, determines the set of classes that they inherit
|
||||||
|
from all the way to the root "object", and then is able to generate a
|
||||||
|
graphviz dot graph from them.
|
||||||
|
"""
|
||||||
|
def __init__(self, class_names, currmodule, show_builtins=False):
|
||||||
|
"""
|
||||||
|
*class_names* is a list of child classes to show bases from.
|
||||||
|
|
||||||
|
If *show_builtins* is True, then Python builtins will be shown
|
||||||
|
in the graph.
|
||||||
|
"""
|
||||||
|
self.class_names = class_names
|
||||||
|
self.classes = self._import_classes(class_names, currmodule)
|
||||||
|
self.all_classes = self._all_classes(self.classes)
|
||||||
|
if len(self.all_classes) == 0:
|
||||||
|
raise InheritanceException('No classes found for '
|
||||||
|
'inheritance diagram')
|
||||||
|
self.show_builtins = show_builtins
|
||||||
|
|
||||||
|
def _import_class_or_module(self, name, currmodule):
|
||||||
|
"""
|
||||||
|
Import a class using its fully-qualified *name*.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
path, base = class_sig_re.match(name).groups()
|
||||||
|
except ValueError:
|
||||||
|
raise InheritanceException('Invalid class or module %r specified '
|
||||||
|
'for inheritance diagram' % name)
|
||||||
|
|
||||||
|
fullname = (path or '') + base
|
||||||
|
path = (path and path.rstrip('.') or '')
|
||||||
|
|
||||||
|
# two possibilities: either it is a module, then import it
|
||||||
|
try:
|
||||||
|
module = __import__(fullname)
|
||||||
|
todoc = sys.modules[fullname]
|
||||||
|
except ImportError:
|
||||||
|
# else it is a class, then import the module
|
||||||
|
if not path:
|
||||||
|
if currmodule:
|
||||||
|
# try the current module
|
||||||
|
path = currmodule
|
||||||
|
else:
|
||||||
|
raise InheritanceException(
|
||||||
|
'Could not import class %r specified for '
|
||||||
|
'inheritance diagram' % base)
|
||||||
|
try:
|
||||||
|
module = __import__(path)
|
||||||
|
todoc = getattr(sys.modules[path], base)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
raise InheritanceException(
|
||||||
|
'Could not import class or module %r specified for '
|
||||||
|
'inheritance diagram' % (path + '.' + base))
|
||||||
|
|
||||||
|
# If a class, just return it
|
||||||
|
if inspect.isclass(todoc):
|
||||||
|
return [todoc]
|
||||||
|
elif inspect.ismodule(todoc):
|
||||||
|
classes = []
|
||||||
|
for cls in todoc.__dict__.values():
|
||||||
|
if inspect.isclass(cls) and cls.__module__ == todoc.__name__:
|
||||||
|
classes.append(cls)
|
||||||
|
return classes
|
||||||
|
raise InheritanceException('%r specified for inheritance diagram is '
|
||||||
|
'not a class or module' % name)
|
||||||
|
|
||||||
|
def _import_classes(self, class_names, currmodule):
|
||||||
|
"""
|
||||||
|
Import a list of classes.
|
||||||
|
"""
|
||||||
|
classes = []
|
||||||
|
for name in class_names:
|
||||||
|
classes.extend(self._import_class_or_module(name, currmodule))
|
||||||
|
return classes
|
||||||
|
|
||||||
|
def _all_classes(self, classes):
|
||||||
|
"""
|
||||||
|
Return a list of all classes that are ancestors of *classes*.
|
||||||
|
"""
|
||||||
|
all_classes = {}
|
||||||
|
|
||||||
|
def recurse(cls):
|
||||||
|
all_classes[cls] = None
|
||||||
|
for c in cls.__bases__:
|
||||||
|
if c not in all_classes:
|
||||||
|
recurse(c)
|
||||||
|
|
||||||
|
for cls in classes:
|
||||||
|
recurse(cls)
|
||||||
|
|
||||||
|
return all_classes.keys()
|
||||||
|
|
||||||
|
def class_name(self, cls, parts=0):
|
||||||
|
"""
|
||||||
|
Given a class object, return a fully-qualified name. This
|
||||||
|
works for things I've tested in matplotlib so far, but may not
|
||||||
|
be completely general.
|
||||||
|
"""
|
||||||
|
module = cls.__module__
|
||||||
|
if module == '__builtin__':
|
||||||
|
fullname = cls.__name__
|
||||||
|
else:
|
||||||
|
fullname = '%s.%s' % (module, cls.__name__)
|
||||||
|
if parts == 0:
|
||||||
|
return fullname
|
||||||
|
name_parts = fullname.split('.')
|
||||||
|
return '.'.join(name_parts[-parts:])
|
||||||
|
|
||||||
|
def get_all_class_names(self):
|
||||||
|
"""
|
||||||
|
Get all of the class names involved in the graph.
|
||||||
|
"""
|
||||||
|
return [self.class_name(x) for x in self.all_classes]
|
||||||
|
|
||||||
|
# These are the default attrs for graphviz
|
||||||
|
default_graph_attrs = {
|
||||||
|
'rankdir': 'LR',
|
||||||
|
'size': '"8.0, 12.0"',
|
||||||
|
}
|
||||||
|
default_node_attrs = {
|
||||||
|
'shape': 'box',
|
||||||
|
'fontsize': 10,
|
||||||
|
'height': 0.25,
|
||||||
|
'fontname': 'Vera Sans, DejaVu Sans, Liberation Sans, '
|
||||||
|
'Arial, Helvetica, sans',
|
||||||
|
'style': '"setlinewidth(0.5)"',
|
||||||
|
}
|
||||||
|
default_edge_attrs = {
|
||||||
|
'arrowsize': 0.5,
|
||||||
|
'style': '"setlinewidth(0.5)"',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _format_node_attrs(self, attrs):
|
||||||
|
return ','.join(['%s=%s' % x for x in attrs.items()])
|
||||||
|
|
||||||
|
def _format_graph_attrs(self, attrs):
|
||||||
|
return ''.join(['%s=%s;\n' % x for x in attrs.items()])
|
||||||
|
|
||||||
|
def generate_dot(self, name, parts=0, urls={}, env=None,
|
||||||
|
graph_attrs={}, node_attrs={}, edge_attrs={}):
|
||||||
|
"""
|
||||||
|
Generate a graphviz dot graph from the classes that
|
||||||
|
were passed in to __init__.
|
||||||
|
|
||||||
|
*name* is the name of the graph.
|
||||||
|
|
||||||
|
*urls* is a dictionary mapping class names to HTTP URLs.
|
||||||
|
|
||||||
|
*graph_attrs*, *node_attrs*, *edge_attrs* are dictionaries containing
|
||||||
|
key/value pairs to pass on as graphviz properties.
|
||||||
|
"""
|
||||||
|
g_attrs = self.default_graph_attrs.copy()
|
||||||
|
n_attrs = self.default_node_attrs.copy()
|
||||||
|
e_attrs = self.default_edge_attrs.copy()
|
||||||
|
g_attrs.update(graph_attrs)
|
||||||
|
n_attrs.update(node_attrs)
|
||||||
|
e_attrs.update(edge_attrs)
|
||||||
|
if env:
|
||||||
|
g_attrs.update(env.config.inheritance_graph_attrs)
|
||||||
|
n_attrs.update(env.config.inheritance_node_attrs)
|
||||||
|
e_attrs.update(env.config.inheritance_edge_attrs)
|
||||||
|
|
||||||
|
res = []
|
||||||
|
res.append('digraph %s {\n' % name)
|
||||||
|
res.append(self._format_graph_attrs(g_attrs))
|
||||||
|
|
||||||
|
for cls in self.all_classes:
|
||||||
|
if not self.show_builtins and cls in __builtins__.values():
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = self.class_name(cls, parts)
|
||||||
|
|
||||||
|
# Write the node
|
||||||
|
this_node_attrs = n_attrs.copy()
|
||||||
|
url = urls.get(self.class_name(cls))
|
||||||
|
if url is not None:
|
||||||
|
this_node_attrs['URL'] = '"%s"' % url
|
||||||
|
res.append(' "%s" [%s];\n' %
|
||||||
|
(name, self._format_node_attrs(this_node_attrs)))
|
||||||
|
|
||||||
|
# Write the edges
|
||||||
|
for base in cls.__bases__:
|
||||||
|
if not self.show_builtins and base in __builtins__.values():
|
||||||
|
continue
|
||||||
|
|
||||||
|
base_name = self.class_name(base, parts)
|
||||||
|
res.append(' "%s" -> "%s" [%s];\n' %
|
||||||
|
(base_name, name,
|
||||||
|
self._format_node_attrs(e_attrs)))
|
||||||
|
res.append('}\n')
|
||||||
|
return ''.join(res)
|
||||||
|
|
||||||
|
|
||||||
|
class inheritance_diagram(nodes.General, nodes.Element):
|
||||||
|
"""
|
||||||
|
A docutils node to use as a placeholder for the inheritance diagram.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InheritanceDiagram(Directive):
|
||||||
|
"""
|
||||||
|
Run when the inheritance_diagram directive is first encountered.
|
||||||
|
"""
|
||||||
|
has_content = False
|
||||||
|
required_arguments = 1
|
||||||
|
optional_arguments = 0
|
||||||
|
final_argument_whitespace = True
|
||||||
|
option_spec = {
|
||||||
|
'parts': directives.nonnegative_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
node = inheritance_diagram()
|
||||||
|
node.document = self.state.document
|
||||||
|
env = self.state.document.settings.env
|
||||||
|
class_names = self.arguments[0].split()
|
||||||
|
|
||||||
|
# Create a graph starting with the list of classes
|
||||||
|
try:
|
||||||
|
graph = InheritanceGraph(class_names, env.currmodule)
|
||||||
|
except InheritanceException, err:
|
||||||
|
return [node.document.reporter.warning(err.args[0],
|
||||||
|
line=self.lineno)]
|
||||||
|
|
||||||
|
# Create xref nodes for each target of the graph's image map and
|
||||||
|
# add them to the doc tree so that Sphinx can resolve the
|
||||||
|
# references to real URLs later. These nodes will eventually be
|
||||||
|
# removed from the doctree after we're done with them.
|
||||||
|
for name in graph.get_all_class_names():
|
||||||
|
refnodes, x = xfileref_role(
|
||||||
|
'class', ':class:`%s`' % name, name, 0, self.state)
|
||||||
|
node.extend(refnodes)
|
||||||
|
# Store the graph object so we can use it to generate the
|
||||||
|
# dot file later
|
||||||
|
node['graph'] = graph
|
||||||
|
# Store the original content for use as a hash
|
||||||
|
node['parts'] = self.options.get('parts', 0)
|
||||||
|
node['content'] = ' '.join(class_names)
|
||||||
|
return [node]
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph_hash(node):
|
||||||
|
return md5(node['content'] + str(node['parts'])).hexdigest()[-10:]
|
||||||
|
|
||||||
|
|
||||||
|
def html_visit_inheritance_diagram(self, node):
|
||||||
|
"""
|
||||||
|
Output the graph for HTML. This will insert a PNG with clickable
|
||||||
|
image map.
|
||||||
|
"""
|
||||||
|
graph = node['graph']
|
||||||
|
parts = node['parts']
|
||||||
|
|
||||||
|
graph_hash = get_graph_hash(node)
|
||||||
|
name = 'inheritance%s' % graph_hash
|
||||||
|
|
||||||
|
# Create a mapping from fully-qualified class names to URLs.
|
||||||
|
urls = {}
|
||||||
|
for child in node:
|
||||||
|
if child.get('refuri') is not None:
|
||||||
|
urls[child['reftitle']] = child.get('refuri')
|
||||||
|
elif child.get('refid') is not None:
|
||||||
|
urls[child['reftitle']] = '#' + child.get('refid')
|
||||||
|
|
||||||
|
dotcode = graph.generate_dot(name, parts, urls, env=self.builder.env)
|
||||||
|
render_dot_html(self, node, dotcode, [], 'inheritance', 'inheritance')
|
||||||
|
raise nodes.SkipNode
|
||||||
|
|
||||||
|
|
||||||
|
def latex_visit_inheritance_diagram(self, node):
|
||||||
|
"""
|
||||||
|
Output the graph for LaTeX. This will insert a PDF.
|
||||||
|
"""
|
||||||
|
graph = node['graph']
|
||||||
|
parts = node['parts']
|
||||||
|
|
||||||
|
graph_hash = get_graph_hash(node)
|
||||||
|
name = 'inheritance%s' % graph_hash
|
||||||
|
|
||||||
|
dotcode = graph.generate_dot(name, parts, urls, env=self.builder.env,
|
||||||
|
graph_attrs={'size': '"6.0,6.0"'})
|
||||||
|
render_dot_latex(self, node, dotcode, [], 'inheritance')
|
||||||
|
raise nodes.SkipNode
|
||||||
|
|
||||||
|
|
||||||
|
def skip(self, node):
|
||||||
|
raise nodes.SkipNode
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.setup_extension('sphinx.ext.graphviz')
|
||||||
|
app.add_node(
|
||||||
|
inheritance_diagram,
|
||||||
|
latex=(latex_visit_inheritance_diagram, None),
|
||||||
|
html=(html_visit_inheritance_diagram, None),
|
||||||
|
text=(skip, None))
|
||||||
|
app.add_directive('inheritance-diagram', InheritanceDiagram)
|
||||||
|
app.add_config_value('inheritance_graph_attrs', {}, False),
|
||||||
|
app.add_config_value('inheritance_node_attrs', {}, False),
|
||||||
|
app.add_config_value('inheritance_edge_attrs', {}, False),
|
Loading…
Reference in New Issue
Block a user