diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:20:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:20:58 +0000 |
commit | ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8 (patch) | |
tree | 3c64877dd20ad1141111c77b3463e95686002b39 /tests/test_extensions | |
parent | Adding debian version 7.2.6-8. (diff) | |
download | sphinx-ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8.tar.xz sphinx-ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8.zip |
Merging upstream version 7.3.7.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/test_extensions')
36 files changed, 13226 insertions, 0 deletions
diff --git a/tests/test_extensions/__init__.py b/tests/test_extensions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_extensions/__init__.py diff --git a/tests/test_extensions/autodoc_util.py b/tests/test_extensions/autodoc_util.py new file mode 100644 index 0000000..7c4da07 --- /dev/null +++ b/tests/test_extensions/autodoc_util.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import Mock + +# NEVER import those objects from sphinx.ext.autodoc directly +from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options +from sphinx.util.docutils import LoggingReporter + +if TYPE_CHECKING: + from typing import Any + + from docutils.statemachine import StringList + + from sphinx.application import Sphinx + + +def do_autodoc( + app: Sphinx, + objtype: str, + name: str, + options: dict[str, Any] | None = None, +) -> StringList: + options = {} if options is None else options.copy() + app.env.temp_data.setdefault('docname', 'index') # set dummy docname + doccls = app.registry.documenters[objtype] + docoptions = process_documenter_options(doccls, app.config, options) + state = Mock() + state.document.settings.tab_width = 8 + bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state) + documenter = doccls(bridge, name) + documenter.generate() + return bridge.result diff --git a/tests/test_extensions/ext_napoleon_pep526_data_google.py b/tests/test_extensions/ext_napoleon_pep526_data_google.py new file mode 100644 index 0000000..d0692e0 --- /dev/null +++ b/tests/test_extensions/ext_napoleon_pep526_data_google.py @@ -0,0 +1,16 @@ +"""Test module for napoleon PEP 526 compatibility with google style""" + +module_level_var: int = 99 +"""This is an example module level variable""" + + +class PEP526GoogleClass: + """Sample class with PEP 526 annotations and google docstring. + + Attributes: + attr1: Attr1 description. + attr2: Attr2 description. + """ + + attr1: int + attr2: str diff --git a/tests/test_extensions/ext_napoleon_pep526_data_numpy.py b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py new file mode 100644 index 0000000..eff7746 --- /dev/null +++ b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py @@ -0,0 +1,21 @@ +"""Test module for napoleon PEP 526 compatibility with numpy style""" + +module_level_var: int = 99 +"""This is an example module level variable""" + + +class PEP526NumpyClass: + """ + Sample class with PEP 526 annotations and numpy docstring + + Attributes + ---------- + attr1: + Attr1 description + + attr2: + Attr2 description + """ + + attr1: int + attr2: str diff --git a/tests/test_extensions/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py new file mode 100644 index 0000000..c3c979f --- /dev/null +++ b/tests/test_extensions/test_ext_apidoc.py @@ -0,0 +1,663 @@ +"""Test the sphinx.apidoc module.""" + +import os.path +from collections import namedtuple + +import pytest + +import sphinx.ext.apidoc +from sphinx.ext.apidoc import main as apidoc_main + + +@pytest.fixture() +def apidoc(rootdir, tmp_path, apidoc_params): + _, kwargs = apidoc_params + coderoot = rootdir / kwargs.get('coderoot', 'test-root') + outdir = tmp_path / 'out' + excludes = [str(coderoot / e) for e in kwargs.get('excludes', [])] + args = ['-o', str(outdir), '-F', str(coderoot), *excludes, *kwargs.get('options', [])] + apidoc_main(args) + return namedtuple('apidoc', 'coderoot,outdir')(coderoot, outdir) + + +@pytest.fixture() +def apidoc_params(request): + pargs = {} + kwargs = {} + + for info in reversed(list(request.node.iter_markers("apidoc"))): + pargs |= dict(enumerate(info.args)) + kwargs.update(info.kwargs) + + args = [pargs[i] for i in sorted(pargs.keys())] + return args, kwargs + + +@pytest.mark.apidoc(coderoot='test-root') +def test_simple(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'index.rst').is_file() + + app = make_app('text', srcdir=outdir) + app.build() + print(app._status.getvalue()) + print(app._warning.getvalue()) + + +@pytest.mark.apidoc( + coderoot='test-apidoc-pep420/a', + options=["--implicit-namespaces"], +) +def test_pep_0420_enabled(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'a.b.c.rst').is_file() + assert (outdir / 'a.b.e.rst').is_file() + assert (outdir / 'a.b.x.rst').is_file() + + with open(outdir / 'a.b.c.rst', encoding='utf-8') as f: + rst = f.read() + assert "automodule:: a.b.c.d\n" in rst + assert "automodule:: a.b.c\n" in rst + + with open(outdir / 'a.b.e.rst', encoding='utf-8') as f: + rst = f.read() + assert "automodule:: a.b.e.f\n" in rst + + with open(outdir / 'a.b.x.rst', encoding='utf-8') as f: + rst = f.read() + assert "automodule:: a.b.x.y\n" in rst + assert "automodule:: a.b.x\n" not in rst + + app = make_app('text', srcdir=outdir) + app.build() + print(app._status.getvalue()) + print(app._warning.getvalue()) + + builddir = outdir / '_build' / 'text' + assert (builddir / 'a.b.c.txt').is_file() + assert (builddir / 'a.b.e.txt').is_file() + assert (builddir / 'a.b.x.txt').is_file() + + with open(builddir / 'a.b.c.txt', encoding='utf-8') as f: + txt = f.read() + assert "a.b.c package\n" in txt + + with open(builddir / 'a.b.e.txt', encoding='utf-8') as f: + txt = f.read() + assert "a.b.e.f module\n" in txt + + with open(builddir / 'a.b.x.txt', encoding='utf-8') as f: + txt = f.read() + assert "a.b.x namespace\n" in txt + + +@pytest.mark.apidoc( + coderoot='test-apidoc-pep420/a', + options=["--implicit-namespaces", "--separate"], +) +def test_pep_0420_enabled_separate(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'a.b.c.rst').is_file() + assert (outdir / 'a.b.e.rst').is_file() + assert (outdir / 'a.b.e.f.rst').is_file() + assert (outdir / 'a.b.x.rst').is_file() + assert (outdir / 'a.b.x.y.rst').is_file() + + with open(outdir / 'a.b.c.rst', encoding='utf-8') as f: + rst = f.read() + assert ".. toctree::\n :maxdepth: 4\n\n a.b.c.d\n" in rst + + with open(outdir / 'a.b.e.rst', encoding='utf-8') as f: + rst = f.read() + assert ".. toctree::\n :maxdepth: 4\n\n a.b.e.f\n" in rst + + with open(outdir / 'a.b.x.rst', encoding='utf-8') as f: + rst = f.read() + assert ".. toctree::\n :maxdepth: 4\n\n a.b.x.y\n" in rst + + app = make_app('text', srcdir=outdir) + app.build() + print(app._status.getvalue()) + print(app._warning.getvalue()) + + builddir = outdir / '_build' / 'text' + assert (builddir / 'a.b.c.txt').is_file() + assert (builddir / 'a.b.e.txt').is_file() + assert (builddir / 'a.b.e.f.txt').is_file() + assert (builddir / 'a.b.x.txt').is_file() + assert (builddir / 'a.b.x.y.txt').is_file() + + with open(builddir / 'a.b.c.txt', encoding='utf-8') as f: + txt = f.read() + assert "a.b.c package\n" in txt + + with open(builddir / 'a.b.e.f.txt', encoding='utf-8') as f: + txt = f.read() + assert "a.b.e.f module\n" in txt + + with open(builddir / 'a.b.x.txt', encoding='utf-8') as f: + txt = f.read() + assert "a.b.x namespace\n" in txt + + +@pytest.mark.apidoc(coderoot='test-apidoc-pep420/a') +def test_pep_0420_disabled(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert not (outdir / 'a.b.c.rst').exists() + assert not (outdir / 'a.b.x.rst').exists() + + app = make_app('text', srcdir=outdir) + app.build() + print(app._status.getvalue()) + print(app._warning.getvalue()) + + +@pytest.mark.apidoc( + coderoot='test-apidoc-pep420/a/b') +def test_pep_0420_disabled_top_level_verify(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'c.rst').is_file() + assert not (outdir / 'x.rst').exists() + + with open(outdir / 'c.rst', encoding='utf-8') as f: + rst = f.read() + assert "c package\n" in rst + assert "automodule:: c.d\n" in rst + assert "automodule:: c\n" in rst + + app = make_app('text', srcdir=outdir) + app.build() + print(app._status.getvalue()) + print(app._warning.getvalue()) + + +@pytest.mark.apidoc( + coderoot='test-apidoc-trailing-underscore') +def test_trailing_underscore(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'package_.rst').is_file() + + app = make_app('text', srcdir=outdir) + app.build() + print(app._status.getvalue()) + print(app._warning.getvalue()) + + builddir = outdir / '_build' / 'text' + with open(builddir / 'package_.txt', encoding='utf-8') as f: + rst = f.read() + assert "package_ package\n" in rst + assert "package_.module_ module\n" in rst + + +@pytest.mark.apidoc( + coderoot='test-apidoc-pep420/a', + excludes=["b/c/d.py", "b/e/f.py", "b/e/__init__.py"], + options=["--implicit-namespaces", "--separate"], +) +def test_excludes(apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'a.rst').is_file() + assert (outdir / 'a.b.rst').is_file() + assert (outdir / 'a.b.c.rst').is_file() # generated because not empty + assert not (outdir / 'a.b.e.rst').is_file() # skipped because of empty after excludes + assert (outdir / 'a.b.x.rst').is_file() + assert (outdir / 'a.b.x.y.rst').is_file() + + +@pytest.mark.apidoc( + coderoot='test-apidoc-pep420/a', + excludes=["b/e"], + options=["--implicit-namespaces", "--separate"], +) +def test_excludes_subpackage_should_be_skipped(apidoc): + """Subpackage exclusion should work.""" + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'a.rst').is_file() + assert (outdir / 'a.b.rst').is_file() + assert (outdir / 'a.b.c.rst').is_file() # generated because not empty + assert not (outdir / 'a.b.e.f.rst').is_file() # skipped because 'b/e' subpackage is skipped + + +@pytest.mark.apidoc( + coderoot='test-apidoc-pep420/a', + excludes=["b/e/f.py"], + options=["--implicit-namespaces", "--separate"], +) +def test_excludes_module_should_be_skipped(apidoc): + """Module exclusion should work.""" + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'a.rst').is_file() + assert (outdir / 'a.b.rst').is_file() + assert (outdir / 'a.b.c.rst').is_file() # generated because not empty + assert not (outdir / 'a.b.e.f.rst').is_file() # skipped because of empty after excludes + + +@pytest.mark.apidoc( + coderoot='test-apidoc-pep420/a', + excludes=[], + options=["--implicit-namespaces", "--separate"], +) +def test_excludes_module_should_not_be_skipped(apidoc): + """Module should be included if no excludes are used.""" + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'a.rst').is_file() + assert (outdir / 'a.b.rst').is_file() + assert (outdir / 'a.b.c.rst').is_file() # generated because not empty + assert (outdir / 'a.b.e.f.rst').is_file() # skipped because of empty after excludes + + +@pytest.mark.apidoc( + coderoot='test-root', + options=[ + '--doc-project', 'プロジェクト名', + '--doc-author', '著者名', + '--doc-version', 'バージョン', + '--doc-release', 'リリース', + ], +) +def test_multibyte_parameters(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + assert (outdir / 'index.rst').is_file() + + conf_py = (outdir / 'conf.py').read_text(encoding='utf8') + assert "project = 'プロジェクト名'" in conf_py + assert "author = '著者名'" in conf_py + assert "version = 'バージョン'" in conf_py + assert "release = 'リリース'" in conf_py + + app = make_app('text', srcdir=outdir) + app.build() + print(app._status.getvalue()) + print(app._warning.getvalue()) + + +@pytest.mark.apidoc( + coderoot='test-root', + options=['--ext-mathjax'], +) +def test_extension_parsed(make_app, apidoc): + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + + with open(outdir / 'conf.py', encoding='utf-8') as f: + rst = f.read() + assert "sphinx.ext.mathjax" in rst + + +@pytest.mark.apidoc( + coderoot='test-apidoc-toc/mypackage', + options=["--implicit-namespaces"], +) +def test_toc_all_references_should_exist_pep420_enabled(make_app, apidoc): + """All references in toc should exist. This test doesn't say if + directories with empty __init__.py and and nothing else should be + skipped, just ensures consistency between what's referenced in the toc + and what is created. This is the variant with pep420 enabled. + """ + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + + toc = extract_toc(outdir / 'mypackage.rst') + + refs = [l.strip() for l in toc.splitlines() if l.strip()] + found_refs = [] + missing_files = [] + for ref in refs: + if ref and ref[0] in (':', '#'): + continue + found_refs.append(ref) + filename = f"{ref}.rst" + if not (outdir / filename).is_file(): + missing_files.append(filename) + + assert len(missing_files) == 0, \ + 'File(s) referenced in TOC not found: {}\n' \ + 'TOC:\n{}'.format(", ".join(missing_files), toc) + + +@pytest.mark.apidoc( + coderoot='test-apidoc-toc/mypackage', +) +def test_toc_all_references_should_exist_pep420_disabled(make_app, apidoc): + """All references in toc should exist. This test doesn't say if + directories with empty __init__.py and and nothing else should be + skipped, just ensures consistency between what's referenced in the toc + and what is created. This is the variant with pep420 disabled. + """ + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + + toc = extract_toc(outdir / 'mypackage.rst') + + refs = [l.strip() for l in toc.splitlines() if l.strip()] + found_refs = [] + missing_files = [] + for ref in refs: + if ref and ref[0] in (':', '#'): + continue + filename = f"{ref}.rst" + found_refs.append(ref) + if not (outdir / filename).is_file(): + missing_files.append(filename) + + assert len(missing_files) == 0, \ + 'File(s) referenced in TOC not found: {}\n' \ + 'TOC:\n{}'.format(", ".join(missing_files), toc) + + +def extract_toc(path): + """Helper: Extract toc section from package rst file""" + with open(path, encoding='utf-8') as f: + rst = f.read() + + # Read out the part containing the toctree + toctree_start = "\n.. toctree::\n" + toctree_end = "\nSubmodules" + + start_idx = rst.index(toctree_start) + end_idx = rst.index(toctree_end, start_idx) + toctree = rst[start_idx + len(toctree_start):end_idx] + + return toctree + + +@pytest.mark.apidoc( + coderoot='test-apidoc-subpackage-in-toc', + options=['--separate'], +) +def test_subpackage_in_toc(make_app, apidoc): + """Make sure that empty subpackages with non-empty subpackages in them + are not skipped (issue #4520) + """ + outdir = apidoc.outdir + assert (outdir / 'conf.py').is_file() + + assert (outdir / 'parent.rst').is_file() + with open(outdir / 'parent.rst', encoding='utf-8') as f: + parent = f.read() + assert 'parent.child' in parent + + assert (outdir / 'parent.child.rst').is_file() + with open(outdir / 'parent.child.rst', encoding='utf-8') as f: + parent_child = f.read() + assert 'parent.child.foo' in parent_child + + assert (outdir / 'parent.child.foo.rst').is_file() + + +def test_private(tmp_path): + (tmp_path / 'hello.py').write_text('', encoding='utf8') + (tmp_path / '_world.py').write_text('', encoding='utf8') + + # without --private option + apidoc_main(['-o', str(tmp_path), str(tmp_path)]) + assert (tmp_path / 'hello.rst').exists() + assert ':private-members:' not in (tmp_path / 'hello.rst').read_text(encoding='utf8') + assert not (tmp_path / '_world.rst').exists() + + # with --private option + apidoc_main(['--private', '-f', '-o', str(tmp_path), str(tmp_path)]) + assert (tmp_path / 'hello.rst').exists() + assert ':private-members:' in (tmp_path / 'hello.rst').read_text(encoding='utf8') + assert (tmp_path / '_world.rst').exists() + + +def test_toc_file(tmp_path): + outdir = tmp_path + (outdir / 'module').mkdir(parents=True, exist_ok=True) + (outdir / 'example.py').write_text('', encoding='utf8') + (outdir / 'module' / 'example.py').write_text('', encoding='utf8') + apidoc_main(['-o', str(tmp_path), str(tmp_path)]) + assert (outdir / 'modules.rst').exists() + + content = (outdir / 'modules.rst').read_text(encoding='utf8') + assert content == ("test_toc_file0\n" + "==============\n" + "\n" + ".. toctree::\n" + " :maxdepth: 4\n" + "\n" + " example\n") + + +def test_module_file(tmp_path): + outdir = tmp_path + (outdir / 'example.py').write_text('', encoding='utf8') + apidoc_main(['-o', str(tmp_path), str(tmp_path)]) + assert (outdir / 'example.rst').exists() + + content = (outdir / 'example.rst').read_text(encoding='utf8') + assert content == ("example module\n" + "==============\n" + "\n" + ".. automodule:: example\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + +def test_module_file_noheadings(tmp_path): + outdir = tmp_path + (outdir / 'example.py').write_text('', encoding='utf8') + apidoc_main(['--no-headings', '-o', str(tmp_path), str(tmp_path)]) + assert (outdir / 'example.rst').exists() + + content = (outdir / 'example.rst').read_text(encoding='utf8') + assert content == (".. automodule:: example\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + +def test_package_file(tmp_path): + outdir = tmp_path + (outdir / 'testpkg').mkdir(parents=True, exist_ok=True) + (outdir / 'testpkg' / '__init__.py').write_text('', encoding='utf8') + (outdir / 'testpkg' / 'hello.py').write_text('', encoding='utf8') + (outdir / 'testpkg' / 'world.py').write_text('', encoding='utf8') + (outdir / 'testpkg' / 'subpkg').mkdir(parents=True, exist_ok=True) + (outdir / 'testpkg' / 'subpkg' / '__init__.py').write_text('', encoding='utf8') + apidoc_main(['-o', str(outdir), str(outdir / 'testpkg')]) + assert (outdir / 'testpkg.rst').exists() + assert (outdir / 'testpkg.subpkg.rst').exists() + + content = (outdir / 'testpkg.rst').read_text(encoding='utf8') + assert content == ("testpkg package\n" + "===============\n" + "\n" + "Subpackages\n" + "-----------\n" + "\n" + ".. toctree::\n" + " :maxdepth: 4\n" + "\n" + " testpkg.subpkg\n" + "\n" + "Submodules\n" + "----------\n" + "\n" + "testpkg.hello module\n" + "--------------------\n" + "\n" + ".. automodule:: testpkg.hello\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n" + "\n" + "testpkg.world module\n" + "--------------------\n" + "\n" + ".. automodule:: testpkg.world\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n" + "\n" + "Module contents\n" + "---------------\n" + "\n" + ".. automodule:: testpkg\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + content = (outdir / 'testpkg.subpkg.rst').read_text(encoding='utf8') + assert content == ("testpkg.subpkg package\n" + "======================\n" + "\n" + "Module contents\n" + "---------------\n" + "\n" + ".. automodule:: testpkg.subpkg\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + +def test_package_file_separate(tmp_path): + outdir = tmp_path + (outdir / 'testpkg').mkdir(parents=True, exist_ok=True) + (outdir / 'testpkg' / '__init__.py').write_text('', encoding='utf8') + (outdir / 'testpkg' / 'example.py').write_text('', encoding='utf8') + apidoc_main(['--separate', '-o', str(tmp_path), str(tmp_path / 'testpkg')]) + assert (outdir / 'testpkg.rst').exists() + assert (outdir / 'testpkg.example.rst').exists() + + content = (outdir / 'testpkg.rst').read_text(encoding='utf8') + assert content == ("testpkg package\n" + "===============\n" + "\n" + "Submodules\n" + "----------\n" + "\n" + ".. toctree::\n" + " :maxdepth: 4\n" + "\n" + " testpkg.example\n" + "\n" + "Module contents\n" + "---------------\n" + "\n" + ".. automodule:: testpkg\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + content = (outdir / 'testpkg.example.rst').read_text(encoding='utf8') + assert content == ("testpkg.example module\n" + "======================\n" + "\n" + ".. automodule:: testpkg.example\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + +def test_package_file_module_first(tmp_path): + outdir = tmp_path + (outdir / 'testpkg').mkdir(parents=True, exist_ok=True) + (outdir / 'testpkg' / '__init__.py').write_text('', encoding='utf8') + (outdir / 'testpkg' / 'example.py').write_text('', encoding='utf8') + apidoc_main(['--module-first', '-o', str(tmp_path), str(tmp_path)]) + + content = (outdir / 'testpkg.rst').read_text(encoding='utf8') + assert content == ("testpkg package\n" + "===============\n" + "\n" + ".. automodule:: testpkg\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n" + "\n" + "Submodules\n" + "----------\n" + "\n" + "testpkg.example module\n" + "----------------------\n" + "\n" + ".. automodule:: testpkg.example\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + +def test_package_file_without_submodules(tmp_path): + outdir = tmp_path + (outdir / 'testpkg').mkdir(parents=True, exist_ok=True) + (outdir / 'testpkg' / '__init__.py').write_text('', encoding='utf8') + apidoc_main(['-o', str(tmp_path), str(tmp_path / 'testpkg')]) + assert (outdir / 'testpkg.rst').exists() + + content = (outdir / 'testpkg.rst').read_text(encoding='utf8') + assert content == ("testpkg package\n" + "===============\n" + "\n" + "Module contents\n" + "---------------\n" + "\n" + ".. automodule:: testpkg\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + +def test_namespace_package_file(tmp_path): + outdir = tmp_path + (outdir / 'testpkg').mkdir(parents=True, exist_ok=True) + (outdir / 'testpkg' / 'example.py').write_text('', encoding='utf8') + apidoc_main(['--implicit-namespace', '-o', str(tmp_path), str(tmp_path / 'testpkg')]) + assert (outdir / 'testpkg.rst').exists() + + content = (outdir / 'testpkg.rst').read_text(encoding='utf8') + assert content == ("testpkg namespace\n" + "=================\n" + "\n" + ".. py:module:: testpkg\n" + "\n" + "Submodules\n" + "----------\n" + "\n" + "testpkg.example module\n" + "----------------------\n" + "\n" + ".. automodule:: testpkg.example\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n") + + +def test_no_duplicates(rootdir, tmp_path): + """Make sure that a ".pyx" and ".so" don't cause duplicate listings. + + We can't use pytest.mark.apidoc here as we use a different set of arguments + to apidoc_main + """ + original_suffixes = sphinx.ext.apidoc.PY_SUFFIXES + try: + # Ensure test works on Windows + sphinx.ext.apidoc.PY_SUFFIXES += ('.so',) + + package = rootdir / 'test-apidoc-duplicates' / 'fish_licence' + outdir = tmp_path / 'out' + apidoc_main(['-o', str(outdir), "-T", str(package), "--implicit-namespaces"]) + + # Ensure the module has been documented + assert os.path.isfile(outdir / 'fish_licence.rst') + + # Ensure the submodule only appears once + text = (outdir / 'fish_licence.rst').read_text(encoding="utf-8") + count_submodules = text.count(r'fish\_licence.halibut module') + assert count_submodules == 1 + + finally: + sphinx.ext.apidoc.PY_SUFFIXES = original_suffixes diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py new file mode 100644 index 0000000..54f81f2 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc.py @@ -0,0 +1,2923 @@ +"""Test the autodoc extension. + +This tests mainly the Documenters; the auto directives are tested in a test +source file translated by test_build. +""" + +from __future__ import annotations + +import functools +import itertools +import operator +import sys +from types import SimpleNamespace +from typing import TYPE_CHECKING +from unittest.mock import Mock +from warnings import catch_warnings + +import pytest +from docutils.statemachine import ViewList + +from sphinx import addnodes +from sphinx.ext.autodoc import ALL, ModuleLevelDocumenter, Options + +from tests.test_extensions.autodoc_util import do_autodoc + +try: + # Enable pyximport to test cython module + import pyximport + pyximport.install() +except ImportError: + pyximport = None + +if TYPE_CHECKING: + from typing import Any + + +def make_directive_bridge(env): + options = Options( + inherited_members=False, + undoc_members=False, + private_members=False, + special_members=False, + imported_members=False, + show_inheritance=False, + no_index=False, + annotation=None, + synopsis='', + platform='', + deprecated=False, + members=[], + member_order='alphabetical', + exclude_members=set(), + ignore_module_all=False, + ) + + directive = SimpleNamespace( + env=env, + genopt=options, + result=ViewList(), + record_dependencies=set(), + state=Mock(), + ) + directive.state.document.settings.tab_width = 8 + + return directive + + +processed_signatures = [] + + +def test_parse_name(app): + def verify(objtype, name, result): + inst = app.registry.documenters[objtype](directive, name) + assert inst.parse_name() + assert (inst.modname, inst.objpath, inst.args, inst.retann) == result + + directive = make_directive_bridge(app.env) + + # for modules + verify('module', 'test_ext_autodoc', ('test_ext_autodoc', [], None, None)) + verify('module', 'test.test_ext_autodoc', ('test.test_ext_autodoc', [], None, None)) + verify('module', 'test(arg)', ('test', [], 'arg', None)) + assert 'signature arguments' in app.warning.getvalue() + + # for functions/classes + verify('function', 'test_ext_autodoc.raises', + ('test_ext_autodoc', ['raises'], None, None)) + verify('function', 'test_ext_autodoc.raises(exc) -> None', + ('test_ext_autodoc', ['raises'], 'exc', 'None')) + directive.env.temp_data['autodoc:module'] = 'test_ext_autodoc' + verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None)) + del directive.env.temp_data['autodoc:module'] + directive.env.ref_context['py:module'] = 'test_ext_autodoc' + verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None)) + verify('class', 'Base', ('test_ext_autodoc', ['Base'], None, None)) + + # for members + directive.env.ref_context['py:module'] = 'sphinx.testing.util' + verify('method', 'SphinxTestApp.cleanup', + ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)) + directive.env.ref_context['py:module'] = 'sphinx.testing.util' + directive.env.ref_context['py:class'] = 'Foo' + directive.env.temp_data['autodoc:class'] = 'SphinxTestApp' + verify('method', 'cleanup', + ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)) + verify('method', 'SphinxTestApp.cleanup', + ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)) + + +def test_format_signature(app): + def process_signature(app, what, name, obj, options, args, retann): + processed_signatures.append((what, name)) + if name == 'bar': + return '42', None + return None + + def skip_member(app, what, name, obj, skip, options): + if name in ('__special1__', '__special2__'): + return skip + if name.startswith('__'): + return True + if name == 'skipmeth': + return True + return None + + app.connect('autodoc-process-signature', process_signature) + app.connect('autodoc-skip-member', skip_member) + + directive = make_directive_bridge(app.env) + + def formatsig(objtype, name, obj, args, retann): + inst = app.registry.documenters[objtype](directive, name) + inst.fullname = name + inst.doc_as_attr = False # for class objtype + inst.parent = object # dummy + inst.object = obj + inst.objpath = [name] + inst.args = args + inst.retann = retann + res = inst.format_signature() + print(res) + return res + + # no signatures for modules + assert formatsig('module', 'test', None, None, None) == '' + + # test for functions + def f(a, b, c=1, **d): + pass + + def g(a='\n'): + pass + assert formatsig('function', 'f', f, None, None) == '(a, b, c=1, **d)' + assert formatsig('function', 'f', f, 'a, b, c, d', None) == '(a, b, c, d)' + assert formatsig('function', 'g', g, None, None) == r"(a='\n')" + + # test for classes + class D: + pass + + class E: + def __init__(self): + pass + + # an empty init and no init are the same + for C in (D, E): + assert formatsig('class', 'D', C, None, None) == '()' + + class SomeMeta(type): + def __call__(cls, a, b=None): + return type.__call__(cls, a, b) + + # these three are all equivalent + class F: + def __init__(self, a, b=None): + pass + + class FNew: + def __new__(cls, a, b=None): + return super().__new__(cls) + + class FMeta(metaclass=SomeMeta): + pass + + # and subclasses should always inherit + class G(F): + pass + + class GNew(FNew): + pass + + class GMeta(FMeta): + pass + + # subclasses inherit + for C in (F, FNew, FMeta, G, GNew, GMeta): + assert formatsig('class', 'C', C, None, None) == '(a, b=None)' + assert formatsig('class', 'C', D, 'a, b', 'X') == '(a, b) -> X' + + class ListSubclass(list): + pass + + # only supported if the python implementation decides to document it + if getattr(list, '__text_signature__', None) is not None: + assert formatsig('class', 'C', ListSubclass, None, None) == '(iterable=(), /)' + else: + assert formatsig('class', 'C', ListSubclass, None, None) == '' + + class ExceptionSubclass(Exception): + pass + + # Exception has no __text_signature__ at least in Python 3.11 + if getattr(Exception, '__text_signature__', None) is None: + assert formatsig('class', 'C', ExceptionSubclass, None, None) == '' + + # __init__ have signature at first line of docstring + directive.env.config.autoclass_content = 'both' + + class F2: + """some docstring for F2.""" + + def __init__(self, *args, **kw): + """ + __init__(a1, a2, kw1=True, kw2=False) + + some docstring for __init__. + """ + + class G2(F2): + pass + + assert formatsig('class', 'F2', F2, None, None) == \ + '(a1, a2, kw1=True, kw2=False)' + assert formatsig('class', 'G2', G2, None, None) == \ + '(a1, a2, kw1=True, kw2=False)' + + # test for methods + class H: + def foo1(self, b, *c): + pass + + def foo2(b, *c): + pass + + def foo3(self, d='\n'): + pass + assert formatsig('method', 'H.foo', H.foo1, None, None) == '(b, *c)' + assert formatsig('method', 'H.foo', H.foo1, 'a', None) == '(a)' + assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)' + assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\n')" + + # test bound methods interpreted as functions + assert formatsig('function', 'foo', H().foo1, None, None) == '(b, *c)' + assert formatsig('function', 'foo', H().foo2, None, None) == '(*c)' + assert formatsig('function', 'foo', H().foo3, None, None) == r"(d='\n')" + + # test exception handling (exception is caught and args is '') + directive.env.config.autodoc_docstring_signature = False + assert formatsig('function', 'int', int, None, None) == '' + + # test processing by event handler + assert formatsig('method', 'bar', H.foo1, None, None) == '42' + + # test functions created via functools.partial + from functools import partial + curried1 = partial(lambda a, b, c: None, 'A') + assert formatsig('function', 'curried1', curried1, None, None) == \ + '(b, c)' + curried2 = partial(lambda a, b, c=42: None, 'A') + assert formatsig('function', 'curried2', curried2, None, None) == \ + '(b, c=42)' + curried3 = partial(lambda a, b, *c: None, 'A') + assert formatsig('function', 'curried3', curried3, None, None) == \ + '(b, *c)' + curried4 = partial(lambda a, b, c=42, *d, **e: None, 'A') + assert formatsig('function', 'curried4', curried4, None, None) == \ + '(b, c=42, *d, **e)' + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_process_signature_typing_generic(app): + actual = do_autodoc(app, 'class', 'target.generic_class.A', {}) + + assert list(actual) == [ + '', + '.. py:class:: A(a, b=None)', + ' :module: target.generic_class', + '', + ' docstring for A', + '', + ] + + +def test_autodoc_process_signature_typehints(app): + captured = [] + + def process_signature(*args): + captured.append(args) + + app.connect('autodoc-process-signature', process_signature) + + def func(x: int, y: int) -> int: + pass + + directive = make_directive_bridge(app.env) + inst = app.registry.documenters['function'](directive, 'func') + inst.fullname = 'func' + inst.object = func + inst.objpath = ['func'] + inst.format_signature() + assert captured == [(app, 'function', 'func', func, + directive.genopt, '(x: int, y: int)', 'int')] + + +def test_get_doc(app): + directive = make_directive_bridge(app.env) + + def getdocl(objtype, obj): + inst = app.registry.documenters[objtype](directive, 'tmp') + inst.parent = object # dummy + inst.object = obj + inst.objpath = [obj.__name__] + inst.doc_as_attr = False + inst.format_signature() # handle docstring signatures! + ds = inst.get_doc() + # for testing purposes, concat them and strip the empty line at the end + res = functools.reduce(operator.iadd, ds, [])[:-1] + print(res) + return res + + # objects without docstring + def f(): + pass + assert getdocl('function', f) == [] + + # standard function, diverse docstring styles... + def f(): + """Docstring""" + + def g(): + """ + Docstring + """ + for func in (f, g): + assert getdocl('function', func) == ['Docstring'] + + # first line vs. other lines indentation + def f(): + """First line + + Other + lines + """ + assert getdocl('function', f) == ['First line', '', 'Other', ' lines'] + + # charset guessing (this module is encoded in utf-8) + def f(): + """Döcstring""" + assert getdocl('function', f) == ['Döcstring'] + + # verify that method docstrings get extracted in both normal case + # and in case of bound method posing as a function + class J: + def foo(self): + """Method docstring""" + assert getdocl('method', J.foo) == ['Method docstring'] + assert getdocl('function', J().foo) == ['Method docstring'] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_new_documenter(app): + class MyDocumenter(ModuleLevelDocumenter): + objtype = 'integer' + directivetype = 'integer' + priority = 100 + + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + return isinstance(member, int) + + def document_members(self, all_members=False): + return + + app.add_autodocumenter(MyDocumenter) + + options = {"members": 'integer'} + actual = do_autodoc(app, 'module', 'target', options) + assert list(actual) == [ + '', + '.. py:module:: target', + '', + '', + '.. py:integer:: integer', + ' :module: target', + '', + ' documentation for the integer', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_attrgetter_using(app): + directive = make_directive_bridge(app.env) + directive.genopt['members'] = ALL + + directive.genopt['inherited_members'] = False + with catch_warnings(record=True): + _assert_getter_works(app, directive, 'class', 'target.Class', ['meth']) + + directive.genopt['inherited_members'] = True + with catch_warnings(record=True): + _assert_getter_works(app, directive, 'class', 'target.inheritance.Derived', ['inheritedmeth']) + + +def _assert_getter_works(app, directive, objtype, name, attrs=(), **kw): + getattr_spy = [] + + def _special_getattr(obj, attr_name, *defargs): + if attr_name in attrs: + getattr_spy.append((obj, attr_name)) + return None + return getattr(obj, attr_name, *defargs) + + app.add_autodoc_attrgetter(type, _special_getattr) + + getattr_spy.clear() + app.registry.documenters[objtype](directive, name).generate(**kw) + + hooked_members = {s[1] for s in getattr_spy} + documented_members = {s[1] for s in processed_signatures} + for attr in attrs: + fullname = '.'.join((name, attr)) + assert attr in hooked_members + assert fullname not in documented_members, f'{fullname!r} not intercepted' + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_py_module(app, warning): + # without py:module + actual = do_autodoc(app, 'method', 'Class.meth') + assert list(actual) == [] + assert ("don't know which module to import for autodocumenting 'Class.meth'" + in warning.getvalue()) + + # with py:module + app.env.ref_context['py:module'] = 'target' + warning.truncate(0) + + actual = do_autodoc(app, 'method', 'Class.meth') + assert list(actual) == [ + '', + '.. py:method:: Class.meth()', + ' :module: target', + '', + ' Function.', + '', + ] + assert ("don't know which module to import for autodocumenting 'Class.meth'" + not in warning.getvalue()) + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_decorator(app): + actual = do_autodoc(app, 'decorator', 'target.decorator.deco1') + assert list(actual) == [ + '', + '.. py:decorator:: deco1', + ' :module: target.decorator', + '', + ' docstring for deco1', + '', + ] + + actual = do_autodoc(app, 'decorator', 'target.decorator.deco2') + assert list(actual) == [ + '', + '.. py:decorator:: deco2(condition, message)', + ' :module: target.decorator', + '', + ' docstring for deco2', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_exception(app): + actual = do_autodoc(app, 'exception', 'target.CustomEx') + assert list(actual) == [ + '', + '.. py:exception:: CustomEx', + ' :module: target', + '', + ' My custom exception.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_warnings(app, warning): + app.env.temp_data['docname'] = 'dummy' + + # can't import module + do_autodoc(app, 'module', 'unknown') + assert "failed to import module 'unknown'" in warning.getvalue() + + # missing function + do_autodoc(app, 'function', 'unknown') + assert "import for autodocumenting 'unknown'" in warning.getvalue() + + do_autodoc(app, 'function', 'target.unknown') + assert "failed to import function 'unknown' from module 'target'" in warning.getvalue() + + # missing method + do_autodoc(app, 'method', 'target.Class.unknown') + assert "failed to import method 'Class.unknown' from module 'target'" in warning.getvalue() + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_attributes(app): + options = {"synopsis": 'Synopsis', + "platform": "Platform", + "deprecated": None} + actual = do_autodoc(app, 'module', 'target', options) + assert list(actual) == [ + '', + '.. py:module:: target', + ' :synopsis: Synopsis', + ' :platform: Platform', + ' :deprecated:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_members(app): + # default (no-members) + actual = do_autodoc(app, 'class', 'target.inheritance.Base') + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ] + + # default ALL-members + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ' .. py:method:: Base.inheritedmeth()', + ' .. py:method:: Base.inheritedstaticmeth(cls)', + ] + + # default specific-members + options = {"members": "inheritedmeth,inheritedstaticmeth"} + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:method:: Base.inheritedmeth()', + ' .. py:method:: Base.inheritedstaticmeth(cls)', + ] + + # ALL-members override autodoc_default_options + options = {"members": None} + app.config.autodoc_default_options["members"] = "inheritedstaticmeth" + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ' .. py:method:: Base.inheritedmeth()', + ' .. py:method:: Base.inheritedstaticmeth(cls)', + ] + + # members override autodoc_default_options + options = {"members": "inheritedmeth"} + app.config.autodoc_default_options["members"] = "inheritedstaticmeth" + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:method:: Base.inheritedmeth()', + ] + + # members extends autodoc_default_options + options = {"members": "+inheritedmeth"} + app.config.autodoc_default_options["members"] = "inheritedstaticmeth" + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:method:: Base.inheritedmeth()', + ' .. py:method:: Base.inheritedstaticmeth(cls)', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_exclude_members(app): + options = {"members": None, + "exclude-members": "inheritedmeth,inheritedstaticmeth"} + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ] + + # members vs exclude-members + options = {"members": "inheritedmeth", + "exclude-members": "inheritedmeth"} + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ] + + # + has no effect when autodoc_default_options are not present + options = {"members": None, + "exclude-members": "+inheritedmeth,inheritedstaticmeth"} + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ] + + # exclude-members overrides autodoc_default_options + options = {"members": None, + "exclude-members": "inheritedmeth"} + app.config.autodoc_default_options["exclude-members"] = "inheritedstaticmeth" + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ' .. py:method:: Base.inheritedstaticmeth(cls)', + ] + + # exclude-members extends autodoc_default_options + options = {"members": None, + "exclude-members": "+inheritedmeth"} + app.config.autodoc_default_options["exclude-members"] = "inheritedstaticmeth" + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ] + + # no exclude-members causes use autodoc_default_options + options = {"members": None} + app.config.autodoc_default_options["exclude-members"] = "inheritedstaticmeth,inheritedmeth" + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ] + + # empty exclude-members cancels autodoc_default_options + options = {"members": None, + "exclude-members": None} + app.config.autodoc_default_options["exclude-members"] = "inheritedstaticmeth,inheritedmeth" + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Base()', + ' .. py:attribute:: Base.inheritedattr', + ' .. py:method:: Base.inheritedclassmeth()', + ' .. py:method:: Base.inheritedmeth()', + ' .. py:method:: Base.inheritedstaticmeth(cls)', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_undoc_members(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:method:: Class.excludemeth()', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_string', + ' .. py:attribute:: Class.mdocattr', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', + ' .. py:attribute:: Class.skipattr', + ' .. py:method:: Class.skipmeth()', + ' .. py:attribute:: Class.udocattr', + ' .. py:method:: Class.undocmeth()', + ] + + # use autodoc_default_options + options = {"members": None} + app.config.autodoc_default_options["undoc-members"] = None + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:method:: Class.excludemeth()', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_string', + ' .. py:attribute:: Class.mdocattr', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', + ' .. py:attribute:: Class.skipattr', + ' .. py:method:: Class.skipmeth()', + ' .. py:attribute:: Class.udocattr', + ' .. py:method:: Class.undocmeth()', + ] + + # options negation work check + options = {"members": None, + "no-undoc-members": None} + app.config.autodoc_default_options["undoc-members"] = None + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:method:: Class.excludemeth()', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_string', + ' .. py:attribute:: Class.mdocattr', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:method:: Class.skipmeth()', + ' .. py:attribute:: Class.udocattr', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_undoc_members_for_metadata_only(app): + # metadata only member is not displayed + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.metadata', options) + assert list(actual) == [ + '', + '.. py:module:: target.metadata', + '', + ] + + # metadata only member is displayed when undoc-member given + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.metadata', options) + assert list(actual) == [ + '', + '.. py:module:: target.metadata', + '', + '', + '.. py:function:: foo()', + ' :module: target.metadata', + '', + ' :meta metadata-only-docstring:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inherited_members(app): + options = {"members": None, + "inherited-members": None} + actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options) + assert list(filter(lambda l: 'method::' in l, actual)) == [ + ' .. py:method:: Derived.inheritedclassmeth()', + ' .. py:method:: Derived.inheritedmeth()', + ' .. py:method:: Derived.inheritedstaticmeth(cls)', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inherited_members_Base(app): + options = {"members": None, + "inherited-members": "Base", + "special-members": None} + + # check methods for object class are shown + actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options) + assert ' .. py:method:: Derived.inheritedmeth()' in actual + assert ' .. py:method:: Derived.inheritedclassmeth' not in actual + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inherited_members_None(app): + options = {"members": None, + "inherited-members": "None", + "special-members": None} + + # check methods for object class are shown + actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options) + assert ' .. py:method:: Derived.__init__()' in actual + assert ' .. py:method:: Derived.__str__()' in actual + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_imported_members(app): + options = {"members": None, + "imported-members": None, + "ignore-module-all": None} + actual = do_autodoc(app, 'module', 'target', options) + assert '.. py:function:: function_to_be_imported(app: ~sphinx.application.Sphinx | None) -> str' in actual + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_special_members(app): + # specific special methods + options = {"undoc-members": None, + "special-members": "__init__,__special1__"} + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.__init__(arg)', + ' .. py:method:: Class.__special1__()', + ] + + # combination with specific members + options = {"members": "attr,docattr", + "undoc-members": None, + "special-members": "__init__,__special1__"} + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.__init__(arg)', + ' .. py:method:: Class.__special1__()', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ] + + # all special methods + options = { + "members": None, + "undoc-members": None, + "special-members": None, + } + if sys.version_info >= (3, 13, 0, 'alpha', 5): + options["exclude-members"] = "__static_attributes__" + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:attribute:: Class.__annotations__', + ' .. py:attribute:: Class.__dict__', + ' .. py:method:: Class.__init__(arg)', + ' .. py:attribute:: Class.__module__', + ' .. py:method:: Class.__special1__()', + ' .. py:method:: Class.__special2__()', + ' .. py:attribute:: Class.__weakref__', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:method:: Class.excludemeth()', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_string', + ' .. py:attribute:: Class.mdocattr', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', + ' .. py:attribute:: Class.skipattr', + ' .. py:method:: Class.skipmeth()', + ' .. py:attribute:: Class.udocattr', + ' .. py:method:: Class.undocmeth()', + ] + + # specific special methods from autodoc_default_options + options = {"undoc-members": None} + app.config.autodoc_default_options["special-members"] = "__special2__" + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.__special2__()', + ] + + # specific special methods option with autodoc_default_options + options = {"undoc-members": None, + "special-members": "__init__,__special1__"} + app.config.autodoc_default_options["special-members"] = "__special2__" + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.__init__(arg)', + ' .. py:method:: Class.__special1__()', + ] + + # specific special methods merge with autodoc_default_options + options = {"undoc-members": None, + "special-members": "+__init__,__special1__"} + app.config.autodoc_default_options["special-members"] = "__special2__" + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.__init__(arg)', + ' .. py:method:: Class.__special1__()', + ' .. py:method:: Class.__special2__()', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_ignore_module_all(app): + # default (no-ignore-module-all) + options = {"members": None} + actual = do_autodoc(app, 'module', 'target', options) + assert list(filter(lambda l: 'class::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ] + + # ignore-module-all + options = {"members": None, + "ignore-module-all": None} + actual = do_autodoc(app, 'module', 'target', options) + assert list(filter(lambda l: 'class::' in l, actual)) == [ + '.. py:class:: Class(arg)', + '.. py:class:: CustomDict', + '.. py:class:: InnerChild()', + '.. py:class:: InstAttCls()', + '.. py:class:: Outer()', + ' .. py:class:: Outer.Inner()', + '.. py:class:: StrRepr', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_noindex(app): + options = {"no-index": None} + actual = do_autodoc(app, 'module', 'target', options) + assert list(actual) == [ + '', + '.. py:module:: target', + ' :no-index:', + '', + ] + + # TODO: :no-index: should be propagated to children of target item. + + actual = do_autodoc(app, 'class', 'target.inheritance.Base', options) + assert list(actual) == [ + '', + '.. py:class:: Base()', + ' :no-index:', + ' :module: target.inheritance', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_subclass_of_builtin_class(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.CustomDict', options) + assert list(actual) == [ + '', + '.. py:class:: CustomDict', + ' :module: target', + '', + ' Docstring.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inner_class(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.Outer', options) + assert list(actual) == [ + '', + '.. py:class:: Outer()', + ' :module: target', + '', + ' Foo', + '', + '', + ' .. py:class:: Outer.Inner()', + ' :module: target', + '', + ' Foo', + '', + '', + ' .. py:method:: Outer.Inner.meth()', + ' :module: target', + '', + ' Foo', + '', + '', + ' .. py:attribute:: Outer.factory', + ' :module: target', + '', + ' alias of :py:class:`dict`', + ] + + actual = do_autodoc(app, 'class', 'target.Outer.Inner', options) + assert list(actual) == [ + '', + '.. py:class:: Inner()', + ' :module: target.Outer', + '', + ' Foo', + '', + '', + ' .. py:method:: Inner.meth()', + ' :module: target.Outer', + '', + ' Foo', + '', + ] + + options['show-inheritance'] = None + actual = do_autodoc(app, 'class', 'target.InnerChild', options) + assert list(actual) == [ + '', + '.. py:class:: InnerChild()', + ' :module: target', '', + ' Bases: :py:class:`~target.Outer.Inner`', + '', + ' InnerChild docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_classmethod(app): + actual = do_autodoc(app, 'method', 'target.inheritance.Base.inheritedclassmeth') + assert list(actual) == [ + '', + '.. py:method:: Base.inheritedclassmeth()', + ' :module: target.inheritance', + ' :classmethod:', + '', + ' Inherited class method.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_staticmethod(app): + actual = do_autodoc(app, 'method', 'target.inheritance.Base.inheritedstaticmeth') + assert list(actual) == [ + '', + '.. py:method:: Base.inheritedstaticmeth(cls)', + ' :module: target.inheritance', + ' :staticmethod:', + '', + ' Inherited static method.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_descriptor(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.descriptor.Class', options) + assert list(actual) == [ + '', + '.. py:class:: Class()', + ' :module: target.descriptor', + '', + '', + ' .. py:attribute:: Class.descr', + ' :module: target.descriptor', + '', + ' Descriptor instance docstring.', + '', + '', + ' .. py:property:: Class.prop', + ' :module: target.descriptor', + '', + ' Property.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_cached_property(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.cached_property.Foo', options) + assert list(actual) == [ + '', + '.. py:class:: Foo()', + ' :module: target.cached_property', + '', + '', + ' .. py:property:: Foo.prop', + ' :module: target.cached_property', + ' :type: int', + '', + '', + ' .. py:property:: Foo.prop_with_type_comment', + ' :module: target.cached_property', + ' :type: int', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_member_order(app): + # case member-order='bysource' + options = {"members": None, + 'member-order': 'bysource', + "undoc-members": None, + 'private-members': None} + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.undocmeth()', + ' .. py:method:: Class.skipmeth()', + ' .. py:method:: Class.excludemeth()', + ' .. py:attribute:: Class.skipattr', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:attribute:: Class.udocattr', + ' .. py:attribute:: Class.mdocattr', + ' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_string', + ' .. py:attribute:: Class._private_inst_attr', + ] + + # case member-order='groupwise' + options = {"members": None, + 'member-order': 'groupwise', + "undoc-members": None, + 'private-members': None} + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.excludemeth()', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', + ' .. py:method:: Class.skipmeth()', + ' .. py:method:: Class.undocmeth()', + ' .. py:attribute:: Class._private_inst_attr', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_string', + ' .. py:attribute:: Class.mdocattr', + ' .. py:attribute:: Class.skipattr', + ' .. py:attribute:: Class.udocattr', + ] + + # case member-order=None + options = {"members": None, + "undoc-members": None, + 'private-members': None} + actual = do_autodoc(app, 'class', 'target.Class', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:attribute:: Class._private_inst_attr', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:method:: Class.excludemeth()', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_string', + ' .. py:attribute:: Class.mdocattr', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', + ' .. py:attribute:: Class.skipattr', + ' .. py:method:: Class.skipmeth()', + ' .. py:attribute:: Class.udocattr', + ' .. py:method:: Class.undocmeth()', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_module_member_order(app): + # case member-order='bysource' + options = {"members": 'foo, Bar, baz, qux, Quux, foobar', + 'member-order': 'bysource', + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.sort_by_all', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:module:: target.sort_by_all', + '.. py:function:: baz()', + '.. py:function:: foo()', + '.. py:class:: Bar()', + '.. py:class:: Quux()', + '.. py:function:: foobar()', + '.. py:function:: qux()', + ] + + # case member-order='bysource' and ignore-module-all + options = {"members": 'foo, Bar, baz, qux, Quux, foobar', + 'member-order': 'bysource', + "undoc-members": None, + "ignore-module-all": None} + actual = do_autodoc(app, 'module', 'target.sort_by_all', options) + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:module:: target.sort_by_all', + '.. py:function:: foo()', + '.. py:class:: Bar()', + '.. py:function:: baz()', + '.. py:function:: qux()', + '.. py:class:: Quux()', + '.. py:function:: foobar()', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_module_scope(app): + app.env.temp_data['autodoc:module'] = 'target' + actual = do_autodoc(app, 'attribute', 'Class.mdocattr') + assert list(actual) == [ + '', + '.. py:attribute:: Class.mdocattr', + ' :module: target', + ' :value: <_io.StringIO object>', + '', + ' should be documented as well - süß', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_class_scope(app): + app.env.temp_data['autodoc:module'] = 'target' + app.env.temp_data['autodoc:class'] = 'Class' + actual = do_autodoc(app, 'attribute', 'mdocattr') + assert list(actual) == [ + '', + '.. py:attribute:: Class.mdocattr', + ' :module: target', + ' :value: <_io.StringIO object>', + '', + ' should be documented as well - süß', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_attributes(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.AttCls', options) + assert list(actual) == [ + '', + '.. py:class:: AttCls()', + ' :module: target', + '', + '', + ' .. py:attribute:: AttCls.a1', + ' :module: target', + ' :value: hello world', + '', + '', + ' .. py:attribute:: AttCls.a2', + ' :module: target', + ' :value: None', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoclass_instance_attributes(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.InstAttCls', options) + assert list(actual) == [ + '', + '.. py:class:: InstAttCls()', + ' :module: target', + '', + ' Class with documented class and instance attributes.', + '', + '', + ' .. py:attribute:: InstAttCls.ca1', + ' :module: target', + " :value: 'a'", + '', + ' Doc comment for class attribute InstAttCls.ca1.', + ' It can have multiple lines.', + '', + '', + ' .. py:attribute:: InstAttCls.ca2', + ' :module: target', + " :value: 'b'", + '', + ' Doc comment for InstAttCls.ca2. One line only.', + '', + '', + ' .. py:attribute:: InstAttCls.ca3', + ' :module: target', + " :value: 'c'", + '', + ' Docstring for class attribute InstAttCls.ca3.', + '', + '', + ' .. py:attribute:: InstAttCls.ia1', + ' :module: target', + '', + ' Doc comment for instance attribute InstAttCls.ia1', + '', + '', + ' .. py:attribute:: InstAttCls.ia2', + ' :module: target', + '', + ' Docstring for instance attribute InstAttCls.ia2.', + '', + ] + + # pick up arbitrary attributes + options = {"members": 'ca1,ia1'} + actual = do_autodoc(app, 'class', 'target.InstAttCls', options) + assert list(actual) == [ + '', + '.. py:class:: InstAttCls()', + ' :module: target', + '', + ' Class with documented class and instance attributes.', + '', + '', + ' .. py:attribute:: InstAttCls.ca1', + ' :module: target', + " :value: 'a'", + '', + ' Doc comment for class attribute InstAttCls.ca1.', + ' It can have multiple lines.', + '', + '', + ' .. py:attribute:: InstAttCls.ia1', + ' :module: target', + '', + ' Doc comment for instance attribute InstAttCls.ia1', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_attributes(app): + actual = do_autodoc(app, 'attribute', 'target.InstAttCls.ia1') + assert list(actual) == [ + '', + '.. py:attribute:: InstAttCls.ia1', + ' :module: target', + '', + ' Doc comment for instance attribute InstAttCls.ia1', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_slots(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.slots', options) + assert list(actual) == [ + '', + '.. py:module:: target.slots', + '', + '', + '.. py:class:: Bar()', + ' :module: target.slots', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Bar.attr1', + ' :module: target.slots', + ' :type: int', + '', + ' docstring of attr1', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.slots', + '', + ' docstring of instance attr2', + '', + '', + ' .. py:attribute:: Bar.attr3', + ' :module: target.slots', + '', + '', + '.. py:class:: Baz()', + ' :module: target.slots', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Baz.attr', + ' :module: target.slots', + '', + '', + '.. py:class:: Foo()', + ' :module: target.slots', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr', + ' :module: target.slots', + '', + ] + + +class _EnumFormatter: + def __init__(self, name: str, *, module: str = 'target.enums') -> None: + self.name = name + self.module = module + + @property + def target(self) -> str: + """The autodoc target class.""" + return f'{self.module}.{self.name}' + + def subtarget(self, name: str) -> str: + """The autodoc sub-target (an attribute, method, etc).""" + return f'{self.target}.{name}' + + def _node( + self, role: str, name: str, doc: str, *, args: str, indent: int, **options: Any, + ) -> list[str]: + prefix = indent * ' ' + tab = ' ' * 3 + + def rst_option(name: str, value: Any) -> str: + value = '' if value in {1, True} else value + return f'{prefix}{tab}:{name}: {value!s}'.rstrip() + + lines = [ + '', + f'{prefix}.. py:{role}:: {name}{args}', + f'{prefix}{tab}:module: {self.module}', + *itertools.starmap(rst_option, options.items()), + ] + if doc: + lines.extend(['', f'{prefix}{tab}{doc}']) + lines.append('') + return lines + + def entry( + self, + entry_name: str, + doc: str = '', + *, + role: str, + args: str = '', + indent: int = 3, + **rst_options: Any, + ) -> list[str]: + """Get the RST lines for a named attribute, method, etc.""" + qualname = f'{self.name}.{entry_name}' + return self._node(role, qualname, doc, args=args, indent=indent, **rst_options) + + def brief(self, doc: str, *, indent: int = 0, **options: Any) -> list[str]: + """Generate the brief part of the class being documented.""" + assert doc, f'enumeration class {self.target!r} should have an explicit docstring' + + if sys.version_info[:2] >= (3, 13) or sys.version_info[:3] >= (3, 12, 3): + args = ('(value, names=<not given>, *values, module=None, ' + 'qualname=None, type=None, start=1, boundary=None)') + elif sys.version_info[:2] >= (3, 12): + args = ('(value, names=None, *values, module=None, ' + 'qualname=None, type=None, start=1, boundary=None)') + elif sys.version_info[:2] >= (3, 11): + args = ('(value, names=None, *, module=None, qualname=None, ' + 'type=None, start=1, boundary=None)') + else: + args = '(value)' + + return self._node('class', self.name, doc, args=args, indent=indent, **options) + + def method( + self, name: str, doc: str, *flags: str, args: str = '()', indent: int = 3, + ) -> list[str]: + rst_options = dict.fromkeys(flags, '') + return self.entry(name, doc, role='method', args=args, indent=indent, **rst_options) + + def member(self, name: str, value: Any, doc: str, *, indent: int = 3) -> list[str]: + rst_options = {'value': repr(value)} + return self.entry(name, doc, role='attribute', indent=indent, **rst_options) + + +@pytest.fixture() +def autodoc_enum_options() -> dict[str, object]: + """Default autodoc options to use when testing enum's documentation.""" + return {"members": None, "undoc-members": None} + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumCls') + options = autodoc_enum_options | {'private-members': None} + + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'a classmethod says good-bye to you.', 'classmethod'), + *fmt.method('say_hello', 'a method says hello to you.'), + *fmt.member('val1', 12, 'doc for val1'), + *fmt.member('val2', 23, 'doc for val2'), + *fmt.member('val3', 34, 'doc for val3'), + *fmt.member('val4', 34, ''), # val4 is alias of val3 + ] + + # Inherited members exclude the native Enum API (in particular + # the 'name' and 'value' properties), unless they were explicitly + # redefined by the user in one of the bases. + actual = do_autodoc(app, 'class', fmt.target, options | {'inherited-members': None}) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'a classmethod says good-bye to you.', 'classmethod'), + *fmt.method('say_hello', 'a method says hello to you.'), + *fmt.member('val1', 12, 'doc for val1'), + *fmt.member('val2', 23, 'doc for val2'), + *fmt.member('val3', 34, 'doc for val3'), + *fmt.member('val4', 34, ''), # val4 is alias of val3 + ] + + # checks for an attribute of EnumCls + actual = do_autodoc(app, 'attribute', fmt.subtarget('val1')) + assert list(actual) == fmt.member('val1', 12, 'doc for val1', indent=0) + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_data_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithDataType') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.member('x', 'x', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'inherited'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.member('x', 'x', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinType') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_type_and_inheritence(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinTypeInherit') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'inherited', 'classmethod'), + *fmt.method('say_hello', 'inherited'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_enum_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinEnumType') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + # override() is overridden at the class level so it should be rendered + *fmt.method('override', 'overridden'), + # say_goodbye() and say_hello() are not rendered since they are inherited + *fmt.member('x', 'x', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('override', 'overridden'), + *fmt.method('say_goodbye', 'inherited', 'classmethod'), + *fmt.method('say_hello', 'inherited'), + *fmt.member('x', 'x', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_and_data_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinAndDataType') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('isupper', 'overridden'), + *fmt.method('say_goodbye', 'overridden', 'classmethod'), + *fmt.method('say_hello', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + # add the special member __str__ (but not the inherited members) + options = autodoc_enum_options | {'special-members': '__str__'} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('__str__', 'overridden'), + *fmt.method('isupper', 'overridden'), + *fmt.method('say_goodbye', 'overridden', 'classmethod'), + *fmt.method('say_hello', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'overridden'), + *fmt.method('say_goodbye', 'overridden', 'classmethod'), + *fmt.method('say_hello', 'overridden'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_with_parent_enum(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithParentEnum') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('isupper', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + # add the special member __str__ (but not the inherited members) + options = autodoc_enum_options | {'special-members': '__str__'} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('__str__', 'overridden'), + *fmt.method('isupper', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'overridden'), + *fmt.method('override', 'inherited'), + *fmt.method('say_goodbye', 'inherited', 'classmethod'), + *fmt.method('say_hello', 'inherited'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_sunder_method(app, autodoc_enum_options): + PRIVATE = {'private-members': None} # sunder methods are recognized as private + + fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumSunderMissingInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumSunderMissingInDataType') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumSunderMissingInClass') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_inherited_sunder_method(app, autodoc_enum_options): + options = autodoc_enum_options | {'private-members': None, 'inherited-members': None} + + fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), + ] + + fmt = _EnumFormatter('EnumSunderMissingInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), + ] + + fmt = _EnumFormatter('EnumSunderMissingInDataType') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'inherited'), + ] + + fmt = _EnumFormatter('EnumSunderMissingInClass') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_custom_name_property(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumNamePropertyInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumNamePropertyInDataType') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumNamePropertyInClass') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'docstring', role='property'), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_inherited_custom_name_property(app, autodoc_enum_options): + options = autodoc_enum_options | {"inherited-members": None} + + fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'inherited', role='property'), + ] + + fmt = _EnumFormatter('EnumNamePropertyInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'inherited', role='property'), + ] + + fmt = _EnumFormatter('EnumNamePropertyInDataType') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'inherited'), + *fmt.entry('name', 'inherited', role='property'), + ] + + fmt = _EnumFormatter('EnumNamePropertyInClass') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'docstring', role='property'), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_descriptor_class(app): + options = {"members": 'CustomDataDescriptor,CustomDataDescriptor2'} + actual = do_autodoc(app, 'module', 'target.descriptor', options) + assert list(actual) == [ + '', + '.. py:module:: target.descriptor', + '', + '', + '.. py:class:: CustomDataDescriptor(doc)', + ' :module: target.descriptor', + '', + ' Descriptor class docstring.', + '', + '', + ' .. py:method:: CustomDataDescriptor.meth()', + ' :module: target.descriptor', + '', + ' Function.', + '', + '', + '.. py:class:: CustomDataDescriptor2(doc)', + ' :module: target.descriptor', + '', + ' Descriptor class with custom metaclass docstring.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automethod_for_builtin(app): + actual = do_autodoc(app, 'method', 'builtins.int.__add__') + assert list(actual) == [ + '', + '.. py:method:: int.__add__(value, /)', + ' :module: builtins', + '', + ' Return self+value.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automethod_for_decorated(app): + actual = do_autodoc(app, 'method', 'target.decorator.Bar.meth') + assert list(actual) == [ + '', + '.. py:method:: Bar.meth(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_abstractmethods(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.abstractmethods', options) + assert list(actual) == [ + '', + '.. py:module:: target.abstractmethods', + '', + '', + '.. py:class:: Base()', + ' :module: target.abstractmethods', + '', + '', + ' .. py:method:: Base.abstractmeth()', + ' :module: target.abstractmethods', + ' :abstractmethod:', + '', + '', + ' .. py:method:: Base.classmeth()', + ' :module: target.abstractmethods', + ' :abstractmethod:', + ' :classmethod:', + '', + '', + ' .. py:method:: Base.coroutinemeth()', + ' :module: target.abstractmethods', + ' :abstractmethod:', + ' :async:', + '', + '', + ' .. py:method:: Base.meth()', + ' :module: target.abstractmethods', + '', + '', + ' .. py:property:: Base.prop', + ' :module: target.abstractmethods', + ' :abstractmethod:', + '', + '', + ' .. py:method:: Base.staticmeth()', + ' :module: target.abstractmethods', + ' :abstractmethod:', + ' :staticmethod:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_partialfunction(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.partialfunction', options) + assert list(actual) == [ + '', + '.. py:module:: target.partialfunction', + '', + '', + '.. py:function:: func1(a, b, c)', + ' :module: target.partialfunction', + '', + ' docstring of func1', + '', + '', + '.. py:function:: func2(b, c)', + ' :module: target.partialfunction', + '', + ' docstring of func1', + '', + '', + '.. py:function:: func3(c)', + ' :module: target.partialfunction', + '', + ' docstring of func3', + '', + '', + '.. py:function:: func4()', + ' :module: target.partialfunction', + '', + ' docstring of func3', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_imported_partialfunction_should_not_shown_without_imported_members(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.imported_members', options) + assert list(actual) == [ + '', + '.. py:module:: target.imported_members', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_bound_method(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.bound_method', options) + assert list(actual) == [ + '', + '.. py:module:: target.bound_method', + '', + '', + '.. py:function:: bound_method()', + ' :module: target.bound_method', + '', + ' Method docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_partialmethod(app): + expected = [ + '', + '.. py:class:: Cell()', + ' :module: target.partialmethod', + '', + ' An example for partialmethod.', + '', + ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', + '', + '', + ' .. py:method:: Cell.set_alive()', + ' :module: target.partialmethod', + '', + ' Make a cell alive.', + '', + '', + ' .. py:method:: Cell.set_state(state)', + ' :module: target.partialmethod', + '', + ' Update state of cell to *state*.', + '', + ] + + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options) + assert list(actual) == expected + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_partialmethod_undoc_members(app): + expected = [ + '', + '.. py:class:: Cell()', + ' :module: target.partialmethod', + '', + ' An example for partialmethod.', + '', + ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', + '', + '', + ' .. py:method:: Cell.set_alive()', + ' :module: target.partialmethod', + '', + ' Make a cell alive.', + '', + '', + ' .. py:method:: Cell.set_dead()', + ' :module: target.partialmethod', + '', + '', + ' .. py:method:: Cell.set_state(state)', + ' :module: target.partialmethod', + '', + ' Update state of cell to *state*.', + '', + ] + + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options) + assert list(actual) == expected + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_typed_instance_variables(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.typed_vars', options) + assert list(actual) == [ + '', + '.. py:module:: target.typed_vars', + '', + '', + '.. py:attribute:: Alias', + ' :module: target.typed_vars', + '', + ' alias of :py:class:`~target.typed_vars.Derived`', + '', + '.. py:class:: Class()', + ' :module: target.typed_vars', + '', + '', + ' .. py:attribute:: Class.attr1', + ' :module: target.typed_vars', + ' :type: int', + ' :value: 0', + '', + '', + ' .. py:attribute:: Class.attr2', + ' :module: target.typed_vars', + ' :type: int', + '', + '', + ' .. py:attribute:: Class.attr3', + ' :module: target.typed_vars', + ' :type: int', + ' :value: 0', + '', + '', + ' .. py:attribute:: Class.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + '', + ' .. py:attribute:: Class.attr5', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr5', + '', + '', + ' .. py:attribute:: Class.attr6', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr6', + '', + '', + ' .. py:attribute:: Class.descr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' This is descr4', + '', + '', + '.. py:class:: Derived()', + ' :module: target.typed_vars', + '', + '', + ' .. py:attribute:: Derived.attr7', + ' :module: target.typed_vars', + ' :type: int', + '', + '', + '.. py:data:: attr1', + ' :module: target.typed_vars', + ' :type: str', + " :value: ''", + '', + ' attr1', + '', + '', + '.. py:data:: attr2', + ' :module: target.typed_vars', + ' :type: str', + '', + ' attr2', + '', + '', + '.. py:data:: attr3', + ' :module: target.typed_vars', + ' :type: str', + " :value: ''", + '', + ' attr3', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_typed_inherited_instance_variables(app): + options = {"members": None, + "undoc-members": None, + "inherited-members": None} + actual = do_autodoc(app, 'class', 'target.typed_vars.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.typed_vars', + '', + '', + ' .. py:attribute:: Derived.attr1', + ' :module: target.typed_vars', + ' :type: int', + ' :value: 0', + '', + '', + ' .. py:attribute:: Derived.attr2', + ' :module: target.typed_vars', + ' :type: int', + '', + '', + ' .. py:attribute:: Derived.attr3', + ' :module: target.typed_vars', + ' :type: int', + ' :value: 0', + '', + '', + ' .. py:attribute:: Derived.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + '', + ' .. py:attribute:: Derived.attr5', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr5', + '', + '', + ' .. py:attribute:: Derived.attr6', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr6', + '', + '', + ' .. py:attribute:: Derived.attr7', + ' :module: target.typed_vars', + ' :type: int', + '', + '', + ' .. py:attribute:: Derived.descr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_GenericAlias(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.genericalias', options) + assert list(actual) == [ + '', + '.. py:module:: target.genericalias', + '', + '', + '.. py:class:: Class()', + ' :module: target.genericalias', + '', + '', + ' .. py:attribute:: Class.T', + ' :module: target.genericalias', + '', + ' A list of int', + '', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', + '', + '', + '.. py:data:: L', + ' :module: target.genericalias', + '', + ' A list of Class', + '', + ' alias of :py:class:`~typing.List`\\ ' + '[:py:class:`~target.genericalias.Class`]', + '', + '', + '.. py:data:: T', + ' :module: target.genericalias', + '', + ' A list of int', + '', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_TypeVar(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.typevar', options) + assert list(actual) == [ + '', + '.. py:module:: target.typevar', + '', + '', + '.. py:class:: Class()', + ' :module: target.typevar', + '', + '', + ' .. py:class:: Class.T1', + ' :module: target.typevar', + '', + ' T1', + '', + " alias of TypeVar('T1')", + '', + '', + ' .. py:class:: Class.T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :py:class:`~datetime.date`', + '', + '', + '.. py:class:: T1', + ' :module: target.typevar', + '', + ' T1', + '', + " alias of TypeVar('T1')", + '', + '', + '.. py:class:: T3', + ' :module: target.typevar', + '', + ' T3', + '', + " alias of TypeVar('T3', int, str)", + '', + '', + '.. py:class:: T4', + ' :module: target.typevar', + '', + ' T4', + '', + " alias of TypeVar('T4', covariant=True)", + '', + '', + '.. py:class:: T5', + ' :module: target.typevar', + '', + ' T5', + '', + " alias of TypeVar('T5', contravariant=True)", + '', + '', + '.. py:class:: T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :py:class:`~datetime.date`', + '', + '', + '.. py:class:: T7', + ' :module: target.typevar', + '', + ' T7', + '', + " alias of TypeVar('T7', bound=\\ :py:class:`int`)", + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_Annotated(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.annotated', options) + assert list(actual) == [ + '', + '.. py:module:: target.annotated', + '', + '', + '.. py:function:: hello(name: str) -> None', + ' :module: target.annotated', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_TYPE_CHECKING(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.TYPE_CHECKING', options) + assert list(actual) == [ + '', + '.. py:module:: target.TYPE_CHECKING', + '', + '', + '.. py:class:: Foo()', + ' :module: target.TYPE_CHECKING', + '', + '', + ' .. py:attribute:: Foo.attr1', + ' :module: target.TYPE_CHECKING', + ' :type: ~_io.StringIO', + '', + '', + '.. py:function:: spam(ham: ~collections.abc.Iterable[str]) -> tuple[~gettext.NullTranslations, bool]', + ' :module: target.TYPE_CHECKING', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_TYPE_CHECKING_circular_import(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'circular_import', options) + assert list(actual) == [ + '', + '.. py:module:: circular_import', + '', + ] + assert sys.modules["circular_import"].a is sys.modules["circular_import.a"] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatch(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.singledispatch', options) + assert list(actual) == [ + '', + '.. py:module:: target.singledispatch', + '', + '', + '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', + ' func(arg: int, kwarg=None)', + ' func(arg: str, kwarg=None)', + ' func(arg: dict, kwarg=None)', + ' :module: target.singledispatch', + '', + ' A function for general use.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.singledispatchmethod', options) + assert list(actual) == [ + '', + '.. py:module:: target.singledispatchmethod', + '', + '', + '.. py:class:: Foo()', + ' :module: target.singledispatchmethod', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', + ' Foo.meth(arg: int, kwarg=None)', + ' Foo.meth(arg: str, kwarg=None)', + ' Foo.meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod', + '', + ' A method for general use.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod_automethod(app): + options = {} + actual = do_autodoc(app, 'method', 'target.singledispatchmethod.Foo.meth', options) + assert list(actual) == [ + '', + '.. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', + ' Foo.meth(arg: int, kwarg=None)', + ' Foo.meth(arg: str, kwarg=None)', + ' Foo.meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod', + '', + ' A method for general use.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod_classmethod(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.singledispatchmethod_classmethod', options) + + assert list(actual) == [ + '', + '.. py:module:: target.singledispatchmethod_classmethod', + '', + '', + '.. py:class:: Foo()', + ' :module: target.singledispatchmethod_classmethod', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.class_meth(arg, kwarg=None)', + ' Foo.class_meth(arg: float, kwarg=None)', + ' Foo.class_meth(arg: int, kwarg=None)', + ' Foo.class_meth(arg: str, kwarg=None)', + ' Foo.class_meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod_classmethod', + ' :classmethod:', + '', + ' A class method for general use.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod_classmethod_automethod(app): + options = {} + actual = do_autodoc(app, 'method', 'target.singledispatchmethod_classmethod.Foo.class_meth', options) + + assert list(actual) == [ + '', + '.. py:method:: Foo.class_meth(arg, kwarg=None)', + ' Foo.class_meth(arg: float, kwarg=None)', + ' Foo.class_meth(arg: int, kwarg=None)', + ' Foo.class_meth(arg: str, kwarg=None)', + ' Foo.class_meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod_classmethod', + ' :classmethod:', + '', + ' A class method for general use.', + '', + ] + + +@pytest.mark.skipif(sys.version_info[:2] >= (3, 13), + reason='Cython does not support Python 3.13 yet.') +@pytest.mark.skipif(pyximport is None, reason='cython is not installed') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_cython(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.cython', options) + assert list(actual) == [ + '', + '.. py:module:: target.cython', + '', + '', + '.. py:class:: Class()', + ' :module: target.cython', + '', + ' Docstring.', + '', + '', + ' .. py:method:: Class.meth(name: str, age: int = 0) -> None', + ' :module: target.cython', + '', + ' Docstring.', + '', + '', + '.. py:function:: foo(x: int, *args, y: str, **kwargs)', + ' :module: target.cython', + '', + ' Docstring.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_final(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.final', options) + assert list(actual) == [ + '', + '.. py:module:: target.final', + '', + '', + '.. py:class:: Class()', + ' :module: target.final', + ' :final:', + '', + ' docstring', + '', + '', + ' .. py:method:: Class.meth1()', + ' :module: target.final', + ' :final:', + '', + ' docstring', + '', + '', + ' .. py:method:: Class.meth2()', + ' :module: target.final', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_overload(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.overload', options) + assert list(actual) == [ + '', + '.. py:module:: target.overload', + '', + '', + '.. py:class:: Bar(x: int, y: int)', + ' Bar(x: str, y: str)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Baz(x: int, y: int)', + ' Baz(x: str, y: str)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Foo(x: int, y: int)', + ' Foo(x: str, y: str)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Math()', + ' :module: target.overload', + '', + ' docstring', + '', + '', + ' .. py:method:: Math.sum(x: int, y: int = 0) -> int', + ' Math.sum(x: float, y: float = 0.0) -> float', + ' Math.sum(x: str, y: str = None) -> str', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:function:: sum(x: int, y: int = 0) -> int', + ' sum(x: float, y: float = 0.0) -> float', + ' sum(x: str, y: str = None) -> str', + ' :module: target.overload', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_overload2(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.overload2', options) + assert list(actual) == [ + '', + '.. py:module:: target.overload2', + '', + '', + '.. py:class:: Baz(x: int, y: int)', + ' Baz(x: str, y: str)', + ' :module: target.overload2', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_pymodule_for_ModuleLevelDocumenter(app): + app.env.ref_context['py:module'] = 'target.classes' + actual = do_autodoc(app, 'class', 'Foo') + assert list(actual) == [ + '', + '.. py:class:: Foo()', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_pymodule_for_ClassLevelDocumenter(app): + app.env.ref_context['py:module'] = 'target.methods' + actual = do_autodoc(app, 'method', 'Base.meth') + assert list(actual) == [ + '', + '.. py:method:: Base.meth()', + ' :module: target.methods', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_pyclass_for_ClassLevelDocumenter(app): + app.env.ref_context['py:module'] = 'target.methods' + app.env.ref_context['py:class'] = 'Base' + actual = do_autodoc(app, 'method', 'meth') + assert list(actual) == [ + '', + '.. py:method:: Base.meth()', + ' :module: target.methods', + '', + ] + + +@pytest.mark.sphinx('dummy', testroot='ext-autodoc') +def test_autodoc(app, status, warning): + app.build(force_all=True) + + content = app.env.get_doctree('index') + assert isinstance(content[3], addnodes.desc) + assert content[3][0].astext() == 'autodoc_dummy_module.test()' + assert content[3][1].astext() == 'Dummy function using dummy.*' + + # issue sphinx-doc/sphinx#2437 + assert content[11][-1].astext() == """Dummy class Bar with alias. + + + +my_name + +alias of Foo""" + assert warning.getvalue() == '' + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_name_conflict(app): + actual = do_autodoc(app, 'class', 'target.name_conflict.foo') + assert list(actual) == [ + '', + '.. py:class:: foo()', + ' :module: target.name_conflict', + '', + ' docstring of target.name_conflict::foo.', + '', + ] + + actual = do_autodoc(app, 'class', 'target.name_conflict.foo.bar') + assert list(actual) == [ + '', + '.. py:class:: bar()', + ' :module: target.name_conflict.foo', + '', + ' docstring of target.name_conflict.foo::bar.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_name_mangling(app): + options = {"members": None, + "undoc-members": None, + "private-members": None} + actual = do_autodoc(app, 'module', 'target.name_mangling', options) + assert list(actual) == [ + '', + '.. py:module:: target.name_mangling', + '', + '', + '.. py:class:: Bar()', + ' :module: target.name_mangling', + '', + '', + ' .. py:attribute:: Bar._Baz__email', + ' :module: target.name_mangling', + ' :value: None', + '', + ' a member having mangled-like name', + '', + '', + ' .. py:attribute:: Bar.__address', + ' :module: target.name_mangling', + ' :value: None', + '', + '', + '.. py:class:: Foo()', + ' :module: target.name_mangling', + '', + '', + ' .. py:attribute:: Foo.__age', + ' :module: target.name_mangling', + ' :value: None', + '', + '', + ' .. py:attribute:: Foo.__name', + ' :module: target.name_mangling', + ' :value: None', + '', + ' name of Foo', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_type_union_operator(app): + options = {'members': None} + actual = do_autodoc(app, 'module', 'target.pep604', options) + assert list(actual) == [ + '', + '.. py:module:: target.pep604', + '', + '', + '.. py:class:: Foo()', + ' :module: target.pep604', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr', + ' :module: target.pep604', + ' :type: int | str', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.meth(x: int | str, y: int | str) -> int | str', + ' :module: target.pep604', + '', + ' docstring', + '', + '', + '.. py:data:: attr', + ' :module: target.pep604', + ' :type: int | str', + '', + ' docstring', + '', + '', + '.. py:function:: sum(x: int | str, y: int | str) -> int | str', + ' :module: target.pep604', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_hide_value(app): + options = {'members': None} + actual = do_autodoc(app, 'module', 'target.hide_value', options) + assert list(actual) == [ + '', + '.. py:module:: target.hide_value', + '', + '', + '.. py:class:: Foo()', + ' :module: target.hide_value', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + '', + ' .. py:attribute:: Foo.SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + '', + '.. py:data:: SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + '', + '.. py:data:: SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_canonical(app): + options = {'members': None, + 'imported-members': None} + actual = do_autodoc(app, 'module', 'target.canonical', options) + assert list(actual) == [ + '', + '.. py:module:: target.canonical', + '', + '', + '.. py:class:: Bar()', + ' :module: target.canonical', + '', + ' docstring', + '', + '', + '.. py:class:: Foo()', + ' :module: target.canonical', + ' :canonical: target.canonical.original.Foo', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.meth()', + ' :module: target.canonical', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_literal_render(app): + def bounded_typevar_rst(name, bound): + return [ + '', + f'.. py:class:: {name}', + ' :module: target.literal', + '', + ' docstring', + '', + f' alias of TypeVar({name!r}, bound={bound})', + '', + ] + + def function_rst(name, sig): + return [ + '', + f'.. py:function:: {name}({sig})', + ' :module: target.literal', + '', + ' docstring', + '', + ] + + # autodoc_typehints_format can take 'short' or 'fully-qualified' values + # and this will be interpreted as 'smart' or 'fully-qualified-except-typing' by restify() + # and 'smart' or 'fully-qualified' by stringify_annotation(). + + options = {'members': None, 'exclude-members': 'MyEnum'} + app.config.autodoc_typehints_format = 'short' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *bounded_typevar_rst('T', r'\ :py:obj:`~typing.Literal`\ [1234]'), + *bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [:py:attr:`~target.literal.MyEnum.a`]'), + *function_rst('bar', 'x: ~typing.Literal[1234]'), + *function_rst('foo', 'x: ~typing.Literal[MyEnum.a]'), + ] + + # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' + # because it is more likely that a user wants to suppress 'typing.*' + app.config.autodoc_typehints_format = 'fully-qualified' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *bounded_typevar_rst('T', r'\ :py:obj:`~typing.Literal`\ [1234]'), + *bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [:py:attr:`target.literal.MyEnum.a`]'), + *function_rst('bar', 'x: typing.Literal[1234]'), + *function_rst('foo', 'x: typing.Literal[target.literal.MyEnum.a]'), + ] diff --git a/tests/test_extensions/test_ext_autodoc_autoattribute.py b/tests/test_extensions/test_ext_autodoc_autoattribute.py new file mode 100644 index 0000000..41fcc99 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_autoattribute.py @@ -0,0 +1,176 @@ +"""Test the autodoc extension. + +This tests mainly the Documenters; the auto directives are tested in a test +source file translated by test_build. +""" + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute(app): + actual = do_autodoc(app, 'attribute', 'target.Class.attr') + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr', + ' :module: target', + " :value: 'bar'", + '', + ' should be documented -- süß', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_novalue(app): + options = {'no-value': None} + actual = do_autodoc(app, 'attribute', 'target.Class.attr', options) + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr', + ' :module: target', + '', + ' should be documented -- süß', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_typed_variable(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr2') + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr2', + ' :module: target.typed_vars', + ' :type: int', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_typed_variable_in_alias(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr2') + assert list(actual) == [ + '', + '.. py:attribute:: Alias.attr2', + ' :module: target.typed_vars', + ' :type: int', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_variable(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr4') + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_variable_in_alias(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr4') + assert list(actual) == [ + '', + '.. py:attribute:: Alias.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_variable_without_comment(app): + actual = do_autodoc(app, 'attribute', 'target.instance_variable.Bar.attr4') + assert list(actual) == [ + '', + '.. py:attribute:: Bar.attr4', + ' :module: target.instance_variable', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_slots_variable_list(app): + actual = do_autodoc(app, 'attribute', 'target.slots.Foo.attr') + assert list(actual) == [ + '', + '.. py:attribute:: Foo.attr', + ' :module: target.slots', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_slots_variable_dict(app): + actual = do_autodoc(app, 'attribute', 'target.slots.Bar.attr1') + assert list(actual) == [ + '', + '.. py:attribute:: Bar.attr1', + ' :module: target.slots', + ' :type: int', + '', + ' docstring of attr1', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_slots_variable_str(app): + actual = do_autodoc(app, 'attribute', 'target.slots.Baz.attr') + assert list(actual) == [ + '', + '.. py:attribute:: Baz.attr', + ' :module: target.slots', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_GenericAlias(app): + actual = do_autodoc(app, 'attribute', 'target.genericalias.Class.T') + assert list(actual) == [ + '', + '.. py:attribute:: Class.T', + ' :module: target.genericalias', + '', + ' A list of int', + '', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_hide_value(app): + actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL1') + assert list(actual) == [ + '', + '.. py:attribute:: Foo.SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + ] + + actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL2') + assert list(actual) == [ + '', + '.. py:attribute:: Foo.SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + ] diff --git a/tests/test_extensions/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py new file mode 100644 index 0000000..3e68d60 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_autoclass.py @@ -0,0 +1,566 @@ +"""Test the autodoc extension. + +This tests mainly the Documenters; the auto directives are tested in a test +source file translated by test_build. +""" + +from __future__ import annotations + +import typing +from typing import Union + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_classes(app): + actual = do_autodoc(app, 'function', 'target.classes.Foo') + assert list(actual) == [ + '', + '.. py:function:: Foo()', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Bar') + assert list(actual) == [ + '', + '.. py:function:: Bar(x, y)', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Baz') + assert list(actual) == [ + '', + '.. py:function:: Baz(x, y)', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Qux') + assert list(actual) == [ + '', + '.. py:function:: Qux(foo, bar)', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_instance_variable(app): + options = {'members': None} + actual = do_autodoc(app, 'class', 'target.instance_variable.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar()', + ' :module: target.instance_variable', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + '', + ' .. py:attribute:: Bar.attr3', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_inherited_instance_variable(app): + options = {'members': None, + 'inherited-members': None} + actual = do_autodoc(app, 'class', 'target.instance_variable.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar()', + ' :module: target.instance_variable', + '', + '', + ' .. py:attribute:: Bar.attr1', + ' :module: target.instance_variable', + '', + ' docstring foo', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + '', + ' .. py:attribute:: Bar.attr3', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_uninitialized_attributes(app): + options = {"members": None, + "inherited-members": None} + actual = do_autodoc(app, 'class', 'target.uninitialized_attributes.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.uninitialized_attributes', + '', + '', + ' .. py:attribute:: Derived.attr1', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Derived.attr3', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_undocumented_uninitialized_attributes(app): + options = {"members": None, + "inherited-members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.uninitialized_attributes.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.uninitialized_attributes', + '', + '', + ' .. py:attribute:: Derived.attr1', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Derived.attr2', + ' :module: target.uninitialized_attributes', + ' :type: str', + '', + '', + ' .. py:attribute:: Derived.attr3', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Derived.attr4', + ' :module: target.uninitialized_attributes', + ' :type: str', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_decorators(app): + actual = do_autodoc(app, 'class', 'target.decorator.Baz') + assert list(actual) == [ + '', + '.. py:class:: Baz(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + actual = do_autodoc(app, 'class', 'target.decorator.Qux') + assert list(actual) == [ + '', + '.. py:class:: Qux(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + actual = do_autodoc(app, 'class', 'target.decorator.Quux') + assert list(actual) == [ + '', + '.. py:class:: Quux(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_properties(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.properties.Foo', options) + assert list(actual) == [ + '', + '.. py:class:: Foo()', + ' :module: target.properties', + '', + ' docstring', + '', + '', + ' .. py:property:: Foo.prop1', + ' :module: target.properties', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:property:: Foo.prop1_with_type_comment', + ' :module: target.properties', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:property:: Foo.prop2', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:property:: Foo.prop2_with_type_comment', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_slots_attribute(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.slots.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar()', + ' :module: target.slots', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Bar.attr1', + ' :module: target.slots', + ' :type: int', + '', + ' docstring of attr1', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.slots', + '', + ' docstring of instance attr2', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_show_inheritance_for_subclass_of_generic_type(app): + options = {'show-inheritance': None} + actual = do_autodoc(app, 'class', 'target.classes.Quux', options) + assert list(actual) == [ + '', + '.. py:class:: Quux(iterable=(), /)', + ' :module: target.classes', + '', + ' Bases: :py:class:`~typing.List`\\ [:py:class:`int` | :py:class:`float`]', + '', + ' A subclass of List[Union[int, float]]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_show_inheritance_for_decendants_of_generic_type(app): + options = {'show-inheritance': None} + actual = do_autodoc(app, 'class', 'target.classes.Corge', options) + assert list(actual) == [ + '', + '.. py:class:: Corge(iterable=(), /)', + ' :module: target.classes', + '', + ' Bases: :py:class:`~target.classes.Quux`', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_process_bases(app): + def autodoc_process_bases(app, name, obj, options, bases): + assert name == 'target.classes.Quux' + assert obj.__module__ == 'target.classes' + assert obj.__name__ == 'Quux' + assert options == {'show-inheritance': True, + 'members': []} + assert bases == [typing.List[Union[int, float]]] # NoQA: UP006 + + bases.pop() + bases.extend([int, str]) + + app.connect('autodoc-process-bases', autodoc_process_bases) + + options = {'show-inheritance': None} + actual = do_autodoc(app, 'class', 'target.classes.Quux', options) + assert list(actual) == [ + '', + '.. py:class:: Quux(iterable=(), /)', + ' :module: target.classes', + '', + ' Bases: :py:class:`int`, :py:class:`str`', + '', + ' A subclass of List[Union[int, float]]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_class(app): + options = {"members": None, + "class-doc-from": "class"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_init(app): + options = {"members": None, + "class-doc-from": "init"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' __init__ docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_both(app): + options = {"members": None, + "class-doc-from": "both"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + ' __init__ docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_alias(app): + def autodoc_process_docstring(*args): + """A handler always raises an error. + This confirms this handler is never called for class aliases. + """ + raise + + app.connect('autodoc-process-docstring', autodoc_process_docstring) + actual = do_autodoc(app, 'class', 'target.classes.Alias') + assert list(actual) == [ + '', + '.. py:attribute:: Alias', + ' :module: target.classes', + '', + ' alias of :py:class:`~target.classes.Foo`', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_alias_having_doccomment(app): + actual = do_autodoc(app, 'class', 'target.classes.OtherAlias') + assert list(actual) == [ + '', + '.. py:attribute:: OtherAlias', + ' :module: target.classes', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_alias_for_imported_object_having_doccomment(app): + actual = do_autodoc(app, 'class', 'target.classes.IntAlias') + assert list(actual) == [ + '', + '.. py:attribute:: IntAlias', + ' :module: target.classes', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_coroutine(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options) + assert list(actual) == [ + '', + '.. py:class:: AsyncClass()', + ' :module: target.coroutine', + '', + '', + ' .. py:method:: AsyncClass.do_asyncgen()', + ' :module: target.coroutine', + ' :async:', + '', + ' A documented async generator', + '', + '', + ' .. py:method:: AsyncClass.do_coroutine()', + ' :module: target.coroutine', + ' :async:', + '', + ' A documented coroutine function', + '', + '', + ' .. py:method:: AsyncClass.do_coroutine2()', + ' :module: target.coroutine', + ' :async:', + ' :classmethod:', + '', + ' A documented coroutine classmethod', + '', + '', + ' .. py:method:: AsyncClass.do_coroutine3()', + ' :module: target.coroutine', + ' :async:', + ' :staticmethod:', + '', + ' A documented coroutine staticmethod', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_NewType_module_level(app): + actual = do_autodoc(app, 'class', 'target.typevar.T6') + assert list(actual) == [ + '', + '.. py:class:: T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :py:class:`~datetime.date`', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_NewType_class_level(app): + actual = do_autodoc(app, 'class', 'target.typevar.Class.T6') + assert list(actual) == [ + '', + '.. py:class:: Class.T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :py:class:`~datetime.date`', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_TypeVar_class_level(app): + actual = do_autodoc(app, 'class', 'target.typevar.T1') + assert list(actual) == [ + '', + '.. py:class:: T1', + ' :module: target.typevar', + '', + ' T1', + '', + " alias of TypeVar('T1')", + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_TypeVar_module_level(app): + actual = do_autodoc(app, 'class', 'target.typevar.Class.T1') + assert list(actual) == [ + '', + '.. py:class:: Class.T1', + ' :module: target.typevar', + '', + ' T1', + '', + " alias of TypeVar('T1')", + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_inherited_instance_variable_with_annotations(app): + options = {'members': None, + 'inherited-members': None} + actual = do_autodoc(app, 'class', 'target.inherited_annotations.NoTypeAnnotation', options) + assert list(actual) == [ + '', + '.. py:class:: NoTypeAnnotation()', + ' :module: target.inherited_annotations', + '', + '', + ' .. py:attribute:: NoTypeAnnotation.a', + ' :module: target.inherited_annotations', + ' :value: 1', + '', + ' Local', + '', + '', + ' .. py:attribute:: NoTypeAnnotation.inherit_me', + ' :module: target.inherited_annotations', + ' :type: int', + '', + ' Inherited', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_no_inherited_instance_variable_with_annotations(app): + options = {'members': None} + actual = do_autodoc(app, 'class', 'target.inherited_annotations.NoTypeAnnotation2', options) + assert list(actual) == [ + '', + '.. py:class:: NoTypeAnnotation2()', + ' :module: target.inherited_annotations', + '', + '', + ' .. py:attribute:: NoTypeAnnotation2.a', + ' :module: target.inherited_annotations', + ' :value: 1', + '', + ' Local', + '', + ] diff --git a/tests/test_extensions/test_ext_autodoc_autodata.py b/tests/test_extensions/test_ext_autodoc_autodata.py new file mode 100644 index 0000000..b794666 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_autodata.py @@ -0,0 +1,106 @@ +"""Test the autodoc extension. + +This tests mainly the Documenters; the auto directives are tested in a test +source file translated by test_build. +""" + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata(app): + actual = do_autodoc(app, 'data', 'target.integer') + assert list(actual) == [ + '', + '.. py:data:: integer', + ' :module: target', + ' :value: 1', + '', + ' documentation for the integer', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_novalue(app): + options = {'no-value': None} + actual = do_autodoc(app, 'data', 'target.integer', options) + assert list(actual) == [ + '', + '.. py:data:: integer', + ' :module: target', + '', + ' documentation for the integer', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_typed_variable(app): + actual = do_autodoc(app, 'data', 'target.typed_vars.attr2') + assert list(actual) == [ + '', + '.. py:data:: attr2', + ' :module: target.typed_vars', + ' :type: str', + '', + ' attr2', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_type_comment(app): + actual = do_autodoc(app, 'data', 'target.typed_vars.attr3') + assert list(actual) == [ + '', + '.. py:data:: attr3', + ' :module: target.typed_vars', + ' :type: str', + " :value: ''", + '', + ' attr3', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_GenericAlias(app): + actual = do_autodoc(app, 'data', 'target.genericalias.T') + assert list(actual) == [ + '', + '.. py:data:: T', + ' :module: target.genericalias', + '', + ' A list of int', + '', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_hide_value(app): + actual = do_autodoc(app, 'data', 'target.hide_value.SENTINEL1') + assert list(actual) == [ + '', + '.. py:data:: SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + ] + + actual = do_autodoc(app, 'data', 'target.hide_value.SENTINEL2') + assert list(actual) == [ + '', + '.. py:data:: SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + ] diff --git a/tests/test_extensions/test_ext_autodoc_autofunction.py b/tests/test_extensions/test_ext_autodoc_autofunction.py new file mode 100644 index 0000000..5dfa42d --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_autofunction.py @@ -0,0 +1,212 @@ +"""Test the autodoc extension. + +This tests mainly the Documenters; the auto directives are tested in a test +source file translated by test_build. +""" + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_classes(app): + actual = do_autodoc(app, 'function', 'target.classes.Foo') + assert list(actual) == [ + '', + '.. py:function:: Foo()', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Bar') + assert list(actual) == [ + '', + '.. py:function:: Bar(x, y)', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Baz') + assert list(actual) == [ + '', + '.. py:function:: Baz(x, y)', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Qux') + assert list(actual) == [ + '', + '.. py:function:: Qux(foo, bar)', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_callable(app): + actual = do_autodoc(app, 'function', 'target.callable.function') + assert list(actual) == [ + '', + '.. py:function:: function(arg1, arg2, **kwargs)', + ' :module: target.callable', + '', + ' A callable object that behaves like a function.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_method(app): + actual = do_autodoc(app, 'function', 'target.callable.method') + assert list(actual) == [ + '', + '.. py:function:: method(arg1, arg2)', + ' :module: target.callable', + '', + ' docstring of Callable.method().', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_builtin_function(app): + actual = do_autodoc(app, 'function', 'os.umask') + assert list(actual) == [ + '', + '.. py:function:: umask(mask, /)', + ' :module: os', + '', + ' Set the current numeric umask and return the previous umask.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_methoddescriptor(app): + actual = do_autodoc(app, 'function', 'builtins.int.__add__') + assert list(actual) == [ + '', + '.. py:function:: __add__(self, value, /)', + ' :module: builtins.int', + '', + ' Return self+value.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_decorated(app): + actual = do_autodoc(app, 'function', 'target.decorator.foo') + assert list(actual) == [ + '', + '.. py:function:: foo(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatch(app): + options = {} + actual = do_autodoc(app, 'function', 'target.singledispatch.func', options) + assert list(actual) == [ + '', + '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', + ' func(arg: int, kwarg=None)', + ' func(arg: str, kwarg=None)', + ' func(arg: dict, kwarg=None)', + ' :module: target.singledispatch', + '', + ' A function for general use.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_cfunction(app): + actual = do_autodoc(app, 'function', 'time.asctime') + assert list(actual) == [ + '', + '.. py:function:: asctime([tuple]) -> string', + ' :module: time', + '', + " Convert a time tuple to a string, e.g. 'Sat Jun 06 16:26:11 1998'.", + ' When the time tuple is not present, current time as returned by localtime()', + ' is used.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_wrapped_function(app): + actual = do_autodoc(app, 'function', 'target.wrappedfunction.slow_function') + assert list(actual) == [ + '', + '.. py:function:: slow_function(message, timeout)', + ' :module: target.wrappedfunction', + '', + ' This function is slow.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_wrapped_function_contextmanager(app): + actual = do_autodoc(app, 'function', 'target.wrappedfunction.feeling_good') + assert list(actual) == [ + '', + '.. py:function:: feeling_good(x: int, y: int) -> ~typing.Generator', + ' :module: target.wrappedfunction', + '', + " You'll feel better in this context!", + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_coroutine(app): + actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc') + assert list(actual) == [ + '', + '.. py:function:: coroutinefunc()', + ' :module: target.functions', + ' :async:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_synchronized_coroutine(app): + actual = do_autodoc(app, 'function', 'target.coroutine.sync_func') + assert list(actual) == [ + '', + '.. py:function:: sync_func()', + ' :module: target.coroutine', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_async_generator(app): + actual = do_autodoc(app, 'function', 'target.functions.asyncgenerator') + assert list(actual) == [ + '', + '.. py:function:: asyncgenerator()', + ' :module: target.functions', + ' :async:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_slice_function_arg(app): + actual = do_autodoc(app, 'function', 'target.functions.slice_arg_func') + assert list(actual) == [ + '', + '.. py:function:: slice_arg_func(arg: float64[:, :])', + ' :module: target.functions', + '', + ] diff --git a/tests/test_extensions/test_ext_autodoc_automodule.py b/tests/test_extensions/test_ext_autodoc_automodule.py new file mode 100644 index 0000000..92565ae --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_automodule.py @@ -0,0 +1,192 @@ +"""Test the autodoc extension. + +This tests mainly the Documenters; the auto directives are tested in a test +source file translated by test_build. +""" + +import sys + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_empty_all(app): + options = {'members': None} + actual = do_autodoc(app, 'module', 'target.empty_all', options) + assert list(actual) == [ + '', + '.. py:module:: target.empty_all', + '', + ' docsting of empty_all module.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automodule(app): + options = {'members': None} + actual = do_autodoc(app, 'module', 'target.module', options) + assert list(actual) == [ + '', + '.. py:module:: target.module', + '', + '', + '.. py:data:: annotated', + ' :module: target.module', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: documented', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automodule_undoc_members(app): + options = {'members': None, + 'undoc-members': None} + actual = do_autodoc(app, 'module', 'target.module', options) + assert list(actual) == [ + '', + '.. py:module:: target.module', + '', + '', + '.. py:data:: annotated', + ' :module: target.module', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: documented', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + '', + '.. py:data:: undoc_annotated', + ' :module: target.module', + ' :type: int', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automodule_special_members(app): + options = {'members': None, + 'special-members': None} + actual = do_autodoc(app, 'module', 'target.module', options) + assert list(actual) == [ + '', + '.. py:module:: target.module', + '', + '', + '.. py:data:: __documented_special__', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + '', + '.. py:data:: annotated', + ' :module: target.module', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: documented', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automodule_inherited_members(app): + options = {'members': None, + 'undoc-members': None, + 'inherited-members': 'Base, list'} + actual = do_autodoc(app, 'module', 'target.inheritance', options) + assert list(actual) == [ + '', + '.. py:module:: target.inheritance', + '', + '', + '.. py:class:: Base()', + ' :module: target.inheritance', + '', + '', + ' .. py:attribute:: Base.inheritedattr', + ' :module: target.inheritance', + ' :value: None', + '', + ' docstring', + '', + '', + ' .. py:method:: Base.inheritedclassmeth()', + ' :module: target.inheritance', + ' :classmethod:', + '', + ' Inherited class method.', + '', + '', + ' .. py:method:: Base.inheritedmeth()', + ' :module: target.inheritance', + '', + ' Inherited function.', + '', + '', + ' .. py:method:: Base.inheritedstaticmeth(cls)', + ' :module: target.inheritance', + ' :staticmethod:', + '', + ' Inherited static method.', + '', + '', + '.. py:class:: Derived()', + ' :module: target.inheritance', + '', + '', + ' .. py:method:: Derived.inheritedmeth()', + ' :module: target.inheritance', + '', + ' Inherited function.', + '', + '', + '.. py:class:: MyList(iterable=(), /)', + ' :module: target.inheritance', + '', + '', + ' .. py:method:: MyList.meth()', + ' :module: target.inheritance', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_mock_imports': ['missing_module', + 'missing_package1', + 'missing_package2', + 'missing_package3', + 'sphinx.missing_module4']}) +@pytest.mark.usefixtures("rollback_sysmodules") +def test_subclass_of_mocked_object(app): + sys.modules.pop('target', None) # unload target module to clear the module cache + + options = {'members': None} + actual = do_autodoc(app, 'module', 'target.need_mocks', options) + assert '.. py:class:: Inherited(*args: ~typing.Any, **kwargs: ~typing.Any)' in actual diff --git a/tests/test_extensions/test_ext_autodoc_autoproperty.py b/tests/test_extensions/test_ext_autodoc_autoproperty.py new file mode 100644 index 0000000..de33117 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_autoproperty.py @@ -0,0 +1,91 @@ +"""Test the autodoc extension. + +This tests mainly the Documenters; the auto directives are tested in a test +source file translated by test_build. +""" + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_properties(app): + actual = do_autodoc(app, 'property', 'target.properties.Foo.prop1') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop1', + ' :module: target.properties', + ' :type: int', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_properties(app): + actual = do_autodoc(app, 'property', 'target.properties.Foo.prop2') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop2', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_properties_with_type_comment(app): + actual = do_autodoc(app, 'property', 'target.properties.Foo.prop1_with_type_comment') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop1_with_type_comment', + ' :module: target.properties', + ' :type: int', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_properties_with_type_comment(app): + actual = do_autodoc(app, 'property', 'target.properties.Foo.prop2_with_type_comment') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop2_with_type_comment', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_cached_properties(app): + actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop', + ' :module: target.cached_property', + ' :type: int', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_cached_properties_with_type_comment(app): + actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop_with_type_comment') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop_with_type_comment', + ' :module: target.cached_property', + ' :type: int', + '', + ] diff --git a/tests/test_extensions/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py new file mode 100644 index 0000000..6c2af5a --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_configs.py @@ -0,0 +1,1743 @@ +"""Test the autodoc extension. This tests mainly for config variables""" + +import platform +import sys +from contextlib import contextmanager + +import pytest + +from sphinx.testing import restructuredtext + +from tests.test_extensions.autodoc_util import do_autodoc + +IS_PYPY = platform.python_implementation() == 'PyPy' + + +@contextmanager +def overwrite_file(path, content): + current_content = path.read_bytes() if path.exists() else None + try: + path.write_text(content, encoding='utf-8') + yield + finally: + if current_content is not None: + path.write_bytes(current_content) + else: + path.unlink() + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoclass_content_class(app): + app.config.autoclass_content = 'class' + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.autoclass_content', options) + assert list(actual) == [ + '', + '.. py:module:: target.autoclass_content', + '', + '', + '.. py:class:: A()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, no __new__', + '', + '', + '.. py:class:: B()', + ' :module: target.autoclass_content', + '', + ' A class having __init__(no docstring), no __new__', + '', + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + '', + '.. py:class:: D()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, __new__(no docstring)', + '', + '', + '.. py:class:: E()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, __new__', + '', + '', + '.. py:class:: F()', + ' :module: target.autoclass_content', + '', + ' A class having both __init__ and __new__', + '', + '', + '.. py:class:: G()', + ' :module: target.autoclass_content', + '', + ' A class inherits __init__ without docstring.', + '', + '', + '.. py:class:: H()', + ' :module: target.autoclass_content', + '', + ' A class inherits __new__ without docstring.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoclass_content_init(app): + app.config.autoclass_content = 'init' + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.autoclass_content', options) + assert list(actual) == [ + '', + '.. py:module:: target.autoclass_content', + '', + '', + '.. py:class:: A()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, no __new__', + '', + '', + '.. py:class:: B()', + ' :module: target.autoclass_content', + '', + ' A class having __init__(no docstring), no __new__', + '', + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' __init__ docstring', + '', + '', + '.. py:class:: D()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, __new__(no docstring)', + '', + '', + '.. py:class:: E()', + ' :module: target.autoclass_content', + '', + ' __new__ docstring', + '', + '', + '.. py:class:: F()', + ' :module: target.autoclass_content', + '', + ' __init__ docstring', + '', + '', + '.. py:class:: G()', + ' :module: target.autoclass_content', + '', + ' __init__ docstring', + '', + '', + '.. py:class:: H()', + ' :module: target.autoclass_content', + '', + ' __new__ docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_class_signature_mixed(app): + app.config.autodoc_class_signature = 'mixed' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.classes.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar(x, y)', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_class_signature_separated_init(app): + app.config.autodoc_class_signature = 'separated' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.classes.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar', + ' :module: target.classes', + '', + '', + ' .. py:method:: Bar.__init__(x, y)', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_class_signature_separated_new(app): + app.config.autodoc_class_signature = 'separated' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.classes.Baz', options) + assert list(actual) == [ + '', + '.. py:class:: Baz', + ' :module: target.classes', + '', + '', + ' .. py:method:: Baz.__new__(cls, x, y)', + ' :module: target.classes', + ' :staticmethod:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoclass_content_both(app): + app.config.autoclass_content = 'both' + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.autoclass_content', options) + assert list(actual) == [ + '', + '.. py:module:: target.autoclass_content', + '', + '', + '.. py:class:: A()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, no __new__', + '', + '', + '.. py:class:: B()', + ' :module: target.autoclass_content', + '', + ' A class having __init__(no docstring), no __new__', + '', + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + ' __init__ docstring', + '', + '', + '.. py:class:: D()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, __new__(no docstring)', + '', + '', + '.. py:class:: E()', + ' :module: target.autoclass_content', + '', + ' A class having no __init__, __new__', + '', + ' __new__ docstring', + '', + '', + '.. py:class:: F()', + ' :module: target.autoclass_content', + '', + ' A class having both __init__ and __new__', + '', + ' __init__ docstring', + '', + '', + '.. py:class:: G()', + ' :module: target.autoclass_content', + '', + ' A class inherits __init__ without docstring.', + '', + ' __init__ docstring', + '', + '', + '.. py:class:: H()', + ' :module: target.autoclass_content', + '', + ' A class inherits __new__ without docstring.', + '', + ' __new__ docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inherit_docstrings(app): + assert app.config.autodoc_inherit_docstrings is True # default + actual = do_autodoc(app, 'method', 'target.inheritance.Derived.inheritedmeth') + assert list(actual) == [ + '', + '.. py:method:: Derived.inheritedmeth()', + ' :module: target.inheritance', + '', + ' Inherited function.', + '', + ] + + # disable autodoc_inherit_docstrings + app.config.autodoc_inherit_docstrings = False + actual = do_autodoc(app, 'method', 'target.inheritance.Derived.inheritedmeth') + assert list(actual) == [ + '', + '.. py:method:: Derived.inheritedmeth()', + ' :module: target.inheritance', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inherit_docstrings_for_inherited_members(app): + options = {"members": None, + "inherited-members": None} + + assert app.config.autodoc_inherit_docstrings is True # default + actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.inheritance', + '', + '', + ' .. py:attribute:: Derived.inheritedattr', + ' :module: target.inheritance', + ' :value: None', + '', + ' docstring', + '', + '', + ' .. py:method:: Derived.inheritedclassmeth()', + ' :module: target.inheritance', + ' :classmethod:', + '', + ' Inherited class method.', + '', + '', + ' .. py:method:: Derived.inheritedmeth()', + ' :module: target.inheritance', + '', + ' Inherited function.', + '', + '', + ' .. py:method:: Derived.inheritedstaticmeth(cls)', + ' :module: target.inheritance', + ' :staticmethod:', + '', + ' Inherited static method.', + '', + ] + + # disable autodoc_inherit_docstrings + app.config.autodoc_inherit_docstrings = False + actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.inheritance', + '', + '', + ' .. py:method:: Derived.inheritedclassmeth()', + ' :module: target.inheritance', + ' :classmethod:', + '', + ' Inherited class method.', + '', + '', + ' .. py:method:: Derived.inheritedstaticmeth(cls)', + ' :module: target.inheritance', + ' :staticmethod:', + '', + ' Inherited static method.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_docstring_signature(app): + options = {"members": None, "special-members": "__init__, __new__"} + actual = do_autodoc(app, 'class', 'target.DocstringSig', options) + assert list(actual) == [ + '', + # FIXME: Ideally this would instead be: `DocstringSig(d, e=1)` but + # currently `ClassDocumenter` does not apply the docstring signature + # logic when extracting a signature from a __new__ or __init__ method. + '.. py:class:: DocstringSig(*new_args, **new_kwargs)', + ' :module: target', + '', + '', + ' .. py:method:: DocstringSig.__init__(self, a, b=1) -> None', + ' :module: target', + '', + ' First line of docstring', + '', + ' rest of docstring', + '', + '', + ' .. py:method:: DocstringSig.__new__(cls, d, e=1) -> DocstringSig', + ' :module: target', + ' :staticmethod:', + '', + ' First line of docstring', + '', + ' rest of docstring', + '', + '', + ' .. py:method:: DocstringSig.meth(FOO, BAR=1) -> BAZ', + ' :module: target', + '', + ' First line of docstring', + '', + ' rest of docstring', + '', + '', + ' .. py:method:: DocstringSig.meth2()', + ' :module: target', + '', + ' First line, no signature', + ' Second line followed by indentation::', + '', + ' indented line', + '', + '', + ' .. py:property:: DocstringSig.prop1', + ' :module: target', + '', + ' First line of docstring', + '', + '', + ' .. py:property:: DocstringSig.prop2', + ' :module: target', + '', + ' First line of docstring', + ' Second line of docstring', + '', + ] + + # disable autodoc_docstring_signature + app.config.autodoc_docstring_signature = False + actual = do_autodoc(app, 'class', 'target.DocstringSig', options) + assert list(actual) == [ + '', + '.. py:class:: DocstringSig(*new_args, **new_kwargs)', + ' :module: target', + '', + '', + ' .. py:method:: DocstringSig.__init__(*init_args, **init_kwargs)', + ' :module: target', + '', + ' __init__(self, a, b=1) -> None', + ' First line of docstring', + '', + ' rest of docstring', + '', + '', + '', + ' .. py:method:: DocstringSig.__new__(cls, *new_args, **new_kwargs)', + ' :module: target', + ' :staticmethod:', + '', + ' __new__(cls, d, e=1) -> DocstringSig', + ' First line of docstring', + '', + ' rest of docstring', + '', + '', + '', + ' .. py:method:: DocstringSig.meth()', + ' :module: target', + '', + ' meth(FOO, BAR=1) -> BAZ', + ' First line of docstring', + '', + ' rest of docstring', + '', + '', + '', + ' .. py:method:: DocstringSig.meth2()', + ' :module: target', + '', + ' First line, no signature', + ' Second line followed by indentation::', + '', + ' indented line', + '', + '', + ' .. py:property:: DocstringSig.prop1', + ' :module: target', + '', + ' DocstringSig.prop1(self)', + ' First line of docstring', + '', + '', + ' .. py:property:: DocstringSig.prop2', + ' :module: target', + '', + ' First line of docstring', + ' Second line of docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoclass_content_and_docstring_signature_class(app): + app.config.autoclass_content = 'class' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.docstring_signature', options) + assert list(actual) == [ + '', + '.. py:module:: target.docstring_signature', + '', + '', + '.. py:class:: A(foo, bar)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: B(foo, bar)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: C(foo, bar)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: D()', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: E()', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: F()', + ' :module: target.docstring_signature', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoclass_content_and_docstring_signature_init(app): + app.config.autoclass_content = 'init' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.docstring_signature', options) + assert list(actual) == [ + '', + '.. py:module:: target.docstring_signature', + '', + '', + '.. py:class:: A(foo, bar)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: B(foo, bar, baz)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: C(foo, bar, baz)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: D(foo, bar, baz)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: E(foo: int, bar: int, baz: int)', + ' E(foo: str, bar: str, baz: str)', + ' E(foo: float, bar: float, baz: float)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: F(foo: int, bar: int, baz: int)', + ' F(foo: str, bar: str, baz: str)', + ' F(foo: float, bar: float, baz: float)', + ' :module: target.docstring_signature', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoclass_content_and_docstring_signature_both(app): + app.config.autoclass_content = 'both' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.docstring_signature', options) + assert list(actual) == [ + '', + '.. py:module:: target.docstring_signature', + '', + '', + '.. py:class:: A(foo, bar)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: B(foo, bar)', + ' :module: target.docstring_signature', + '', + ' B(foo, bar, baz)', + '', + '', + '.. py:class:: C(foo, bar)', + ' :module: target.docstring_signature', + '', + ' C(foo, bar, baz)', + '', + '', + '.. py:class:: D(foo, bar, baz)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: E(foo: int, bar: int, baz: int)', + ' E(foo: str, bar: str, baz: str)', + ' E(foo: float, bar: float, baz: float)', + ' :module: target.docstring_signature', + '', + '', + '.. py:class:: F(foo: int, bar: int, baz: int)', + ' F(foo: str, bar: str, baz: str)', + ' F(foo: float, bar: float, baz: float)', + ' :module: target.docstring_signature', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +@pytest.mark.usefixtures("rollback_sysmodules") +def test_mocked_module_imports(app, warning): + sys.modules.pop('target', None) # unload target module to clear the module cache + + # no autodoc_mock_imports + options = {"members": 'TestAutodoc,decoratedFunction,func,Alias'} + actual = do_autodoc(app, 'module', 'target.need_mocks', options) + assert list(actual) == [] + assert "autodoc: failed to import module 'need_mocks'" in warning.getvalue() + + # with autodoc_mock_imports + app.config.autodoc_mock_imports = [ + 'missing_module', + 'missing_package1', + 'missing_package2', + 'missing_package3', + 'sphinx.missing_module4', + ] + + warning.truncate(0) + actual = do_autodoc(app, 'module', 'target.need_mocks', options) + assert list(actual) == [ + '', + '.. py:module:: target.need_mocks', + '', + '', + '.. py:data:: Alias', + ' :module: target.need_mocks', + '', + ' docstring', + '', + '', + '.. py:class:: TestAutodoc()', + ' :module: target.need_mocks', + '', + ' TestAutodoc docstring.', + '', + '', + ' .. py:attribute:: TestAutodoc.Alias', + ' :module: target.need_mocks', + '', + ' docstring', + '', + '', + ' .. py:method:: TestAutodoc.decoratedMethod()', + ' :module: target.need_mocks', + '', + ' TestAutodoc::decoratedMethod docstring', + '', + '', + '.. py:function:: decoratedFunction()', + ' :module: target.need_mocks', + '', + ' decoratedFunction docstring', + '', + '', + '.. py:function:: func(arg: missing_module.Class)', + ' :module: target.need_mocks', + '', + ' a function takes mocked object as an argument', + '', + ] + assert warning.getvalue() == '' + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "signature"}) +def test_autodoc_typehints_signature(app): + if sys.version_info[:2] <= (3, 10): + type_o = "~typing.Any | None" + else: + type_o = "~typing.Any" + + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.typehints', options) + assert list(actual) == [ + '', + '.. py:module:: target.typehints', + '', + '', + '.. py:data:: CONST1', + ' :module: target.typehints', + ' :type: int', + '', + '', + '.. py:data:: CONST2', + ' :module: target.typehints', + ' :type: int', + ' :value: 1', + '', + ' docstring', + '', + '', + '.. py:data:: CONST3', + ' :module: target.typehints', + ' :type: ~pathlib.PurePosixPath', + " :value: PurePosixPath('/a/b/c')", + '', + ' docstring', + '', + '', + '.. py:class:: Math(s: str, o: %s = None)' % type_o, + ' :module: target.typehints', + '', + '', + ' .. py:attribute:: Math.CONST1', + ' :module: target.typehints', + ' :type: int', + '', + '', + ' .. py:attribute:: Math.CONST2', + ' :module: target.typehints', + ' :type: int', + ' :value: 1', + '', + '', + ' .. py:attribute:: Math.CONST3', + ' :module: target.typehints', + ' :type: ~pathlib.PurePosixPath', + " :value: PurePosixPath('/a/b/c')", + '', + '', + ' .. py:method:: Math.decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.horse(a: str, b: int) -> None', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.incr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.nothing() -> None', + ' :module: target.typehints', + '', + '', + ' .. py:property:: Math.path', + ' :module: target.typehints', + ' :type: ~pathlib.PurePosixPath', + '', + '', + ' .. py:property:: Math.prop', + ' :module: target.typehints', + ' :type: int', + '', + '', + '.. py:class:: NewAnnotation(i: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: NewComment(i: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: SignatureFromMetaclass(a: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: T', + ' :module: target.typehints', + '', + ' docstring', + '', + " alias of TypeVar('T', bound=\\ :py:class:`~pathlib.PurePosixPath`)", + '', + '', + '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' + 'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + '.. py:function:: incr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + '.. py:function:: missing_attr(c, a: str, b: Optional[str] = None) -> str', + ' :module: target.typehints', + '', + '', + '.. py:function:: tuple_args(x: tuple[int, int | str]) -> tuple[int, int]', + ' :module: target.typehints', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "none"}) +def test_autodoc_typehints_none(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.typehints', options) + assert list(actual) == [ + '', + '.. py:module:: target.typehints', + '', + '', + '.. py:data:: CONST1', + ' :module: target.typehints', + '', + '', + '.. py:data:: CONST2', + ' :module: target.typehints', + ' :value: 1', + '', + ' docstring', + '', + '', + '.. py:data:: CONST3', + ' :module: target.typehints', + " :value: PurePosixPath('/a/b/c')", + '', + ' docstring', + '', + '', + '.. py:class:: Math(s, o=None)', + ' :module: target.typehints', + '', + '', + ' .. py:attribute:: Math.CONST1', + ' :module: target.typehints', + '', + '', + ' .. py:attribute:: Math.CONST2', + ' :module: target.typehints', + ' :value: 1', + '', + '', + ' .. py:attribute:: Math.CONST3', + ' :module: target.typehints', + " :value: PurePosixPath('/a/b/c')", + '', + '', + ' .. py:method:: Math.decr(a, b=1)', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.horse(a, b)', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.incr(a, b=1)', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.nothing()', + ' :module: target.typehints', + '', + '', + ' .. py:property:: Math.path', + ' :module: target.typehints', + '', + '', + ' .. py:property:: Math.prop', + ' :module: target.typehints', + '', + '', + '.. py:class:: NewAnnotation(i)', + ' :module: target.typehints', + '', + '', + '.. py:class:: NewComment(i)', + ' :module: target.typehints', + '', + '', + '.. py:class:: SignatureFromMetaclass(a)', + ' :module: target.typehints', + '', + '', + '.. py:class:: T', + ' :module: target.typehints', + '', + ' docstring', + '', + " alias of TypeVar('T', bound=\\ :py:class:`~pathlib.PurePosixPath`)", + '', + '', + '.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a, b=1)', + ' :module: target.typehints', + '', + '', + '.. py:function:: incr(a, b=1)', + ' :module: target.typehints', + '', + '', + '.. py:function:: missing_attr(c, a, b=None)', + ' :module: target.typehints', + '', + '', + '.. py:function:: tuple_args(x)', + ' :module: target.typehints', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': 'none'}) +def test_autodoc_typehints_none_for_overload(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.overload', options) + assert list(actual) == [ + '', + '.. py:module:: target.overload', + '', + '', + '.. py:class:: Bar(x, y)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Baz(x, y)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Foo(x, y)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Math()', + ' :module: target.overload', + '', + ' docstring', + '', + '', + ' .. py:method:: Math.sum(x, y=None)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:function:: sum(x, y=None)', + ' :module: target.overload', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description"}, + freshenv=True) +def test_autodoc_typehints_description(app): + app.build() + context = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert ('target.typehints.incr(a, b=1)\n' + '\n' + ' Parameters:\n' + ' * **a** (*int*)\n' + '\n' + ' * **b** (*int*)\n' + '\n' + ' Return type:\n' + ' int\n' + in context) + assert ('target.typehints.tuple_args(x)\n' + '\n' + ' Parameters:\n' + ' **x** (*tuple**[**int**, **int** | **str**]*)\n' + '\n' + ' Return type:\n' + ' tuple[int, int]\n' + in context) + + # Overloads still get displayed in the signature + assert ('target.overload.sum(x: int, y: int = 0) -> int\n' + 'target.overload.sum(x: float, y: float = 0.0) -> float\n' + 'target.overload.sum(x: str, y: str = None) -> str\n' + '\n' + ' docstring\n' + in context) + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description", + 'autodoc_typehints_description_target': 'documented'}) +def test_autodoc_typehints_description_no_undoc(app): + # No :type: or :rtype: will be injected for `incr`, which does not have + # a description for its parameters or its return. `tuple_args` does + # describe them, so :type: and :rtype: will be added. + with overwrite_file(app.srcdir / 'index.rst', + '.. autofunction:: target.typehints.incr\n' + '\n' + '.. autofunction:: target.typehints.decr\n' + '\n' + ' :returns: decremented number\n' + '\n' + '.. autofunction:: target.typehints.tuple_args\n' + '\n' + ' :param x: arg\n' + ' :return: another tuple\n'): + app.build() + # Restore the original content of the file + context = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert ('target.typehints.incr(a, b=1)\n' + '\n' + 'target.typehints.decr(a, b=1)\n' + '\n' + ' Returns:\n' + ' decremented number\n' + '\n' + ' Return type:\n' + ' int\n' + '\n' + 'target.typehints.tuple_args(x)\n' + '\n' + ' Parameters:\n' + ' **x** (*tuple**[**int**, **int** | **str**]*) -- arg\n' + '\n' + ' Returns:\n' + ' another tuple\n' + '\n' + ' Return type:\n' + ' tuple[int, int]\n' + in context) + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description", + 'autodoc_typehints_description_target': 'documented_params'}) +def test_autodoc_typehints_description_no_undoc_doc_rtype(app): + # No :type: will be injected for `incr`, which does not have a description + # for its parameters or its return, just :rtype: will be injected due to + # autodoc_typehints_description_target. `tuple_args` does describe both, so + # :type: and :rtype: will be added. `nothing` has no parameters but a return + # type of None, which will be added. + with overwrite_file(app.srcdir / 'index.rst', + '.. autofunction:: target.typehints.incr\n' + '\n' + '.. autofunction:: target.typehints.decr\n' + '\n' + ' :returns: decremented number\n' + '\n' + '.. autofunction:: target.typehints.tuple_args\n' + '\n' + ' :param x: arg\n' + ' :return: another tuple\n' + '\n' + '.. autofunction:: target.typehints.Math.nothing\n' + '\n' + '.. autofunction:: target.typehints.Math.horse\n' + '\n' + ' :return: nothing\n'): + app.build() + context = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert context == ( + 'target.typehints.incr(a, b=1)\n' + '\n' + ' Return type:\n' + ' int\n' + '\n' + 'target.typehints.decr(a, b=1)\n' + '\n' + ' Returns:\n' + ' decremented number\n' + '\n' + ' Return type:\n' + ' int\n' + '\n' + 'target.typehints.tuple_args(x)\n' + '\n' + ' Parameters:\n' + ' **x** (*tuple**[**int**, **int** | **str**]*) -- arg\n' + '\n' + ' Returns:\n' + ' another tuple\n' + '\n' + ' Return type:\n' + ' tuple[int, int]\n' + '\n' + 'target.typehints.Math.nothing(self)\n' + '\n' + 'target.typehints.Math.horse(self, a, b)\n' + '\n' + ' Returns:\n' + ' nothing\n' + '\n' + ' Return type:\n' + ' None\n' + ) + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description"}) +def test_autodoc_typehints_description_with_documented_init(app): + with overwrite_file(app.srcdir / 'index.rst', + '.. autoclass:: target.typehints._ClassWithDocumentedInit\n' + ' :special-members: __init__\n'): + app.build() + context = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert context == ( + 'class target.typehints._ClassWithDocumentedInit(x, *args, **kwargs)\n' + '\n' + ' Class docstring.\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*)\n' + '\n' + ' * **args** (*int*)\n' + '\n' + ' * **kwargs** (*int*)\n' + '\n' + ' __init__(x, *args, **kwargs)\n' + '\n' + ' Init docstring.\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*) -- Some integer\n' + '\n' + ' * **args** (*int*) -- Some integer\n' + '\n' + ' * **kwargs** (*int*) -- Some integer\n' + '\n' + ' Return type:\n' + ' None\n' + ) + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description", + 'autodoc_typehints_description_target': 'documented'}) +def test_autodoc_typehints_description_with_documented_init_no_undoc(app): + with overwrite_file(app.srcdir / 'index.rst', + '.. autoclass:: target.typehints._ClassWithDocumentedInit\n' + ' :special-members: __init__\n'): + app.build() + context = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert context == ( + 'class target.typehints._ClassWithDocumentedInit(x, *args, **kwargs)\n' + '\n' + ' Class docstring.\n' + '\n' + ' __init__(x, *args, **kwargs)\n' + '\n' + ' Init docstring.\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*) -- Some integer\n' + '\n' + ' * **args** (*int*) -- Some integer\n' + '\n' + ' * **kwargs** (*int*) -- Some integer\n' + ) + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description", + 'autodoc_typehints_description_target': 'documented_params'}) +def test_autodoc_typehints_description_with_documented_init_no_undoc_doc_rtype(app): + # see test_autodoc_typehints_description_with_documented_init_no_undoc + # returnvalue_and_documented_params should not change class or method + # docstring. + with overwrite_file(app.srcdir / 'index.rst', + '.. autoclass:: target.typehints._ClassWithDocumentedInit\n' + ' :special-members: __init__\n'): + app.build() + context = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert context == ( + 'class target.typehints._ClassWithDocumentedInit(x, *args, **kwargs)\n' + '\n' + ' Class docstring.\n' + '\n' + ' __init__(x, *args, **kwargs)\n' + '\n' + ' Init docstring.\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*) -- Some integer\n' + '\n' + ' * **args** (*int*) -- Some integer\n' + '\n' + ' * **kwargs** (*int*) -- Some integer\n' + ) + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description"}) +def test_autodoc_typehints_description_for_invalid_node(app): + text = ".. py:function:: hello; world" + restructuredtext.parse(app, text) # raises no error + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "both"}) +def test_autodoc_typehints_both(app): + with overwrite_file(app.srcdir / 'index.rst', + '.. autofunction:: target.typehints.incr\n' + '\n' + '.. autofunction:: target.typehints.tuple_args\n' + '\n' + '.. autofunction:: target.overload.sum\n'): + app.build() + context = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert ('target.typehints.incr(a: int, b: int = 1) -> int\n' + '\n' + ' Parameters:\n' + ' * **a** (*int*)\n' + '\n' + ' * **b** (*int*)\n' + '\n' + ' Return type:\n' + ' int\n' + in context) + assert ('target.typehints.tuple_args(x: tuple[int, int | str]) -> tuple[int, int]\n' + '\n' + ' Parameters:\n' + ' **x** (*tuple**[**int**, **int** | **str**]*)\n' + '\n' + ' Return type:\n' + ' tuple[int, int]\n' + in context) + + # Overloads still get displayed in the signature + assert ('target.overload.sum(x: int, y: int = 0) -> int\n' + 'target.overload.sum(x: float, y: float = 0.0) -> float\n' + 'target.overload.sum(x: str, y: str = None) -> str\n' + '\n' + ' docstring\n' + in context) + + +@pytest.mark.sphinx('text', testroot='ext-autodoc') +def test_autodoc_type_aliases(app): + # default + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options) + assert list(actual) == [ + '', + '.. py:module:: target.autodoc_type_aliases', + '', + '', + '.. py:class:: Foo()', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr1', + ' :module: target.autodoc_type_aliases', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr2', + ' :module: target.autodoc_type_aliases', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:function:: mult(x: int, y: int) -> int', + ' mult(x: float, y: float) -> float', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: read(r: ~_io.BytesIO) -> ~_io.StringIO', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: sum(x: int, y: int) -> int', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:data:: variable', + ' :module: target.autodoc_type_aliases', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: variable2', + ' :module: target.autodoc_type_aliases', + ' :type: int', + ' :value: None', + '', + ' docstring', + '', + '', + '.. py:data:: variable3', + ' :module: target.autodoc_type_aliases', + ' :type: int | None', + '', + ' docstring', + '', + ] + + # define aliases + app.config.autodoc_type_aliases = {'myint': 'myint', + 'io.StringIO': 'my.module.StringIO'} + actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options) + assert list(actual) == [ + '', + '.. py:module:: target.autodoc_type_aliases', + '', + '', + '.. py:class:: Foo()', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr1', + ' :module: target.autodoc_type_aliases', + ' :type: myint', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr2', + ' :module: target.autodoc_type_aliases', + ' :type: myint', + '', + ' docstring', + '', + '', + '.. py:function:: mult(x: myint, y: myint) -> myint', + ' mult(x: float, y: float) -> float', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: read(r: ~_io.BytesIO) -> my.module.StringIO', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: sum(x: myint, y: myint) -> myint', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:data:: variable', + ' :module: target.autodoc_type_aliases', + ' :type: myint', + '', + ' docstring', + '', + '', + '.. py:data:: variable2', + ' :module: target.autodoc_type_aliases', + ' :type: myint', + ' :value: None', + '', + ' docstring', + '', + '', + '.. py:data:: variable3', + ' :module: target.autodoc_type_aliases', + ' :type: myint | None', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('text', testroot='ext-autodoc', + srcdir='autodoc_typehints_description_and_type_aliases', + confoverrides={'autodoc_typehints': "description", + 'autodoc_type_aliases': {'myint': 'myint'}}) +def test_autodoc_typehints_description_and_type_aliases(app): + with overwrite_file(app.srcdir / 'autodoc_type_aliases.rst', + '.. autofunction:: target.autodoc_type_aliases.sum'): + app.build() + context = (app.outdir / 'autodoc_type_aliases.txt').read_text(encoding='utf8') + assert context == ( + 'target.autodoc_type_aliases.sum(x, y)\n' + '\n' + ' docstring\n' + '\n' + ' Parameters:\n' + ' * **x** (*myint*)\n' + '\n' + ' * **y** (*myint*)\n' + '\n' + ' Return type:\n' + ' myint\n' + ) + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints_format': "fully-qualified"}) +def test_autodoc_typehints_format_fully_qualified(app): + if sys.version_info[:2] <= (3, 10): + type_o = "typing.Any | None" + else: + type_o = "typing.Any" + + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.typehints', options) + assert list(actual) == [ + '', + '.. py:module:: target.typehints', + '', + '', + '.. py:data:: CONST1', + ' :module: target.typehints', + ' :type: int', + '', + '', + '.. py:data:: CONST2', + ' :module: target.typehints', + ' :type: int', + ' :value: 1', + '', + ' docstring', + '', + '', + '.. py:data:: CONST3', + ' :module: target.typehints', + ' :type: pathlib.PurePosixPath', + " :value: PurePosixPath('/a/b/c')", + '', + ' docstring', + '', + '', + '.. py:class:: Math(s: str, o: %s = None)' % type_o, + ' :module: target.typehints', + '', + '', + ' .. py:attribute:: Math.CONST1', + ' :module: target.typehints', + ' :type: int', + '', + '', + ' .. py:attribute:: Math.CONST2', + ' :module: target.typehints', + ' :type: int', + ' :value: 1', + '', + '', + ' .. py:attribute:: Math.CONST3', + ' :module: target.typehints', + ' :type: pathlib.PurePosixPath', + " :value: PurePosixPath('/a/b/c')", + '', + '', + ' .. py:method:: Math.decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.horse(a: str, b: int) -> None', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.incr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.nothing() -> None', + ' :module: target.typehints', + '', + '', + ' .. py:property:: Math.path', + ' :module: target.typehints', + ' :type: pathlib.PurePosixPath', + '', + '', + ' .. py:property:: Math.prop', + ' :module: target.typehints', + ' :type: int', + '', + '', + '.. py:class:: NewAnnotation(i: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: NewComment(i: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: SignatureFromMetaclass(a: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: T', + ' :module: target.typehints', + '', + ' docstring', + '', + " alias of TypeVar('T', bound=\\ :py:class:`pathlib.PurePosixPath`)", + '', + '', + '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' + 'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + '.. py:function:: incr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + '.. py:function:: missing_attr(c, a: str, b: Optional[str] = None) -> str', + ' :module: target.typehints', + '', + '', + '.. py:function:: tuple_args(x: tuple[int, int | str]) -> tuple[int, int]', + ' :module: target.typehints', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints_format': "fully-qualified"}) +def test_autodoc_typehints_format_fully_qualified_for_class_alias(app): + actual = do_autodoc(app, 'class', 'target.classes.Alias') + assert list(actual) == [ + '', + '.. py:attribute:: Alias', + ' :module: target.classes', + '', + ' alias of :py:class:`target.classes.Foo`', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints_format': "fully-qualified"}) +def test_autodoc_typehints_format_fully_qualified_for_generic_alias(app): + actual = do_autodoc(app, 'data', 'target.genericalias.L') + assert list(actual) == [ + '', + '.. py:data:: L', + ' :module: target.genericalias', + '', + ' A list of Class', + '', + ' alias of :py:class:`~typing.List`\\ [:py:class:`target.genericalias.Class`]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints_format': "fully-qualified"}) +def test_autodoc_typehints_format_fully_qualified_for_newtype_alias(app): + actual = do_autodoc(app, 'class', 'target.typevar.T6') + assert list(actual) == [ + '', + '.. py:class:: T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :py:class:`datetime.date`', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_default_options(app): + if ( + (3, 11, 7) <= sys.version_info < (3, 12) + or sys.version_info >= (3, 12, 1) + ): + list_of_weak_references = " list of weak references to the object" + else: + list_of_weak_references = " list of weak references to the object (if defined)" + + # no settings + actual = do_autodoc(app, 'class', 'target.enums.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' not in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: target.CustomIter' not in actual + actual = do_autodoc(app, 'module', 'target') + assert '.. py:function:: function_to_be_imported(app)' not in actual + + # with :members: + app.config.autodoc_default_options = {'members': None} + actual = do_autodoc(app, 'class', 'target.enums.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + + # with :members: = True + app.config.autodoc_default_options = {'members': None} + actual = do_autodoc(app, 'class', 'target.enums.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + + # with :members: and :undoc-members: + app.config.autodoc_default_options = { + 'members': None, + 'undoc-members': None, + } + actual = do_autodoc(app, 'class', 'target.enums.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' in actual + assert ' .. py:attribute:: EnumCls.val4' in actual + + # with :special-members: + # Note that :members: must be *on* for :special-members: to work. + app.config.autodoc_default_options = { + 'members': None, + 'special-members': None, + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' in actual + assert list_of_weak_references in actual + + # :exclude-members: None - has no effect. Unlike :members:, + # :special-members:, etc. where None == "include all", here None means + # "no/false/off". + app.config.autodoc_default_options = { + 'members': None, + 'exclude-members': None, + } + actual = do_autodoc(app, 'class', 'target.enums.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + app.config.autodoc_default_options = { + 'members': None, + 'special-members': None, + 'exclude-members': None, + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' in actual + assert list_of_weak_references in actual + assert ' .. py:method:: CustomIter.snafucate()' in actual + assert ' Makes this snafucated.' in actual + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_default_options_with_values(app): + if ( + (3, 11, 7) <= sys.version_info < (3, 12) + or sys.version_info >= (3, 12, 1) + ): + list_of_weak_references = " list of weak references to the object" + else: + list_of_weak_references = " list of weak references to the object (if defined)" + + # with :members: + app.config.autodoc_default_options = {'members': 'val1,val2'} + actual = do_autodoc(app, 'class', 'target.enums.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' in actual + assert ' .. py:attribute:: EnumCls.val2' in actual + assert ' .. py:attribute:: EnumCls.val3' not in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + + # with :member-order: + app.config.autodoc_default_options = { + 'members': None, + 'member-order': 'bysource', + } + actual = do_autodoc(app, 'class', 'target.Class') + assert list(filter(lambda l: '::' in l, actual)) == [ + '.. py:class:: Class(arg)', + ' .. py:method:: Class.meth()', + ' .. py:method:: Class.skipmeth()', + ' .. py:method:: Class.excludemeth()', + ' .. py:attribute:: Class.attr', + ' .. py:attribute:: Class.docattr', + ' .. py:attribute:: Class.udocattr', + ' .. py:attribute:: Class.mdocattr', + ' .. py:method:: Class.moore(a, e, f) -> happiness', + ' .. py:attribute:: Class.inst_attr_inline', + ' .. py:attribute:: Class.inst_attr_comment', + ' .. py:attribute:: Class.inst_attr_string', + ] + + # with :special-members: + app.config.autodoc_default_options = { + 'special-members': '__init__,__iter__', + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' not in actual + assert list_of_weak_references not in actual + + # with :exclude-members: + app.config.autodoc_default_options = { + 'members': None, + 'exclude-members': 'val1', + } + actual = do_autodoc(app, 'class', 'target.enums.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' not in actual + assert ' .. py:attribute:: EnumCls.val2' in actual + assert ' .. py:attribute:: EnumCls.val3' in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + app.config.autodoc_default_options = { + 'members': None, + 'special-members': None, + 'exclude-members': '__weakref__,snafucate', + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' not in actual + assert list_of_weak_references not in actual + assert ' .. py:method:: CustomIter.snafucate()' not in actual + assert ' Makes this snafucated.' not in actual diff --git a/tests/test_extensions/test_ext_autodoc_events.py b/tests/test_extensions/test_ext_autodoc_events.py new file mode 100644 index 0000000..c0af254 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_events.py @@ -0,0 +1,118 @@ +"""Test the autodoc extension. This tests mainly for autodoc events""" + +import pytest + +from sphinx.ext.autodoc import between, cut_lines + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_process_docstring(app): + def on_process_docstring(app, what, name, obj, options, lines): + lines.clear() + lines.append('my docstring') + + app.connect('autodoc-process-docstring', on_process_docstring) + + actual = do_autodoc(app, 'function', 'target.process_docstring.func') + assert list(actual) == [ + '', + '.. py:function:: func()', + ' :module: target.process_docstring', + '', + ' my docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_process_docstring_for_nondatadescriptor(app): + def on_process_docstring(app, what, name, obj, options, lines): + raise + + app.connect('autodoc-process-docstring', on_process_docstring) + + actual = do_autodoc(app, 'attribute', 'target.AttCls.a1') + assert list(actual) == [ + '', + '.. py:attribute:: AttCls.a1', + ' :module: target', + ' :value: hello world', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_cut_lines(app): + app.connect('autodoc-process-docstring', + cut_lines(2, 2, ['function'])) + + actual = do_autodoc(app, 'function', 'target.process_docstring.func') + assert list(actual) == [ + '', + '.. py:function:: func()', + ' :module: target.process_docstring', + '', + ' second line', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_between(app): + app.connect('autodoc-process-docstring', + between('---', ['function'])) + + actual = do_autodoc(app, 'function', 'target.process_docstring.func') + assert list(actual) == [ + '', + '.. py:function:: func()', + ' :module: target.process_docstring', + '', + ' second line', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_between_exclude(app): + app.connect('autodoc-process-docstring', + between('---', ['function'], exclude=True)) + + actual = do_autodoc(app, 'function', 'target.process_docstring.func') + assert list(actual) == [ + '', + '.. py:function:: func()', + ' :module: target.process_docstring', + '', + ' first line', + ' third line', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_skip_module_member(app): + def autodoc_skip_member(app, what, name, obj, skip, options): + if name == "Class": + return True # Skip "Class" class in __all__ + elif name == "raises": + return False # Show "raises()" function (not in __all__) + return None + + app.connect('autodoc-skip-member', autodoc_skip_member) + + options = {"members": None} + actual = do_autodoc(app, 'module', 'target', options) + assert list(actual) == [ + '', + '.. py:module:: target', + '', + '', + '.. py:function:: raises(exc, func, *args, **kwds)', + ' :module: target', + '', + ' Raise AssertionError if ``func(*args, **kwds)`` does not raise *exc*.', + '', + ] diff --git a/tests/test_extensions/test_ext_autodoc_mock.py b/tests/test_extensions/test_ext_autodoc_mock.py new file mode 100644 index 0000000..3b90693 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_mock.py @@ -0,0 +1,152 @@ +"""Test the autodoc extension.""" + +from __future__ import annotations + +import abc +import sys +from importlib import import_module +from typing import TypeVar + +import pytest + +from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock, undecorate + + +def test_MockModule(): + mock = _MockModule('mocked_module') + assert isinstance(mock.some_attr, _MockObject) + assert isinstance(mock.some_method, _MockObject) + assert isinstance(mock.attr1.attr2, _MockObject) + assert isinstance(mock.attr1.attr2.meth(), _MockObject) + + assert repr(mock.some_attr) == 'mocked_module.some_attr' + assert repr(mock.some_method) == 'mocked_module.some_method' + assert repr(mock.attr1.attr2) == 'mocked_module.attr1.attr2' + assert repr(mock.attr1.attr2.meth) == 'mocked_module.attr1.attr2.meth' + + assert repr(mock) == 'mocked_module' + + +def test_MockObject(): + mock = _MockObject() + assert isinstance(mock.some_attr, _MockObject) + assert isinstance(mock.some_method, _MockObject) + assert isinstance(mock.attr1.attr2, _MockObject) + assert isinstance(mock.attr1.attr2.meth(), _MockObject) + + # subclassing + class SubClass(mock.SomeClass): + """docstring of SubClass""" + + def method(self): + return "string" + + obj = SubClass() + assert SubClass.__doc__ == "docstring of SubClass" + assert isinstance(obj, SubClass) + assert obj.method() == "string" + assert isinstance(obj.other_method(), SubClass) + + # parametrized type + T = TypeVar('T') + + class SubClass2(mock.SomeClass[T]): + """docstring of SubClass""" + + obj2 = SubClass2() + assert SubClass2.__doc__ == "docstring of SubClass" + assert isinstance(obj2, SubClass2) + + +def test_mock(): + modname = 'sphinx.unknown' + submodule = modname + '.submodule' + assert modname not in sys.modules + with pytest.raises(ImportError): + import_module(modname) + + with mock([modname]): + import_module(modname) + assert modname in sys.modules + assert isinstance(sys.modules[modname], _MockModule) + + # submodules are also mocked + import_module(submodule) + assert submodule in sys.modules + assert isinstance(sys.modules[submodule], _MockModule) + + assert modname not in sys.modules + with pytest.raises(ImportError): + import_module(modname) + + +def test_mock_does_not_follow_upper_modules(): + with mock(['sphinx.unknown.module']): # NoQA: SIM117 + with pytest.raises(ImportError): + import_module('sphinx.unknown') + + +def test_abc_MockObject(): + mock = _MockObject() + + class Base: + @abc.abstractmethod + def __init__(self): + pass + + class Derived(Base, mock.SubClass): + pass + + obj = Derived() + assert isinstance(obj, Base) + assert isinstance(obj, _MockObject) + assert isinstance(obj.some_method(), Derived) + + +def test_mock_decorator(): + mock = _MockObject() + + @mock.function_deco + def func(): + pass + + class Foo: + @mock.method_deco + def meth(self): + pass + + @classmethod + @mock.method_deco + def class_meth(cls): + pass + + @mock.class_deco + class Bar: + pass + + @mock.funcion_deco(Foo) + class Baz: + pass + + assert undecorate(func).__name__ == "func" + assert undecorate(Foo.meth).__name__ == "meth" + assert undecorate(Foo.class_meth).__name__ == "class_meth" + assert undecorate(Bar).__name__ == "Bar" + assert undecorate(Baz).__name__ == "Baz" + + +def test_ismock(): + with mock(['sphinx.unknown']): + mod1 = import_module('sphinx.unknown') + mod2 = import_module('sphinx.application') + + class Inherited(mod1.Class): + pass + + assert ismock(mod1) is True + assert ismock(mod1.Class) is True + assert ismock(mod1.submod.Class) is True + assert ismock(Inherited) is False + + assert ismock(mod2) is False + assert ismock(mod2.Sphinx) is False diff --git a/tests/test_extensions/test_ext_autodoc_preserve_defaults.py b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py new file mode 100644 index 0000000..c1a00ab --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py @@ -0,0 +1,192 @@ +"""Test the autodoc extension.""" + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_preserve_defaults': True}) +def test_preserve_defaults(app): + color = "0xFFFFFF" + + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.preserve_defaults', options) + assert list(actual) == [ + '', + '.. py:module:: target.preserve_defaults', + '', + '', + '.. py:class:: Class()', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + ' .. py:method:: Class.clsmeth(name: str = CONSTANT, sentinel: ~typing.Any = ' + 'SENTINEL, now: ~datetime.datetime = datetime.now(), color: int = %s, *, ' + 'kwarg1, kwarg2=%s) -> None' % (color, color), + ' :module: target.preserve_defaults', + ' :classmethod:', + '', + ' docstring', + '', + '', + ' .. py:method:: Class.meth(name: str = CONSTANT, sentinel: ~typing.Any = ' + 'SENTINEL, now: ~datetime.datetime = datetime.now(), color: int = %s, *, ' + 'kwarg1, kwarg2=%s) -> None' % (color, color), + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + '.. py:class:: MultiLine()', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + ' .. py:property:: MultiLine.prop1', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + ' .. py:property:: MultiLine.prop2', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + ' .. py:property:: MultiLine.prop3', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + ' .. py:property:: MultiLine.prop4', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + ' .. py:property:: MultiLine.prop5', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + '.. py:function:: foo(name: str = CONSTANT, sentinel: ~typing.Any = SENTINEL, ' + 'now: ~datetime.datetime = datetime.now(), color: int = %s, *, kwarg1, ' + 'kwarg2=%s) -> None' % (color, color), + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + '.. py:function:: get_sentinel(custom=SENTINEL)', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_preserve_defaults': True}) +def test_preserve_defaults_special_constructs(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.preserve_defaults_special_constructs', options) + + # * dataclasses.dataclass: + # - __init__ source code is not available + # - default values specified at class level are not discovered + # - values wrapped in a field(...) expression cannot be analyzed + # easily even if annotations were to be parsed + # * typing.NamedTuple: + # - __init__ source code is not available + # - default values specified at class level are not discovered + # * collections.namedtuple: + # - default values are specified as "default=(d1, d2, ...)" + # + # In the future, it might be possible to find some additional default + # values by parsing the source code of the annotations but the task is + # rather complex. + + assert list(actual) == [ + '', + '.. py:module:: target.preserve_defaults_special_constructs', + '', + '', + '.. py:class:: DataClass(' + 'a: int, b: object = <object object>, c: list[int] = <factory>)', + ' :module: target.preserve_defaults_special_constructs', + '', + ' docstring', + '', + '', + '.. py:class:: DataClassNoInit()', + ' :module: target.preserve_defaults_special_constructs', + '', + ' docstring', + '', + '', + '.. py:class:: MyNamedTuple1(' + 'a: int, b: object = <object object>, c: list[int] = [1, 2, 3])', + ' :module: target.preserve_defaults_special_constructs', + '', + ' docstring', + '', + '', + ' .. py:attribute:: MyNamedTuple1.a', + ' :module: target.preserve_defaults_special_constructs', + ' :type: int', + '', + ' Alias for field number 0', + '', + '', + ' .. py:attribute:: MyNamedTuple1.b', + ' :module: target.preserve_defaults_special_constructs', + ' :type: object', + '', + ' Alias for field number 1', + '', + '', + ' .. py:attribute:: MyNamedTuple1.c', + ' :module: target.preserve_defaults_special_constructs', + ' :type: list[int]', + '', + ' Alias for field number 2', + '', + '', + '.. py:class:: MyNamedTuple2(a=0, b=<object object>)', + ' :module: target.preserve_defaults_special_constructs', + '', + ' docstring', + '', + '', + '.. py:class:: MyTypedDict', + ' :module: target.preserve_defaults_special_constructs', + '', + ' docstring', + '', + '', + '.. py:data:: SENTINEL', + ' :module: target.preserve_defaults_special_constructs', + ' :value: <object object>', + '', + ' docstring', + '', + '', + '.. py:function:: foo(x, y, z=SENTINEL)', + ' :module: target.preserve_defaults_special_constructs', + '', + ' docstring', + '', + '', + '.. py:function:: ze_lambda(z=SENTINEL)', + ' :module: target.preserve_defaults_special_constructs', + '', + ' docstring', + '', + ] diff --git a/tests/test_extensions/test_ext_autodoc_private_members.py b/tests/test_extensions/test_ext_autodoc_private_members.py new file mode 100644 index 0000000..bf14414 --- /dev/null +++ b/tests/test_extensions/test_ext_autodoc_private_members.py @@ -0,0 +1,158 @@ +"""Test the autodoc extension. This tests mainly for private-members option. +""" + +import pytest + +from tests.test_extensions.autodoc_util import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_field(app): + app.config.autoclass_content = 'class' + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.private', options) + assert list(actual) == [ + '', + '.. py:module:: target.private', + '', + '', + '.. py:data:: _PUBLIC_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta public:', + '', + '', + '.. py:function:: _public_function(name)', + ' :module: target.private', + '', + ' public_function is a docstring().', + '', + ' :meta public:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_field_and_private_members(app): + app.config.autoclass_content = 'class' + options = {"members": None, + "private-members": None} + actual = do_autodoc(app, 'module', 'target.private', options) + assert list(actual) == [ + '', + '.. py:module:: target.private', + '', + '', + '.. py:data:: PRIVATE_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta private:', + '', + '', + '.. py:data:: _PUBLIC_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta public:', + '', + '', + '.. py:function:: _public_function(name)', + ' :module: target.private', + '', + ' public_function is a docstring().', + '', + ' :meta public:', + '', + '', + '.. py:function:: private_function(name)', + ' :module: target.private', + '', + ' private_function is a docstring().', + '', + ' :meta private:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_members(app): + app.config.autoclass_content = 'class' + options = {"members": None, + "private-members": "_PUBLIC_CONSTANT,_public_function"} + actual = do_autodoc(app, 'module', 'target.private', options) + assert list(actual) == [ + '', + '.. py:module:: target.private', + '', + '', + '.. py:data:: _PUBLIC_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta public:', + '', + '', + '.. py:function:: _public_function(name)', + ' :module: target.private', + '', + ' public_function is a docstring().', + '', + ' :meta public:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_attributes(app): + app.config.autoclass_content = 'class' + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.private.Foo', options) + assert list(actual) == [ + '', + '.. py:class:: Foo()', + ' :module: target.private', + '', + '', + ' .. py:attribute:: Foo._public_attribute', + ' :module: target.private', + ' :value: 47', + '', + ' A public class attribute whose name starts with an underscore.', + '', + ' :meta public:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_attributes_and_private_members(app): + app.config.autoclass_content = 'class' + options = {"members": None, + "private-members": None} + actual = do_autodoc(app, 'class', 'target.private.Foo', options) + assert list(actual) == [ + '', + '.. py:class:: Foo()', + ' :module: target.private', + '', + '', + ' .. py:attribute:: Foo._public_attribute', + ' :module: target.private', + ' :value: 47', + '', + ' A public class attribute whose name starts with an underscore.', + '', + ' :meta public:', + '', + '', + ' .. py:attribute:: Foo.private_attribute', + ' :module: target.private', + ' :value: 11', + '', + ' A private class attribute whose name does not start with an underscore.', + '', + ' :meta private:', + '', + ] diff --git a/tests/test_extensions/test_ext_autosectionlabel.py b/tests/test_extensions/test_ext_autosectionlabel.py new file mode 100644 index 0000000..f854ecf --- /dev/null +++ b/tests/test_extensions/test_ext_autosectionlabel.py @@ -0,0 +1,77 @@ +"""Test sphinx.ext.autosectionlabel extension.""" + +import re + +import pytest + + +@pytest.mark.sphinx('html', testroot='ext-autosectionlabel') +def test_autosectionlabel_html(app, status, warning, skipped_labels=False): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + html = ('<li><p><a class="reference internal" href="#introduce-of-sphinx">' + '<span class=".*?">Introduce of Sphinx</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + html = ('<li><p><a class="reference internal" href="#installation">' + '<span class="std std-ref">Installation</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + html = ('<li><p><a class="reference internal" href="#for-windows-users">' + '<span class="std std-ref">For Windows users</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + html = ('<li><p><a class="reference internal" href="#for-unix-users">' + '<span class="std std-ref">For UNIX users</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + html = ('<li><p><a class="reference internal" href="#linux">' + '<span class="std std-ref">Linux</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + html = ('<li><p><a class="reference internal" href="#freebsd">' + '<span class="std std-ref">FreeBSD</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + # for smart_quotes (refs: #4027) + html = ('<li><p><a class="reference internal" ' + 'href="#this-one-s-got-an-apostrophe">' + '<span class="std std-ref">This one’s got an apostrophe' + '</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + +# Reuse test definition from above, just change the test root directory +@pytest.mark.sphinx('html', testroot='ext-autosectionlabel-prefix-document') +def test_autosectionlabel_prefix_document_html(app, status, warning): + test_autosectionlabel_html(app, status, warning) + + +@pytest.mark.sphinx('html', testroot='ext-autosectionlabel', + confoverrides={'autosectionlabel_maxdepth': 3}) +def test_autosectionlabel_maxdepth(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + # depth: 1 + html = ('<li><p><a class="reference internal" href="#test-ext-autosectionlabel">' + '<span class=".*?">test-ext-autosectionlabel</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + # depth: 2 + html = ('<li><p><a class="reference internal" href="#installation">' + '<span class="std std-ref">Installation</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + # depth: 3 + html = ('<li><p><a class="reference internal" href="#for-windows-users">' + '<span class="std std-ref">For Windows users</span></a></p></li>') + assert re.search(html, content, re.DOTALL) + + # depth: 4 + html = '<li><p><span class="xref std std-ref">Linux</span></p></li>' + assert re.search(html, content, re.DOTALL) + + assert "WARNING: undefined label: 'linux'" in warning.getvalue() diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py new file mode 100644 index 0000000..d761978 --- /dev/null +++ b/tests/test_extensions/test_ext_autosummary.py @@ -0,0 +1,686 @@ +"""Test the autosummary extension.""" + +import sys +from io import StringIO +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.ext.autosummary import ( + autosummary_table, + autosummary_toc, + extract_summary, + import_by_name, + mangle_signature, +) +from sphinx.ext.autosummary.generate import ( + AutosummaryEntry, + generate_autosummary_content, + generate_autosummary_docs, +) +from sphinx.ext.autosummary.generate import main as autogen_main +from sphinx.testing.util import assert_node, etree_parse +from sphinx.util.docutils import new_document + +try: + from contextlib import chdir +except ImportError: + from sphinx.util.osutil import _chdir as chdir + +html_warnfile = StringIO() + + +default_kw = { + 'testroot': 'autosummary', + 'confoverrides': { + 'extensions': ['sphinx.ext.autosummary'], + 'autosummary_generate': True, + 'autosummary_generate_overwrite': False, + 'source_suffix': '.rst', + }, +} + + +@pytest.fixture(autouse=True) +def _unload_target_module(): + sys.modules.pop('target', None) + + +def test_mangle_signature(): + TEST = """ + () :: () + (a, b, c, d, e) :: (a, b, c, d, e) + (a, b, c=1, d=2, e=3) :: (a, b[, c, d, e]) + (a, b, aaa=1, bbb=1, ccc=1, eee=1, fff=1, ggg=1, hhh=1, iii=1, jjj=1)\ + :: (a, b[, aaa, bbb, ccc, ...]) + (a, b, c=(), d=<foo>) :: (a, b[, c, d]) + (a, b, c='foobar()', d=123) :: (a, b[, c, d]) + (a, b[, c]) :: (a, b[, c]) + (a, b[, cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]) :: (a, b[, ...) + (a, b='c=d, e=f, g=h', c=3) :: (a[, b, c]) + (a, b="c=d, e=f, g=h", c=3) :: (a[, b, c]) + (a, b='c=d, \\'e=f,\\' g=h', c=3) :: (a[, b, c]) + (a, b='c=d, ', e='\\\\' g=h, c=3) :: (a[, b, e, c]) + (a, b={'c=d, ': 3, '\\\\': 3}) :: (a[, b]) + (a=1, b=2, c=3) :: ([a, b, c]) + (a=1, b=<SomeClass: a, b, c>, c=3) :: ([a, b, c]) + (a=1, b=T(a=1, b=2), c=3) :: ([a, b, c]) + (a: Tuple[int, str], b: int) -> str :: (a, b) + """ + + TEST = [[y.strip() for y in x.split("::")] for x in TEST.split("\n") + if '::' in x] + for inp, outp in TEST: + res = mangle_signature(inp).strip().replace("\u00a0", " ") + assert res == outp, (f"'{inp}' -> '{res}' != '{outp}'") + + +def test_extract_summary(capsys): + settings = Mock(language_code='en', + id_prefix='', + auto_id_prefix='', + pep_reference=False, + rfc_reference=False) + document = new_document('', settings) + + # normal case + doc = ['', + 'This is a first sentence. And second one.', + '', + 'Second block is here'] + assert extract_summary(doc, document) == 'This is a first sentence.' + + # inliner case + doc = ['This sentence contains *emphasis text having dots.*,', + 'it does not break sentence.'] + assert extract_summary(doc, document) == ' '.join(doc) + + # abbreviations + doc = ['Blabla, i.e. bla.'] + assert extract_summary(doc, document) == ' '.join(doc) + + doc = ['Blabla, (i.e. bla).'] + assert extract_summary(doc, document) == ' '.join(doc) + + doc = ['Blabla, e.g. bla.'] + assert extract_summary(doc, document) == ' '.join(doc) + + doc = ['Blabla, (e.g. bla).'] + assert extract_summary(doc, document) == ' '.join(doc) + + doc = ['Blabla, et al. bla.'] + assert extract_summary(doc, document) == ' '.join(doc) + + # literal + doc = ['blah blah::'] + assert extract_summary(doc, document) == 'blah blah.' + + # heading + doc = ['blah blah', + '========='] + assert extract_summary(doc, document) == 'blah blah' + + doc = ['=========', + 'blah blah', + '========='] + assert extract_summary(doc, document) == 'blah blah' + + # hyperlink target + doc = ['Do `this <https://www.sphinx-doc.org/>`_ and that. ' + 'blah blah blah.'] + assert (extract_summary(doc, document) == + 'Do `this <https://www.sphinx-doc.org/>`_ and that.') + + _, err = capsys.readouterr() + assert err == '' + + +@pytest.mark.sphinx('dummy', **default_kw) +def test_get_items_summary(make_app, app_params): + import sphinx.ext.autosummary + import sphinx.ext.autosummary.generate + args, kwargs = app_params + app = make_app(*args, **kwargs) + sphinx.ext.autosummary.generate.setup_documenters(app) + # monkey-patch Autosummary.get_items so we can easily get access to it's + # results.. + orig_get_items = sphinx.ext.autosummary.Autosummary.get_items + + autosummary_items = {} + + def new_get_items(self, names, *args, **kwargs): + results = orig_get_items(self, names, *args, **kwargs) + for name, result in zip(names, results): + autosummary_items[name] = result # NoQA: PERF403 + return results + + def handler(app, what, name, obj, options, lines): + assert isinstance(lines, list) + + # ensure no docstring is processed twice: + assert 'THIS HAS BEEN HANDLED' not in lines + lines.append('THIS HAS BEEN HANDLED') + app.connect('autodoc-process-docstring', handler) + + sphinx.ext.autosummary.Autosummary.get_items = new_get_items + try: + app.build(force_all=True) + finally: + sphinx.ext.autosummary.Autosummary.get_items = orig_get_items + + html_warnings = app._warning.getvalue() + assert html_warnings == '' + + expected_values = { + 'withSentence': 'I have a sentence which spans multiple lines.', + 'noSentence': "this doesn't start with a capital.", + 'emptyLine': "This is the real summary", + 'module_attr': 'This is a module attribute', + 'C.class_attr': 'This is a class attribute', + 'C.instance_attr': 'This is an instance attribute', + 'C.prop_attr1': 'This is a function docstring', + 'C.prop_attr2': 'This is a attribute docstring', + 'C.C2': 'This is a nested inner class docstring', + } + for key, expected in expected_values.items(): + assert autosummary_items[key][2] == expected, 'Summary for %s was %r -'\ + ' expected %r' % (key, autosummary_items[key], expected) + + # check an item in detail + assert 'func' in autosummary_items + func_attrs = ('func', + '(arg_, *args, **kwargs)', + 'Test function take an argument ended with underscore.', + 'dummy_module.func') + assert autosummary_items['func'] == func_attrs + + +def str_content(elem): + if elem.text is not None: + return elem.text + else: + return ''.join(str_content(e) for e in elem) + + +@pytest.mark.sphinx('xml', **default_kw) +def test_escaping(app, status, warning): + app.build(force_all=True) + + outdir = Path(app.builder.outdir) + + docpage = outdir / 'underscore_module_.xml' + assert docpage.exists() + + title = etree_parse(docpage).find('section/title') + + assert str_content(title) == 'underscore_module_' + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module(app): + import autosummary_dummy_module + template = Mock() + + generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, + template, None, False, app, False, {}) + assert template.render.call_args[0][0] == 'module' + + context = template.render.call_args[0][1] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Exc', 'Foo', '_Baz', '_Exc', + '__all__', '__builtins__', '__cached__', '__doc__', + '__file__', '__name__', '__package__', '_quux', 'bar', + 'non_imported_member', 'quuz', 'qux'] + assert context['functions'] == ['bar'] + assert context['all_functions'] == ['_quux', 'bar'] + assert context['classes'] == ['Foo'] + assert context['all_classes'] == ['Foo', '_Baz'] + assert context['exceptions'] == ['Exc'] + assert context['all_exceptions'] == ['Exc', '_Exc'] + assert context['attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] + assert context['all_attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] + assert context['fullname'] == 'autosummary_dummy_module' + assert context['module'] == 'autosummary_dummy_module' + assert context['objname'] == '' + assert context['name'] == '' + assert context['objtype'] == 'module' + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module___all__(app): + import autosummary_dummy_module + template = Mock() + app.config.autosummary_ignore_module_all = False + + generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, + template, None, False, app, False, {}) + assert template.render.call_args[0][0] == 'module' + + context = template.render.call_args[0][1] + assert context['members'] == ['CONSTANT1', 'Exc', 'Foo', '_Baz', 'bar', 'qux', 'path'] + assert context['functions'] == ['bar'] + assert context['all_functions'] == ['bar'] + assert context['classes'] == ['Foo'] + assert context['all_classes'] == ['Foo', '_Baz'] + assert context['exceptions'] == ['Exc'] + assert context['all_exceptions'] == ['Exc'] + assert context['attributes'] == ['CONSTANT1', 'qux'] + assert context['all_attributes'] == ['CONSTANT1', 'qux'] + assert context['fullname'] == 'autosummary_dummy_module' + assert context['module'] == 'autosummary_dummy_module' + assert context['objname'] == '' + assert context['name'] == '' + assert context['objtype'] == 'module' + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_skipped(app): + import autosummary_dummy_module + template = Mock() + + def skip_member(app, what, name, obj, skip, options): + if name in ('Foo', 'bar', 'Exc'): + return True + return None + + app.connect('autodoc-skip-member', skip_member) + generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, + template, None, False, app, False, {}) + context = template.render.call_args[0][1] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', '_Baz', '_Exc', '__all__', + '__builtins__', '__cached__', '__doc__', '__file__', + '__name__', '__package__', '_quux', 'non_imported_member', + 'quuz', 'qux'] + assert context['functions'] == [] + assert context['classes'] == [] + assert context['exceptions'] == [] + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_imported_members(app): + import autosummary_dummy_module + template = Mock() + + generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, + template, None, True, app, False, {}) + assert template.render.call_args[0][0] == 'module' + + context = template.render.call_args[0][1] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Class', 'Exc', 'Foo', 'Union', + '_Baz', '_Exc', '__all__', '__builtins__', '__cached__', + '__doc__', '__file__', '__loader__', '__name__', + '__package__', '__spec__', '_quux', 'bar', + 'considered_as_imported', 'non_imported_member', 'path', + 'quuz', 'qux'] + assert context['functions'] == ['bar'] + assert context['all_functions'] == ['_quux', 'bar'] + assert context['classes'] == ['Class', 'Foo'] + assert context['all_classes'] == ['Class', 'Foo', '_Baz'] + assert context['exceptions'] == ['Exc'] + assert context['all_exceptions'] == ['Exc', '_Exc'] + assert context['attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] + assert context['all_attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] + assert context['fullname'] == 'autosummary_dummy_module' + assert context['module'] == 'autosummary_dummy_module' + assert context['objname'] == '' + assert context['name'] == '' + assert context['objtype'] == 'module' + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_imported_members_inherited_module(app): + import autosummary_dummy_inherited_module + template = Mock() + + generate_autosummary_content('autosummary_dummy_inherited_module', + autosummary_dummy_inherited_module, None, + template, None, True, app, False, {}) + assert template.render.call_args[0][0] == 'module' + + context = template.render.call_args[0][1] + assert context['members'] == ['Foo', 'InheritedAttrClass', '__all__', '__builtins__', '__cached__', + '__doc__', '__file__', '__loader__', '__name__', + '__package__', '__spec__'] + assert context['functions'] == [] + assert context['classes'] == ['Foo', 'InheritedAttrClass'] + assert context['exceptions'] == [] + assert context['all_exceptions'] == [] + assert context['attributes'] == [] + assert context['all_attributes'] == [] + assert context['fullname'] == 'autosummary_dummy_inherited_module' + assert context['module'] == 'autosummary_dummy_inherited_module' + assert context['objname'] == '' + assert context['name'] == '' + assert context['objtype'] == 'module' + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary') +def test_autosummary_generate(app, status, warning): + app.build(force_all=True) + + doctree = app.env.get_doctree('index') + assert_node(doctree, (nodes.paragraph, + nodes.paragraph, + addnodes.tabular_col_spec, + autosummary_table, + [autosummary_toc, addnodes.toctree])) + assert_node(doctree[3], + [autosummary_table, nodes.table, nodes.tgroup, (nodes.colspec, + nodes.colspec, + [nodes.tbody, (nodes.row, + nodes.row, + nodes.row, + nodes.row, + nodes.row, + nodes.row, + nodes.row, + nodes.row)])]) + assert_node(doctree[4][0], addnodes.toctree, caption="An autosummary") + + assert len(doctree[3][0][0][2]) == 8 + assert doctree[3][0][0][2][0].astext() == 'autosummary_dummy_module\n\n' + assert doctree[3][0][0][2][1].astext() == 'autosummary_dummy_module.Foo()\n\n' + assert doctree[3][0][0][2][2].astext() == 'autosummary_dummy_module.Foo.Bar()\n\n' + assert doctree[3][0][0][2][3].astext() == 'autosummary_dummy_module.Foo.value\n\ndocstring' + assert doctree[3][0][0][2][4].astext() == 'autosummary_dummy_module.bar(x[, y])\n\n' + assert doctree[3][0][0][2][5].astext() == 'autosummary_dummy_module.qux\n\na module-level attribute' + assert doctree[3][0][0][2][6].astext() == 'autosummary_dummy_inherited_module.InheritedAttrClass()\n\n' + assert doctree[3][0][0][2][7].astext() == 'autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr\n\nother docstring' + + module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text(encoding='utf8') + + assert (' .. autosummary::\n' + ' \n' + ' Foo\n' + ' \n' in module) + assert (' .. autosummary::\n' + ' \n' + ' CONSTANT1\n' + ' qux\n' + ' quuz\n' + ' non_imported_member\n' + ' \n' in module) + + Foo = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').read_text(encoding='utf8') + assert '.. automethod:: __init__' in Foo + assert (' .. autosummary::\n' + ' \n' + ' ~Foo.__init__\n' + ' ~Foo.bar\n' + ' \n' in Foo) + assert (' .. autosummary::\n' + ' \n' + ' ~Foo.CONSTANT3\n' + ' ~Foo.CONSTANT4\n' + ' ~Foo.baz\n' + ' ~Foo.value\n' + ' \n' in Foo) + + FooBar = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.Bar.rst').read_text(encoding='utf8') + assert ('.. currentmodule:: autosummary_dummy_module\n' + '\n' + '.. autoclass:: Foo.Bar\n' in FooBar) + + Foo_value = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.value.rst').read_text(encoding='utf8') + assert ('.. currentmodule:: autosummary_dummy_module\n' + '\n' + '.. autoattribute:: Foo.value' in Foo_value) + + qux = (app.srcdir / 'generated' / 'autosummary_dummy_module.qux.rst').read_text(encoding='utf8') + assert ('.. currentmodule:: autosummary_dummy_module\n' + '\n' + '.. autodata:: qux' in qux) + + InheritedAttrClass = (app.srcdir / 'generated' / 'autosummary_dummy_inherited_module.InheritedAttrClass.rst').read_text(encoding='utf8') + print(InheritedAttrClass) + assert '.. automethod:: __init__' in Foo + assert (' .. autosummary::\n' + ' \n' + ' ~InheritedAttrClass.__init__\n' + ' ~InheritedAttrClass.bar\n' + ' \n' in InheritedAttrClass) + assert (' .. autosummary::\n' + ' \n' + ' ~InheritedAttrClass.CONSTANT3\n' + ' ~InheritedAttrClass.CONSTANT4\n' + ' ~InheritedAttrClass.baz\n' + ' ~InheritedAttrClass.subclassattr\n' + ' ~InheritedAttrClass.value\n' + ' \n' in InheritedAttrClass) + + InheritedAttrClass_subclassattr = (app.srcdir / 'generated' / 'autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr.rst').read_text(encoding='utf8') + assert ('.. currentmodule:: autosummary_dummy_inherited_module\n' + '\n' + '.. autoattribute:: InheritedAttrClass.subclassattr' in InheritedAttrClass_subclassattr) + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary', + confoverrides={'autosummary_generate_overwrite': False}) +def test_autosummary_generate_overwrite1(app_params, make_app): + args, kwargs = app_params + srcdir = kwargs.get('srcdir') + + (srcdir / 'generated').mkdir(parents=True, exist_ok=True) + (srcdir / 'generated' / 'autosummary_dummy_module.rst').write_text('', encoding='utf8') + + app = make_app(*args, **kwargs) + content = (srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text(encoding='utf8') + assert content == '' + assert 'autosummary_dummy_module.rst' not in app._warning.getvalue() + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary', + confoverrides={'autosummary_generate_overwrite': True}) +def test_autosummary_generate_overwrite2(app_params, make_app): + args, kwargs = app_params + srcdir = kwargs.get('srcdir') + + (srcdir / 'generated').mkdir(parents=True, exist_ok=True) + (srcdir / 'generated' / 'autosummary_dummy_module.rst').write_text('', encoding='utf8') + + app = make_app(*args, **kwargs) + content = (srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text(encoding='utf8') + assert content != '' + assert 'autosummary_dummy_module.rst' not in app._warning.getvalue() + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive') +@pytest.mark.usefixtures("rollback_sysmodules") +def test_autosummary_recursive(app, status, warning): + sys.modules.pop('package', None) # unload target module to clear the module cache + + app.build() + + # autosummary having :recursive: option + assert (app.srcdir / 'generated' / 'package.rst').exists() + assert (app.srcdir / 'generated' / 'package.module.rst').exists() + assert (app.srcdir / 'generated' / 'package.module_importfail.rst').exists() is False + assert (app.srcdir / 'generated' / 'package.package.rst').exists() + assert (app.srcdir / 'generated' / 'package.package.module.rst').exists() + + # autosummary not having :recursive: option + assert (app.srcdir / 'generated' / 'package2.rst').exists() + assert (app.srcdir / 'generated' / 'package2.module.rst').exists() is False + + # Check content of recursively generated stub-files + content = (app.srcdir / 'generated' / 'package.rst').read_text(encoding='utf8') + assert 'package.module' in content + assert 'package.package' in content + assert 'package.module_importfail' in content + + content = (app.srcdir / 'generated' / 'package.package.rst').read_text(encoding='utf8') + assert 'package.package.module' in content + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive', + srcdir='test_autosummary_recursive_skips_mocked_modules', + confoverrides={'autosummary_mock_imports': ['package.package']}) +@pytest.mark.usefixtures("rollback_sysmodules") +def test_autosummary_recursive_skips_mocked_modules(app, status, warning): + sys.modules.pop('package', None) # unload target module to clear the module cache + app.build() + + assert (app.srcdir / 'generated' / 'package.rst').exists() + assert (app.srcdir / 'generated' / 'package.module.rst').exists() + assert (app.srcdir / 'generated' / 'package.package.rst').exists() is False + assert (app.srcdir / 'generated' / 'package.package.module.rst').exists() is False + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-filename-map') +def test_autosummary_filename_map(app, status, warning): + app.build() + + assert (app.srcdir / 'generated' / 'module_mangled.rst').exists() + assert not (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').exists() + assert (app.srcdir / 'generated' / 'bar.rst').exists() + assert not (app.srcdir / 'generated' / 'autosummary_dummy_module.bar.rst').exists() + assert (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').exists() + + html_warnings = app._warning.getvalue() + assert html_warnings == '' + + +@pytest.mark.sphinx('latex', **default_kw) +def test_autosummary_latex_table_colspec(app, status, warning): + app.build(force_all=True) + result = (app.outdir / 'python.tex').read_text(encoding='utf8') + print(status.getvalue()) + print(warning.getvalue()) + assert r'\begin{longtable}{\X{1}{2}\X{1}{2}}' in result + assert r'p{0.5\linewidth}' not in result + + +def test_import_by_name(): + import sphinx + import sphinx.ext.autosummary + + prefixed_name, obj, parent, modname = import_by_name('sphinx') + assert prefixed_name == 'sphinx' + assert obj is sphinx + assert parent is None + assert modname == 'sphinx' + + prefixed_name, obj, parent, modname = import_by_name('sphinx.ext.autosummary.__name__') + assert prefixed_name == 'sphinx.ext.autosummary.__name__' + assert obj is sphinx.ext.autosummary.__name__ + assert parent is sphinx.ext.autosummary + assert modname == 'sphinx.ext.autosummary' + + prefixed_name, obj, parent, modname = \ + import_by_name('sphinx.ext.autosummary.Autosummary.get_items') + assert prefixed_name == 'sphinx.ext.autosummary.Autosummary.get_items' + assert obj == sphinx.ext.autosummary.Autosummary.get_items + assert parent is sphinx.ext.autosummary.Autosummary + assert modname == 'sphinx.ext.autosummary' + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-mock_imports') +def test_autosummary_mock_imports(app, status, warning): + try: + app.build() + assert warning.getvalue() == '' + + # generated/foo is generated successfully + assert app.env.get_doctree('generated/foo') + finally: + sys.modules.pop('foo', None) # unload foo module + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-imported_members') +def test_autosummary_imported_members(app, status, warning): + try: + app.build() + # generated/foo is generated successfully + assert app.env.get_doctree('generated/autosummary_dummy_package') + + module = (app.srcdir / 'generated' / 'autosummary_dummy_package.rst').read_text(encoding='utf8') + assert (' .. autosummary::\n' + ' \n' + ' Bar\n' + ' \n' in module) + assert (' .. autosummary::\n' + ' \n' + ' foo\n' + ' \n' in module) + finally: + sys.modules.pop('autosummary_dummy_package', None) + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_all') +def test_autosummary_module_all(app, status, warning): + try: + app.build() + # generated/foo is generated successfully + assert app.env.get_doctree('generated/autosummary_dummy_package_all') + module = (app.srcdir / 'generated' / 'autosummary_dummy_package_all.rst').read_text(encoding='utf8') + assert (' .. autosummary::\n' + ' \n' + ' PublicBar\n' + ' \n' in module) + assert (' .. autosummary::\n' + ' \n' + ' public_foo\n' + ' public_baz\n' + ' \n' in module) + assert ('.. autosummary::\n' + ' :toctree:\n' + ' :recursive:\n\n' + ' autosummary_dummy_package_all.extra_dummy_module\n\n' in module) + finally: + sys.modules.pop('autosummary_dummy_package_all', None) + + +@pytest.mark.sphinx(testroot='ext-autodoc', + confoverrides={'extensions': ['sphinx.ext.autosummary']}) +def test_generate_autosummary_docs_property(app): + with patch('sphinx.ext.autosummary.generate.find_autosummary_in_files') as mock: + mock.return_value = [AutosummaryEntry('target.methods.Base.prop', 'prop', None, False)] + generate_autosummary_docs([], output_dir=app.srcdir, app=app) + + content = (app.srcdir / 'target.methods.Base.prop.rst').read_text(encoding='utf8') + assert content == ("target.methods.Base.prop\n" + "========================\n" + "\n" + ".. currentmodule:: target.methods\n" + "\n" + ".. autoproperty:: Base.prop") + + +@pytest.mark.sphinx(testroot='ext-autosummary-skip-member') +def test_autosummary_skip_member(app): + app.build() + + content = (app.srcdir / 'generate' / 'target.Foo.rst').read_text(encoding='utf8') + assert 'Foo.skipmeth' not in content + assert 'Foo._privatemeth' in content + + +@pytest.mark.sphinx(testroot='ext-autosummary-template') +def test_autosummary_template(app): + app.build() + + content = (app.srcdir / 'generate' / 'target.Foo.rst').read_text(encoding='utf8') + assert 'EMPTY' in content + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary', + confoverrides={'autosummary_generate': []}) +def test_empty_autosummary_generate(app, status, warning): + app.build() + assert ("WARNING: autosummary: failed to import autosummary_importfail" + in warning.getvalue()) + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary', + confoverrides={'autosummary_generate': ['unknown']}) +def test_invalid_autosummary_generate(app, status, warning): + assert 'WARNING: autosummary_generate: file not found: unknown.rst' in warning.getvalue() + + +def test_autogen(rootdir, tmp_path): + with chdir(rootdir / 'test-templating'): + args = ['-o', str(tmp_path), '-t', '.', 'autosummary_templating.txt'] + autogen_main(args) + assert (tmp_path / 'sphinx.application.TemplateBridge.rst').exists() diff --git a/tests/test_extensions/test_ext_coverage.py b/tests/test_extensions/test_ext_coverage.py new file mode 100644 index 0000000..c9e9ba9 --- /dev/null +++ b/tests/test_extensions/test_ext_coverage.py @@ -0,0 +1,101 @@ +"""Test the coverage builder.""" + +import pickle + +import pytest + + +@pytest.mark.sphinx('coverage') +def test_build(app, status, warning): + app.build(force_all=True) + + py_undoc = (app.outdir / 'python.txt').read_text(encoding='utf8') + assert py_undoc.startswith('Undocumented Python objects\n' + '===========================\n') + assert 'autodoc_target\n--------------\n' in py_undoc + assert ' * Class -- missing methods:\n' in py_undoc + assert ' * raises\n' in py_undoc + assert ' * function\n' not in py_undoc # these two are documented + assert ' * Class\n' not in py_undoc # in autodoc.txt + + assert " * mod -- No module named 'mod'" in py_undoc # in the "failed import" section + + assert "undocumented py" not in status.getvalue() + + c_undoc = (app.outdir / 'c.txt').read_text(encoding='utf8') + assert c_undoc.startswith('Undocumented C API elements\n' + '===========================\n') + assert 'api.h' in c_undoc + assert ' * Py_SphinxTest' in c_undoc + + undoc_py, undoc_c, py_undocumented, py_documented = pickle.loads((app.outdir / 'undoc.pickle').read_bytes()) + assert len(undoc_c) == 1 + # the key is the full path to the header file, which isn't testable + assert list(undoc_c.values())[0] == {('function', 'Py_SphinxTest')} + + assert 'autodoc_target' in undoc_py + assert 'funcs' in undoc_py['autodoc_target'] + assert 'raises' in undoc_py['autodoc_target']['funcs'] + assert 'classes' in undoc_py['autodoc_target'] + assert 'Class' in undoc_py['autodoc_target']['classes'] + assert 'undocmeth' in undoc_py['autodoc_target']['classes']['Class'] + + assert "undocumented c" not in status.getvalue() + + +@pytest.mark.sphinx('coverage', testroot='ext-coverage') +def test_coverage_ignore_pyobjects(app, status, warning): + app.build(force_all=True) + actual = (app.outdir / 'python.txt').read_text(encoding='utf8') + expected = '''\ +Undocumented Python objects +=========================== + +Statistics +---------- + ++----------------------+----------+--------------+ +| Module | Coverage | Undocumented | ++======================+==========+==============+ +| coverage_not_ignored | 0.00% | 2 | ++----------------------+----------+--------------+ +| TOTAL | 0.00% | 2 | ++----------------------+----------+--------------+ + +coverage_not_ignored +-------------------- + +Classes: + * Documented -- missing methods: + + - not_ignored1 + - not_ignored2 + * NotIgnored + +''' + assert actual == expected + + +@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True}) +def test_show_missing_items(app, status, warning): + app.build(force_all=True) + + assert "undocumented" in status.getvalue() + + assert "py function raises" in status.getvalue() + assert "py class Base" in status.getvalue() + assert "py method Class.roger" in status.getvalue() + + assert "c api Py_SphinxTest [ function]" in status.getvalue() + + +@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True}) +def test_show_missing_items_quiet(app, status, warning): + app.quiet = True + app.build(force_all=True) + + assert "undocumented python function: autodoc_target :: raises" in warning.getvalue() + assert "undocumented python class: autodoc_target :: Base" in warning.getvalue() + assert "undocumented python method: autodoc_target :: Class :: roger" in warning.getvalue() + + assert "undocumented c api: Py_SphinxTest [function]" in warning.getvalue() diff --git a/tests/test_extensions/test_ext_doctest.py b/tests/test_extensions/test_ext_doctest.py new file mode 100644 index 0000000..ab0dd62 --- /dev/null +++ b/tests/test_extensions/test_ext_doctest.py @@ -0,0 +1,135 @@ +"""Test the doctest extension.""" +import os +from collections import Counter + +import pytest +from docutils import nodes +from packaging.specifiers import InvalidSpecifier +from packaging.version import InvalidVersion + +from sphinx.ext.doctest import is_allowed_version + +cleanup_called = 0 + + +@pytest.mark.sphinx('doctest', testroot='ext-doctest') +def test_build(app, status, warning): + global cleanup_called + cleanup_called = 0 + app.build(force_all=True) + assert app.statuscode == 0, f'failures in doctests:\n{status.getvalue()}' + # in doctest.txt, there are two named groups and the default group, + # so the cleanup function must be called three times + assert cleanup_called == 3, 'testcleanup did not get executed enough times' + + +@pytest.mark.sphinx('dummy', testroot='ext-doctest') +def test_highlight_language_default(app, status, warning): + app.build() + doctree = app.env.get_doctree('doctest') + for node in doctree.findall(nodes.literal_block): + assert node['language'] in {'python', 'pycon', 'none'} + + +@pytest.mark.sphinx('dummy', testroot='ext-doctest', + confoverrides={'highlight_language': 'python'}) +def test_highlight_language_python3(app, status, warning): + app.build() + doctree = app.env.get_doctree('doctest') + for node in doctree.findall(nodes.literal_block): + assert node['language'] in {'python', 'pycon', 'none'} + + +def test_is_allowed_version(): + assert is_allowed_version('<3.4', '3.3') is True + assert is_allowed_version('<3.4', '3.3') is True + assert is_allowed_version('<3.2', '3.3') is False + assert is_allowed_version('<=3.4', '3.3') is True + assert is_allowed_version('<=3.2', '3.3') is False + assert is_allowed_version('==3.3', '3.3') is True + assert is_allowed_version('==3.4', '3.3') is False + assert is_allowed_version('>=3.2', '3.3') is True + assert is_allowed_version('>=3.4', '3.3') is False + assert is_allowed_version('>3.2', '3.3') is True + assert is_allowed_version('>3.4', '3.3') is False + assert is_allowed_version('~=3.4', '3.4.5') is True + assert is_allowed_version('~=3.4', '3.5.0') is True + + # invalid spec + with pytest.raises(InvalidSpecifier): + is_allowed_version('&3.4', '3.5') + + # invalid version + with pytest.raises(InvalidVersion): + is_allowed_version('>3.4', 'Sphinx') + + +def cleanup_call(): + global cleanup_called + cleanup_called += 1 + + +recorded_calls = Counter() + + +@pytest.mark.sphinx('doctest', testroot='ext-doctest-skipif') +def test_skipif(app, status, warning): + """Tests for the :skipif: option + + The tests are separated into a different test root directory since the + ``app`` object only evaluates options once in its lifetime. If these tests + were combined with the other doctest tests, the ``:skipif:`` evaluations + would be recorded only on the first ``app.build(force_all=True)`` run, i.e. + in ``test_build`` above, and the assertion below would fail. + + """ + global recorded_calls + recorded_calls = Counter() + app.build(force_all=True) + if app.statuscode != 0: + raise AssertionError('failures in doctests:' + status.getvalue()) + # The `:skipif:` expressions are always run. + # Actual tests and setup/cleanup code is only run if the `:skipif:` + # expression evaluates to a False value. + # Global setup/cleanup are run before/after evaluating the `:skipif:` + # option in each directive - thus 11 additional invocations for each on top + # of the ones made for the whole test file. + assert recorded_calls == {('doctest_global_setup', 'body', True): 13, + ('testsetup', ':skipif:', True): 1, + ('testsetup', ':skipif:', False): 1, + ('testsetup', 'body', False): 1, + ('doctest', ':skipif:', True): 1, + ('doctest', ':skipif:', False): 1, + ('doctest', 'body', False): 1, + ('testcode', ':skipif:', True): 1, + ('testcode', ':skipif:', False): 1, + ('testcode', 'body', False): 1, + ('testoutput-1', ':skipif:', True): 1, + ('testoutput-2', ':skipif:', True): 1, + ('testoutput-2', ':skipif:', False): 1, + ('testcleanup', ':skipif:', True): 1, + ('testcleanup', ':skipif:', False): 1, + ('testcleanup', 'body', False): 1, + ('doctest_global_cleanup', 'body', True): 13} + + +def record(directive, part, should_skip): + recorded_calls[(directive, part, should_skip)] += 1 + return f'Recorded {directive} {part} {should_skip}' + + +@pytest.mark.sphinx('doctest', testroot='ext-doctest-with-autodoc') +def test_reporting_with_autodoc(app, status, warning, capfd): + # Patch builder to get a copy of the output + written = [] + app.builder._warn_out = written.append + app.build(force_all=True) + + failures = [line.replace(os.sep, '/') + for line in '\n'.join(written).splitlines() + if line.startswith('File')] + + assert 'File "dir/inner.rst", line 1, in default' in failures + assert 'File "dir/bar.py", line ?, in default' in failures + assert 'File "foo.py", line ?, in default' in failures + assert 'File "index.rst", line 4, in default' in failures diff --git a/tests/test_extensions/test_ext_duration.py b/tests/test_extensions/test_ext_duration.py new file mode 100644 index 0000000..4fa4dfc --- /dev/null +++ b/tests/test_extensions/test_ext_duration.py @@ -0,0 +1,14 @@ +"""Test sphinx.ext.duration extension.""" + +import re + +import pytest + + +@pytest.mark.sphinx('dummy', testroot='basic', + confoverrides={'extensions': ['sphinx.ext.duration']}) +def test_githubpages(app, status, warning): + app.build() + + assert 'slowest reading durations' in status.getvalue() + assert re.search('\\d+\\.\\d{3} index\n', status.getvalue()) diff --git a/tests/test_extensions/test_ext_extlinks.py b/tests/test_extensions/test_ext_extlinks.py new file mode 100644 index 0000000..7634db6 --- /dev/null +++ b/tests/test_extensions/test_ext_extlinks.py @@ -0,0 +1,45 @@ +import pytest + + +@pytest.mark.sphinx('html', testroot='ext-extlinks-hardcoded-urls', + confoverrides={'extlinks_detect_hardcoded_links': False}) +def test_extlinks_detect_candidates(app, warning): + app.build() + assert warning.getvalue() == '' + + +@pytest.mark.sphinx('html', testroot='ext-extlinks-hardcoded-urls') +def test_replaceable_uris_emit_extlinks_warnings(app, warning): + app.build() + warning_output = warning.getvalue() + + # there should be exactly three warnings for replaceable URLs + message = ( + "index.rst:%d: WARNING: hardcoded link 'https://github.com/sphinx-doc/sphinx/issues/1' " + "could be replaced by an extlink (try using '%s' instead)" + ) + assert message % (11, ":issue:`1`") in warning_output + assert message % (13, ":issue:`inline replaceable link <1>`") in warning_output + assert message % (15, ":issue:`replaceable link <1>`") in warning_output + + +@pytest.mark.sphinx('html', testroot='ext-extlinks-hardcoded-urls-multiple-replacements') +def test_all_replacements_suggested_if_multiple_replacements_possible(app, warning): + app.build() + warning_output = warning.getvalue() + # there should be six warnings for replaceable URLs, three pairs per link + assert warning_output.count("WARNING: hardcoded link") == 6 + message = ( + "index.rst:%d: WARNING: hardcoded link 'https://github.com/octocat' " + "could be replaced by an extlink (try using '%s' instead)" + ) + assert message % (14, ":user:`octocat`") in warning_output + assert message % (16, ":user:`inline replaceable link <octocat>`") in warning_output + assert message % (18, ":user:`replaceable link <octocat>`") in warning_output + message = ( + "index.rst:%d: WARNING: hardcoded link 'https://github.com/octocat' " + "could be replaced by an extlink (try using '%s' instead)" + ) + assert message % (14, ":repo:`octocat`") in warning_output + assert message % (16, ":repo:`inline replaceable link <octocat>`") in warning_output + assert message % (18, ":repo:`replaceable link <octocat>`") in warning_output diff --git a/tests/test_extensions/test_ext_githubpages.py b/tests/test_extensions/test_ext_githubpages.py new file mode 100644 index 0000000..879b6d1 --- /dev/null +++ b/tests/test_extensions/test_ext_githubpages.py @@ -0,0 +1,26 @@ +"""Test sphinx.ext.githubpages extension.""" + +import pytest + + +@pytest.mark.sphinx('html', testroot='ext-githubpages') +def test_githubpages(app, status, warning): + app.build(force_all=True) + assert (app.outdir / '.nojekyll').exists() + assert not (app.outdir / 'CNAME').exists() + + +@pytest.mark.sphinx('html', testroot='ext-githubpages', + confoverrides={'html_baseurl': 'https://sphinx-doc.github.io'}) +def test_no_cname_for_github_io_domain(app, status, warning): + app.build(force_all=True) + assert (app.outdir / '.nojekyll').exists() + assert not (app.outdir / 'CNAME').exists() + + +@pytest.mark.sphinx('html', testroot='ext-githubpages', + confoverrides={'html_baseurl': 'https://sphinx-doc.org'}) +def test_cname_for_custom_domain(app, status, warning): + app.build(force_all=True) + assert (app.outdir / '.nojekyll').exists() + assert (app.outdir / 'CNAME').read_text(encoding='utf8') == 'sphinx-doc.org' diff --git a/tests/test_extensions/test_ext_graphviz.py b/tests/test_extensions/test_ext_graphviz.py new file mode 100644 index 0000000..866a92a --- /dev/null +++ b/tests/test_extensions/test_ext_graphviz.py @@ -0,0 +1,196 @@ +"""Test sphinx.ext.graphviz extension.""" + +import re +import sys + +import pytest + +from sphinx.ext.graphviz import ClickableMapDefinition + + +@pytest.mark.sphinx('html', testroot='ext-graphviz') +@pytest.mark.usefixtures('if_graphviz_found') +def test_graphviz_png_html(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + html = (r'<figure class="align-default" .*?>\s*' + r'<div class="graphviz"><img .*?/></div>\s*<figcaption>\s*' + r'<p><span class="caption-text">caption of graph</span>.*</p>\s*' + r'</figcaption>\s*</figure>') + assert re.search(html, content, re.DOTALL) + + html = 'Hello <div class="graphviz"><img .*?/></div>\n graphviz world' + assert re.search(html, content, re.DOTALL) + + html = ('<img src=".*?" alt="digraph foo {\nbaz -> qux\n}" ' + 'class="graphviz neato-graph" />') + assert re.search(html, content, re.DOTALL) + + html = (r'<figure class="align-right" .*?>\s*' + r'<div class="graphviz"><img .*?/></div>\s*<figcaption>\s*' + r'<p><span class="caption-text">on <em>right</em></span>.*</p>\s*' + r'</figcaption>\s*</figure>') + assert re.search(html, content, re.DOTALL) + + html = (r'<div align=\"center\" class=\"align-center\">' + r'<div class="graphviz"><img src=\".*\.png\" alt=\"digraph foo {\n' + r'centered\n' + r'}\" class="graphviz" /></div>\n</div>') + assert re.search(html, content, re.DOTALL) + + +@pytest.mark.sphinx('html', testroot='ext-graphviz', + confoverrides={'graphviz_output_format': 'svg'}) +@pytest.mark.usefixtures('if_graphviz_found') +def test_graphviz_svg_html(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + html = (r'<figure class=\"align-default\" .*?>\n' + r'<div class="graphviz"><object data=\".*\.svg\".*>\n' + r'\s*<p class=\"warning\">digraph foo {\n' + r'bar -> baz\n' + r'}</p></object></div>\n' + r'<figcaption>\n' + r'<p><span class=\"caption-text\">caption of graph</span>.*</p>\n' + r'</figcaption>\n' + r'</figure>') + assert re.search(html, content, re.DOTALL) + + html = (r'Hello <div class="graphviz"><object.*>\n' + r'\s*<p class=\"warning\">graph</p></object></div>\n' + r' graphviz world') + assert re.search(html, content, re.DOTALL) + + html = (r'<figure class=\"align-right\" .*\>\n' + r'<div class="graphviz"><object data=\".*\.svg\".*>\n' + r'\s*<p class=\"warning\">digraph bar {\n' + r'foo -> bar\n' + r'}</p></object></div>\n' + r'<figcaption>\n' + r'<p><span class=\"caption-text\">on <em>right</em></span>.*</p>\n' + r'</figcaption>\n' + r'</figure>') + assert re.search(html, content, re.DOTALL) + + html = (r'<div align=\"center\" class=\"align-center\">' + r'<div class="graphviz"><object data=\".*\.svg\".*>\n' + r'\s*<p class=\"warning\">digraph foo {\n' + r'centered\n' + r'}</p></object></div>\n' + r'</div>') + assert re.search(html, content, re.DOTALL) + + image_re = r'.*data="([^"]+)".*?digraph test' + image_path_match = re.search(image_re, content, re.DOTALL) + assert image_path_match + + image_path = image_path_match.group(1) + image_content = (app.outdir / image_path).read_text(encoding='utf8') + if sys.platform == 'win32': + assert '".\\_static\\' not in image_content + assert r'<ns0:image ns1:href="..\_static\images\test.svg"' in image_content + assert r'<ns0:a ns1:href="..\_static\images\test.svg"' in image_content + else: + assert '"./_static/' not in image_content + assert '<ns0:image ns1:href="../_static/images/test.svg"' in image_content + assert '<ns0:a ns1:href="../_static/images/test.svg"' in image_content + assert '<ns0:a ns1:href="..#graphviz"' in image_content + + +@pytest.mark.sphinx('latex', testroot='ext-graphviz') +@pytest.mark.usefixtures('if_graphviz_found') +def test_graphviz_latex(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'python.tex').read_text(encoding='utf8') + macro = ('\\\\begin{figure}\\[htbp\\]\n\\\\centering\n\\\\capstart\n\n' + '\\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf}\n' + '\\\\caption{caption of graph}\\\\label{.*}\\\\end{figure}') + assert re.search(macro, content, re.DOTALL) + + macro = 'Hello \\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf} graphviz world' + assert re.search(macro, content, re.DOTALL) + + macro = ('\\\\begin{wrapfigure}{r}{0pt}\n\\\\centering\n' + '\\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf}\n' + '\\\\caption{on \\\\sphinxstyleemphasis{right}}' + '\\\\label{.*}\\\\end{wrapfigure}') + assert re.search(macro, content, re.DOTALL) + + macro = (r'\{\\hfill' + r'\\sphinxincludegraphics\[\]{graphviz-.*}' + r'\\hspace\*{\\fill}}') + assert re.search(macro, content, re.DOTALL) + + +@pytest.mark.sphinx('html', testroot='ext-graphviz', confoverrides={'language': 'xx'}) +@pytest.mark.usefixtures('if_graphviz_found') +def test_graphviz_i18n(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + html = '<img src=".*?" alt="digraph {\n BAR -> BAZ\n}" class="graphviz" />' + assert re.search(html, content, re.MULTILINE) + + +def test_graphviz_parse_mapfile(): + # empty graph + code = ('# digraph {\n' + '# }\n') + content = ('<map id="%3" name="%3">\n' + '</map>') + cmap = ClickableMapDefinition('dummy.map', content, code) + assert cmap.filename == 'dummy.map' + assert cmap.id == 'grapvizb08107169e' + assert len(cmap.clickable) == 0 + assert cmap.generate_clickable_map() == '' + + # normal graph + code = ('digraph {\n' + ' foo [href="https://www.google.com/"];\n' + ' foo -> bar;\n' + '}\n') + content = ('<map id="%3" name="%3">\n' + '<area shape="poly" id="node1" href="https://www.google.com/" title="foo" alt=""' + ' coords="77,29,76,22,70,15,62,10,52,7,41,5,30,7,20,10,12,15,7,22,5,29,7,37,12,' + '43,20,49,30,52,41,53,52,52,62,49,70,43,76,37"/>\n' + '</map>') + cmap = ClickableMapDefinition('dummy.map', content, code) + assert cmap.filename == 'dummy.map' + assert cmap.id == 'grapvizff087ab863' + assert len(cmap.clickable) == 1 + assert cmap.generate_clickable_map() == content.replace('%3', cmap.id) + + # inheritance-diagram:: sphinx.builders.html + content = ( + '<map id="inheritance66ff5471b9" name="inheritance66ff5471b9">\n' + '<area shape="rect" id="node1" title="Builds target formats from the reST sources."' + ' alt="" coords="26,95,125,110"/>\n' + '<area shape="rect" id="node5" title="Builds standalone HTML docs."' + ' alt="" coords="179,95,362,110"/>\n' + '<area shape="rect" id="node2" title="buildinfo file manipulator." ' + ' alt="" coords="14,64,138,80"/>\n' + '<area shape="rect" id="node3" title="The container of stylesheets."' + ' alt="" coords="3,34,148,49"/>\n' + '<area shape="rect" id="node4" title="A StandaloneHTMLBuilder that creates all HTML' + ' pages as "index.html" in" alt="" coords="395,64,569,80"/>\n' + '<area shape="rect" id="node7" title="An abstract builder that serializes' + ' the generated HTML." alt="" coords="392,95,571,110"/>\n' + '<area shape="rect" id="node9" title="A StandaloneHTMLBuilder subclass that puts' + ' the whole document tree on one" alt="" coords="393,125,570,141"/>\n' + '<area shape="rect" id="node6" title="A builder that dumps the generated HTML' + ' into JSON files." alt="" coords="602,80,765,95"/>\n' + '<area shape="rect" id="node8" title="A Builder that dumps the generated HTML' + ' into pickle files." alt="" coords="602,110,765,125"/>\n' + '<area shape="rect" id="node10" title="The metadata of stylesheet."' + ' alt="" coords="11,3,141,19"/>\n' + '</map>' + ) + cmap = ClickableMapDefinition('dummy.map', content, 'dummy_code') + assert cmap.filename == 'dummy.map' + assert cmap.id == 'inheritance66ff5471b9' + assert len(cmap.clickable) == 0 + assert cmap.generate_clickable_map() == '' diff --git a/tests/test_extensions/test_ext_ifconfig.py b/tests/test_extensions/test_ext_ifconfig.py new file mode 100644 index 0000000..3e46b1e --- /dev/null +++ b/tests/test_extensions/test_ext_ifconfig.py @@ -0,0 +1,28 @@ +"""Test sphinx.ext.ifconfig extension.""" + +import docutils.utils +import pytest + +from sphinx import addnodes +from sphinx.testing import restructuredtext + + +@pytest.mark.sphinx('text', testroot='ext-ifconfig') +def test_ifconfig(app, status, warning): + app.build(force_all=True) + result = (app.outdir / 'index.txt').read_text(encoding='utf8') + assert 'spam' in result + assert 'ham' not in result + + +def test_ifconfig_content_line_number(app): + app.setup_extension("sphinx.ext.ifconfig") + text = (".. ifconfig:: confval1\n" + + "\n" + + " Some link here: :ref:`abc`\n") + doc = restructuredtext.parse(app, text) + xrefs = list(doc.findall(condition=addnodes.pending_xref)) + assert len(xrefs) == 1 + source, line = docutils.utils.get_source_line(xrefs[0]) + assert 'index.rst' in source + assert line == 3 diff --git a/tests/test_extensions/test_ext_imgconverter.py b/tests/test_extensions/test_ext_imgconverter.py new file mode 100644 index 0000000..c1d2061 --- /dev/null +++ b/tests/test_extensions/test_ext_imgconverter.py @@ -0,0 +1,35 @@ +"""Test sphinx.ext.imgconverter extension.""" + +import subprocess + +import pytest + + +@pytest.fixture() +def _if_converter_found(app): + image_converter = getattr(app.config, 'image_converter', '') + try: + if image_converter: + # print the image_converter version, to check that the command is available + subprocess.run([image_converter, '-version'], capture_output=True, check=False) + return + except OSError: # No such file or directory + pass + + pytest.skip('image_converter "%s" is not available' % image_converter) + + +@pytest.mark.usefixtures('_if_converter_found') +@pytest.mark.sphinx('latex', testroot='ext-imgconverter') +def test_ext_imgconverter(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'python.tex').read_text(encoding='utf8') + + # supported image (not converted) + assert '\\sphinxincludegraphics{{img}.pdf}' in content + + # non supported image (converted) + assert '\\sphinxincludegraphics{{svgimg}.png}' in content + assert not (app.outdir / 'svgimg.svg').exists() + assert (app.outdir / 'svgimg.png').exists() diff --git a/tests/test_extensions/test_ext_imgmockconverter.py b/tests/test_extensions/test_ext_imgmockconverter.py new file mode 100644 index 0000000..4c3c64e --- /dev/null +++ b/tests/test_extensions/test_ext_imgmockconverter.py @@ -0,0 +1,17 @@ +"""Test image converter with identical basenames""" + +import pytest + + +@pytest.mark.sphinx('latex', testroot='ext-imgmockconverter') +def test_ext_imgmockconverter(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'python.tex').read_text(encoding='utf8') + + # check identical basenames give distinct files + assert '\\sphinxincludegraphics{{svgimg}.pdf}' in content + assert '\\sphinxincludegraphics{{svgimg1}.pdf}' in content + assert not (app.outdir / 'svgimg.svg').exists() + assert (app.outdir / 'svgimg.pdf').exists() + assert (app.outdir / 'svgimg1.pdf').exists() diff --git a/tests/test_extensions/test_ext_inheritance_diagram.py b/tests/test_extensions/test_ext_inheritance_diagram.py new file mode 100644 index 0000000..c13ccea --- /dev/null +++ b/tests/test_extensions/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.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 / '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.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() diff --git a/tests/test_extensions/test_ext_intersphinx.py b/tests/test_extensions/test_ext_intersphinx.py new file mode 100644 index 0000000..ef5a9b1 --- /dev/null +++ b/tests/test_extensions/test_ext_intersphinx.py @@ -0,0 +1,584 @@ +"""Test the intersphinx extension.""" + +import http.server +from unittest import mock + +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.ext.intersphinx import ( + INVENTORY_FILENAME, + _get_safe_url, + _strip_basic_auth, + fetch_inventory, + inspect_main, + load_mappings, + missing_reference, + normalize_intersphinx_mapping, +) +from sphinx.ext.intersphinx import setup as intersphinx_setup +from sphinx.util.console import strip_colors + +from tests.test_util.intersphinx_data import INVENTORY_V2, INVENTORY_V2_NO_VERSION +from tests.utils import http_server + + +def fake_node(domain, type, target, content, **attrs): + contnode = nodes.emphasis(content, content) + node = addnodes.pending_xref('') + node['reftarget'] = target + node['reftype'] = type + node['refdomain'] = domain + node.attributes.update(attrs) + node += contnode + return node, contnode + + +def reference_check(app, *args, **kwds): + node, contnode = fake_node(*args, **kwds) + return missing_reference(app, app.env, node, contnode) + + +def set_config(app, mapping): + app.config.intersphinx_mapping = mapping + app.config.intersphinx_cache_limit = 0 + app.config.intersphinx_disabled_reftypes = [] + + +@mock.patch('sphinx.ext.intersphinx.InventoryFile') +@mock.patch('sphinx.ext.intersphinx._read_from_url') +def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, warning): # NoQA: PT019 + intersphinx_setup(app) + _read_from_url().readline.return_value = b'# Sphinx inventory version 2' + + # same uri and inv, not redirected + _read_from_url().url = 'https://hostname/' + INVENTORY_FILENAME + fetch_inventory(app, 'https://hostname/', 'https://hostname/' + INVENTORY_FILENAME) + assert 'intersphinx inventory has moved' not in status.getvalue() + assert InventoryFile.load.call_args[0][1] == 'https://hostname/' + + # same uri and inv, redirected + status.seek(0) + status.truncate(0) + _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME + + fetch_inventory(app, 'https://hostname/', 'https://hostname/' + INVENTORY_FILENAME) + assert status.getvalue() == ('intersphinx inventory has moved: ' + 'https://hostname/%s -> https://hostname/new/%s\n' % + (INVENTORY_FILENAME, INVENTORY_FILENAME)) + assert InventoryFile.load.call_args[0][1] == 'https://hostname/new' + + # different uri and inv, not redirected + status.seek(0) + status.truncate(0) + _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME + + fetch_inventory(app, 'https://hostname/', 'https://hostname/new/' + INVENTORY_FILENAME) + assert 'intersphinx inventory has moved' not in status.getvalue() + assert InventoryFile.load.call_args[0][1] == 'https://hostname/' + + # different uri and inv, redirected + status.seek(0) + status.truncate(0) + _read_from_url().url = 'https://hostname/other/' + INVENTORY_FILENAME + + fetch_inventory(app, 'https://hostname/', 'https://hostname/new/' + INVENTORY_FILENAME) + assert status.getvalue() == ('intersphinx inventory has moved: ' + 'https://hostname/new/%s -> https://hostname/other/%s\n' % + (INVENTORY_FILENAME, INVENTORY_FILENAME)) + assert InventoryFile.load.call_args[0][1] == 'https://hostname/' + + +def test_missing_reference(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, { + 'https://docs.python.org/': str(inv_file), + 'py3k': ('https://docs.python.org/py3k/', str(inv_file)), + 'py3krel': ('py3k', str(inv_file)), # relative path + 'py3krelparent': ('../../py3k', str(inv_file)), # relative path, parent dir + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + inv = app.env.intersphinx_inventory + + assert inv['py:module']['module2'] == \ + ('foo', '2.0', 'https://docs.python.org/foo.html#module-module2', '-') + + # check resolution when a target is found + rn = reference_check(app, 'py', 'func', 'module1.func', 'foo') + assert isinstance(rn, nodes.reference) + assert rn['refuri'] == 'https://docs.python.org/sub/foo.html#module1.func' + assert rn['reftitle'] == '(in foo v2.0)' + assert rn[0].astext() == 'foo' + + # create unresolvable nodes and check None return value + assert reference_check(app, 'py', 'foo', 'module1.func', 'foo') is None + assert reference_check(app, 'py', 'func', 'foo', 'foo') is None + assert reference_check(app, 'py', 'func', 'foo', 'foo') is None + + # check handling of prefixes + + # prefix given, target found: prefix is stripped + rn = reference_check(app, 'py', 'mod', 'py3k:module2', 'py3k:module2') + assert rn[0].astext() == 'module2' + + # prefix given, but not in title: nothing stripped + rn = reference_check(app, 'py', 'mod', 'py3k:module2', 'module2') + assert rn[0].astext() == 'module2' + + # prefix given, but explicit: nothing stripped + rn = reference_check(app, 'py', 'mod', 'py3k:module2', 'py3k:module2', + refexplicit=True) + assert rn[0].astext() == 'py3k:module2' + + # prefix given, target not found and nonexplicit title: prefix is not stripped + node, contnode = fake_node('py', 'mod', 'py3k:unknown', 'py3k:unknown', + refexplicit=False) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None + assert contnode[0].astext() == 'py3k:unknown' + + # prefix given, target not found and explicit title: nothing is changed + node, contnode = fake_node('py', 'mod', 'py3k:unknown', 'py3k:unknown', + refexplicit=True) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None + assert contnode[0].astext() == 'py3k:unknown' + + # check relative paths + rn = reference_check(app, 'py', 'mod', 'py3krel:module1', 'foo') + assert rn['refuri'] == 'py3k/foo.html#module-module1' + + rn = reference_check(app, 'py', 'mod', 'py3krelparent:module1', 'foo') + assert rn['refuri'] == '../../py3k/foo.html#module-module1' + + rn = reference_check(app, 'py', 'mod', 'py3krel:module1', 'foo', refdoc='sub/dir/test') + assert rn['refuri'] == '../../py3k/foo.html#module-module1' + + rn = reference_check(app, 'py', 'mod', 'py3krelparent:module1', 'foo', + refdoc='sub/dir/test') + assert rn['refuri'] == '../../../../py3k/foo.html#module-module1' + + # check refs of standard domain + rn = reference_check(app, 'std', 'doc', 'docname', 'docname') + assert rn['refuri'] == 'https://docs.python.org/docname.html' + + +def test_missing_reference_pydomain(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, { + 'https://docs.python.org/': str(inv_file), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + # no context data + kwargs = {} + node, contnode = fake_node('py', 'func', 'func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None + + # py:module context helps to search objects + kwargs = {'py:module': 'module1'} + node, contnode = fake_node('py', 'func', 'func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'func()' + + # py:attr context helps to search objects + kwargs = {'py:module': 'module1'} + node, contnode = fake_node('py', 'attr', 'Foo.bar', 'Foo.bar', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'Foo.bar' + + +def test_missing_reference_stddomain(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, { + 'cmd': ('https://docs.python.org/', str(inv_file)), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + # no context data + kwargs = {} + node, contnode = fake_node('std', 'option', '-l', '-l', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None + + # std:program context helps to search objects + kwargs = {'std:program': 'ls'} + node, contnode = fake_node('std', 'option', '-l', 'ls -l', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'ls -l' + + # refers inventory by name + kwargs = {} + node, contnode = fake_node('std', 'option', 'cmd:ls -l', '-l', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == '-l' + + # term reference (normal) + node, contnode = fake_node('std', 'term', 'a term', 'a term') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'a term' + + # term reference (case insensitive) + node, contnode = fake_node('std', 'term', 'A TERM', 'A TERM') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'A TERM' + + # label reference (normal) + node, contnode = fake_node('std', 'ref', 'The-Julia-Domain', 'The-Julia-Domain') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'The Julia Domain' + + # label reference (case insensitive) + node, contnode = fake_node('std', 'ref', 'the-julia-domain', 'the-julia-domain') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'The Julia Domain' + + +@pytest.mark.sphinx('html', testroot='ext-intersphinx-cppdomain') +def test_missing_reference_cppdomain(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, { + 'https://docs.python.org/': str(inv_file), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + app.build() + html = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<a class="reference external"' + ' href="https://docs.python.org/index.html#cpp_foo_bar"' + ' title="(in foo v2.0)">' + '<code class="xref cpp cpp-class docutils literal notranslate">' + '<span class="pre">Bar</span></code></a>' in html) + assert ('<a class="reference external"' + ' href="https://docs.python.org/index.html#foons"' + ' title="(in foo v2.0)"><span class="n"><span class="pre">foons</span></span></a>' in html) + assert ('<a class="reference external"' + ' href="https://docs.python.org/index.html#foons_bartype"' + ' title="(in foo v2.0)"><span class="n"><span class="pre">bartype</span></span></a>' in html) + + +def test_missing_reference_jsdomain(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, { + 'https://docs.python.org/': str(inv_file), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + # no context data + kwargs = {} + node, contnode = fake_node('js', 'meth', 'baz', 'baz()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None + + # js:module and js:object context helps to search objects + kwargs = {'js:module': 'foo', 'js:object': 'bar'} + node, contnode = fake_node('js', 'meth', 'baz', 'baz()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'baz()' + + +def test_missing_reference_disabled_domain(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, { + 'inv': ('https://docs.python.org/', str(inv_file)), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + def case(*, term, doc, py): + def assert_(rn, expected): + if expected is None: + assert rn is None + else: + assert rn.astext() == expected + + kwargs = {} + + node, contnode = fake_node('std', 'term', 'a term', 'a term', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'a term' if term else None) + + node, contnode = fake_node('std', 'term', 'inv:a term', 'a term', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'a term') + + node, contnode = fake_node('std', 'doc', 'docname', 'docname', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'docname' if doc else None) + + node, contnode = fake_node('std', 'doc', 'inv:docname', 'docname', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'docname') + + # an arbitrary ref in another domain + node, contnode = fake_node('py', 'func', 'module1.func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'func()' if py else None) + + node, contnode = fake_node('py', 'func', 'inv:module1.func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'func()') + + # the base case, everything should resolve + assert app.config.intersphinx_disabled_reftypes == [] + case(term=True, doc=True, py=True) + + # disabled a single ref type + app.config.intersphinx_disabled_reftypes = ['std:doc'] + case(term=True, doc=False, py=True) + + # disabled a whole domain + app.config.intersphinx_disabled_reftypes = ['std:*'] + case(term=False, doc=False, py=True) + + # disabled all domains + app.config.intersphinx_disabled_reftypes = ['*'] + case(term=False, doc=False, py=False) + + +def test_inventory_not_having_version(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2_NO_VERSION) + set_config(app, { + 'https://docs.python.org/': str(inv_file), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + rn = reference_check(app, 'py', 'mod', 'module1', 'foo') + assert isinstance(rn, nodes.reference) + assert rn['refuri'] == 'https://docs.python.org/foo.html#module-module1' + assert rn['reftitle'] == '(in foo)' + assert rn[0].astext() == 'Long Module desc' + + +def test_load_mappings_warnings(tmp_path, app, status, warning): + """ + load_mappings issues a warning if new-style mapping + identifiers are not string + """ + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, { + 'https://docs.python.org/': str(inv_file), + 'py3k': ('https://docs.python.org/py3k/', str(inv_file)), + 'repoze.workflow': ('https://docs.repoze.org/workflow/', str(inv_file)), + 'django-taggit': ('https://django-taggit.readthedocs.org/en/latest/', + str(inv_file)), + 12345: ('https://www.sphinx-doc.org/en/stable/', str(inv_file)), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + warnings = warning.getvalue().splitlines() + assert len(warnings) == 2 + assert "The pre-Sphinx 1.0 'intersphinx_mapping' format is " in warnings[0] + assert 'intersphinx identifier 12345 is not string. Ignored' in warnings[1] + + +def test_load_mappings_fallback(tmp_path, app, status, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + set_config(app, {}) + + # connect to invalid path + app.config.intersphinx_mapping = { + 'fallback': ('https://docs.python.org/py3k/', '/invalid/inventory/path'), + } + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + assert "failed to reach any of the inventories" in warning.getvalue() + + rn = reference_check(app, 'py', 'func', 'module1.func', 'foo') + assert rn is None + + # clear messages + status.truncate(0) + warning.truncate(0) + + # add fallbacks to mapping + app.config.intersphinx_mapping = { + 'fallback': ('https://docs.python.org/py3k/', ('/invalid/inventory/path', + str(inv_file))), + } + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + assert "encountered some issues with some of the inventories" in status.getvalue() + assert warning.getvalue() == "" + + rn = reference_check(app, 'py', 'func', 'module1.func', 'foo') + assert isinstance(rn, nodes.reference) + + +class TestStripBasicAuth: + """Tests for sphinx.ext.intersphinx._strip_basic_auth()""" + + def test_auth_stripped(self): + """Basic auth creds stripped from URL containing creds""" + url = 'https://user:12345@domain.com/project/objects.inv' + expected = 'https://domain.com/project/objects.inv' + actual = _strip_basic_auth(url) + assert expected == actual + + def test_no_auth(self): + """Url unchanged if param doesn't contain basic auth creds""" + url = 'https://domain.com/project/objects.inv' + expected = 'https://domain.com/project/objects.inv' + actual = _strip_basic_auth(url) + assert expected == actual + + def test_having_port(self): + """Basic auth creds correctly stripped from URL containing creds even if URL + contains port + """ + url = 'https://user:12345@domain.com:8080/project/objects.inv' + expected = 'https://domain.com:8080/project/objects.inv' + actual = _strip_basic_auth(url) + assert expected == actual + + +def test_getsafeurl_authed(): + """_get_safe_url() with a url with basic auth""" + url = 'https://user:12345@domain.com/project/objects.inv' + expected = 'https://user@domain.com/project/objects.inv' + actual = _get_safe_url(url) + assert expected == actual + + +def test_getsafeurl_authed_having_port(): + """_get_safe_url() with a url with basic auth having port""" + url = 'https://user:12345@domain.com:8080/project/objects.inv' + expected = 'https://user@domain.com:8080/project/objects.inv' + actual = _get_safe_url(url) + assert expected == actual + + +def test_getsafeurl_unauthed(): + """_get_safe_url() with a url without basic auth""" + url = 'https://domain.com/project/objects.inv' + expected = 'https://domain.com/project/objects.inv' + actual = _get_safe_url(url) + assert expected == actual + + +def test_inspect_main_noargs(capsys): + """inspect_main interface, without arguments""" + assert inspect_main([]) == 1 + + expected = ( + "Print out an inventory file.\n" + "Error: must specify local path or URL to an inventory file." + ) + stdout, stderr = capsys.readouterr() + assert stdout == "" + assert stderr == expected + "\n" + + +def test_inspect_main_file(capsys, tmp_path): + """inspect_main interface, with file argument""" + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + + inspect_main([str(inv_file)]) + + stdout, stderr = capsys.readouterr() + assert stdout.startswith("c:function\n") + assert stderr == "" + + +def test_inspect_main_url(capsys): + """inspect_main interface, with url argument""" + class InventoryHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200, "OK") + self.end_headers() + self.wfile.write(INVENTORY_V2) + + def log_message(*args, **kwargs): + # Silenced. + pass + + with http_server(InventoryHandler) as server: + url = f'http://localhost:{server.server_port}/{INVENTORY_FILENAME}' + inspect_main([url]) + + stdout, stderr = capsys.readouterr() + assert stdout.startswith("c:function\n") + assert stderr == "" + + +@pytest.mark.sphinx('html', testroot='ext-intersphinx-role') +def test_intersphinx_role(app, warning): + inv_file = app.srcdir / 'inventory' + inv_file.write_bytes(INVENTORY_V2) + app.config.intersphinx_mapping = { + 'inv': ('https://example.org/', str(inv_file)), + } + app.config.intersphinx_cache_limit = 0 + app.config.nitpicky = True + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + warnings = strip_colors(warning.getvalue()).splitlines() + index_path = app.srcdir / 'index.rst' + assert warnings == [ + f"{index_path}:21: WARNING: role for external cross-reference not found in domain 'py': 'nope'", + f"{index_path}:28: WARNING: role for external cross-reference not found in domains 'cpp', 'std': 'nope'", + f"{index_path}:39: WARNING: inventory for external cross-reference not found: 'invNope'", + f"{index_path}:44: WARNING: role for external cross-reference not found in domain 'c': 'function' (perhaps you meant one of: 'func', 'identifier', 'type')", + f"{index_path}:45: WARNING: role for external cross-reference not found in domains 'cpp', 'std': 'function' (perhaps you meant one of: 'cpp:func', 'cpp:identifier', 'cpp:type')", + f'{index_path}:9: WARNING: external py:mod reference target not found: module3', + f'{index_path}:14: WARNING: external py:mod reference target not found: module10', + f'{index_path}:19: WARNING: external py:meth reference target not found: inv:Foo.bar', + ] + + html = '<a class="reference external" href="https://example.org/{}" title="(in foo v2.0)">' + assert html.format('foo.html#module-module1') in content + assert html.format('foo.html#module-module2') in content + + assert html.format('sub/foo.html#module1.func') in content + + # default domain + assert html.format('index.html#std_uint8_t') in content + + # std roles without domain prefix + assert html.format('docname.html') in content + assert html.format('index.html#cmdoption-ls-l') in content + + # explicit inventory + assert html.format('cfunc.html#CFunc') in content + + # explicit title + assert html.format('index.html#foons') in content diff --git a/tests/test_extensions/test_ext_math.py b/tests/test_extensions/test_ext_math.py new file mode 100644 index 0000000..b673f83 --- /dev/null +++ b/tests/test_extensions/test_ext_math.py @@ -0,0 +1,389 @@ +"""Test math extensions.""" + +import re +import shutil +import subprocess +import warnings + +import pytest +from docutils import nodes + +from sphinx.ext.mathjax import MATHJAX_URL +from sphinx.testing.util import assert_node + + +def has_binary(binary): + try: + subprocess.check_output([binary]) + except FileNotFoundError: + return False + except OSError: + pass + return True + + +@pytest.mark.skipif(not has_binary('dvipng'), + reason='Requires dvipng" binary') +@pytest.mark.sphinx('html', testroot='ext-math-simple', + confoverrides={'extensions': ['sphinx.ext.imgmath']}) +def test_imgmath_png(app, status, warning): + app.build(force_all=True) + if "LaTeX command 'latex' cannot be run" in warning.getvalue(): + msg = 'LaTeX command "latex" is not available' + raise pytest.skip.Exception(msg) + if "dvipng command 'dvipng' cannot be run" in warning.getvalue(): + msg = 'dvipng command "dvipng" is not available' + raise pytest.skip.Exception(msg) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + shutil.rmtree(app.outdir) + html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.png"' + r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') + assert re.search(html, content, re.DOTALL) + + +@pytest.mark.skipif(not has_binary('dvisvgm'), + reason='Requires dvisvgm" binary') +@pytest.mark.sphinx('html', testroot='ext-math-simple', + confoverrides={'extensions': ['sphinx.ext.imgmath'], + 'imgmath_image_format': 'svg'}) +def test_imgmath_svg(app, status, warning): + app.build(force_all=True) + if "LaTeX command 'latex' cannot be run" in warning.getvalue(): + msg = 'LaTeX command "latex" is not available' + raise pytest.skip.Exception(msg) + if "dvisvgm command 'dvisvgm' cannot be run" in warning.getvalue(): + msg = 'dvisvgm command "dvisvgm" is not available' + raise pytest.skip.Exception(msg) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + shutil.rmtree(app.outdir) + html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.svg"' + r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') + assert re.search(html, content, re.DOTALL) + + +@pytest.mark.skipif(not has_binary('dvisvgm'), + reason='Requires dvisvgm" binary') +@pytest.mark.sphinx('html', testroot='ext-math-simple', + confoverrides={'extensions': ['sphinx.ext.imgmath'], + 'imgmath_image_format': 'svg', + 'imgmath_embed': True}) +def test_imgmath_svg_embed(app, status, warning): + app.build(force_all=True) + if "LaTeX command 'latex' cannot be run" in warning.getvalue(): + msg = 'LaTeX command "latex" is not available' + raise pytest.skip.Exception(msg) + if "dvisvgm command 'dvisvgm' cannot be run" in warning.getvalue(): + msg = 'dvisvgm command "dvisvgm" is not available' + raise pytest.skip.Exception(msg) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + shutil.rmtree(app.outdir) + html = r'<img src="data:image/svg\+xml;base64,[\w\+/=]+"' + assert re.search(html, content, re.DOTALL) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'mathjax_options': {'integrity': 'sha384-0123456789'}}) +def test_mathjax_options(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + shutil.rmtree(app.outdir) + assert ('<script async="async" integrity="sha384-0123456789" ' + 'src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">' + '</script>' in content) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_align(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + shutil.rmtree(app.outdir) + html = (r'<div class="math notranslate nohighlight">\s*' + r'\\\[ \\begin\{align\}\\begin\{aligned\}S \&= \\pi r\^2\\\\' + r'V \&= \\frac\{4\}\{3\} \\pi r\^3\\end\{aligned\}\\end\{align\} \\\]</div>') + assert re.search(html, content, re.DOTALL) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'math_number_all': True, + 'extensions': ['sphinx.ext.mathjax']}) +def test_math_number_all_mathjax(app, status, warning): + app.build() + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + html = (r'<div class="math notranslate nohighlight" id="equation-index-0">\s*' + r'<span class="eqno">\(1\)<a .*>\xb6</a></span>\\\[a\^2\+b\^2=c\^2\\\]</div>') + assert re.search(html, content, re.DOTALL) + + +@pytest.mark.sphinx('latex', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_math_number_all_latex(app, status, warning): + app.build() + + content = (app.outdir / 'python.tex').read_text(encoding='utf8') + macro = (r'\\begin{equation\*}\s*' + r'\\begin{split}a\^2\+b\^2=c\^2\\end{split}\s*' + r'\\end{equation\*}') + assert re.search(macro, content, re.DOTALL) + + macro = r'Inline \\\(E=mc\^2\\\)' + assert re.search(macro, content, re.DOTALL) + + macro = (r'\\begin{equation\*}\s*' + r'\\begin{split}e\^{i\\pi}\+1=0\\end{split}\s+' + r'\\end{equation\*}') + assert re.search(macro, content, re.DOTALL) + + macro = (r'\\begin{align\*}\\!\\begin{aligned}\s*' + r'S &= \\pi r\^2\\\\\s*' + r'V &= \\frac\{4}\{3} \\pi r\^3\\\\\s*' + r'\\end{aligned}\\end{align\*}') + assert re.search(macro, content, re.DOTALL) + + macro = r'Referencing equation \\eqref{equation:math:foo}.' + assert re.search(macro, content, re.DOTALL) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'math_eqref_format': 'Eq.{number}'}) +def test_math_eqref_format_html(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'math.html').read_text(encoding='utf8') + html = ('<p>Referencing equation <a class="reference internal" ' + 'href="#equation-foo">Eq.1</a> and <a class="reference internal" ' + 'href="#equation-foo">Eq.1</a>.</p>') + assert html in content + + +@pytest.mark.sphinx('latex', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'math_eqref_format': 'Eq.{number}'}) +def test_math_eqref_format_latex(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'python.tex').read_text(encoding='utf8') + macro = (r'Referencing equation Eq.\\ref{equation:math:foo} and ' + r'Eq.\\ref{equation:math:foo}.') + assert re.search(macro, content, re.DOTALL) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'numfig': True, + 'math_numfig': True}) +def test_mathjax_numfig_html(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'math.html').read_text(encoding='utf8') + html = ('<div class="math notranslate nohighlight" id="equation-math-0">\n' + '<span class="eqno">(1.2)') + assert html in content + html = ('<p>Referencing equation <a class="reference internal" ' + 'href="#equation-foo">(1.1)</a> and ' + '<a class="reference internal" href="#equation-foo">(1.1)</a>.</p>') + assert html in content + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.imgmath'], + 'numfig': True, + 'numfig_secnum_depth': 0, + 'math_numfig': True}) +def test_imgmath_numfig_html(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'page.html').read_text(encoding='utf8') + html = '<span class="eqno">(3)<a class="headerlink" href="#equation-bar"' + assert html in content + html = ('<p>Referencing equations <a class="reference internal" ' + 'href="math.html#equation-foo">(1)</a> and ' + '<a class="reference internal" href="#equation-bar">(3)</a>.</p>') + assert html in content + + +@pytest.mark.sphinx('dummy', testroot='ext-math-compat') +def test_math_compat(app, status, warning): + with warnings.catch_warnings(record=True): + app.build(force_all=True) + doctree = app.env.get_and_resolve_doctree('index', app.builder) + + assert_node(doctree, + [nodes.document, nodes.section, (nodes.title, + [nodes.section, (nodes.title, + nodes.paragraph)], + nodes.section)]) + assert_node(doctree[0][1][1], + ('Inline: ', + [nodes.math, "E=mc^2"], + '\nInline my math: ', + [nodes.math, "E = mc^2"])) + assert_node(doctree[0][2], + ([nodes.title, "block"], + [nodes.math_block, "a^2+b^2=c^2\n\n"], + [nodes.paragraph, "Second math"], + [nodes.math_block, "e^{i\\pi}+1=0\n\n"], + [nodes.paragraph, "Multi math equations"], + [nodes.math_block, "E = mc^2"])) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'mathjax3_config': {'extensions': ['tex2jax.js']}}) +def test_mathjax3_config(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + assert ('<script defer="defer" src="%s">' % MATHJAX_URL in content) + assert ('<script>window.MathJax = {"extensions": ["tex2jax.js"]}</script>' in content) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'mathjax2_config': {'extensions': ['tex2jax.js']}}) +def test_mathjax2_config(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<script async="async" src="%s">' % MATHJAX_URL in content) + assert ('<script type="text/x-mathjax-config">' + 'MathJax.Hub.Config({"extensions": ["tex2jax.js"]})' + '</script>' in content) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'mathjax_options': {'async': 'async'}, + 'mathjax3_config': {'extensions': ['tex2jax.js']}}) +def test_mathjax_options_async_for_mathjax3(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + assert ('<script async="async" src="%s">' % MATHJAX_URL in content) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'mathjax_options': {'defer': 'defer'}, + 'mathjax2_config': {'extensions': ['tex2jax.js']}}) +def test_mathjax_options_defer_for_mathjax2(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<script defer="defer" src="%s">' % MATHJAX_URL in content) + + +@pytest.mark.sphinx( + 'html', testroot='ext-math', + confoverrides={ + 'extensions': ['sphinx.ext.mathjax'], + 'mathjax_path': 'MathJax.js', + }, +) +def test_mathjax_path(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<script async="async" src="_static/MathJax.js"></script>' in content + + +@pytest.mark.sphinx( + 'html', testroot='ext-math', + confoverrides={ + 'extensions': ['sphinx.ext.mathjax'], + 'mathjax_path': 'MathJax.js?config=scipy-mathjax', + }, +) +def test_mathjax_path_config(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<script async="async" src="_static/MathJax.js?config=scipy-mathjax"></script>' in content + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_only_if_document_having_math(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + content = (app.outdir / 'nomath.html').read_text(encoding='utf8') + assert MATHJAX_URL not in content + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_not_installed_if_no_equations(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert 'MathJax.js' not in content + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_if_no_equations_when_forced(app, status, warning): + app.set_html_assets_policy('always') + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + content = (app.outdir / 'nomath.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + +@pytest.mark.sphinx('html', testroot='ext-math-include', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_if_included_file_has_equations(app): + app.build(force_all=True) + + # no real equations at the rst level, but includes "included" + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + # no real equations at the rst level, but includes "math.rst" + content = (app.outdir / 'included.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + content = (app.outdir / 'math.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + +@pytest.mark.sphinx('singlehtml', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_only_if_document_having_math_singlehtml(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + +@pytest.mark.sphinx('singlehtml', testroot='basic', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_not_installed_if_no_equations_singlehtml(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert 'MathJax.js' not in content + + +@pytest.mark.sphinx('singlehtml', testroot='ext-math-include', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_if_included_file_has_equations_singlehtml(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content diff --git a/tests/test_extensions/test_ext_napoleon.py b/tests/test_extensions/test_ext_napoleon.py new file mode 100644 index 0000000..466bd49 --- /dev/null +++ b/tests/test_extensions/test_ext_napoleon.py @@ -0,0 +1,218 @@ +"""Tests for :mod:`sphinx.ext.napoleon.__init__` module.""" + +import functools +from collections import namedtuple +from unittest import mock + +import pytest + +from sphinx.application import Sphinx +from sphinx.ext.napoleon import Config, _process_docstring, _skip_member, setup + + +def simple_decorator(f): + """ + A simple decorator that does nothing, for tests to use. + """ + @functools.wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +def _private_doc(): + """module._private_doc.DOCSTRING""" + pass + + +def _private_undoc(): + pass + + +def __special_doc__(): + """module.__special_doc__.DOCSTRING""" + pass + + +def __special_undoc__(): + pass + + +class SampleClass: + def _private_doc(self): + """SampleClass._private_doc.DOCSTRING""" + pass + + def _private_undoc(self): + pass + + def __special_doc__(self): + """SampleClass.__special_doc__.DOCSTRING""" + pass + + def __special_undoc__(self): + pass + + @simple_decorator + def __decorated_func__(self): + """Doc""" + pass + + +class SampleError(Exception): + def _private_doc(self): + """SampleError._private_doc.DOCSTRING""" + pass + + def _private_undoc(self): + pass + + def __special_doc__(self): + """SampleError.__special_doc__.DOCSTRING""" + pass + + def __special_undoc__(self): + pass + + +SampleNamedTuple = namedtuple('SampleNamedTuple', 'user_id block_type def_id') + + +class TestProcessDocstring: + def test_modify_in_place(self): + lines = ['Summary line.', + '', + 'Args:', + ' arg1: arg1 description'] + app = mock.Mock() + app.config = Config() + _process_docstring(app, 'class', 'SampleClass', SampleClass, + mock.Mock(), lines) + + expected = ['Summary line.', + '', + ':param arg1: arg1 description', + ''] + assert expected == lines + + +class TestSetup: + def test_unknown_app_type(self): + setup(object()) + + def test_add_config_values(self): + app = mock.Mock(Sphinx) + setup(app) + for name in Config._config_values: + has_config = False + for method_name, args, _kwargs in app.method_calls: + if ( + method_name == 'add_config_value' and + args[0] == name + ): + has_config = True + if not has_config: + pytest.fail('Config value was not added to app %s' % name) + + has_process_docstring = False + has_skip_member = False + for method_name, args, _kwargs in app.method_calls: + if method_name == 'connect': + if ( + args[0] == 'autodoc-process-docstring' and + args[1] == _process_docstring + ): + has_process_docstring = True + elif ( + args[0] == 'autodoc-skip-member' and + args[1] == _skip_member + ): + has_skip_member = True + if not has_process_docstring: + pytest.fail('autodoc-process-docstring never connected') + if not has_skip_member: + pytest.fail('autodoc-skip-member never connected') + + +class TestSkipMember: + def assert_skip(self, what, member, obj, expect_default_skip, config_name): + skip = True + app = mock.Mock() + app.config = Config() + setattr(app.config, config_name, True) + if expect_default_skip: + assert None is _skip_member(app, what, member, obj, skip, mock.Mock()) + else: + assert _skip_member(app, what, member, obj, skip, mock.Mock()) is False + setattr(app.config, config_name, False) + assert None is _skip_member(app, what, member, obj, skip, mock.Mock()) + + def test_namedtuple(self): + # Since python 3.7, namedtuple._asdict() has not been documented + # because there is no way to check the method is a member of the + # namedtuple class. This testcase confirms only it does not + # raise an error on building document (refs: #1455) + self.assert_skip('class', '_asdict', + SampleNamedTuple._asdict, True, + 'napoleon_include_private_with_doc') + + def test_class_private_doc(self): + self.assert_skip('class', '_private_doc', + SampleClass._private_doc, False, + 'napoleon_include_private_with_doc') + + def test_class_private_undoc(self): + self.assert_skip('class', '_private_undoc', + SampleClass._private_undoc, True, + 'napoleon_include_private_with_doc') + + def test_class_special_doc(self): + self.assert_skip('class', '__special_doc__', + SampleClass.__special_doc__, False, + 'napoleon_include_special_with_doc') + + def test_class_special_undoc(self): + self.assert_skip('class', '__special_undoc__', + SampleClass.__special_undoc__, True, + 'napoleon_include_special_with_doc') + + def test_class_decorated_doc(self): + self.assert_skip('class', '__decorated_func__', + SampleClass.__decorated_func__, False, + 'napoleon_include_special_with_doc') + + def test_exception_private_doc(self): + self.assert_skip('exception', '_private_doc', + SampleError._private_doc, False, + 'napoleon_include_private_with_doc') + + def test_exception_private_undoc(self): + self.assert_skip('exception', '_private_undoc', + SampleError._private_undoc, True, + 'napoleon_include_private_with_doc') + + def test_exception_special_doc(self): + self.assert_skip('exception', '__special_doc__', + SampleError.__special_doc__, False, + 'napoleon_include_special_with_doc') + + def test_exception_special_undoc(self): + self.assert_skip('exception', '__special_undoc__', + SampleError.__special_undoc__, True, + 'napoleon_include_special_with_doc') + + def test_module_private_doc(self): + self.assert_skip('module', '_private_doc', _private_doc, False, + 'napoleon_include_private_with_doc') + + def test_module_private_undoc(self): + self.assert_skip('module', '_private_undoc', _private_undoc, True, + 'napoleon_include_private_with_doc') + + def test_module_special_doc(self): + self.assert_skip('module', '__special_doc__', __special_doc__, False, + 'napoleon_include_special_with_doc') + + def test_module_special_undoc(self): + self.assert_skip('module', '__special_undoc__', __special_undoc__, True, + 'napoleon_include_special_with_doc') diff --git a/tests/test_extensions/test_ext_napoleon_docstring.py b/tests/test_extensions/test_ext_napoleon_docstring.py new file mode 100644 index 0000000..d7ef489 --- /dev/null +++ b/tests/test_extensions/test_ext_napoleon_docstring.py @@ -0,0 +1,2703 @@ +"""Tests for :mod:`sphinx.ext.napoleon.docstring` module.""" + +import re +import zlib +from collections import namedtuple +from inspect import cleandoc +from itertools import product +from textwrap import dedent +from unittest import mock + +import pytest + +from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping +from sphinx.ext.napoleon import Config +from sphinx.ext.napoleon.docstring import ( + GoogleDocstring, + NumpyDocstring, + _convert_numpy_type_spec, + _recombine_set_tokens, + _token_type, + _tokenize_type_spec, +) +from sphinx.testing.util import etree_parse + +from tests.test_extensions.ext_napoleon_pep526_data_google import PEP526GoogleClass +from tests.test_extensions.ext_napoleon_pep526_data_numpy import PEP526NumpyClass + + +class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): + """Sample namedtuple subclass + + Attributes + ---------- + attr1 : Arbitrary type + Quick description of attr1 + attr2 : Another arbitrary type + Quick description of attr2 + attr3 : Type + + Adds a newline after the type + + """ + + # To avoid creating a dict, as a namedtuple doesn't have it: + __slots__ = () + + def __new__(cls, attr1, attr2=None): + return super().__new__(cls, attr1, attr2) + + +class TestNamedtupleSubclass: + def test_attributes_docstring(self): + config = Config() + actual = str(NumpyDocstring(cleandoc(NamedtupleSubclass.__doc__), + config=config, app=None, what='class', + name='NamedtupleSubclass', obj=NamedtupleSubclass)) + expected = """\ +Sample namedtuple subclass + +.. attribute:: attr1 + + Quick description of attr1 + + :type: Arbitrary type + +.. attribute:: attr2 + + Quick description of attr2 + + :type: Another arbitrary type + +.. attribute:: attr3 + + Adds a newline after the type + + :type: Type +""" + + assert expected == actual + + +class TestInlineAttribute: + inline_google_docstring = ('inline description with ' + '``a : in code``, ' + 'a :ref:`reference`, ' + 'a `link <https://foo.bar>`_, ' + 'a :meta public:, ' + 'a :meta field: value and ' + 'an host:port and HH:MM strings.') + + @staticmethod + def _docstring(source): + rst = GoogleDocstring(source, config=Config(), app=None, what='attribute', name='some_data', obj=0) + return str(rst) + + def test_class_data_member(self): + source = 'data member description:\n\n- a: b' + actual = self._docstring(source).splitlines() + assert actual == ['data member description:', '', '- a: b'] + + def test_class_data_member_inline(self): + source = f'CustomType: {self.inline_google_docstring}' + actual = self._docstring(source).splitlines() + assert actual == [self.inline_google_docstring, '', ':type: CustomType'] + + def test_class_data_member_inline_no_type(self): + source = self.inline_google_docstring + actual = self._docstring(source).splitlines() + assert actual == [source] + + def test_class_data_member_inline_ref_in_type(self): + source = f':class:`int`: {self.inline_google_docstring}' + actual = self._docstring(source).splitlines() + assert actual == [self.inline_google_docstring, '', ':type: :class:`int`'] + + +class TestGoogleDocstring: + docstrings = [( + """Single line summary""", + """Single line summary""", + ), ( + """ + Single line summary + + Extended description + + """, + """ + Single line summary + + Extended description + """, + ), ( + """ + Single line summary + + Args: + arg1(str):Extended + description of arg1 + """, + """ + Single line summary + + :Parameters: **arg1** (*str*) -- Extended + description of arg1 + """, + ), ( + """ + Single line summary + + Args: + arg1(str):Extended + description of arg1 + arg2 ( int ) : Extended + description of arg2 + + Keyword Args: + kwarg1(str):Extended + description of kwarg1 + kwarg2 ( int ) : Extended + description of kwarg2""", + """ + Single line summary + + :Parameters: * **arg1** (*str*) -- Extended + description of arg1 + * **arg2** (*int*) -- Extended + description of arg2 + + :Keyword Arguments: * **kwarg1** (*str*) -- Extended + description of kwarg1 + * **kwarg2** (*int*) -- Extended + description of kwarg2 + """, + ), ( + """ + Single line summary + + Arguments: + arg1(str):Extended + description of arg1 + arg2 ( int ) : Extended + description of arg2 + + Keyword Arguments: + kwarg1(str):Extended + description of kwarg1 + kwarg2 ( int ) : Extended + description of kwarg2""", + """ + Single line summary + + :Parameters: * **arg1** (*str*) -- Extended + description of arg1 + * **arg2** (*int*) -- Extended + description of arg2 + + :Keyword Arguments: * **kwarg1** (*str*) -- Extended + description of kwarg1 + * **kwarg2** (*int*) -- Extended + description of kwarg2 + """, + ), ( + """ + Single line summary + + Return: + str:Extended + description of return value + """, + """ + Single line summary + + :returns: *str* -- Extended + description of return value + """, + ), ( + """ + Single line summary + + Returns: + str:Extended + description of return value + """, + """ + Single line summary + + :returns: *str* -- Extended + description of return value + """, + ), ( + """ + Single line summary + + Returns: + Extended + description of return value + """, + """ + Single line summary + + :returns: Extended + description of return value + """, + ), ( + """ + Single line summary + + Returns: + Extended + """, + """ + Single line summary + + :returns: Extended + """, + ), ( + """ + Single line summary + + Args: + arg1(str):Extended + description of arg1 + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + """, + """ + Single line summary + + :Parameters: * **arg1** (*str*) -- Extended + description of arg1 + * **\\*args** -- Variable length argument list. + * **\\*\\*kwargs** -- Arbitrary keyword arguments. + """, + ), ( + """ + Single line summary + + Args: + arg1 (list(int)): Description + arg2 (list[int]): Description + arg3 (dict(str, int)): Description + arg4 (dict[str, int]): Description + """, + """ + Single line summary + + :Parameters: * **arg1** (*list(int)*) -- Description + * **arg2** (*list[int]*) -- Description + * **arg3** (*dict(str, int)*) -- Description + * **arg4** (*dict[str, int]*) -- Description + """, + ), ( + """ + Single line summary + + Receive: + arg1 (list(int)): Description + arg2 (list[int]): Description + """, + """ + Single line summary + + :Receives: * **arg1** (*list(int)*) -- Description + * **arg2** (*list[int]*) -- Description + """, + ), ( + """ + Single line summary + + Receives: + arg1 (list(int)): Description + arg2 (list[int]): Description + """, + """ + Single line summary + + :Receives: * **arg1** (*list(int)*) -- Description + * **arg2** (*list[int]*) -- Description + """, + ), ( + """ + Single line summary + + Yield: + str:Extended + description of yielded value + """, + """ + Single line summary + + :Yields: *str* -- Extended + description of yielded value + """, + ), ( + """ + Single line summary + + Yields: + Extended + description of yielded value + """, + """ + Single line summary + + :Yields: Extended + description of yielded value + """, + ), ( + """ + Single line summary + + Args: + + arg1 (list of str): Extended + description of arg1. + arg2 (tuple of int): Extended + description of arg2. + arg3 (tuple of list of float): Extended + description of arg3. + arg4 (int, float, or list of bool): Extended + description of arg4. + arg5 (list of int, float, or bool): Extended + description of arg5. + arg6 (list of int or float): Extended + description of arg6. + """, + """ + Single line summary + + :Parameters: * **arg1** (*list of str*) -- Extended + description of arg1. + * **arg2** (*tuple of int*) -- Extended + description of arg2. + * **arg3** (*tuple of list of float*) -- Extended + description of arg3. + * **arg4** (*int, float, or list of bool*) -- Extended + description of arg4. + * **arg5** (*list of int, float, or bool*) -- Extended + description of arg5. + * **arg6** (*list of int or float*) -- Extended + description of arg6. + """, + )] + + def test_sphinx_admonitions(self): + admonition_map = { + 'Attention': 'attention', + 'Caution': 'caution', + 'Danger': 'danger', + 'Error': 'error', + 'Hint': 'hint', + 'Important': 'important', + 'Note': 'note', + 'Tip': 'tip', + 'Todo': 'todo', + 'Warning': 'warning', + 'Warnings': 'warning', + } + config = Config() + for section, admonition in admonition_map.items(): + # Multiline + actual = str(GoogleDocstring(f"{section}:\n" + " this is the first line\n" + "\n" + " and this is the second line\n", + config)) + expect = (f".. {admonition}::\n" + "\n" + " this is the first line\n" + " \n" + " and this is the second line\n" + ) + assert expect == actual + + # Single line + actual = str(GoogleDocstring(f"{section}:\n" + " this is a single line\n", + config)) + expect = f".. {admonition}:: this is a single line\n" + assert expect == actual + + def test_docstrings(self): + config = Config( + napoleon_use_param=False, + napoleon_use_rtype=False, + napoleon_use_keyword=False, + ) + for docstring, expected in self.docstrings: + actual = str(GoogleDocstring(dedent(docstring), config)) + expected = dedent(expected) + assert expected == actual + + def test_parameters_with_class_reference(self): + docstring = """\ +Construct a new XBlock. + +This class should only be used by runtimes. + +Arguments: + runtime (:class:`~typing.Dict`\\[:class:`int`,:class:`str`\\]): Use it to + access the environment. It is available in XBlock code + as ``self.runtime``. + + field_data (:class:`FieldData`): Interface used by the XBlock + fields to access their data from wherever it is persisted. + + scope_ids (:class:`ScopeIds`): Identifiers needed to resolve scopes. + +""" + + actual = str(GoogleDocstring(docstring)) + expected = """\ +Construct a new XBlock. + +This class should only be used by runtimes. + +:param runtime: Use it to + access the environment. It is available in XBlock code + as ``self.runtime``. +:type runtime: :class:`~typing.Dict`\\[:class:`int`,:class:`str`\\] +:param field_data: Interface used by the XBlock + fields to access their data from wherever it is persisted. +:type field_data: :class:`FieldData` +:param scope_ids: Identifiers needed to resolve scopes. +:type scope_ids: :class:`ScopeIds` +""" + assert expected == actual + + def test_attributes_with_class_reference(self): + docstring = """\ +Attributes: + in_attr(:class:`numpy.ndarray`): super-dooper attribute +""" + + actual = str(GoogleDocstring(docstring)) + expected = """\ +.. attribute:: in_attr + + super-dooper attribute + + :type: :class:`numpy.ndarray` +""" + assert expected == actual + + docstring = """\ +Attributes: + in_attr(numpy.ndarray): super-dooper attribute +""" + + actual = str(GoogleDocstring(docstring)) + expected = """\ +.. attribute:: in_attr + + super-dooper attribute + + :type: numpy.ndarray +""" + + def test_attributes_with_use_ivar(self): + docstring = """\ +Attributes: + foo (int): blah blah + bar (str): blah blah +""" + + config = Config(napoleon_use_ivar=True) + actual = str(GoogleDocstring(docstring, config, obj=self.__class__)) + expected = """\ +:ivar foo: blah blah +:vartype foo: int +:ivar bar: blah blah +:vartype bar: str +""" + assert expected == actual + + def test_code_block_in_returns_section(self): + docstring = """ +Returns: + foobar: foo:: + + codecode + codecode +""" + expected = """ +:returns: + + foo:: + + codecode + codecode +:rtype: foobar +""" + actual = str(GoogleDocstring(docstring)) + assert expected == actual + + def test_colon_in_return_type(self): + docstring = """Example property. + +Returns: + :py:class:`~.module.submodule.SomeClass`: an example instance + if available, None if not available. +""" + expected = """Example property. + +:returns: an example instance + if available, None if not available. +:rtype: :py:class:`~.module.submodule.SomeClass` +""" + actual = str(GoogleDocstring(docstring)) + assert expected == actual + + def test_xrefs_in_return_type(self): + docstring = """Example Function + +Returns: + :class:`numpy.ndarray`: A :math:`n \\times 2` array containing + a bunch of math items +""" + expected = """Example Function + +:returns: A :math:`n \\times 2` array containing + a bunch of math items +:rtype: :class:`numpy.ndarray` +""" + actual = str(GoogleDocstring(docstring)) + assert expected == actual + + def test_raises_types(self): + docstrings = [(""" +Example Function + +Raises: + RuntimeError: + A setting wasn't specified, or was invalid. + ValueError: + Something something value error. + :py:class:`AttributeError` + errors for missing attributes. + ~InvalidDimensionsError + If the dimensions couldn't be parsed. + `InvalidArgumentsError` + If the arguments are invalid. + :exc:`~ValueError` + If the arguments are wrong. + +""", """ +Example Function + +:raises RuntimeError: A setting wasn't specified, or was invalid. +:raises ValueError: Something something value error. +:raises AttributeError: errors for missing attributes. +:raises ~InvalidDimensionsError: If the dimensions couldn't be parsed. +:raises InvalidArgumentsError: If the arguments are invalid. +:raises ~ValueError: If the arguments are wrong. +"""), + ################################ + (""" +Example Function + +Raises: + InvalidDimensionsError + +""", """ +Example Function + +:raises InvalidDimensionsError: +"""), + ################################ + (""" +Example Function + +Raises: + Invalid Dimensions Error + +""", """ +Example Function + +:raises Invalid Dimensions Error: +"""), + ################################ + (""" +Example Function + +Raises: + Invalid Dimensions Error: With description + +""", """ +Example Function + +:raises Invalid Dimensions Error: With description +"""), + ################################ + (""" +Example Function + +Raises: + InvalidDimensionsError: If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises InvalidDimensionsError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises: + Invalid Dimensions Error: If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises Invalid Dimensions Error: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises: + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises If the dimensions couldn't be parsed.: +"""), + ################################ + (""" +Example Function + +Raises: + :class:`exc.InvalidDimensionsError` + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: +"""), + ################################ + (""" +Example Function + +Raises: + :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises: + :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed, + then a :class:`exc.InvalidDimensionsError` will be raised. + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed, + then a :class:`exc.InvalidDimensionsError` will be raised. +"""), + ################################ + (""" +Example Function + +Raises: + :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed. + :class:`exc.InvalidArgumentsError`: If the arguments are invalid. + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. +:raises exc.InvalidArgumentsError: If the arguments are invalid. +"""), + ################################ + (""" +Example Function + +Raises: + :class:`exc.InvalidDimensionsError` + :class:`exc.InvalidArgumentsError` + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: +:raises exc.InvalidArgumentsError: +""")] + for docstring, expected in docstrings: + actual = str(GoogleDocstring(docstring)) + assert expected == actual + + def test_kwargs_in_arguments(self): + docstring = """Allows to create attributes binded to this device. + +Some other paragraph. + +Code sample for usage:: + + dev.bind(loopback=Loopback) + dev.loopback.configure() + +Arguments: + **kwargs: name/class pairs that will create resource-managers + bound as instance attributes to this instance. See code + example above. +""" + expected = """Allows to create attributes binded to this device. + +Some other paragraph. + +Code sample for usage:: + + dev.bind(loopback=Loopback) + dev.loopback.configure() + +:param \\*\\*kwargs: name/class pairs that will create resource-managers + bound as instance attributes to this instance. See code + example above. +""" + actual = str(GoogleDocstring(docstring)) + assert expected == actual + + def test_section_header_formatting(self): + docstrings = [(""" +Summary line + +Example: + Multiline reStructuredText + literal code block + +""", """ +Summary line + +.. rubric:: Example + +Multiline reStructuredText +literal code block +"""), + ################################ + (""" +Summary line + +Example:: + + Multiline reStructuredText + literal code block + +""", """ +Summary line + +Example:: + + Multiline reStructuredText + literal code block +"""), + ################################ + (""" +Summary line + +:Example: + + Multiline reStructuredText + literal code block + +""", """ +Summary line + +:Example: + + Multiline reStructuredText + literal code block +""")] + for docstring, expected in docstrings: + actual = str(GoogleDocstring(docstring)) + assert expected == actual + + def test_list_in_parameter_description(self): + docstring = """One line summary. + +Parameters: + no_list (int): + one_bullet_empty (int): + * + one_bullet_single_line (int): + - first line + one_bullet_two_lines (int): + + first line + continued + two_bullets_single_line (int): + - first line + - second line + two_bullets_two_lines (int): + * first line + continued + * second line + continued + one_enumeration_single_line (int): + 1. first line + one_enumeration_two_lines (int): + 1) first line + continued + two_enumerations_one_line (int): + (iii) first line + (iv) second line + two_enumerations_two_lines (int): + a. first line + continued + b. second line + continued + one_definition_one_line (int): + item 1 + first line + one_definition_two_lines (int): + item 1 + first line + continued + two_definitions_one_line (int): + item 1 + first line + item 2 + second line + two_definitions_two_lines (int): + item 1 + first line + continued + item 2 + second line + continued + one_definition_blank_line (int): + item 1 + + first line + + extra first line + + two_definitions_blank_lines (int): + item 1 + + first line + + extra first line + + item 2 + + second line + + extra second line + + definition_after_inline_text (int): text line + + item 1 + first line + + definition_after_normal_text (int): + text line + + item 1 + first line +""" + + expected = """One line summary. + +:param no_list: +:type no_list: int +:param one_bullet_empty: + * +:type one_bullet_empty: int +:param one_bullet_single_line: + - first line +:type one_bullet_single_line: int +:param one_bullet_two_lines: + + first line + continued +:type one_bullet_two_lines: int +:param two_bullets_single_line: + - first line + - second line +:type two_bullets_single_line: int +:param two_bullets_two_lines: + * first line + continued + * second line + continued +:type two_bullets_two_lines: int +:param one_enumeration_single_line: + 1. first line +:type one_enumeration_single_line: int +:param one_enumeration_two_lines: + 1) first line + continued +:type one_enumeration_two_lines: int +:param two_enumerations_one_line: + (iii) first line + (iv) second line +:type two_enumerations_one_line: int +:param two_enumerations_two_lines: + a. first line + continued + b. second line + continued +:type two_enumerations_two_lines: int +:param one_definition_one_line: + item 1 + first line +:type one_definition_one_line: int +:param one_definition_two_lines: + item 1 + first line + continued +:type one_definition_two_lines: int +:param two_definitions_one_line: + item 1 + first line + item 2 + second line +:type two_definitions_one_line: int +:param two_definitions_two_lines: + item 1 + first line + continued + item 2 + second line + continued +:type two_definitions_two_lines: int +:param one_definition_blank_line: + item 1 + + first line + + extra first line +:type one_definition_blank_line: int +:param two_definitions_blank_lines: + item 1 + + first line + + extra first line + + item 2 + + second line + + extra second line +:type two_definitions_blank_lines: int +:param definition_after_inline_text: text line + + item 1 + first line +:type definition_after_inline_text: int +:param definition_after_normal_text: text line + + item 1 + first line +:type definition_after_normal_text: int +""" + config = Config(napoleon_use_param=True) + actual = str(GoogleDocstring(docstring, config)) + assert expected == actual + + expected = """One line summary. + +:Parameters: * **no_list** (*int*) + * **one_bullet_empty** (*int*) -- + + * + * **one_bullet_single_line** (*int*) -- + + - first line + * **one_bullet_two_lines** (*int*) -- + + + first line + continued + * **two_bullets_single_line** (*int*) -- + + - first line + - second line + * **two_bullets_two_lines** (*int*) -- + + * first line + continued + * second line + continued + * **one_enumeration_single_line** (*int*) -- + + 1. first line + * **one_enumeration_two_lines** (*int*) -- + + 1) first line + continued + * **two_enumerations_one_line** (*int*) -- + + (iii) first line + (iv) second line + * **two_enumerations_two_lines** (*int*) -- + + a. first line + continued + b. second line + continued + * **one_definition_one_line** (*int*) -- + + item 1 + first line + * **one_definition_two_lines** (*int*) -- + + item 1 + first line + continued + * **two_definitions_one_line** (*int*) -- + + item 1 + first line + item 2 + second line + * **two_definitions_two_lines** (*int*) -- + + item 1 + first line + continued + item 2 + second line + continued + * **one_definition_blank_line** (*int*) -- + + item 1 + + first line + + extra first line + * **two_definitions_blank_lines** (*int*) -- + + item 1 + + first line + + extra first line + + item 2 + + second line + + extra second line + * **definition_after_inline_text** (*int*) -- text line + + item 1 + first line + * **definition_after_normal_text** (*int*) -- text line + + item 1 + first line +""" + config = Config(napoleon_use_param=False) + actual = str(GoogleDocstring(docstring, config)) + assert expected == actual + + def test_custom_generic_sections(self): + + docstrings = (("""\ +Really Important Details: + You should listen to me! +""", """.. rubric:: Really Important Details + +You should listen to me! +"""), + ("""\ +Sooper Warning: + Stop hitting yourself! +""", """:Warns: **Stop hitting yourself!** +"""), + ("""\ +Params Style: + arg1 (int): Description of arg1 + arg2 (str): Description of arg2 + +""", """\ +:Params Style: * **arg1** (*int*) -- Description of arg1 + * **arg2** (*str*) -- Description of arg2 +"""), + ("""\ +Returns Style: + description of custom section + +""", """:Returns Style: description of custom section +""")) + + testConfig = Config(napoleon_custom_sections=['Really Important Details', + ('Sooper Warning', 'warns'), + ('Params Style', 'params_style'), + ('Returns Style', 'returns_style')]) + + for docstring, expected in docstrings: + actual = str(GoogleDocstring(docstring, testConfig)) + assert expected == actual + + def test_noindex(self): + docstring = """ +Attributes: + arg + description + +Methods: + func(i, j) + description +""" + + expected = """ +.. attribute:: arg + :no-index: + + description + +.. method:: func(i, j) + :no-index: + + + description +""" # NoQA: W293 + config = Config() + actual = str(GoogleDocstring(docstring, config=config, app=None, what='module', + options={'no-index': True})) + assert expected == actual + + def test_keywords_with_types(self): + docstring = """\ +Do as you please + +Keyword Args: + gotham_is_yours (None): shall interfere. +""" + actual = str(GoogleDocstring(docstring)) + expected = """\ +Do as you please + +:keyword gotham_is_yours: shall interfere. +:kwtype gotham_is_yours: None +""" + assert expected == actual + + def test_pep526_annotations(self): + # Test class attributes annotations + config = Config( + napoleon_attr_annotations=True, + ) + actual = str(GoogleDocstring(cleandoc(PEP526GoogleClass.__doc__), config, app=None, what="class", + obj=PEP526GoogleClass)) + expected = """\ +Sample class with PEP 526 annotations and google docstring. + +.. attribute:: attr1 + + Attr1 description. + + :type: int + +.. attribute:: attr2 + + Attr2 description. + + :type: str +""" + assert expected == actual + + def test_preprocess_types(self): + docstring = """\ +Do as you please + +Yield: + str:Extended +""" + actual = str(GoogleDocstring(docstring)) + expected = """\ +Do as you please + +:Yields: *str* -- Extended +""" + assert expected == actual + + config = Config(napoleon_preprocess_types=True) + actual = str(GoogleDocstring(docstring, config)) + expected = """\ +Do as you please + +:Yields: :py:class:`str` -- Extended +""" + assert expected == actual + + +class TestNumpyDocstring: + docstrings = [( + """Single line summary""", + """Single line summary""", + ), ( + """ + Single line summary + + Extended description + + """, + """ + Single line summary + + Extended description + """, + ), ( + """ + Single line summary + + Parameters + ---------- + arg1:str + Extended + description of arg1 + """, + """ + Single line summary + + :Parameters: **arg1** (:class:`str`) -- Extended + description of arg1 + """, + ), ( + """ + Single line summary + + Parameters + ---------- + arg1:str + Extended + description of arg1 + arg2 : int + Extended + description of arg2 + + Keyword Arguments + ----------------- + kwarg1:str + Extended + description of kwarg1 + kwarg2 : int + Extended + description of kwarg2 + """, + """ + Single line summary + + :Parameters: * **arg1** (:class:`str`) -- Extended + description of arg1 + * **arg2** (:class:`int`) -- Extended + description of arg2 + + :Keyword Arguments: * **kwarg1** (:class:`str`) -- Extended + description of kwarg1 + * **kwarg2** (:class:`int`) -- Extended + description of kwarg2 + """, + ), ( + """ + Single line summary + + Return + ------ + str + Extended + description of return value + """, + """ + Single line summary + + :returns: :class:`str` -- Extended + description of return value + """, + ), ( + """ + Single line summary + + Returns + ------- + str + Extended + description of return value + """, + """ + Single line summary + + :returns: :class:`str` -- Extended + description of return value + """, + ), ( + """ + Single line summary + + Parameters + ---------- + arg1:str + Extended description of arg1 + *args: + Variable length argument list. + **kwargs: + Arbitrary keyword arguments. + """, + """ + Single line summary + + :Parameters: * **arg1** (:class:`str`) -- Extended description of arg1 + * **\\*args** -- Variable length argument list. + * **\\*\\*kwargs** -- Arbitrary keyword arguments. + """, + ), ( + """ + Single line summary + + Parameters + ---------- + arg1:str + Extended description of arg1 + *args, **kwargs: + Variable length argument list and arbitrary keyword arguments. + """, + """ + Single line summary + + :Parameters: * **arg1** (:class:`str`) -- Extended description of arg1 + * **\\*args, \\*\\*kwargs** -- Variable length argument list and arbitrary keyword arguments. + """, + ), ( + """ + Single line summary + + Receive + ------- + arg1:str + Extended + description of arg1 + arg2 : int + Extended + description of arg2 + """, + """ + Single line summary + + :Receives: * **arg1** (:class:`str`) -- Extended + description of arg1 + * **arg2** (:class:`int`) -- Extended + description of arg2 + """, + ), ( + """ + Single line summary + + Receives + -------- + arg1:str + Extended + description of arg1 + arg2 : int + Extended + description of arg2 + """, + """ + Single line summary + + :Receives: * **arg1** (:class:`str`) -- Extended + description of arg1 + * **arg2** (:class:`int`) -- Extended + description of arg2 + """, + ), ( + """ + Single line summary + + Yield + ----- + str + Extended + description of yielded value + """, + """ + Single line summary + + :Yields: :class:`str` -- Extended + description of yielded value + """, + ), ( + """ + Single line summary + + Yields + ------ + str + Extended + description of yielded value + """, + """ + Single line summary + + :Yields: :class:`str` -- Extended + description of yielded value + """, + )] + + def test_sphinx_admonitions(self): + admonition_map = { + 'Attention': 'attention', + 'Caution': 'caution', + 'Danger': 'danger', + 'Error': 'error', + 'Hint': 'hint', + 'Important': 'important', + 'Note': 'note', + 'Tip': 'tip', + 'Todo': 'todo', + 'Warning': 'warning', + 'Warnings': 'warning', + } + config = Config() + for section, admonition in admonition_map.items(): + # Multiline + actual = str(NumpyDocstring(f"{section}\n" + f"{'-' * len(section)}\n" + " this is the first line\n" + "\n" + " and this is the second line\n", + config)) + expect = (f".. {admonition}::\n" + "\n" + " this is the first line\n" + " \n" + " and this is the second line\n" + ) + assert expect == actual + + # Single line + actual = str(NumpyDocstring(f"{section}\n" + f"{'-' * len(section)}\n" + f" this is a single line\n", + config)) + expect = f".. {admonition}:: this is a single line\n" + assert expect == actual + + def test_docstrings(self): + config = Config( + napoleon_use_param=False, + napoleon_use_rtype=False, + napoleon_use_keyword=False, + napoleon_preprocess_types=True) + for docstring, expected in self.docstrings: + actual = str(NumpyDocstring(dedent(docstring), config)) + expected = dedent(expected) + assert expected == actual + + def test_type_preprocessor(self): + docstring = dedent(""" + Single line summary + + Parameters + ---------- + arg1:str + Extended + description of arg1 + """) + + config = Config(napoleon_preprocess_types=False, napoleon_use_param=False) + actual = str(NumpyDocstring(docstring, config)) + expected = dedent(""" + Single line summary + + :Parameters: **arg1** (*str*) -- Extended + description of arg1 + """) + assert expected == actual + + def test_parameters_with_class_reference(self): + docstring = """\ +Parameters +---------- +param1 : :class:`MyClass <name.space.MyClass>` instance + +Other Parameters +---------------- +param2 : :class:`MyClass <name.space.MyClass>` instance + +""" + + config = Config(napoleon_use_param=False) + actual = str(NumpyDocstring(docstring, config)) + expected = """\ +:Parameters: **param1** (:class:`MyClass <name.space.MyClass>` instance) + +:Other Parameters: **param2** (:class:`MyClass <name.space.MyClass>` instance) +""" + assert expected == actual + + config = Config(napoleon_use_param=True) + actual = str(NumpyDocstring(docstring, config)) + expected = """\ +:param param1: +:type param1: :class:`MyClass <name.space.MyClass>` instance + +:param param2: +:type param2: :class:`MyClass <name.space.MyClass>` instance +""" + assert expected == actual + + def test_multiple_parameters(self): + docstring = """\ +Parameters +---------- +x1, x2 : array_like + Input arrays, description of ``x1``, ``x2``. + +""" + + config = Config(napoleon_use_param=False) + actual = str(NumpyDocstring(docstring, config)) + expected = """\ +:Parameters: **x1, x2** (*array_like*) -- Input arrays, description of ``x1``, ``x2``. +""" + assert expected == actual + + config = Config(napoleon_use_param=True) + actual = str(NumpyDocstring(dedent(docstring), config)) + expected = """\ +:param x1: Input arrays, description of ``x1``, ``x2``. +:type x1: array_like +:param x2: Input arrays, description of ``x1``, ``x2``. +:type x2: array_like +""" + assert expected == actual + + def test_parameters_without_class_reference(self): + docstring = """\ +Parameters +---------- +param1 : MyClass instance + +""" + + config = Config(napoleon_use_param=False) + actual = str(NumpyDocstring(docstring, config)) + expected = """\ +:Parameters: **param1** (*MyClass instance*) +""" + assert expected == actual + + config = Config(napoleon_use_param=True) + actual = str(NumpyDocstring(dedent(docstring), config)) + expected = """\ +:param param1: +:type param1: MyClass instance +""" + assert expected == actual + + def test_see_also_refs(self): + docstring = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +See Also +-------- +some, other, funcs +otherfunc : relationship + +""" + + actual = str(NumpyDocstring(docstring)) + + expected = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +.. seealso:: + + :obj:`some`, :obj:`other`, :obj:`funcs` + \n\ + :obj:`otherfunc` + relationship +""" + assert expected == actual + + docstring = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +See Also +-------- +some, other, funcs +otherfunc : relationship + +""" + + config = Config() + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + + expected = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +.. seealso:: + + :obj:`some`, :obj:`other`, :obj:`funcs` + \n\ + :obj:`otherfunc` + relationship +""" + assert expected == actual + + docstring = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +See Also +-------- +some, other, :func:`funcs` +otherfunc : relationship + +""" + translations = { + "other": "MyClass.other", + "otherfunc": ":func:`~my_package.otherfunc`", + } + config = Config(napoleon_type_aliases=translations) + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + + expected = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +.. seealso:: + + :obj:`some`, :obj:`MyClass.other`, :func:`funcs` + \n\ + :func:`~my_package.otherfunc` + relationship +""" + assert expected == actual + + def test_colon_in_return_type(self): + docstring = """ +Summary + +Returns +------- +:py:class:`~my_mod.my_class` + an instance of :py:class:`~my_mod.my_class` +""" + + expected = """ +Summary + +:returns: an instance of :py:class:`~my_mod.my_class` +:rtype: :py:class:`~my_mod.my_class` +""" + + config = Config() + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + + assert expected == actual + + def test_underscore_in_attribute(self): + docstring = """ +Attributes +---------- + +arg_ : type + some description +""" + + expected = """ +:ivar arg_: some description +:vartype arg_: type +""" + + config = Config(napoleon_use_ivar=True) + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "class")) + + assert expected == actual + + def test_underscore_in_attribute_strip_signature_backslash(self): + docstring = """ +Attributes +---------- + +arg_ : type + some description +""" + + expected = """ +:ivar arg\\_: some description +:vartype arg\\_: type +""" + + config = Config(napoleon_use_ivar=True) + config.strip_signature_backslash = True + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "class")) + + assert expected == actual + + def test_return_types(self): + docstring = dedent(""" + Returns + ------- + DataFrame + a dataframe + """) + expected = dedent(""" + :returns: a dataframe + :rtype: :class:`~pandas.DataFrame` + """) + translations = { + "DataFrame": "~pandas.DataFrame", + } + config = Config( + napoleon_use_param=True, + napoleon_use_rtype=True, + napoleon_preprocess_types=True, + napoleon_type_aliases=translations, + ) + actual = str(NumpyDocstring(docstring, config)) + assert expected == actual + + def test_yield_types(self): + docstring = dedent(""" + Example Function + + Yields + ------ + scalar or array-like + The result of the computation + """) + expected = dedent(""" + Example Function + + :Yields: :term:`scalar` or :class:`array-like <numpy.ndarray>` -- The result of the computation + """) + translations = { + "scalar": ":term:`scalar`", + "array-like": ":class:`array-like <numpy.ndarray>`", + } + config = Config(napoleon_type_aliases=translations, napoleon_preprocess_types=True) + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + assert expected == actual + + def test_raises_types(self): + docstrings = [(""" +Example Function + +Raises +------ + RuntimeError + + A setting wasn't specified, or was invalid. + ValueError + + Something something value error. + +""", """ +Example Function + +:raises RuntimeError: A setting wasn't specified, or was invalid. +:raises ValueError: Something something value error. +"""), + ################################ + (""" +Example Function + +Raises +------ +InvalidDimensionsError + +""", """ +Example Function + +:raises InvalidDimensionsError: +"""), + ################################ + (""" +Example Function + +Raises +------ +Invalid Dimensions Error + +""", """ +Example Function + +:raises Invalid Dimensions Error: +"""), + ################################ + (""" +Example Function + +Raises +------ +Invalid Dimensions Error + With description + +""", """ +Example Function + +:raises Invalid Dimensions Error: With description +"""), + ################################ + (""" +Example Function + +Raises +------ +InvalidDimensionsError + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises InvalidDimensionsError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises +------ +Invalid Dimensions Error + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises Invalid Dimensions Error: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises +------ +If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises If the dimensions couldn't be parsed.: +"""), + ################################ + (""" +Example Function + +Raises +------ +:class:`exc.InvalidDimensionsError` + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: +"""), + ################################ + (""" +Example Function + +Raises +------ +:class:`exc.InvalidDimensionsError` + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises +------ +:class:`exc.InvalidDimensionsError` + If the dimensions couldn't be parsed, + then a :class:`exc.InvalidDimensionsError` will be raised. + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed, + then a :class:`exc.InvalidDimensionsError` will be raised. +"""), + ################################ + (""" +Example Function + +Raises +------ +:class:`exc.InvalidDimensionsError` + If the dimensions couldn't be parsed. +:class:`exc.InvalidArgumentsError` + If the arguments are invalid. + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. +:raises exc.InvalidArgumentsError: If the arguments are invalid. +"""), + ################################ + (""" +Example Function + +Raises +------ +CustomError + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises package.CustomError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises +------ +AnotherError + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises ~package.AnotherError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises +------ +:class:`exc.InvalidDimensionsError` +:class:`exc.InvalidArgumentsError` + +""", """ +Example Function + +:raises exc.InvalidDimensionsError: +:raises exc.InvalidArgumentsError: +""")] + for docstring, expected in docstrings: + translations = { + "CustomError": "package.CustomError", + "AnotherError": ":py:exc:`~package.AnotherError`", + } + config = Config(napoleon_type_aliases=translations, napoleon_preprocess_types=True) + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + assert expected == actual + + def test_xrefs_in_return_type(self): + docstring = """ +Example Function + +Returns +------- +:class:`numpy.ndarray` + A :math:`n \\times 2` array containing + a bunch of math items +""" + expected = """ +Example Function + +:returns: A :math:`n \\times 2` array containing + a bunch of math items +:rtype: :class:`numpy.ndarray` +""" + config = Config() + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + assert expected == actual + + def test_section_header_underline_length(self): + docstrings = [(""" +Summary line + +Example +- +Multiline example +body + +""", """ +Summary line + +Example +- +Multiline example +body +"""), + ################################ + (""" +Summary line + +Example +-- +Multiline example +body + +""", """ +Summary line + +.. rubric:: Example + +Multiline example +body +"""), + ################################ + (""" +Summary line + +Example +------- +Multiline example +body + +""", """ +Summary line + +.. rubric:: Example + +Multiline example +body +"""), + ################################ + (""" +Summary line + +Example +------------ +Multiline example +body + +""", """ +Summary line + +.. rubric:: Example + +Multiline example +body +""")] + for docstring, expected in docstrings: + actual = str(NumpyDocstring(docstring)) + assert expected == actual + + def test_list_in_parameter_description(self): + docstring = """One line summary. + +Parameters +---------- +no_list : int +one_bullet_empty : int + * +one_bullet_single_line : int + - first line +one_bullet_two_lines : int + + first line + continued +two_bullets_single_line : int + - first line + - second line +two_bullets_two_lines : int + * first line + continued + * second line + continued +one_enumeration_single_line : int + 1. first line +one_enumeration_two_lines : int + 1) first line + continued +two_enumerations_one_line : int + (iii) first line + (iv) second line +two_enumerations_two_lines : int + a. first line + continued + b. second line + continued +one_definition_one_line : int + item 1 + first line +one_definition_two_lines : int + item 1 + first line + continued +two_definitions_one_line : int + item 1 + first line + item 2 + second line +two_definitions_two_lines : int + item 1 + first line + continued + item 2 + second line + continued +one_definition_blank_line : int + item 1 + + first line + + extra first line + +two_definitions_blank_lines : int + item 1 + + first line + + extra first line + + item 2 + + second line + + extra second line + +definition_after_normal_text : int + text line + + item 1 + first line +""" + + expected = """One line summary. + +:param no_list: +:type no_list: int +:param one_bullet_empty: + * +:type one_bullet_empty: int +:param one_bullet_single_line: + - first line +:type one_bullet_single_line: int +:param one_bullet_two_lines: + + first line + continued +:type one_bullet_two_lines: int +:param two_bullets_single_line: + - first line + - second line +:type two_bullets_single_line: int +:param two_bullets_two_lines: + * first line + continued + * second line + continued +:type two_bullets_two_lines: int +:param one_enumeration_single_line: + 1. first line +:type one_enumeration_single_line: int +:param one_enumeration_two_lines: + 1) first line + continued +:type one_enumeration_two_lines: int +:param two_enumerations_one_line: + (iii) first line + (iv) second line +:type two_enumerations_one_line: int +:param two_enumerations_two_lines: + a. first line + continued + b. second line + continued +:type two_enumerations_two_lines: int +:param one_definition_one_line: + item 1 + first line +:type one_definition_one_line: int +:param one_definition_two_lines: + item 1 + first line + continued +:type one_definition_two_lines: int +:param two_definitions_one_line: + item 1 + first line + item 2 + second line +:type two_definitions_one_line: int +:param two_definitions_two_lines: + item 1 + first line + continued + item 2 + second line + continued +:type two_definitions_two_lines: int +:param one_definition_blank_line: + item 1 + + first line + + extra first line +:type one_definition_blank_line: int +:param two_definitions_blank_lines: + item 1 + + first line + + extra first line + + item 2 + + second line + + extra second line +:type two_definitions_blank_lines: int +:param definition_after_normal_text: text line + + item 1 + first line +:type definition_after_normal_text: int +""" + config = Config(napoleon_use_param=True) + actual = str(NumpyDocstring(docstring, config)) + assert expected == actual + + expected = """One line summary. + +:Parameters: * **no_list** (:class:`int`) + * **one_bullet_empty** (:class:`int`) -- + + * + * **one_bullet_single_line** (:class:`int`) -- + + - first line + * **one_bullet_two_lines** (:class:`int`) -- + + + first line + continued + * **two_bullets_single_line** (:class:`int`) -- + + - first line + - second line + * **two_bullets_two_lines** (:class:`int`) -- + + * first line + continued + * second line + continued + * **one_enumeration_single_line** (:class:`int`) -- + + 1. first line + * **one_enumeration_two_lines** (:class:`int`) -- + + 1) first line + continued + * **two_enumerations_one_line** (:class:`int`) -- + + (iii) first line + (iv) second line + * **two_enumerations_two_lines** (:class:`int`) -- + + a. first line + continued + b. second line + continued + * **one_definition_one_line** (:class:`int`) -- + + item 1 + first line + * **one_definition_two_lines** (:class:`int`) -- + + item 1 + first line + continued + * **two_definitions_one_line** (:class:`int`) -- + + item 1 + first line + item 2 + second line + * **two_definitions_two_lines** (:class:`int`) -- + + item 1 + first line + continued + item 2 + second line + continued + * **one_definition_blank_line** (:class:`int`) -- + + item 1 + + first line + + extra first line + * **two_definitions_blank_lines** (:class:`int`) -- + + item 1 + + first line + + extra first line + + item 2 + + second line + + extra second line + * **definition_after_normal_text** (:class:`int`) -- text line + + item 1 + first line +""" + config = Config(napoleon_use_param=False, napoleon_preprocess_types=True) + actual = str(NumpyDocstring(docstring, config)) + assert expected == actual + + def test_token_type(self): + tokens = ( + ("1", "literal"), + ("-4.6", "literal"), + ("2j", "literal"), + ("'string'", "literal"), + ('"another_string"', "literal"), + ("{1, 2}", "literal"), + ("{'va{ue', 'set'}", "literal"), + ("optional", "control"), + ("default", "control"), + (", ", "delimiter"), + (" of ", "delimiter"), + (" or ", "delimiter"), + (": ", "delimiter"), + ("True", "obj"), + ("None", "obj"), + ("name", "obj"), + (":py:class:`Enum`", "reference"), + ) + + for token, expected in tokens: + actual = _token_type(token) + assert expected == actual + + def test_tokenize_type_spec(self): + specs = ( + "str", + "defaultdict", + "int, float, or complex", + "int or float or None, optional", + "list of list of int or float, optional", + "tuple of list of str, float, or int", + '{"F", "C", "N"}', + "{'F', 'C', 'N'}, default: 'F'", + "{'F', 'C', 'N or C'}, default 'F'", + "str, default: 'F or C'", + "int, default: None", + "int, default None", + "int, default :obj:`None`", + '"ma{icious"', + r"'with \'quotes\''", + ) + + tokens = ( + ["str"], + ["defaultdict"], + ["int", ", ", "float", ", or ", "complex"], + ["int", " or ", "float", " or ", "None", ", ", "optional"], + ["list", " of ", "list", " of ", "int", " or ", "float", ", ", "optional"], + ["tuple", " of ", "list", " of ", "str", ", ", "float", ", or ", "int"], + ["{", '"F"', ", ", '"C"', ", ", '"N"', "}"], + ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "'F'"], + ["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"], + ["str", ", ", "default", ": ", "'F or C'"], + ["int", ", ", "default", ": ", "None"], + ["int", ", ", "default", " ", "None"], + ["int", ", ", "default", " ", ":obj:`None`"], + ['"ma{icious"'], + [r"'with \'quotes\''"], + ) + + for spec, expected in zip(specs, tokens): + actual = _tokenize_type_spec(spec) + assert expected == actual + + def test_recombine_set_tokens(self): + tokens = ( + ["{", "1", ", ", "2", "}"], + ["{", '"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], + ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "None"], + ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", " ", "None"], + ) + + combined_tokens = ( + ["{1, 2}"], + ['{"F", "C", "N"}', ", ", "optional"], + ["{'F', 'C', 'N'}", ", ", "default", ": ", "None"], + ["{'F', 'C', 'N'}", ", ", "default", " ", "None"], + ) + + for tokens_, expected in zip(tokens, combined_tokens): + actual = _recombine_set_tokens(tokens_) + assert expected == actual + + def test_recombine_set_tokens_invalid(self): + tokens = ( + ["{", "1", ", ", "2"], + ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], + ["{", "1", ", ", "2", ", ", "default", ": ", "None"], + ) + combined_tokens = ( + ["{1, 2"], + ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], + ["{1, 2", ", ", "default", ": ", "None"], + ) + + for tokens_, expected in zip(tokens, combined_tokens): + actual = _recombine_set_tokens(tokens_) + assert expected == actual + + def test_convert_numpy_type_spec(self): + translations = { + "DataFrame": "pandas.DataFrame", + } + + specs = ( + "", + "optional", + "str, optional", + "int or float or None, default: None", + "list of tuple of str, optional", + "int, default None", + '{"F", "C", "N"}', + "{'F', 'C', 'N'}, default: 'N'", + "{'F', 'C', 'N'}, default 'N'", + "DataFrame, optional", + ) + + converted = ( + "", + "*optional*", + ":class:`str`, *optional*", + ":class:`int` or :class:`float` or :obj:`None`, *default*: :obj:`None`", + ":class:`list` of :class:`tuple` of :class:`str`, *optional*", + ":class:`int`, *default* :obj:`None`", + '``{"F", "C", "N"}``', + "``{'F', 'C', 'N'}``, *default*: ``'N'``", + "``{'F', 'C', 'N'}``, *default* ``'N'``", + ":class:`pandas.DataFrame`, *optional*", + ) + + for spec, expected in zip(specs, converted): + actual = _convert_numpy_type_spec(spec, translations=translations) + assert expected == actual + + def test_parameter_types(self): + docstring = dedent("""\ + Parameters + ---------- + param1 : DataFrame + the data to work on + param2 : int or float or None, optional + a parameter with different types + param3 : dict-like, optional + a optional mapping + param4 : int or float or None, optional + a optional parameter with different types + param5 : {"F", "C", "N"}, optional + a optional parameter with fixed values + param6 : int, default None + different default format + param7 : mapping of hashable to str, optional + a optional mapping + param8 : ... or Ellipsis + ellipsis + param9 : tuple of list of int + a parameter with tuple of list of int + """) + expected = dedent("""\ + :param param1: the data to work on + :type param1: :class:`DataFrame` + :param param2: a parameter with different types + :type param2: :class:`int` or :class:`float` or :obj:`None`, *optional* + :param param3: a optional mapping + :type param3: :term:`dict-like <mapping>`, *optional* + :param param4: a optional parameter with different types + :type param4: :class:`int` or :class:`float` or :obj:`None`, *optional* + :param param5: a optional parameter with fixed values + :type param5: ``{"F", "C", "N"}``, *optional* + :param param6: different default format + :type param6: :class:`int`, *default* :obj:`None` + :param param7: a optional mapping + :type param7: :term:`mapping` of :term:`hashable` to :class:`str`, *optional* + :param param8: ellipsis + :type param8: :obj:`... <Ellipsis>` or :obj:`Ellipsis` + :param param9: a parameter with tuple of list of int + :type param9: :class:`tuple` of :class:`list` of :class:`int` + """) + translations = { + "dict-like": ":term:`dict-like <mapping>`", + "mapping": ":term:`mapping`", + "hashable": ":term:`hashable`", + } + config = Config( + napoleon_use_param=True, + napoleon_use_rtype=True, + napoleon_preprocess_types=True, + napoleon_type_aliases=translations, + ) + actual = str(NumpyDocstring(docstring, config)) + assert expected == actual + + def test_token_type_invalid(self, warning): + tokens = ( + "{1, 2", + "}", + "'abc", + "def'", + '"ghi', + 'jkl"', + ) + errors = ( + r".+: invalid value set \(missing closing brace\):", + r".+: invalid value set \(missing opening brace\):", + r".+: malformed string literal \(missing closing quote\):", + r".+: malformed string literal \(missing opening quote\):", + r".+: malformed string literal \(missing closing quote\):", + r".+: malformed string literal \(missing opening quote\):", + ) + for token, error in zip(tokens, errors): + try: + _token_type(token) + finally: + raw_warnings = warning.getvalue() + warnings = [w for w in raw_warnings.split("\n") if w.strip()] + + assert len(warnings) == 1 + assert re.compile(error).match(warnings[0]) + warning.truncate(0) + + @pytest.mark.parametrize( + ("name", "expected"), + [ + ("x, y, z", "x, y, z"), + ("*args, **kwargs", r"\*args, \*\*kwargs"), + ("*x, **y", r"\*x, \*\*y"), + ], + ) + def test_escape_args_and_kwargs(self, name, expected): + numpy_docstring = NumpyDocstring("") + actual = numpy_docstring._escape_args_and_kwargs(name) + + assert actual == expected + + def test_pep526_annotations(self): + # test class attributes annotations + config = Config( + napoleon_attr_annotations=True, + ) + actual = str(NumpyDocstring(cleandoc(PEP526NumpyClass.__doc__), config, app=None, what="class", + obj=PEP526NumpyClass)) + expected = """\ +Sample class with PEP 526 annotations and numpy docstring + +.. attribute:: attr1 + + Attr1 description + + :type: int + +.. attribute:: attr2 + + Attr2 description + + :type: str +""" + print(actual) + assert expected == actual + + +@pytest.mark.sphinx('text', testroot='ext-napoleon', + confoverrides={'autodoc_typehints': 'description', + 'autodoc_typehints_description_target': 'all'}) +def test_napoleon_and_autodoc_typehints_description_all(app, status, warning): + app.build() + content = (app.outdir / 'typehints.txt').read_text(encoding='utf-8') + assert content == ( + 'typehints\n' + '*********\n' + '\n' + 'mypackage.typehints.hello(x, *args, **kwargs)\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*) -- X\n' + '\n' + ' * ***args** (*int*) -- Additional arguments.\n' + '\n' + ' * ****kwargs** (*int*) -- Extra arguments.\n' + '\n' + ' Return type:\n' + ' None\n' + ) + + +@pytest.mark.sphinx('text', testroot='ext-napoleon', + confoverrides={'autodoc_typehints': 'description', + 'autodoc_typehints_description_target': 'documented_params'}) +def test_napoleon_and_autodoc_typehints_description_documented_params(app, status, warning): + app.build() + content = (app.outdir / 'typehints.txt').read_text(encoding='utf-8') + assert content == ( + 'typehints\n' + '*********\n' + '\n' + 'mypackage.typehints.hello(x, *args, **kwargs)\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*) -- X\n' + '\n' + ' * ***args** (*int*) -- Additional arguments.\n' + '\n' + ' * ****kwargs** (*int*) -- Extra arguments.\n' + ) + + +@pytest.mark.sphinx('html', testroot='ext-napoleon-paramtype', freshenv=True) +def test_napoleon_keyword_and_paramtype(app, tmp_path): + inv_file = tmp_path / 'objects.inv' + inv_file.write_bytes(b'''\ +# Sphinx inventory version 2 +# Project: Intersphinx Test +# Version: 42 +# The remainder of this file is compressed using zlib. +''' + zlib.compress(b'''\ +None py:data 1 none.html - +list py:class 1 list.html - +int py:class 1 int.html - +''')) # NoQA: W291 + app.config.intersphinx_mapping = {'python': ('127.0.0.1:5555', str(inv_file))} + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + app.build(force_all=True) + + etree = etree_parse(app.outdir / 'index.html') + + for name, typename in product(('keyword', 'kwarg', 'kwparam'), ('paramtype', 'kwtype')): + param = f'{name}_{typename}' + li_ = list(etree.findall(f'.//li/p/strong[.="{param}"]/../..')) + assert len(li_) == 1 + li = li_[0] + + text = li.text or ''.join(li.itertext()) + assert text == f'{param} (list[int]) \u2013 some param' + + a_ = list(li.findall('.//a[@class="reference external"]')) + + assert len(a_) == 2 + for a, uri in zip(a_, ('list.html', 'int.html')): + assert a.attrib['href'] == f'127.0.0.1:5555/{uri}' + assert a.attrib['title'] == '(in Intersphinx Test v42)' diff --git a/tests/test_extensions/test_ext_todo.py b/tests/test_extensions/test_ext_todo.py new file mode 100644 index 0000000..1903f9f --- /dev/null +++ b/tests/test_extensions/test_ext_todo.py @@ -0,0 +1,108 @@ +"""Test sphinx.ext.todo extension.""" + +import re + +import pytest + + +@pytest.mark.sphinx('html', testroot='ext-todo', freshenv=True, + confoverrides={'todo_include_todos': True, 'todo_emit_warnings': True}) +def test_todo(app, status, warning): + todos = [] + + def on_todo_defined(app, node): + todos.append(node) + + app.connect('todo-defined', on_todo_defined) + app.build(force_all=True) + + # check todolist + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<p class="admonition-title">Todo</p>\n' + '<p>todo in foo</p>') in content + + assert ('<p class="admonition-title">Todo</p>\n' + '<p>todo in bar</p>') in content + + # check todo + content = (app.outdir / 'foo.html').read_text(encoding='utf8') + assert ('<p class="admonition-title">Todo</p>\n' + '<p>todo in foo</p>') in content + + assert ('<p class="admonition-title">Todo</p>\n' + '<p>todo in param field</p>') in content + + # check emitted warnings + assert 'WARNING: TODO entry found: todo in foo' in warning.getvalue() + assert 'WARNING: TODO entry found: todo in bar' in warning.getvalue() + + # check handled event + assert len(todos) == 3 + assert {todo[1].astext() for todo in todos} == {'todo in foo', + 'todo in bar', + 'todo in param field'} + + +@pytest.mark.sphinx('html', testroot='ext-todo', freshenv=True, + confoverrides={'todo_include_todos': False, 'todo_emit_warnings': True}) +def test_todo_not_included(app, status, warning): + todos = [] + + def on_todo_defined(app, node): + todos.append(node) + + app.connect('todo-defined', on_todo_defined) + app.build(force_all=True) + + # check todolist + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<p class="admonition-title">Todo</p>\n' + '<p>todo in foo</p>') not in content + + assert ('<p class="admonition-title">Todo</p>\n' + '<p>todo in bar</p>') not in content + + # check todo + content = (app.outdir / 'foo.html').read_text(encoding='utf8') + assert ('<p class="admonition-title">Todo</p>\n' + '<p>todo in foo</p>') not in content + + # check emitted warnings + assert 'WARNING: TODO entry found: todo in foo' in warning.getvalue() + assert 'WARNING: TODO entry found: todo in bar' in warning.getvalue() + + # check handled event + assert len(todos) == 3 + assert {todo[1].astext() for todo in todos} == {'todo in foo', + 'todo in bar', + 'todo in param field'} + + +@pytest.mark.sphinx('latex', testroot='ext-todo', freshenv=True, + confoverrides={'todo_include_todos': True}) +def test_todo_valid_link(app, status, warning): + """ + Test that the inserted "original entry" links for todo items have a target + that exists in the LaTeX output. The target was previously incorrectly + omitted (GitHub issue #1020). + """ + # Ensure the LaTeX output is built. + app.build(force_all=True) + + content = (app.outdir / 'python.tex').read_text(encoding='utf8') + + # Look for the link to foo. Note that there are two of them because the + # source document uses todolist twice. We could equally well look for links + # to bar. + link = (r'{\\hyperref\[\\detokenize{(.*?foo.*?)}]{\\sphinxcrossref{' + r'\\sphinxstyleemphasis{original entry}}}}') + m = re.findall(link, content) + assert len(m) == 4 + target = m[0] + + # Look for the targets of this link. + labels = re.findall(r'\\label{\\detokenize{([^}]*)}}', content) + matched = [l for l in labels if l == target] + + # If everything is correct we should have exactly one target. + assert len(matched) == 1 diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py new file mode 100644 index 0000000..b2c6fc0 --- /dev/null +++ b/tests/test_extensions/test_ext_viewcode.py @@ -0,0 +1,137 @@ +"""Test sphinx.ext.viewcode extension.""" + +import re +import shutil + +import pytest + + +def check_viewcode_output(app, warning): + warnings = re.sub(r'\\+', '/', warning.getvalue()) + assert re.findall( + r"index.rst:\d+: WARNING: Object named 'func1' not found in include " + + r"file .*/spam/__init__.py'", + warnings, + ) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert result.count('href="_modules/spam/mod1.html#func1"') == 2 + assert result.count('href="_modules/spam/mod2.html#func2"') == 2 + assert result.count('href="_modules/spam/mod1.html#Class1"') == 2 + assert result.count('href="_modules/spam/mod2.html#Class2"') == 2 + assert result.count('@decorator') == 1 + + # test that the class attribute is correctly documented + assert result.count('this is Class3') == 2 + assert 'this is the class attribute class_attr' in result + # the next assert fails, until the autodoc bug gets fixed + assert result.count('this is the class attribute class_attr') == 2 + + result = (app.outdir / '_modules/spam/mod1.html').read_text(encoding='utf8') + result = re.sub('<span class="[^"]{,2}">', '<span>', result) # filter pygments classes + assert ('<div class="viewcode-block" id="Class1">\n' + '<a class="viewcode-back" href="../../index.html#spam.Class1">[docs]</a>\n') in result + assert '<span>@decorator</span>\n' in result + assert '<span>class</span> <span>Class1</span><span>:</span>\n' in result + assert '<span> </span><span>"""</span>\n' in result + assert '<span> this is Class1</span>\n' in result + assert '<span> """</span>\n' in result + + return result + + +@pytest.mark.sphinx(testroot='ext-viewcode', freshenv=True, + confoverrides={"viewcode_line_numbers": True}) +def test_viewcode_linenos(app, warning): + shutil.rmtree(app.outdir / '_modules', ignore_errors=True) + app.build(force_all=True) + + result = check_viewcode_output(app, warning) + assert '<span class="linenos"> 1</span>' in result + + +@pytest.mark.sphinx(testroot='ext-viewcode', freshenv=True, + confoverrides={"viewcode_line_numbers": False}) +def test_viewcode(app, warning): + shutil.rmtree(app.outdir / '_modules', ignore_errors=True) + app.build(force_all=True) + + result = check_viewcode_output(app, warning) + assert 'class="linenos">' not in result + + +@pytest.mark.sphinx('epub', testroot='ext-viewcode') +def test_viewcode_epub_default(app, status, warning): + shutil.rmtree(app.outdir) + app.build(force_all=True) + + assert not (app.outdir / '_modules/spam/mod1.xhtml').exists() + + result = (app.outdir / 'index.xhtml').read_text(encoding='utf8') + assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 0 + + +@pytest.mark.sphinx('epub', testroot='ext-viewcode', + confoverrides={'viewcode_enable_epub': True}) +def test_viewcode_epub_enabled(app, status, warning): + app.build(force_all=True) + + assert (app.outdir / '_modules/spam/mod1.xhtml').exists() + + result = (app.outdir / 'index.xhtml').read_text(encoding='utf8') + assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 2 + + +@pytest.mark.sphinx(testroot='ext-viewcode', tags=['test_linkcode']) +def test_linkcode(app, status, warning): + app.build(filenames=[app.srcdir / 'objects.rst']) + + stuff = (app.outdir / 'objects.html').read_text(encoding='utf8') + + assert 'https://foobar/source/foolib.py' in stuff + assert 'https://foobar/js/' in stuff + assert 'https://foobar/c/' in stuff + assert 'https://foobar/cpp/' in stuff + + +@pytest.mark.sphinx(testroot='ext-viewcode-find', freshenv=True) +def test_local_source_files(app, status, warning): + def find_source(app, modname): + if modname == 'not_a_package': + source = (app.srcdir / 'not_a_package/__init__.py').read_text(encoding='utf8') + tags = { + 'func1': ('def', 1, 1), + 'Class1': ('class', 1, 1), + 'not_a_package.submodule.func1': ('def', 1, 1), + 'not_a_package.submodule.Class1': ('class', 1, 1), + } + else: + source = (app.srcdir / 'not_a_package/submodule.py').read_text(encoding='utf8') + tags = { + 'not_a_package.submodule.func1': ('def', 11, 15), + 'Class1': ('class', 19, 22), + 'not_a_package.submodule.Class1': ('class', 19, 22), + 'Class3': ('class', 25, 30), + 'not_a_package.submodule.Class3.class_attr': ('other', 29, 29), + } + return (source, tags) + + app.connect('viewcode-find-source', find_source) + app.build(force_all=True) + + warnings = re.sub(r'\\+', '/', warning.getvalue()) + assert re.findall( + r"index.rst:\d+: WARNING: Object named 'func1' not found in include " + + r"file .*/not_a_package/__init__.py'", + warnings, + ) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert result.count('href="_modules/not_a_package.html#func1"') == 1 + assert result.count('href="_modules/not_a_package.html#not_a_package.submodule.func1"') == 1 + assert result.count('href="_modules/not_a_package/submodule.html#Class1"') == 1 + assert result.count('href="_modules/not_a_package/submodule.html#Class3"') == 1 + assert result.count('href="_modules/not_a_package/submodule.html#not_a_package.submodule.Class1"') == 1 + + assert result.count('href="_modules/not_a_package/submodule.html#not_a_package.submodule.Class3.class_attr"') == 1 + assert result.count('This is the class attribute class_attr') == 1 diff --git a/tests/test_extensions/test_extension.py b/tests/test_extensions/test_extension.py new file mode 100644 index 0000000..d74743c --- /dev/null +++ b/tests/test_extensions/test_extension.py @@ -0,0 +1,23 @@ +"""Test sphinx.extension module.""" + +import pytest + +from sphinx.errors import VersionRequirementError +from sphinx.extension import Extension, verify_needs_extensions + + +def test_needs_extensions(app): + # empty needs_extensions + assert app.config.needs_extensions == {} + verify_needs_extensions(app, app.config) + + # needs_extensions fulfilled + app.config.needs_extensions = {'test.extension': '3.9'} + app.extensions['test.extension'] = Extension('test.extension', 'test.extension', version='3.10') + verify_needs_extensions(app, app.config) + + # needs_extensions not fulfilled + app.config.needs_extensions = {'test.extension': '3.11'} + app.extensions['test.extension'] = Extension('test.extension', 'test.extension', version='3.10') + with pytest.raises(VersionRequirementError): + verify_needs_extensions(app, app.config) |