1"""
2    test_apidoc
3    ~~~~~~~~~~~
4
5    Test the sphinx.apidoc module.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11from collections import namedtuple
12
13import pytest
14
15from sphinx.ext.apidoc import main as apidoc_main
16from sphinx.testing.path import path
17
18
19@pytest.fixture()
20def apidoc(rootdir, tempdir, apidoc_params):
21    _, kwargs = apidoc_params
22    coderoot = rootdir / kwargs.get('coderoot', 'test-root')
23    outdir = tempdir / 'out'
24    excludes = [coderoot / e for e in kwargs.get('excludes', [])]
25    args = ['-o', outdir, '-F', coderoot] + excludes + kwargs.get('options', [])
26    apidoc_main(args)
27    return namedtuple('apidoc', 'coderoot,outdir')(coderoot, outdir)
28
29
30@pytest.fixture
31def apidoc_params(request):
32    if hasattr(request.node, 'iter_markers'):  # pytest-3.6.0 or newer
33        markers = request.node.iter_markers("apidoc")
34    else:
35        markers = request.node.get_marker("apidoc")
36    pargs = {}
37    kwargs = {}
38
39    if markers is not None:
40        for info in reversed(list(markers)):
41            for i, a in enumerate(info.args):
42                pargs[i] = a
43            kwargs.update(info.kwargs)
44
45    args = [pargs[i] for i in sorted(pargs.keys())]
46    return args, kwargs
47
48
49@pytest.mark.apidoc(coderoot='test-root')
50def test_simple(make_app, apidoc):
51    outdir = apidoc.outdir
52    assert (outdir / 'conf.py').isfile()
53    assert (outdir / 'index.rst').isfile()
54
55    app = make_app('text', srcdir=outdir)
56    app.build()
57    print(app._status.getvalue())
58    print(app._warning.getvalue())
59
60
61@pytest.mark.apidoc(
62    coderoot='test-apidoc-pep420/a',
63    options=["--implicit-namespaces"],
64)
65def test_pep_0420_enabled(make_app, apidoc):
66    outdir = apidoc.outdir
67    assert (outdir / 'conf.py').isfile()
68    assert (outdir / 'a.b.c.rst').isfile()
69    assert (outdir / 'a.b.e.rst').isfile()
70    assert (outdir / 'a.b.x.rst').isfile()
71
72    with open(outdir / 'a.b.c.rst') as f:
73        rst = f.read()
74        assert "automodule:: a.b.c.d\n" in rst
75        assert "automodule:: a.b.c\n" in rst
76
77    with open(outdir / 'a.b.e.rst') as f:
78        rst = f.read()
79        assert "automodule:: a.b.e.f\n" in rst
80
81    with open(outdir / 'a.b.x.rst') as f:
82        rst = f.read()
83        assert "automodule:: a.b.x.y\n" in rst
84        assert "automodule:: a.b.x\n" not in rst
85
86    app = make_app('text', srcdir=outdir)
87    app.build()
88    print(app._status.getvalue())
89    print(app._warning.getvalue())
90
91    builddir = outdir / '_build' / 'text'
92    assert (builddir / 'a.b.c.txt').isfile()
93    assert (builddir / 'a.b.e.txt').isfile()
94    assert (builddir / 'a.b.x.txt').isfile()
95
96    with open(builddir / 'a.b.c.txt') as f:
97        txt = f.read()
98        assert "a.b.c package\n" in txt
99
100    with open(builddir / 'a.b.e.txt') as f:
101        txt = f.read()
102        assert "a.b.e.f module\n" in txt
103
104    with open(builddir / 'a.b.x.txt') as f:
105        txt = f.read()
106        assert "a.b.x namespace\n" in txt
107
108
109@pytest.mark.apidoc(
110    coderoot='test-apidoc-pep420/a',
111    options=["--implicit-namespaces", "--separate"],
112)
113def test_pep_0420_enabled_separate(make_app, apidoc):
114    outdir = apidoc.outdir
115    assert (outdir / 'conf.py').isfile()
116    assert (outdir / 'a.b.c.rst').isfile()
117    assert (outdir / 'a.b.e.rst').isfile()
118    assert (outdir / 'a.b.e.f.rst').isfile()
119    assert (outdir / 'a.b.x.rst').isfile()
120    assert (outdir / 'a.b.x.y.rst').isfile()
121
122    with open(outdir / 'a.b.c.rst') as f:
123        rst = f.read()
124        assert ".. toctree::\n   :maxdepth: 4\n\n   a.b.c.d\n" in rst
125
126    with open(outdir / 'a.b.e.rst') as f:
127        rst = f.read()
128        assert ".. toctree::\n   :maxdepth: 4\n\n   a.b.e.f\n" in rst
129
130    with open(outdir / 'a.b.x.rst') as f:
131        rst = f.read()
132        assert ".. toctree::\n   :maxdepth: 4\n\n   a.b.x.y\n" in rst
133
134    app = make_app('text', srcdir=outdir)
135    app.build()
136    print(app._status.getvalue())
137    print(app._warning.getvalue())
138
139    builddir = outdir / '_build' / 'text'
140    assert (builddir / 'a.b.c.txt').isfile()
141    assert (builddir / 'a.b.e.txt').isfile()
142    assert (builddir / 'a.b.e.f.txt').isfile()
143    assert (builddir / 'a.b.x.txt').isfile()
144    assert (builddir / 'a.b.x.y.txt').isfile()
145
146    with open(builddir / 'a.b.c.txt') as f:
147        txt = f.read()
148        assert "a.b.c package\n" in txt
149
150    with open(builddir / 'a.b.e.f.txt') as f:
151        txt = f.read()
152        assert "a.b.e.f module\n" in txt
153
154    with open(builddir / 'a.b.x.txt') as f:
155        txt = f.read()
156        assert "a.b.x namespace\n" in txt
157
158
159@pytest.mark.apidoc(coderoot='test-apidoc-pep420/a')
160def test_pep_0420_disabled(make_app, apidoc):
161    outdir = apidoc.outdir
162    assert (outdir / 'conf.py').isfile()
163    assert not (outdir / 'a.b.c.rst').exists()
164    assert not (outdir / 'a.b.x.rst').exists()
165
166    app = make_app('text', srcdir=outdir)
167    app.build()
168    print(app._status.getvalue())
169    print(app._warning.getvalue())
170
171
172@pytest.mark.apidoc(
173    coderoot='test-apidoc-pep420/a/b')
174def test_pep_0420_disabled_top_level_verify(make_app, apidoc):
175    outdir = apidoc.outdir
176    assert (outdir / 'conf.py').isfile()
177    assert (outdir / 'c.rst').isfile()
178    assert not (outdir / 'x.rst').exists()
179
180    with open(outdir / 'c.rst') as f:
181        rst = f.read()
182        assert "c package\n" in rst
183        assert "automodule:: c.d\n" in rst
184        assert "automodule:: c\n" in rst
185
186    app = make_app('text', srcdir=outdir)
187    app.build()
188    print(app._status.getvalue())
189    print(app._warning.getvalue())
190
191
192@pytest.mark.apidoc(
193    coderoot='test-apidoc-trailing-underscore')
194def test_trailing_underscore(make_app, apidoc):
195    outdir = apidoc.outdir
196    assert (outdir / 'conf.py').isfile()
197    assert (outdir / 'package_.rst').isfile()
198
199    app = make_app('text', srcdir=outdir)
200    app.build()
201    print(app._status.getvalue())
202    print(app._warning.getvalue())
203
204    builddir = outdir / '_build' / 'text'
205    with open(builddir / 'package_.txt') as f:
206        rst = f.read()
207        assert "package_ package\n" in rst
208        assert "package_.module_ module\n" in rst
209
210
211@pytest.mark.apidoc(
212    coderoot='test-apidoc-pep420/a',
213    excludes=["b/c/d.py", "b/e/f.py", "b/e/__init__.py"],
214    options=["--implicit-namespaces", "--separate"],
215)
216def test_excludes(apidoc):
217    outdir = apidoc.outdir
218    assert (outdir / 'conf.py').isfile()
219    assert (outdir / 'a.rst').isfile()
220    assert (outdir / 'a.b.rst').isfile()
221    assert (outdir / 'a.b.c.rst').isfile()  # generated because not empty
222    assert not (outdir / 'a.b.e.rst').isfile()  # skipped because of empty after excludes
223    assert (outdir / 'a.b.x.rst').isfile()
224    assert (outdir / 'a.b.x.y.rst').isfile()
225
226
227@pytest.mark.apidoc(
228    coderoot='test-apidoc-pep420/a',
229    excludes=["b/e"],
230    options=["--implicit-namespaces", "--separate"],
231)
232def test_excludes_subpackage_should_be_skipped(apidoc):
233    """Subpackage exclusion should work."""
234    outdir = apidoc.outdir
235    assert (outdir / 'conf.py').isfile()
236    assert (outdir / 'a.rst').isfile()
237    assert (outdir / 'a.b.rst').isfile()
238    assert (outdir / 'a.b.c.rst').isfile()  # generated because not empty
239    assert not (outdir / 'a.b.e.f.rst').isfile()  # skipped because 'b/e' subpackage is skipped
240
241
242@pytest.mark.apidoc(
243    coderoot='test-apidoc-pep420/a',
244    excludes=["b/e/f.py"],
245    options=["--implicit-namespaces", "--separate"],
246)
247def test_excludes_module_should_be_skipped(apidoc):
248    """Module exclusion should work."""
249    outdir = apidoc.outdir
250    assert (outdir / 'conf.py').isfile()
251    assert (outdir / 'a.rst').isfile()
252    assert (outdir / 'a.b.rst').isfile()
253    assert (outdir / 'a.b.c.rst').isfile()  # generated because not empty
254    assert not (outdir / 'a.b.e.f.rst').isfile()  # skipped because of empty after excludes
255
256
257@pytest.mark.apidoc(
258    coderoot='test-apidoc-pep420/a',
259    excludes=[],
260    options=["--implicit-namespaces", "--separate"],
261)
262def test_excludes_module_should_not_be_skipped(apidoc):
263    """Module should be included if no excludes are used."""
264    outdir = apidoc.outdir
265    assert (outdir / 'conf.py').isfile()
266    assert (outdir / 'a.rst').isfile()
267    assert (outdir / 'a.b.rst').isfile()
268    assert (outdir / 'a.b.c.rst').isfile()  # generated because not empty
269    assert (outdir / 'a.b.e.f.rst').isfile()  # skipped because of empty after excludes
270
271
272@pytest.mark.apidoc(
273    coderoot='test-root',
274    options=[
275        '--doc-project', 'プロジェクト名',
276        '--doc-author', '著者名',
277        '--doc-version', 'バージョン',
278        '--doc-release', 'リリース',
279    ],
280)
281def test_multibyte_parameters(make_app, apidoc):
282    outdir = apidoc.outdir
283    assert (outdir / 'conf.py').isfile()
284    assert (outdir / 'index.rst').isfile()
285
286    conf_py = (outdir / 'conf.py').read_text()
287    assert "project = 'プロジェクト名'" in conf_py
288    assert "author = '著者名'" in conf_py
289    assert "version = 'バージョン'" in conf_py
290    assert "release = 'リリース'" in conf_py
291
292    app = make_app('text', srcdir=outdir)
293    app.build()
294    print(app._status.getvalue())
295    print(app._warning.getvalue())
296
297
298@pytest.mark.apidoc(
299    coderoot='test-root',
300    options=['--ext-mathjax'],
301)
302def test_extension_parsed(make_app, apidoc):
303    outdir = apidoc.outdir
304    assert (outdir / 'conf.py').isfile()
305
306    with open(outdir / 'conf.py') as f:
307        rst = f.read()
308        assert "sphinx.ext.mathjax" in rst
309
310
311@pytest.mark.apidoc(
312    coderoot='test-apidoc-toc/mypackage',
313    options=["--implicit-namespaces"],
314)
315def test_toc_all_references_should_exist_pep420_enabled(make_app, apidoc):
316    """All references in toc should exist. This test doesn't say if
317       directories with empty __init__.py and and nothing else should be
318       skipped, just ensures consistency between what's referenced in the toc
319       and what is created. This is the variant with pep420 enabled.
320    """
321    outdir = apidoc.outdir
322    assert (outdir / 'conf.py').isfile()
323
324    toc = extract_toc(outdir / 'mypackage.rst')
325
326    refs = [l.strip() for l in toc.splitlines() if l.strip()]
327    found_refs = []
328    missing_files = []
329    for ref in refs:
330        if ref and ref[0] in (':', '#'):
331            continue
332        found_refs.append(ref)
333        filename = "{}.rst".format(ref)
334        if not (outdir / filename).isfile():
335            missing_files.append(filename)
336
337    assert len(missing_files) == 0, \
338        'File(s) referenced in TOC not found: {}\n' \
339        'TOC:\n{}'.format(", ".join(missing_files), toc)
340
341
342@pytest.mark.apidoc(
343    coderoot='test-apidoc-toc/mypackage',
344)
345def test_toc_all_references_should_exist_pep420_disabled(make_app, apidoc):
346    """All references in toc should exist. This test doesn't say if
347       directories with empty __init__.py and and nothing else should be
348       skipped, just ensures consistency between what's referenced in the toc
349       and what is created. This is the variant with pep420 disabled.
350    """
351    outdir = apidoc.outdir
352    assert (outdir / 'conf.py').isfile()
353
354    toc = extract_toc(outdir / 'mypackage.rst')
355
356    refs = [l.strip() for l in toc.splitlines() if l.strip()]
357    found_refs = []
358    missing_files = []
359    for ref in refs:
360        if ref and ref[0] in (':', '#'):
361            continue
362        filename = "{}.rst".format(ref)
363        found_refs.append(ref)
364        if not (outdir / filename).isfile():
365            missing_files.append(filename)
366
367    assert len(missing_files) == 0, \
368        'File(s) referenced in TOC not found: {}\n' \
369        'TOC:\n{}'.format(", ".join(missing_files), toc)
370
371
372def extract_toc(path):
373    """Helper: Extract toc section from package rst file"""
374    with open(path) as f:
375        rst = f.read()
376
377    # Read out the part containing the toctree
378    toctree_start = "\n.. toctree::\n"
379    toctree_end = "\nSubmodules"
380
381    start_idx = rst.index(toctree_start)
382    end_idx = rst.index(toctree_end, start_idx)
383    toctree = rst[start_idx + len(toctree_start):end_idx]
384
385    return toctree
386
387
388@pytest.mark.apidoc(
389    coderoot='test-apidoc-subpackage-in-toc',
390    options=['--separate']
391)
392def test_subpackage_in_toc(make_app, apidoc):
393    """Make sure that empty subpackages with non-empty subpackages in them
394       are not skipped (issue #4520)
395    """
396    outdir = apidoc.outdir
397    assert (outdir / 'conf.py').isfile()
398
399    assert (outdir / 'parent.rst').isfile()
400    with open(outdir / 'parent.rst') as f:
401        parent = f.read()
402    assert 'parent.child' in parent
403
404    assert (outdir / 'parent.child.rst').isfile()
405    with open(outdir / 'parent.child.rst') as f:
406        parent_child = f.read()
407    assert 'parent.child.foo' in parent_child
408
409    assert (outdir / 'parent.child.foo.rst').isfile()
410
411
412def test_private(tempdir):
413    (tempdir / 'hello.py').write_text('')
414    (tempdir / '_world.py').write_text('')
415
416    # without --private option
417    apidoc_main(['-o', tempdir, tempdir])
418    assert (tempdir / 'hello.rst').exists()
419    assert ':private-members:' not in (tempdir / 'hello.rst').read_text()
420    assert not (tempdir / '_world.rst').exists()
421
422    # with --private option
423    apidoc_main(['--private', '-f', '-o', tempdir, tempdir])
424    assert (tempdir / 'hello.rst').exists()
425    assert ':private-members:' in (tempdir / 'hello.rst').read_text()
426    assert (tempdir / '_world.rst').exists()
427
428
429def test_toc_file(tempdir):
430    outdir = path(tempdir)
431    (outdir / 'module').makedirs()
432    (outdir / 'example.py').write_text('')
433    (outdir / 'module' / 'example.py').write_text('')
434    apidoc_main(['-o', tempdir, tempdir])
435    assert (outdir / 'modules.rst').exists()
436
437    content = (outdir / 'modules.rst').read_text()
438    assert content == ("test_toc_file0\n"
439                       "==============\n"
440                       "\n"
441                       ".. toctree::\n"
442                       "   :maxdepth: 4\n"
443                       "\n"
444                       "   example\n")
445
446
447def test_module_file(tempdir):
448    outdir = path(tempdir)
449    (outdir / 'example.py').write_text('')
450    apidoc_main(['-o', tempdir, tempdir])
451    assert (outdir / 'example.rst').exists()
452
453    content = (outdir / 'example.rst').read_text()
454    assert content == ("example module\n"
455                       "==============\n"
456                       "\n"
457                       ".. automodule:: example\n"
458                       "   :members:\n"
459                       "   :undoc-members:\n"
460                       "   :show-inheritance:\n")
461
462
463def test_module_file_noheadings(tempdir):
464    outdir = path(tempdir)
465    (outdir / 'example.py').write_text('')
466    apidoc_main(['--no-headings', '-o', tempdir, tempdir])
467    assert (outdir / 'example.rst').exists()
468
469    content = (outdir / 'example.rst').read_text()
470    assert content == (".. automodule:: example\n"
471                       "   :members:\n"
472                       "   :undoc-members:\n"
473                       "   :show-inheritance:\n")
474
475
476def test_package_file(tempdir):
477    outdir = path(tempdir)
478    (outdir / 'testpkg').makedirs()
479    (outdir / 'testpkg' / '__init__.py').write_text('')
480    (outdir / 'testpkg' / 'hello.py').write_text('')
481    (outdir / 'testpkg' / 'world.py').write_text('')
482    (outdir / 'testpkg' / 'subpkg').makedirs()
483    (outdir / 'testpkg' / 'subpkg' / '__init__.py').write_text('')
484    apidoc_main(['-o', tempdir, tempdir / 'testpkg'])
485    assert (outdir / 'testpkg.rst').exists()
486    assert (outdir / 'testpkg.subpkg.rst').exists()
487
488    content = (outdir / 'testpkg.rst').read_text()
489    assert content == ("testpkg package\n"
490                       "===============\n"
491                       "\n"
492                       "Subpackages\n"
493                       "-----------\n"
494                       "\n"
495                       ".. toctree::\n"
496                       "   :maxdepth: 4\n"
497                       "\n"
498                       "   testpkg.subpkg\n"
499                       "\n"
500                       "Submodules\n"
501                       "----------\n"
502                       "\n"
503                       "testpkg.hello module\n"
504                       "--------------------\n"
505                       "\n"
506                       ".. automodule:: testpkg.hello\n"
507                       "   :members:\n"
508                       "   :undoc-members:\n"
509                       "   :show-inheritance:\n"
510                       "\n"
511                       "testpkg.world module\n"
512                       "--------------------\n"
513                       "\n"
514                       ".. automodule:: testpkg.world\n"
515                       "   :members:\n"
516                       "   :undoc-members:\n"
517                       "   :show-inheritance:\n"
518                       "\n"
519                       "Module contents\n"
520                       "---------------\n"
521                       "\n"
522                       ".. automodule:: testpkg\n"
523                       "   :members:\n"
524                       "   :undoc-members:\n"
525                       "   :show-inheritance:\n")
526
527    content = (outdir / 'testpkg.subpkg.rst').read_text()
528    assert content == ("testpkg.subpkg package\n"
529                       "======================\n"
530                       "\n"
531                       "Module contents\n"
532                       "---------------\n"
533                       "\n"
534                       ".. automodule:: testpkg.subpkg\n"
535                       "   :members:\n"
536                       "   :undoc-members:\n"
537                       "   :show-inheritance:\n")
538
539
540def test_package_file_separate(tempdir):
541    outdir = path(tempdir)
542    (outdir / 'testpkg').makedirs()
543    (outdir / 'testpkg' / '__init__.py').write_text('')
544    (outdir / 'testpkg' / 'example.py').write_text('')
545    apidoc_main(['--separate', '-o', tempdir, tempdir / 'testpkg'])
546    assert (outdir / 'testpkg.rst').exists()
547    assert (outdir / 'testpkg.example.rst').exists()
548
549    content = (outdir / 'testpkg.rst').read_text()
550    assert content == ("testpkg package\n"
551                       "===============\n"
552                       "\n"
553                       "Submodules\n"
554                       "----------\n"
555                       "\n"
556                       ".. toctree::\n"
557                       "   :maxdepth: 4\n"
558                       "\n"
559                       "   testpkg.example\n"
560                       "\n"
561                       "Module contents\n"
562                       "---------------\n"
563                       "\n"
564                       ".. automodule:: testpkg\n"
565                       "   :members:\n"
566                       "   :undoc-members:\n"
567                       "   :show-inheritance:\n")
568
569    content = (outdir / 'testpkg.example.rst').read_text()
570    assert content == ("testpkg.example module\n"
571                       "======================\n"
572                       "\n"
573                       ".. automodule:: testpkg.example\n"
574                       "   :members:\n"
575                       "   :undoc-members:\n"
576                       "   :show-inheritance:\n")
577
578
579def test_package_file_module_first(tempdir):
580    outdir = path(tempdir)
581    (outdir / 'testpkg').makedirs()
582    (outdir / 'testpkg' / '__init__.py').write_text('')
583    (outdir / 'testpkg' / 'example.py').write_text('')
584    apidoc_main(['--module-first', '-o', tempdir, tempdir])
585
586    content = (outdir / 'testpkg.rst').read_text()
587    assert content == ("testpkg package\n"
588                       "===============\n"
589                       "\n"
590                       ".. automodule:: testpkg\n"
591                       "   :members:\n"
592                       "   :undoc-members:\n"
593                       "   :show-inheritance:\n"
594                       "\n"
595                       "Submodules\n"
596                       "----------\n"
597                       "\n"
598                       "testpkg.example module\n"
599                       "----------------------\n"
600                       "\n"
601                       ".. automodule:: testpkg.example\n"
602                       "   :members:\n"
603                       "   :undoc-members:\n"
604                       "   :show-inheritance:\n")
605
606
607def test_package_file_without_submodules(tempdir):
608    outdir = path(tempdir)
609    (outdir / 'testpkg').makedirs()
610    (outdir / 'testpkg' / '__init__.py').write_text('')
611    apidoc_main(['-o', tempdir, tempdir / 'testpkg'])
612    assert (outdir / 'testpkg.rst').exists()
613
614    content = (outdir / 'testpkg.rst').read_text()
615    assert content == ("testpkg package\n"
616                       "===============\n"
617                       "\n"
618                       "Module contents\n"
619                       "---------------\n"
620                       "\n"
621                       ".. automodule:: testpkg\n"
622                       "   :members:\n"
623                       "   :undoc-members:\n"
624                       "   :show-inheritance:\n")
625
626
627def test_namespace_package_file(tempdir):
628    outdir = path(tempdir)
629    (outdir / 'testpkg').makedirs()
630    (outdir / 'testpkg' / 'example.py').write_text('')
631    apidoc_main(['--implicit-namespace', '-o', tempdir, tempdir / 'testpkg'])
632    assert (outdir / 'testpkg.rst').exists()
633
634    content = (outdir / 'testpkg.rst').read_text()
635    assert content == ("testpkg namespace\n"
636                       "=================\n"
637                       "\n"
638                       "Submodules\n"
639                       "----------\n"
640                       "\n"
641                       "testpkg.example module\n"
642                       "----------------------\n"
643                       "\n"
644                       ".. automodule:: testpkg.example\n"
645                       "   :members:\n"
646                       "   :undoc-members:\n"
647                       "   :show-inheritance:\n")
648