Add top-classes option to sphinx.ext.inheritance_diagram

This will limit the scope of inheritance traversal
This commit is contained in:
Mr. Senko 2016-01-21 11:11:06 +02:00
parent 72e70b1b36
commit 9486f0d8f7
7 changed files with 165 additions and 7 deletions

View File

@ -17,6 +17,8 @@ Features added
* C++, add a ``cpp:expr`` role for inserting inline C++ expressions or types.
* #3638: Allow to change a label of reference to equation using
``math_eqref_format``
* Add ``top-classes`` option for the ``sphinx.ext.inheritance_diagram``
extension to limit the scope of inheritance graphs.
Features removed
----------------
@ -52,8 +54,10 @@ Bugs fixed
Testing
--------
* Add tests for the ``sphinx.ext.inheritance_diagram`` extension.
Release 1.6.3 (in development)
==============================

View File

@ -42,6 +42,58 @@ It adds this directive:
.. versionchanged:: 1.5
Added ``caption`` option
It also supports a ``top-classes`` option which requires one or more class
names separated by comma. If specified inheritance traversal will stop at the
specified class names. Given the following Python module::
"""
A
/ \
B C
/ \ / \
E D F
"""
class A(object):
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
class E(B):
pass
class F(C):
pass
If you have specified a module in the inheritance diagram like this::
.. inheritance-diagram::
dummy.test
:top-classes: dummy.test.B, dummy.test.C
any base classes which are ancestors to ``top-classes`` and are also defined
in the same module will be rendered as stand alone nodes. In this example
class A will be rendered as stand alone node in the graph. This is a known
issue due to how this extension works internally.
If you don't want class A (or any other ancestors) to be visible then specify
only the classes you would like to generate the diagram for like this::
.. inheritance-diagram::
dummy.test.D
dummy.test.E
dummy.test.F
:top-classes: dummy.test.B, dummy.test.C
.. versionchanged:: 1.7
Added ``top-classes`` option to limit the scope of inheritance graphs.
New config values are:

View File

@ -133,8 +133,8 @@ class InheritanceGraph(object):
graphviz dot graph from them.
"""
def __init__(self, class_names, currmodule, show_builtins=False,
private_bases=False, parts=0):
# type: (unicode, str, bool, bool, int) -> None
private_bases=False, parts=0, top_classes=[]):
# type: (unicode, str, bool, bool, int, List[Any]) -> None
"""*class_names* is a list of child classes to show bases from.
If *show_builtins* is True, then Python builtins will be shown
@ -143,7 +143,7 @@ class InheritanceGraph(object):
self.class_names = class_names
classes = self._import_classes(class_names, currmodule)
self.class_info = self._class_info(classes, show_builtins,
private_bases, parts)
private_bases, parts, top_classes)
if not self.class_info:
raise InheritanceException('No classes found for '
'inheritance diagram')
@ -156,13 +156,16 @@ class InheritanceGraph(object):
classes.extend(import_classes(name, currmodule))
return classes
def _class_info(self, classes, show_builtins, private_bases, parts):
# type: (List[Any], bool, bool, int) -> List[Tuple[unicode, unicode, List[unicode], unicode]] # NOQA
def _class_info(self, classes, show_builtins, private_bases, parts, top_classes):
# type: (List[Any], bool, bool, int, List[Any]) -> List[Tuple[unicode, unicode, List[unicode], unicode]] # NOQA
"""Return name and bases for all classes that are ancestors of
*classes*.
*parts* gives the number of dotted name parts that is removed from the
displayed node names.
*top_classes* gives the name(s) of the top most ancestor class to traverse
to. Multiple names can be specified separated by comma.
"""
all_classes = {}
py_builtins = vars(builtins).values()
@ -192,6 +195,10 @@ class InheritanceGraph(object):
baselist = [] # type: List[unicode]
all_classes[cls] = (nodename, fullname, baselist, tooltip)
if fullname in top_classes:
return
for base in cls.__bases__:
if not show_builtins and base in py_builtins:
continue
@ -321,6 +328,7 @@ class InheritanceDiagram(Directive):
'parts': directives.nonnegative_int,
'private-bases': directives.flag,
'caption': directives.unchanged,
'top-classes': directives.unchanged_required,
}
def run(self):
@ -333,13 +341,19 @@ class InheritanceDiagram(Directive):
# Store the original content for use as a hash
node['parts'] = self.options.get('parts', 0)
node['content'] = ', '.join(class_names)
node['top-classes'] = []
for cls in self.options.get('top-classes', '').split(','):
cls = cls.strip()
if cls:
node['top-classes'].append(cls)
# Create a graph starting with the list of classes
try:
graph = InheritanceGraph(
class_names, env.ref_context.get('py:module'),
parts=node['parts'],
private_bases='private-bases' in self.options)
private_bases='private-bases' in self.options,
top_classes=node['top-classes'])
except InheritanceException as err:
return [node.document.reporter.warning(err.args[0],
line=self.lineno)]

View File

@ -0,0 +1,6 @@
Diagram using module with 2 top classes
=======================================
.. inheritance-diagram::
dummy.test
:top-classes: dummy.test.B, dummy.test.C

View File

@ -0,0 +1,7 @@
Diagram using 1 top class
=========================
.. inheritance-diagram::
dummy.test
:top-classes: dummy.test.B

View File

@ -0,0 +1,9 @@
Diagram using 2 top classes
===========================
.. inheritance-diagram::
dummy.test.F
dummy.test.D
dummy.test.E
:top-classes: dummy.test.B, dummy.test.C

View File

@ -13,6 +13,7 @@ import os
import pytest
from sphinx.ext.inheritance_diagram import InheritanceDiagram
@pytest.mark.sphinx(buildername="html", testroot="inheritance")
@pytest.mark.usefixtures('if_graphviz_found')
def test_inheritance_diagram(app, status, warning):
@ -51,7 +52,8 @@ def test_inheritance_diagram(app, status, warning):
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None),
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
('dummy.test.D', 'dummy.test.D',
['dummy.test.B', 'dummy.test.C'], None),
('dummy.test.B', 'dummy.test.B', ['dummy.test.A'], None)
]
@ -65,3 +67,67 @@ def test_inheritance_diagram(app, status, warning):
('D', 'dummy.test.D', ['B', 'C'], None),
('B', 'dummy.test.B', ['A'], None)
]
# inheritance diagram with 1 top class
# :top-classes: dummy.test.B
# rendering should be
# A
# \
# B C
# / \ / \
# E D F
#
for cls in graphs['diagram_w_1_top_class'].class_info:
assert cls in [
('dummy.test.A', 'dummy.test.A', [], None),
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None),
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
('dummy.test.D', 'dummy.test.D',
['dummy.test.B', 'dummy.test.C'], None),
('dummy.test.B', 'dummy.test.B', [], None)
]
# inheritance diagram with 2 top classes
# :top-classes: dummy.test.B, dummy.test.C
# Note: we're specifying separate classes, not the entire module here
# rendering should be
#
# B C
# / \ / \
# E D F
#
for cls in graphs['diagram_w_2_top_classes'].class_info:
assert cls in [
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
('dummy.test.C', 'dummy.test.C', [], None),
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
('dummy.test.D', 'dummy.test.D',
['dummy.test.B', 'dummy.test.C'], None),
('dummy.test.B', 'dummy.test.B', [], None)
]
# inheritance diagram with 2 top classes and specifiying the entire module
# rendering should be
#
# A
# B C
# / \ / \
# E D F
#
# Note: dummy.test.A is included in the graph before its descendants are even processed
# b/c we've specified to load the entire module. The way InheritanceGraph works it is very
# hard to exclude parent classes once after they have been included in the graph.
# If you'd like to not show class A in the graph don't specify the entire module.
# this is a known issue.
for cls in graphs['diagram_module_w_2_top_classes'].class_info:
assert cls in [
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
('dummy.test.C', 'dummy.test.C', [], None),
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
('dummy.test.D', 'dummy.test.D',
['dummy.test.B', 'dummy.test.C'], None),
('dummy.test.B', 'dummy.test.B', [], None),
('dummy.test.A', 'dummy.test.A', [], None),
]