summaryrefslogtreecommitdiffstats
path: root/tests/test_extensions
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:58 +0000
commitffcb4b87846b4e4a2d9eee8df4b7ec40365878b8 (patch)
tree3c64877dd20ad1141111c77b3463e95686002b39 /tests/test_extensions
parentAdding debian version 7.2.6-8. (diff)
downloadsphinx-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')
-rw-r--r--tests/test_extensions/__init__.py0
-rw-r--r--tests/test_extensions/autodoc_util.py33
-rw-r--r--tests/test_extensions/ext_napoleon_pep526_data_google.py16
-rw-r--r--tests/test_extensions/ext_napoleon_pep526_data_numpy.py21
-rw-r--r--tests/test_extensions/test_ext_apidoc.py663
-rw-r--r--tests/test_extensions/test_ext_autodoc.py2923
-rw-r--r--tests/test_extensions/test_ext_autodoc_autoattribute.py176
-rw-r--r--tests/test_extensions/test_ext_autodoc_autoclass.py566
-rw-r--r--tests/test_extensions/test_ext_autodoc_autodata.py106
-rw-r--r--tests/test_extensions/test_ext_autodoc_autofunction.py212
-rw-r--r--tests/test_extensions/test_ext_autodoc_automodule.py192
-rw-r--r--tests/test_extensions/test_ext_autodoc_autoproperty.py91
-rw-r--r--tests/test_extensions/test_ext_autodoc_configs.py1743
-rw-r--r--tests/test_extensions/test_ext_autodoc_events.py118
-rw-r--r--tests/test_extensions/test_ext_autodoc_mock.py152
-rw-r--r--tests/test_extensions/test_ext_autodoc_preserve_defaults.py192
-rw-r--r--tests/test_extensions/test_ext_autodoc_private_members.py158
-rw-r--r--tests/test_extensions/test_ext_autosectionlabel.py77
-rw-r--r--tests/test_extensions/test_ext_autosummary.py686
-rw-r--r--tests/test_extensions/test_ext_coverage.py101
-rw-r--r--tests/test_extensions/test_ext_doctest.py135
-rw-r--r--tests/test_extensions/test_ext_duration.py14
-rw-r--r--tests/test_extensions/test_ext_extlinks.py45
-rw-r--r--tests/test_extensions/test_ext_githubpages.py26
-rw-r--r--tests/test_extensions/test_ext_graphviz.py196
-rw-r--r--tests/test_extensions/test_ext_ifconfig.py28
-rw-r--r--tests/test_extensions/test_ext_imgconverter.py35
-rw-r--r--tests/test_extensions/test_ext_imgmockconverter.py17
-rw-r--r--tests/test_extensions/test_ext_inheritance_diagram.py342
-rw-r--r--tests/test_extensions/test_ext_intersphinx.py584
-rw-r--r--tests/test_extensions/test_ext_math.py389
-rw-r--r--tests/test_extensions/test_ext_napoleon.py218
-rw-r--r--tests/test_extensions/test_ext_napoleon_docstring.py2703
-rw-r--r--tests/test_extensions/test_ext_todo.py108
-rw-r--r--tests/test_extensions/test_ext_viewcode.py137
-rw-r--r--tests/test_extensions/test_extension.py23
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 -&gt; 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 -&gt; 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 -&gt; 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 -&gt; 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 &quot;index.html&quot; 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 \&amp;= \\pi r\^2\\\\'
+ r'V \&amp;= \\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>&quot;&quot;&quot;</span>\n' in result
+ assert '<span> this is Class1</span>\n' in result
+ assert '<span> &quot;&quot;&quot;</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)