summaryrefslogtreecommitdiffstats
path: root/tests/test_extensions/test_ext_inheritance_diagram.py
blob: 45a5ff0dda99fc12698d4d9943fdb6f6d21fd387 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
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.build(force_all=True)
    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.build(force_all=True)

    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.MULTILINE)

    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.build(force_all=True)

    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.MULTILINE)

    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.build(force_all=True)

    content = (app.outdir / 'projectnamenotset.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.MULTILINE)


@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.build(force_all=True)

    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.MULTILINE)


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()