"""Test sphinx.ext.inheritance_diagram extension.""" from __future__ import annotations import re import sys import zlib from pathlib import Path import pytest from sphinx.ext.inheritance_diagram import ( InheritanceDiagram, InheritanceException, import_classes, ) from sphinx.ext.intersphinx._load import load_mappings, validate_intersphinx_mapping @pytest.mark.sphinx('html', testroot='inheritance') @pytest.mark.usefixtures('if_graphviz_found') def test_inheritance_diagram(app): # 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 = Path(node.document.current_source).stem graphs[source] = node['graph'] return result InheritanceDiagram.run = new_run try: app.build(force_all=True) finally: InheritanceDiagram.run = orig_run assert app.statuscode == 0 html_warnings = app.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 = { 'example': ('https://example.org', str(inv_file)), } app.config.intersphinx_cache_limit = 0 validate_intersphinx_mapping(app, app.config) load_mappings(app) app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') base_maps = re.findall('', content) pattern = ( '
\n' '
' 'Inheritance diagram of test.Foo
\n
\n

' 'Test Foo!\xb6

\n
\n
\n' ) assert re.search(pattern, content, re.MULTILINE) subdir_content = (app.outdir / 'subdir/page1.html').read_text(encoding='utf8') subdir_maps = re.findall('', 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 validate_intersphinx_mapping(app, app.config) load_mappings(app) app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') base_svgs = re.findall('\n' '
' '\n' '

Inheritance diagram of test.Foo

' '
\n
\n

' 'Test Foo!\xb6

\n
\n\n' ) assert re.search(pattern, content, re.MULTILINE) subdir_content = (app.outdir / 'subdir/page1.html').read_text(encoding='utf8') subdir_svgs = re.findall( '\n' '
' 'Inheritance diagram of test.Foo
\n
\n

' 'Test Foo!\xb6

\n
\n\n' ) assert re.search(pattern, content, re.MULTILINE) def test_import_classes(rootdir): from sphinx.parsers import Parser, RSTParser from sphinx.util.i18n import CatalogInfo saved_path = sys.path.copy() sys.path.insert(0, str(rootdir / 'test-ext-inheritance_diagram')) try: from example.sphinx import DummyClass # type: ignore[import-not-found] # 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[:] = saved_path