Add :include-subclasses: option to sphinx.ext.inheritance_diagram (#8159)

When this option is given, all sub-classes of the classes listed as arguments to `inheritance-diagram` are included in the diagram.

This makes it possible to generate a complete inheritance tree by just listing the base class and adding `:include-subclasses:`.

This is different from specifying the module that contains this base class, as this module might include other classes that should not be part of the diagram.

Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
Walter Dörwald 2025-02-02 21:52:23 +01:00 committed by GitHub
parent 34519be296
commit 50fb4a9dbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 15 deletions

View File

@ -91,6 +91,9 @@ Features added
* #12507: Add the :ref:`collapsible <collapsible-admonitions>` option
to admonition directives.
Patch by Chris Sewell.
* #8191, #8159: Add :rst:dir:`inheritance-diagram:include-subclasses` option to
the :rst:dir:`inheritance-diagram` directive.
Patch by Walter Dörwald.
Bugs fixed
----------

View File

@ -100,6 +100,24 @@ It adds this directive:
.. versionchanged:: 1.7
Added ``top-classes`` option to limit the scope of inheritance graphs.
.. rst:directive:option:: include-subclasses
:type: no value
.. versionadded:: 8.2
If given, any subclass of the classes will be added to the diagram too.
Given the Python module from above, you can specify
your inheritance diagram like this:
.. code-block:: rst
.. inheritance-diagram:: dummy.test.A
:include-subclasses:
This will include the classes A, B, C, D, E and F in the inheritance diagram
but no other classes in the module ``dummy.test``.
Examples
--------

View File

@ -52,7 +52,7 @@ from sphinx.ext.graphviz import (
from sphinx.util.docutils import SphinxDirective
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from collections.abc import Collection, Iterable, Iterator, Sequence, Set
from typing import Any, ClassVar, Final
from docutils.nodes import Node
@ -106,7 +106,7 @@ def try_import(objname: str) -> Any:
return None
def import_classes(name: str, currmodule: str) -> Any:
def import_classes(name: str, currmodule: str) -> list[type[Any]]:
"""Import a class using its fully-qualified *name*."""
target = None
@ -156,7 +156,8 @@ class InheritanceGraph:
private_bases: bool = False,
parts: int = 0,
aliases: dict[str, str] | None = None,
top_classes: Sequence[Any] = (),
top_classes: Set[str] = frozenset(),
include_subclasses: bool = False,
) -> None:
"""*class_names* is a list of child classes to show bases from.
@ -164,7 +165,12 @@ class InheritanceGraph:
in the graph.
"""
self.class_names = class_names
classes = self._import_classes(class_names, currmodule)
classes: Collection[type[Any]] = self._import_classes(class_names, currmodule)
if include_subclasses:
classes_set = {*classes}
for cls in tuple(classes_set):
classes_set.update(_subclasses(cls))
classes = classes_set
self.class_info = self._class_info(
classes, show_builtins, private_bases, parts, aliases, top_classes
)
@ -172,21 +178,23 @@ class InheritanceGraph:
msg = 'No classes found for inheritance diagram'
raise InheritanceException(msg)
def _import_classes(self, class_names: list[str], currmodule: str) -> list[Any]:
def _import_classes(
self, class_names: list[str], currmodule: str
) -> Sequence[type[Any]]:
"""Import a list of classes."""
classes: list[Any] = []
classes: list[type[Any]] = []
for name in class_names:
classes.extend(import_classes(name, currmodule))
return classes
def _class_info(
self,
classes: list[Any],
classes: Collection[type[Any]],
show_builtins: bool,
private_bases: bool,
parts: int,
aliases: dict[str, str] | None,
top_classes: Sequence[Any],
top_classes: Set[str],
) -> list[tuple[str, str, Sequence[str], str | None]]:
"""Return name and bases for all classes that are ancestors of
*classes*.
@ -205,7 +213,7 @@ class InheritanceGraph:
"""
all_classes = {}
def recurse(cls: Any) -> None:
def recurse(cls: type[Any]) -> None:
if not show_builtins and cls in PY_BUILTINS:
return
if not private_bases and cls.__name__.startswith('_'):
@ -248,7 +256,7 @@ class InheritanceGraph:
]
def class_name(
self, cls: Any, parts: int = 0, aliases: dict[str, str] | None = None
self, cls: type[Any], parts: int = 0, aliases: dict[str, str] | None = None
) -> str:
"""Given a class object, return a fully-qualified name.
@ -377,6 +385,7 @@ class InheritanceDiagram(SphinxDirective):
'private-bases': directives.flag,
'caption': directives.unchanged,
'top-classes': directives.unchanged_required,
'include-subclasses': directives.flag,
}
def run(self) -> list[Node]:
@ -387,11 +396,11 @@ class InheritanceDiagram(SphinxDirective):
# 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)
node['top-classes'] = frozenset({
cls_stripped
for cls in self.options.get('top-classes', '').split(',')
if (cls_stripped := cls.strip())
})
# Create a graph starting with the list of classes
try:
@ -402,6 +411,7 @@ class InheritanceDiagram(SphinxDirective):
private_bases='private-bases' in self.options,
aliases=self.config.inheritance_alias,
top_classes=node['top-classes'],
include_subclasses='include-subclasses' in self.options,
)
except InheritanceException as err:
return [node.document.reporter.warning(err, line=self.lineno)]
@ -428,6 +438,12 @@ class InheritanceDiagram(SphinxDirective):
return [figure]
def _subclasses(cls: type[Any]) -> Iterator[type[Any]]:
yield cls
for sub_cls in cls.__subclasses__():
yield from _subclasses(sub_cls)
def get_graph_hash(node: inheritance_diagram) -> str:
encoded = (node['content'] + str(node['parts'])).encode()
return hashlib.md5(encoded, usedforsecurity=False).hexdigest()[-10:]