summaryrefslogtreecommitdiffstats
path: root/tests/test_ext_inheritance_diagram.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_ext_inheritance_diagram.py')
-rw-r--r--tests/test_ext_inheritance_diagram.py342
1 files changed, 342 insertions, 0 deletions
diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py
new file mode 100644
index 0000000..9ace5ad
--- /dev/null
+++ b/tests/test_ext_inheritance_diagram.py
@@ -0,0 +1,342 @@
+"""Test sphinx.ext.inheritance_diagram extension."""
+
+import os
+import re
+import sys
+import zlib
+
+import pytest
+
+from sphinx.ext.inheritance_diagram import (
+ InheritanceDiagram,
+ InheritanceException,
+ import_classes,
+)
+from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping
+
+
+@pytest.mark.sphinx(buildername="html", testroot="inheritance")
+@pytest.mark.usefixtures('if_graphviz_found')
+def test_inheritance_diagram(app, status, warning):
+ # monkey-patch InheritaceDiagram.run() so we can get access to its
+ # results.
+ orig_run = InheritanceDiagram.run
+ graphs = {}
+
+ def new_run(self):
+ result = orig_run(self)
+ node = result[0]
+ source = os.path.basename(node.document.current_source).replace(".rst", "")
+ graphs[source] = node['graph']
+ return result
+
+ InheritanceDiagram.run = new_run
+
+ try:
+ app.builder.build_all()
+ finally:
+ InheritanceDiagram.run = orig_run
+
+ assert app.statuscode == 0
+
+ html_warnings = warning.getvalue()
+ assert html_warnings == ""
+
+ # note: it is better to split these asserts into separate test functions
+ # but I can't figure out how to build only a specific .rst file
+
+ # basic inheritance diagram showing all classes
+ for cls in graphs['basic_diagram'].class_info:
+ # use in b/c traversing order is different sometimes
+ 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', ['dummy.test.A'], None),
+ ]
+
+ # inheritance diagram using :parts: 1 option
+ for cls in graphs['diagram_w_parts'].class_info:
+ assert cls in [
+ ('A', 'dummy.test.A', [], None),
+ ('F', 'dummy.test.F', ['C'], None),
+ ('C', 'dummy.test.C', ['A'], None),
+ ('E', 'dummy.test.E', ['B'], None),
+ ('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 specifying 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),
+ ]
+
+ # inheritance diagram involving a base class nested within another class
+ for cls in graphs['diagram_w_nested_classes'].class_info:
+ assert cls in [
+ ('dummy.test_nested.A', 'dummy.test_nested.A', [], None),
+ ('dummy.test_nested.C', 'dummy.test_nested.C', ['dummy.test_nested.A.B'], None),
+ ('dummy.test_nested.A.B', 'dummy.test_nested.A.B', [], None),
+ ]
+
+
+# An external inventory to test intersphinx links in inheritance diagrams
+external_inventory = b'''\
+# Sphinx inventory version 2
+# Project: external
+# Version: 1.0
+# The remainder of this file is compressed using zlib.
+''' + zlib.compress(b'''\
+external.other.Bob py:class 1 foo.html#external.other.Bob -
+''')
+
+
+@pytest.mark.sphinx('html', testroot='ext-inheritance_diagram')
+@pytest.mark.usefixtures('if_graphviz_found')
+def test_inheritance_diagram_png_html(tmp_path, app):
+ inv_file = tmp_path / 'inventory'
+ inv_file.write_bytes(external_inventory)
+ app.config.intersphinx_mapping = {
+ 'https://example.org': str(inv_file),
+ }
+ app.config.intersphinx_cache_limit = 0
+ normalize_intersphinx_mapping(app, app.config)
+ load_mappings(app)
+
+ app.builder.build_all()
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ base_maps = re.findall('<map .+\n.+\n</map>', content)
+
+ pattern = ('<figure class="align-default" id="id1">\n'
+ '<div class="graphviz">'
+ '<img src="_images/inheritance-\\w+.png" alt="Inheritance diagram of test.Foo" '
+ 'class="inheritance graphviz" /></div>\n<figcaption>\n<p>'
+ '<span class="caption-text">Test Foo!</span><a class="headerlink" href="#id1" '
+ 'title="Link to this image">\xb6</a></p>\n</figcaption>\n</figure>\n')
+ assert re.search(pattern, content, re.M)
+
+ subdir_content = (app.outdir / 'subdir/page1.html').read_text(encoding='utf8')
+ subdir_maps = re.findall('<map .+\n.+\n</map>', subdir_content)
+ subdir_maps = [re.sub('href="(\\S+)"', 'href="subdir/\\g<1>"', s) for s in subdir_maps]
+
+ # Go through the clickmap for every PNG inheritance diagram
+ for diagram_content in base_maps + subdir_maps:
+ # Verify that an intersphinx link was created via the external inventory
+ if 'subdir.' in diagram_content:
+ assert "https://example.org" in diagram_content
+
+ # Extract every link in the inheritance diagram
+ for href in re.findall('href="(\\S+?)"', diagram_content):
+ if '://' in href:
+ # Verify that absolute URLs are not prefixed with ../
+ assert href.startswith("https://example.org/")
+ else:
+ # Verify that relative URLs point to existing documents
+ reluri = href.rsplit('#', 1)[0] # strip the anchor at the end
+ assert (app.outdir / reluri).exists()
+
+
+@pytest.mark.sphinx('html', testroot='ext-inheritance_diagram',
+ confoverrides={'graphviz_output_format': 'svg'})
+@pytest.mark.usefixtures('if_graphviz_found')
+def test_inheritance_diagram_svg_html(tmp_path, app):
+ inv_file = tmp_path / 'inventory'
+ inv_file.write_bytes(external_inventory)
+ app.config.intersphinx_mapping = {
+ "subdir": ('https://example.org', str(inv_file)),
+ }
+ app.config.intersphinx_cache_limit = 0
+ normalize_intersphinx_mapping(app, app.config)
+ load_mappings(app)
+
+ app.builder.build_all()
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ base_svgs = re.findall('<object data="(_images/inheritance-\\w+.svg?)"', content)
+
+ pattern = ('<figure class="align-default" id="id1">\n'
+ '<div class="graphviz">'
+ '<object data="_images/inheritance-\\w+.svg" '
+ 'type="image/svg\\+xml" class="inheritance graphviz">\n'
+ '<p class=\"warning\">Inheritance diagram of test.Foo</p>'
+ '</object></div>\n<figcaption>\n<p><span class="caption-text">'
+ 'Test Foo!</span><a class="headerlink" href="#id1" '
+ 'title="Link to this image">\xb6</a></p>\n</figcaption>\n</figure>\n')
+
+ assert re.search(pattern, content, re.M)
+
+ subdir_content = (app.outdir / 'subdir/page1.html').read_text(encoding='utf8')
+ subdir_svgs = re.findall('<object data="../(_images/inheritance-\\w+.svg?)"', subdir_content)
+
+ # Go through every SVG inheritance diagram
+ for diagram in base_svgs + subdir_svgs:
+ diagram_content = (app.outdir / diagram).read_text(encoding='utf8')
+
+ # Verify that an intersphinx link was created via the external inventory
+ if 'subdir.' in diagram_content:
+ assert "https://example.org" in diagram_content
+
+ # Extract every link in the inheritance diagram
+ for href in re.findall('href="(\\S+?)"', diagram_content):
+ if '://' in href:
+ # Verify that absolute URLs are not prefixed with ../
+ assert href.startswith("https://example.org/")
+ else:
+ # Verify that relative URLs point to existing documents
+ reluri = href.rsplit('#', 1)[0] # strip the anchor at the end
+ abs_uri = (app.outdir / app.builder.imagedir / reluri).resolve()
+ assert abs_uri.exists()
+
+
+@pytest.mark.sphinx('latex', testroot='ext-inheritance_diagram')
+@pytest.mark.usefixtures('if_graphviz_found')
+def test_inheritance_diagram_latex(app, status, warning):
+ app.builder.build_all()
+
+ content = (app.outdir / 'python.tex').read_text(encoding='utf8')
+
+ pattern = ('\\\\begin{figure}\\[htbp]\n\\\\centering\n\\\\capstart\n\n'
+ '\\\\sphinxincludegraphics\\[\\]{inheritance-\\w+.pdf}\n'
+ '\\\\caption{Test Foo!}\\\\label{\\\\detokenize{index:id1}}\\\\end{figure}')
+ assert re.search(pattern, content, re.M)
+
+
+@pytest.mark.sphinx('html', testroot='ext-inheritance_diagram',
+ srcdir='ext-inheritance_diagram-alias')
+@pytest.mark.usefixtures('if_graphviz_found')
+def test_inheritance_diagram_latex_alias(app, status, warning):
+ app.config.inheritance_alias = {'test.Foo': 'alias.Foo'}
+ app.builder.build_all()
+
+ doc = app.env.get_and_resolve_doctree('index', app)
+ aliased_graph = doc.children[0].children[3]['graph'].class_info
+ assert len(aliased_graph) == 4
+ assert ('test.DocSubDir2', 'test.DocSubDir2', ['test.DocSubDir1'], None) in aliased_graph
+ assert ('test.DocSubDir1', 'test.DocSubDir1', ['test.DocHere'], None) in aliased_graph
+ assert ('test.DocHere', 'test.DocHere', ['alias.Foo'], None) in aliased_graph
+ assert ('alias.Foo', 'alias.Foo', [], None) in aliased_graph
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ pattern = ('<figure class="align-default" id="id1">\n'
+ '<div class="graphviz">'
+ '<img src="_images/inheritance-\\w+.png" alt="Inheritance diagram of test.Foo" '
+ 'class="inheritance graphviz" /></div>\n<figcaption>\n<p>'
+ '<span class="caption-text">Test Foo!</span><a class="headerlink" href="#id1" '
+ 'title="Link to this image">\xb6</a></p>\n</figcaption>\n</figure>\n')
+ assert re.search(pattern, content, re.M)
+
+
+def test_import_classes(rootdir):
+ from sphinx.parsers import Parser, RSTParser
+ from sphinx.util.i18n import CatalogInfo
+
+ try:
+ sys.path.append(str(rootdir / 'test-ext-inheritance_diagram'))
+ from example.sphinx import DummyClass
+
+ # got exception for unknown class or module
+ with pytest.raises(InheritanceException):
+ import_classes('unknown', None)
+ with pytest.raises(InheritanceException):
+ import_classes('unknown.Unknown', None)
+
+ # got exception InheritanceException for wrong class or module
+ # not AttributeError (refs: #4019)
+ with pytest.raises(InheritanceException):
+ import_classes('unknown', '.')
+ with pytest.raises(InheritanceException):
+ import_classes('unknown.Unknown', '.')
+ with pytest.raises(InheritanceException):
+ import_classes('.', None)
+
+ # a module having no classes
+ classes = import_classes('sphinx', None)
+ assert classes == []
+
+ classes = import_classes('sphinx', 'foo')
+ assert classes == []
+
+ # all of classes in the module
+ classes = import_classes('sphinx.parsers', None)
+ assert set(classes) == {Parser, RSTParser}
+
+ # specified class in the module
+ classes = import_classes('sphinx.parsers.Parser', None)
+ assert classes == [Parser]
+
+ # specified class in current module
+ classes = import_classes('Parser', 'sphinx.parsers')
+ assert classes == [Parser]
+
+ # relative module name to current module
+ classes = import_classes('i18n.CatalogInfo', 'sphinx.util')
+ assert classes == [CatalogInfo]
+
+ # got exception for functions
+ with pytest.raises(InheritanceException):
+ import_classes('encode_uri', 'sphinx.util')
+
+ # import submodule on current module (refs: #3164)
+ classes = import_classes('sphinx', 'example')
+ assert classes == [DummyClass]
+ finally:
+ sys.path.pop()