summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:59 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:59 +0000
commit5de84c9242643f786eff03726286578726d7d390 (patch)
tree8e8eadab2b786c41d7b8a2cdafbb467588928ad0 /tests
parentReleasing progress-linux version 7.2.6-8~progress7.99u1. (diff)
downloadsphinx-5de84c9242643f786eff03726286578726d7d390.tar.xz
sphinx-5de84c9242643f786eff03726286578726d7d390.zip
Merging upstream version 7.3.7.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py30
-rw-r--r--tests/js/searchtools.js99
-rw-r--r--tests/roots/test-changes/base.rst3
-rw-r--r--tests/roots/test-changes/conf.py2
-rw-r--r--tests/roots/test-domain-cpp/operator-lookup.rst28
-rw-r--r--tests/roots/test-domain-py/module.rst6
-rw-r--r--tests/roots/test-ext-autodoc/target/enums.py231
-rw-r--r--tests/roots/test-ext-autodoc/target/functions.py3
-rw-r--r--tests/roots/test-ext-autodoc/target/inherited_annotations.py17
-rw-r--r--tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py31
-rw-r--r--tests/roots/test-ext-doctest-skipif/conf.py2
-rw-r--r--tests/roots/test-ext-doctest/doctest.txt2
-rw-r--r--tests/roots/test-ext-imgmockconverter/mocksvgconverter.py4
-rw-r--r--tests/roots/test-ext-intersphinx-role/index.rst4
-rw-r--r--tests/roots/test-ext-math-include/conf.py0
-rw-r--r--tests/roots/test-ext-math-include/included.rst6
-rw-r--r--tests/roots/test-ext-math-include/index.rst7
-rw-r--r--tests/roots/test-ext-math-include/math.rst4
-rw-r--r--tests/roots/test-ext-napoleon-paramtype/conf.py15
-rw-r--r--tests/roots/test-ext-napoleon-paramtype/index.rst8
-rw-r--r--tests/roots/test-ext-napoleon-paramtype/pkg/__init__.py0
-rw-r--r--tests/roots/test-ext-napoleon-paramtype/pkg/bar.py10
-rw-r--r--tests/roots/test-ext-napoleon-paramtype/pkg/foo.py27
-rw-r--r--tests/roots/test-ext-viewcode/conf.py6
-rw-r--r--tests/roots/test-footnotes/index.rst8
-rw-r--r--tests/roots/test-images/index.rst4
-rw-r--r--tests/roots/test-intl/definition_terms.txt2
-rw-r--r--tests/roots/test-intl/external_links.txt12
-rw-r--r--tests/roots/test-intl/raw.txt2
-rw-r--r--tests/roots/test-intl/refs.txt2
-rw-r--r--tests/roots/test-intl/refs_inconsistency.txt2
-rw-r--r--tests/roots/test-intl/versionchange.txt2
-rw-r--r--tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po4
-rw-r--r--tests/roots/test-intl/xx/LC_MESSAGES/external_links.po12
-rw-r--r--tests/roots/test-intl/xx/LC_MESSAGES/raw.po4
-rw-r--r--tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po2
-rw-r--r--tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py2
-rw-r--r--tests/roots/test-linkcheck-anchors-ignore/conf.py2
-rw-r--r--tests/roots/test-linkcheck-documents_exclude/conf.py2
-rw-r--r--tests/roots/test-linkcheck-localserver-anchor/conf.py2
-rw-r--r--tests/roots/test-linkcheck-localserver-https/conf.py2
-rw-r--r--tests/roots/test-linkcheck-localserver-warn-redirects/conf.py2
-rw-r--r--tests/roots/test-linkcheck-localserver/conf.py2
-rw-r--r--tests/roots/test-linkcheck-raw-node/conf.py2
-rw-r--r--tests/roots/test-linkcheck-raw-node/index.rst2
-rw-r--r--tests/roots/test-linkcheck-too-many-retries/conf.py2
-rw-r--r--tests/roots/test-linkcheck/conf.py2
-rw-r--r--tests/roots/test-manpage_url/index.rst10
-rw-r--r--tests/roots/test-need-escaped/index.rst2
-rw-r--r--tests/roots/test-roles-download/index.rst2
-rw-r--r--tests/roots/test-root/conf.py4
-rw-r--r--tests/roots/test-root/images.txt3
-rw-r--r--tests/roots/test-root/index.txt6
-rw-r--r--tests/roots/test-root/markup.txt5
-rw-r--r--tests/roots/test-toctree/index.rst6
-rw-r--r--tests/roots/test-versioning/insert_beginning.txt2
-rw-r--r--tests/test_addnodes.py8
-rw-r--r--tests/test_application.py18
-rw-r--r--tests/test_build_html.py1841
-rw-r--r--tests/test_builders/__init__.py0
-rw-r--r--tests/test_builders/conftest.py28
-rw-r--r--tests/test_builders/test_build.py (renamed from tests/test_build.py)38
-rw-r--r--tests/test_builders/test_build_changes.py (renamed from tests/test_build_changes.py)2
-rw-r--r--tests/test_builders/test_build_dirhtml.py (renamed from tests/test_build_dirhtml.py)0
-rw-r--r--tests/test_builders/test_build_epub.py (renamed from tests/test_build_epub.py)38
-rw-r--r--tests/test_builders/test_build_gettext.py (renamed from tests/test_build_gettext.py)18
-rw-r--r--tests/test_builders/test_build_html.py378
-rw-r--r--tests/test_builders/test_build_html_5_output.py276
-rw-r--r--tests/test_builders/test_build_html_assets.py152
-rw-r--r--tests/test_builders/test_build_html_code.py46
-rw-r--r--tests/test_builders/test_build_html_download.py62
-rw-r--r--tests/test_builders/test_build_html_highlight.py61
-rw-r--r--tests/test_builders/test_build_html_image.py80
-rw-r--r--tests/test_builders/test_build_html_maths.py58
-rw-r--r--tests/test_builders/test_build_html_numfig.py487
-rw-r--r--tests/test_builders/test_build_html_tocdepth.py94
-rw-r--r--tests/test_builders/test_build_latex.py (renamed from tests/test_build_latex.py)274
-rw-r--r--tests/test_builders/test_build_linkcheck.py (renamed from tests/test_build_linkcheck.py)401
-rw-r--r--tests/test_builders/test_build_manpage.py (renamed from tests/test_build_manpage.py)7
-rw-r--r--tests/test_builders/test_build_texinfo.py (renamed from tests/test_build_texinfo.py)35
-rw-r--r--tests/test_builders/test_build_text.py (renamed from tests/test_build_text.py)28
-rw-r--r--tests/test_builders/test_build_warnings.py89
-rw-r--r--tests/test_builders/test_builder.py (renamed from tests/test_builder.py)5
-rw-r--r--tests/test_builders/xpath_data.py8
-rw-r--r--tests/test_builders/xpath_util.py79
-rw-r--r--tests/test_config/__init__.py0
-rw-r--r--tests/test_config/test_config.py (renamed from tests/test_config.py)342
-rw-r--r--tests/test_config/test_correct_year.py (renamed from tests/test_correct_year.py)0
-rw-r--r--tests/test_directives/__init__.py0
-rw-r--r--tests/test_directives/test_directive_code.py (renamed from tests/test_directive_code.py)36
-rw-r--r--tests/test_directives/test_directive_object_description.py (renamed from tests/test_directive_object_description.py)0
-rw-r--r--tests/test_directives/test_directive_only.py (renamed from tests/test_directive_only.py)4
-rw-r--r--tests/test_directives/test_directive_option.py40
-rw-r--r--tests/test_directives/test_directive_other.py (renamed from tests/test_directive_other.py)0
-rw-r--r--tests/test_directives/test_directive_patch.py (renamed from tests/test_directive_patch.py)0
-rw-r--r--tests/test_directives/test_directives_no_typesetting.py (renamed from tests/test_directives_no_typesetting.py)0
-rw-r--r--tests/test_domain_py.py2123
-rw-r--r--tests/test_domains/__init__.py0
-rw-r--r--tests/test_domains/test_domain_c.py (renamed from tests/test_domain_c.py)38
-rw-r--r--tests/test_domains/test_domain_cpp.py (renamed from tests/test_domain_cpp.py)63
-rw-r--r--tests/test_domains/test_domain_js.py (renamed from tests/test_domain_js.py)6
-rw-r--r--tests/test_domains/test_domain_py.py1015
-rw-r--r--tests/test_domains/test_domain_py_canonical.py77
-rw-r--r--tests/test_domains/test_domain_py_fields.py326
-rw-r--r--tests/test_domains/test_domain_py_pyfunction.py396
-rw-r--r--tests/test_domains/test_domain_py_pyobject.py487
-rw-r--r--tests/test_domains/test_domain_rst.py (renamed from tests/test_domain_rst.py)0
-rw-r--r--tests/test_domains/test_domain_std.py (renamed from tests/test_domain_std.py)34
-rw-r--r--tests/test_environment/__init__.py0
-rw-r--r--tests/test_environment/test_environment.py (renamed from tests/test_environment.py)0
-rw-r--r--tests/test_environment/test_environment_indexentries.py (renamed from tests/test_environment_indexentries.py)0
-rw-r--r--tests/test_environment/test_environment_record_dependencies.py (renamed from tests/test_environment_record_dependencies.py)0
-rw-r--r--tests/test_environment/test_environment_toctree.py (renamed from tests/test_environment_toctree.py)34
-rw-r--r--tests/test_events.py10
-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.py (renamed from tests/ext_napoleon_pep526_data_google.py)2
-rw-r--r--tests/test_extensions/ext_napoleon_pep526_data_numpy.py (renamed from tests/ext_napoleon_pep526_data_numpy.py)1
-rw-r--r--tests/test_extensions/test_ext_apidoc.py (renamed from tests/test_ext_apidoc.py)20
-rw-r--r--tests/test_extensions/test_ext_autodoc.py (renamed from tests/test_ext_autodoc.py)626
-rw-r--r--tests/test_extensions/test_ext_autodoc_autoattribute.py (renamed from tests/test_ext_autodoc_autoattribute.py)2
-rw-r--r--tests/test_extensions/test_ext_autodoc_autoclass.py (renamed from tests/test_ext_autodoc_autoclass.py)55
-rw-r--r--tests/test_extensions/test_ext_autodoc_autodata.py (renamed from tests/test_ext_autodoc_autodata.py)2
-rw-r--r--tests/test_extensions/test_ext_autodoc_autofunction.py (renamed from tests/test_ext_autodoc_autofunction.py)13
-rw-r--r--tests/test_extensions/test_ext_autodoc_automodule.py (renamed from tests/test_ext_autodoc_automodule.py)2
-rw-r--r--tests/test_extensions/test_ext_autodoc_autoproperty.py (renamed from tests/test_ext_autodoc_autoproperty.py)2
-rw-r--r--tests/test_extensions/test_ext_autodoc_configs.py (renamed from tests/test_ext_autodoc_configs.py)48
-rw-r--r--tests/test_extensions/test_ext_autodoc_events.py (renamed from tests/test_ext_autodoc_events.py)2
-rw-r--r--tests/test_extensions/test_ext_autodoc_mock.py (renamed from tests/test_ext_autodoc_mock.py)0
-rw-r--r--tests/test_extensions/test_ext_autodoc_preserve_defaults.py (renamed from tests/test_ext_autodoc_preserve_defaults.py)2
-rw-r--r--tests/test_extensions/test_ext_autodoc_private_members.py (renamed from tests/test_ext_autodoc_private_members.py)2
-rw-r--r--tests/test_extensions/test_ext_autosectionlabel.py (renamed from tests/test_ext_autosectionlabel.py)28
-rw-r--r--tests/test_extensions/test_ext_autosummary.py (renamed from tests/test_ext_autosummary.py)10
-rw-r--r--tests/test_extensions/test_ext_coverage.py (renamed from tests/test_ext_coverage.py)8
-rw-r--r--tests/test_extensions/test_ext_doctest.py (renamed from tests/test_ext_doctest.py)11
-rw-r--r--tests/test_extensions/test_ext_duration.py (renamed from tests/test_ext_duration.py)0
-rw-r--r--tests/test_extensions/test_ext_extlinks.py (renamed from tests/test_ext_extlinks.py)0
-rw-r--r--tests/test_extensions/test_ext_githubpages.py (renamed from tests/test_ext_githubpages.py)6
-rw-r--r--tests/test_extensions/test_ext_graphviz.py (renamed from tests/test_ext_graphviz.py)44
-rw-r--r--tests/test_extensions/test_ext_ifconfig.py (renamed from tests/test_ext_ifconfig.py)2
-rw-r--r--tests/test_extensions/test_ext_imgconverter.py (renamed from tests/test_ext_imgconverter.py)5
-rw-r--r--tests/test_extensions/test_ext_imgmockconverter.py (renamed from tests/test_ext_imgmockconverter.py)2
-rw-r--r--tests/test_extensions/test_ext_inheritance_diagram.py (renamed from tests/test_ext_inheritance_diagram.py)20
-rw-r--r--tests/test_extensions/test_ext_intersphinx.py (renamed from tests/test_ext_intersphinx.py)134
-rw-r--r--tests/test_extensions/test_ext_math.py (renamed from tests/test_ext_math.py)104
-rw-r--r--tests/test_extensions/test_ext_napoleon.py (renamed from tests/test_ext_napoleon.py)2
-rw-r--r--tests/test_extensions/test_ext_napoleon_docstring.py (renamed from tests/test_ext_napoleon_docstring.py)51
-rw-r--r--tests/test_extensions/test_ext_todo.py (renamed from tests/test_ext_todo.py)7
-rw-r--r--tests/test_extensions/test_ext_viewcode.py (renamed from tests/test_ext_viewcode.py)20
-rw-r--r--tests/test_extensions/test_extension.py (renamed from tests/test_extension.py)0
-rw-r--r--tests/test_intl/__init__.py0
-rw-r--r--tests/test_intl/test_catalogs.py (renamed from tests/test_catalogs.py)0
-rw-r--r--tests/test_intl/test_intl.py (renamed from tests/test_intl.py)339
-rw-r--r--tests/test_intl/test_locale.py (renamed from tests/test_locale.py)0
-rw-r--r--tests/test_markup/__init__.py0
-rw-r--r--tests/test_markup/test_markup.py (renamed from tests/test_markup.py)12
-rw-r--r--tests/test_markup/test_metadata.py (renamed from tests/test_metadata.py)0
-rw-r--r--tests/test_markup/test_parser.py (renamed from tests/test_parser.py)0
-rw-r--r--tests/test_markup/test_smartquotes.py (renamed from tests/test_smartquotes.py)7
-rw-r--r--tests/test_pycode/__init__.py0
-rw-r--r--tests/test_pycode/test_pycode.py (renamed from tests/test_pycode.py)0
-rw-r--r--tests/test_pycode/test_pycode_ast.py (renamed from tests/test_pycode_ast.py)10
-rw-r--r--tests/test_pycode/test_pycode_parser.py (renamed from tests/test_pycode_parser.py)0
-rw-r--r--tests/test_quickstart.py2
-rw-r--r--tests/test_search.py60
-rw-r--r--tests/test_theming.py131
-rw-r--r--tests/test_theming/__init__.py0
-rw-r--r--tests/test_theming/test_html_theme.py35
-rw-r--r--tests/test_theming/test_templating.py (renamed from tests/test_templating.py)21
-rw-r--r--tests/test_theming/test_theming.py227
-rw-r--r--tests/test_theming/theme.conf7
-rw-r--r--tests/test_theming/theme.toml10
-rw-r--r--tests/test_toctree.py6
-rw-r--r--tests/test_transforms/__init__.py0
-rw-r--r--tests/test_transforms/test_transforms_move_module_targets.py (renamed from tests/test_transforms_move_module_targets.py)0
-rw-r--r--tests/test_transforms/test_transforms_post_transforms.py (renamed from tests/test_transforms_post_transforms.py)3
-rw-r--r--tests/test_transforms/test_transforms_post_transforms_code.py (renamed from tests/test_transforms_post_transforms_code.py)0
-rw-r--r--tests/test_transforms/test_transforms_reorder_nodes.py (renamed from tests/test_transforms_reorder_nodes.py)0
-rw-r--r--tests/test_util/__init__.py0
-rw-r--r--tests/test_util/intersphinx_data.py52
-rw-r--r--tests/test_util/test_util.py (renamed from tests/test_util.py)0
-rw-r--r--tests/test_util/test_util_console.py90
-rw-r--r--tests/test_util/test_util_display.py (renamed from tests/test_util_display.py)22
-rw-r--r--tests/test_util/test_util_docstrings.py (renamed from tests/test_util_docstrings.py)0
-rw-r--r--tests/test_util/test_util_docutils.py (renamed from tests/test_util_docutils.py)0
-rw-r--r--tests/test_util/test_util_fileutil.py (renamed from tests/test_util_fileutil.py)0
-rw-r--r--tests/test_util/test_util_i18n.py (renamed from tests/test_util_i18n.py)2
-rw-r--r--tests/test_util/test_util_images.py (renamed from tests/test_util_images.py)0
-rw-r--r--tests/test_util/test_util_inspect.py (renamed from tests/test_util_inspect.py)118
-rw-r--r--tests/test_util/test_util_inventory.py (renamed from tests/test_util_inventory.py)60
-rw-r--r--tests/test_util/test_util_logging.py (renamed from tests/test_util_logging.py)24
-rw-r--r--tests/test_util/test_util_matching.py (renamed from tests/test_util_matching.py)0
-rw-r--r--tests/test_util/test_util_nodes.py (renamed from tests/test_util_nodes.py)4
-rw-r--r--tests/test_util/test_util_rst.py (renamed from tests/test_util_rst.py)0
-rw-r--r--tests/test_util/test_util_template.py (renamed from tests/test_util_template.py)0
-rw-r--r--tests/test_util/test_util_typing.py (renamed from tests/test_util_typing.py)243
-rw-r--r--tests/test_util/typing_test_data.py (renamed from tests/typing_test_data.py)4
-rw-r--r--tests/test_writers/__init__.py0
-rw-r--r--tests/test_writers/test_api_translator.py (renamed from tests/test_api_translator.py)0
-rw-r--r--tests/test_writers/test_docutilsconf.py (renamed from tests/test_docutilsconf.py)4
-rw-r--r--tests/test_writers/test_writer_latex.py (renamed from tests/test_writer_latex.py)0
-rw-r--r--tests/utils.py130
202 files changed, 7800 insertions, 5394 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
index 1b909bd..1c8d525 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,14 +1,25 @@
+from __future__ import annotations
+
import os
+import sys
from pathlib import Path
+from typing import TYPE_CHECKING
import docutils
import pytest
import sphinx
import sphinx.locale
+import sphinx.pycode
+from sphinx.testing.util import _clean_up_global_state
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
-def _init_console(locale_dir=sphinx.locale._LOCALE_DIR, catalog='sphinx'):
+def _init_console(
+ locale_dir: str | None = sphinx.locale._LOCALE_DIR, catalog: str = 'sphinx',
+) -> tuple[sphinx.locale.NullTranslations, bool]:
"""Monkeypatch ``init_console`` to skip its action.
Some tests rely on warning messages in English. We don't want
@@ -20,7 +31,7 @@ def _init_console(locale_dir=sphinx.locale._LOCALE_DIR, catalog='sphinx'):
sphinx.locale.init_console = _init_console
-pytest_plugins = 'sphinx.testing.fixtures'
+pytest_plugins = ['sphinx.testing.fixtures']
# Exclude 'roots' dirs for pytest test collector
collect_ignore = ['roots']
@@ -29,12 +40,21 @@ os.environ['SPHINX_AUTODOC_RELOAD_MODULES'] = '1'
@pytest.fixture(scope='session')
-def rootdir():
- return Path(__file__).parent.absolute() / 'roots'
+def rootdir() -> Path:
+ return Path(__file__).parent.resolve() / 'roots'
-def pytest_report_header(config):
+def pytest_report_header(config: pytest.Config) -> str:
header = f"libraries: Sphinx-{sphinx.__display_version__}, docutils-{docutils.__version__}"
if hasattr(config, '_tmp_path_factory'):
header += f"\nbase tmp_path: {config._tmp_path_factory.getbasetemp()}"
return header
+
+
+@pytest.fixture(autouse=True)
+def _cleanup_docutils() -> Iterator[None]:
+ saved_path = sys.path
+ yield # run the test
+ sys.path[:] = saved_path
+
+ _clean_up_global_state()
diff --git a/tests/js/searchtools.js b/tests/js/searchtools.js
index c9e0c43..4f9984d 100644
--- a/tests/js/searchtools.js
+++ b/tests/js/searchtools.js
@@ -21,7 +21,59 @@ describe('Basic html theme search', function() {
"&lt;no title&gt;",
"",
null,
- 2,
+ 5,
+ "index.rst"
+ ]];
+ expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits);
+ });
+
+ it('should be able to search for multiple terms', function() {
+ index = {
+ alltitles: {
+ 'Main Page': [[0, 'main-page']],
+ },
+ docnames:["index"],
+ filenames:["index.rst"],
+ terms:{main:0, page:0},
+ titles:["Main Page"],
+ titleterms:{ main:0, page:0 }
+ }
+ Search.setIndex(index);
+
+ searchterms = ['main', 'page'];
+ excluded = [];
+ terms = index.terms;
+ titleterms = index.titleterms;
+ hits = [[
+ 'index',
+ 'Main Page',
+ '',
+ null,
+ 15,
+ 'index.rst']];
+ expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits);
+ });
+
+ it('should partially-match "sphinx" when in title index', function() {
+ index = {
+ docnames:["index"],
+ filenames:["index.rst"],
+ terms:{'useful': 0, 'utilities': 0},
+ titles:["sphinx_utils module"],
+ titleterms:{'sphinx_utils': 0}
+ }
+ Search.setIndex(index);
+ searchterms = ['sphinx'];
+ excluded = [];
+ terms = index.terms;
+ titleterms = index.titleterms;
+
+ hits = [[
+ "index",
+ "sphinx_utils module",
+ "",
+ null,
+ 7,
"index.rst"
]];
expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits);
@@ -31,6 +83,51 @@ describe('Basic html theme search', function() {
});
+describe("htmlToText", function() {
+
+ const testHTML = `<html>
+ <body>
+ <script src="directory/filename.js"></script>
+ <div class="body" role="main">
+ <script>
+ console.log('dynamic');
+ </script>
+ <style>
+ div.body p.centered {
+ text-align: center;
+ margin-top: 25px;
+ }
+ </style>
+ <!-- main content -->
+ <section id="getting-started">
+ <h1>Getting Started</h1>
+ <p>Some text</p>
+ </section>
+ <section id="other-section">
+ <h1>Other Section</h1>
+ <p>Other text</p>
+ </section>
+ <section id="yet-another-section">
+ <h1>Yet Another Section</h1>
+ <p>More text</p>
+ </section>
+ </div>
+ </body>
+ </html>`;
+
+ it("basic case", () => {
+ expect(Search.htmlToText(testHTML).trim().split(/\s+/)).toEqual([
+ 'Getting', 'Started', 'Some', 'text',
+ 'Other', 'Section', 'Other', 'text',
+ 'Yet', 'Another', 'Section', 'More', 'text'
+ ]);
+ });
+
+ it("will start reading from the anchor", () => {
+ expect(Search.htmlToText(testHTML, '#other-section').trim().split(/\s+/)).toEqual(['Other', 'Section', 'Other', 'text']);
+ });
+});
+
// This is regression test for https://github.com/sphinx-doc/sphinx/issues/3150
describe('splitQuery regression tests', () => {
diff --git a/tests/roots/test-changes/base.rst b/tests/roots/test-changes/base.rst
index a1b2839..81d90e6 100644
--- a/tests/roots/test-changes/base.rst
+++ b/tests/roots/test-changes/base.rst
@@ -10,6 +10,9 @@ Version markup
.. deprecated:: 0.6
Boring stuff.
+.. versionremoved:: 0.6
+ Goodbye boring stuff.
+
.. versionadded:: 1.2
First paragraph of versionadded.
diff --git a/tests/roots/test-changes/conf.py b/tests/roots/test-changes/conf.py
index c3b2169..29bea6a 100644
--- a/tests/roots/test-changes/conf.py
+++ b/tests/roots/test-changes/conf.py
@@ -1,4 +1,4 @@
project = 'Sphinx ChangesBuilder tests'
-copyright = '2007-2023 by the Sphinx team, see AUTHORS'
+copyright = '2007-2024 by the Sphinx team, see AUTHORS'
version = '0.6'
release = '0.6alpha1'
diff --git a/tests/roots/test-domain-cpp/operator-lookup.rst b/tests/roots/test-domain-cpp/operator-lookup.rst
new file mode 100644
index 0000000..671b91b
--- /dev/null
+++ b/tests/roots/test-domain-cpp/operator-lookup.rst
@@ -0,0 +1,28 @@
+When doing name resolution there are 4 different idenOrOps:
+
+- identifier
+- built-in operator
+- user-defined literal
+- type conversion
+
+.. cpp:function:: int g()
+.. cpp:function:: int operator+(int, int)
+.. cpp:function:: int operator""_lit()
+
+.. cpp:class:: B
+
+ .. cpp:function:: operator int()
+
+ Functions that can't be found:
+
+ - :cpp:func:`int h()`
+ - :cpp:func:`int operator+(bool, bool)`
+ - :cpp:func:`int operator""_udl()`
+ - :cpp:func:`operator bool()`
+
+ Functions that should be found:
+
+ - :cpp:func:`int g()`
+ - :cpp:func:`int operator+(int, int)`
+ - :cpp:func:`int operator""_lit()`
+ - :cpp:func:`operator int()`
diff --git a/tests/roots/test-domain-py/module.rst b/tests/roots/test-domain-py/module.rst
index 4a28068..70098f6 100644
--- a/tests/roots/test-domain-py/module.rst
+++ b/tests/roots/test-domain-py/module.rst
@@ -58,3 +58,9 @@ module
.. py:module:: object
.. py:function:: sum()
+
+.. py:data:: test
+ :type: typing.Literal[2]
+
+.. py:data:: test2
+ :type: typing.Literal[-2]
diff --git a/tests/roots/test-ext-autodoc/target/enums.py b/tests/roots/test-ext-autodoc/target/enums.py
index c69455f..6b27316 100644
--- a/tests/roots/test-ext-autodoc/target/enums.py
+++ b/tests/roots/test-ext-autodoc/target/enums.py
@@ -1,10 +1,46 @@
+# ruff: NoQA: D403, PIE796
import enum
+from typing import final
+
+
+class MemberType:
+ """Custom data type with a simple API."""
+
+ # this mangled attribute will never be shown on subclasses
+ # even if :inherited-members: and :private-members: are set
+ __slots__ = ('__data',)
+
+ def __new__(cls, value):
+ self = object.__new__(cls)
+ self.__data = value
+ return self
+
+ def __str__(self):
+ """inherited"""
+ return self.__data
+
+ def __repr__(self):
+ return repr(self.__data)
+
+ def __reduce__(self):
+ # data types must be pickable, otherwise enum classes using this data
+ # type will be forced to be non-pickable and have their __module__ set
+ # to '<unknown>' instead of, for instance, '__main__'
+ return self.__class__, (self.__data,)
+
+ @final
+ @property
+ def dtype(self):
+ """docstring"""
+ return 'str'
+
+ def isupper(self):
+ """inherited"""
+ return self.__data.isupper()
class EnumCls(enum.Enum):
- """
- this is enum class
- """
+ """this is enum class"""
#: doc for val1
val1 = 12
@@ -15,9 +51,194 @@ class EnumCls(enum.Enum):
def say_hello(self):
"""a method says hello to you."""
- pass
@classmethod
def say_goodbye(cls):
"""a classmethod says good-bye to you."""
- pass
+
+
+class EnumClassWithDataType(MemberType, enum.Enum):
+ """this is enum class"""
+
+ x = 'x'
+
+ def say_hello(self):
+ """docstring"""
+
+ @classmethod
+ def say_goodbye(cls):
+ """docstring"""
+
+
+class ToUpperCase: # not inheriting from enum.Enum
+ @property
+ def value(self): # bypass enum.Enum.value
+ """uppercased"""
+ return str(self._value_).upper() # type: ignore[attr-defined]
+
+
+class Greeter:
+ def say_hello(self):
+ """inherited"""
+
+ @classmethod
+ def say_goodbye(cls):
+ """inherited"""
+
+
+class EnumClassWithMixinType(ToUpperCase, enum.Enum):
+ """this is enum class"""
+
+ x = 'x'
+
+ def say_hello(self):
+ """docstring"""
+
+ @classmethod
+ def say_goodbye(cls):
+ """docstring"""
+
+
+class EnumClassWithMixinTypeInherit(Greeter, ToUpperCase, enum.Enum):
+ """this is enum class"""
+
+ x = 'x'
+
+
+class Overridden(enum.Enum):
+ def override(self):
+ """inherited"""
+ return 1
+
+
+class EnumClassWithMixinEnumType(Greeter, Overridden, enum.Enum):
+ """this is enum class"""
+
+ x = 'x'
+
+ def override(self):
+ """overridden"""
+ return 2
+
+
+class EnumClassWithMixinAndDataType(Greeter, ToUpperCase, MemberType, enum.Enum):
+ """this is enum class"""
+
+ x = 'x'
+
+ def say_hello(self):
+ """overridden"""
+
+ @classmethod
+ def say_goodbye(cls):
+ """overridden"""
+
+ def isupper(self):
+ """overridden"""
+ return False
+
+ def __str__(self):
+ """overridden"""
+ return super().__str__()
+
+
+class _ParentEnum(Greeter, Overridden, enum.Enum):
+ """docstring"""
+
+
+class EnumClassWithParentEnum(ToUpperCase, MemberType, _ParentEnum, enum.Enum):
+ """this is enum class"""
+
+ x = 'x'
+
+ def isupper(self):
+ """overridden"""
+ return False
+
+ def __str__(self):
+ """overridden"""
+ return super().__str__()
+
+
+class _SunderMissingInNonEnumMixin:
+ @classmethod
+ def _missing_(cls, value):
+ """inherited"""
+ return super()._missing_(value) # type: ignore[misc]
+
+
+class _SunderMissingInEnumMixin(enum.Enum):
+ @classmethod
+ def _missing_(cls, value):
+ """inherited"""
+ return super()._missing_(value)
+
+
+class _SunderMissingInDataType(MemberType):
+ @classmethod
+ def _missing_(cls, value):
+ """inherited"""
+ return super()._missing_(value) # type: ignore[misc]
+
+
+class EnumSunderMissingInNonEnumMixin(_SunderMissingInNonEnumMixin, enum.Enum):
+ """this is enum class"""
+
+
+class EnumSunderMissingInEnumMixin(_SunderMissingInEnumMixin, enum.Enum):
+ """this is enum class"""
+
+
+class EnumSunderMissingInDataType(_SunderMissingInDataType, enum.Enum):
+ """this is enum class"""
+
+
+class EnumSunderMissingInClass(enum.Enum):
+ """this is enum class"""
+
+ @classmethod
+ def _missing_(cls, value):
+ """docstring"""
+ return super()._missing_(value)
+
+
+class _NamePropertyInNonEnumMixin:
+ @property
+ def name(self):
+ """inherited"""
+ return super().name # type: ignore[misc]
+
+
+class _NamePropertyInEnumMixin(enum.Enum):
+ @property
+ def name(self):
+ """inherited"""
+ return super().name
+
+
+class _NamePropertyInDataType(MemberType):
+ @property
+ def name(self):
+ """inherited"""
+ return super().name # type: ignore[misc]
+
+
+class EnumNamePropertyInNonEnumMixin(_NamePropertyInNonEnumMixin, enum.Enum):
+ """this is enum class"""
+
+
+class EnumNamePropertyInEnumMixin(_NamePropertyInEnumMixin, enum.Enum):
+ """this is enum class"""
+
+
+class EnumNamePropertyInDataType(_NamePropertyInDataType, enum.Enum):
+ """this is enum class"""
+
+
+class EnumNamePropertyInClass(enum.Enum):
+ """this is enum class"""
+
+ @property
+ def name(self):
+ """docstring"""
+ return super().name
diff --git a/tests/roots/test-ext-autodoc/target/functions.py b/tests/roots/test-ext-autodoc/target/functions.py
index b62aa70..0265fb3 100644
--- a/tests/roots/test-ext-autodoc/target/functions.py
+++ b/tests/roots/test-ext-autodoc/target/functions.py
@@ -17,3 +17,6 @@ partial_coroutinefunc = partial(coroutinefunc)
builtin_func = print
partial_builtin_func = partial(print)
+
+def slice_arg_func(arg: 'float64[:, :]'):
+ pass
diff --git a/tests/roots/test-ext-autodoc/target/inherited_annotations.py b/tests/roots/test-ext-autodoc/target/inherited_annotations.py
new file mode 100644
index 0000000..3ae58a8
--- /dev/null
+++ b/tests/roots/test-ext-autodoc/target/inherited_annotations.py
@@ -0,0 +1,17 @@
+"""
+ Test case for #11387 corner case involving inherited
+ members with type annotations on python 3.9 and earlier
+"""
+
+class HasTypeAnnotatedMember:
+ inherit_me: int
+ """Inherited"""
+
+class NoTypeAnnotation(HasTypeAnnotatedMember):
+ a = 1
+ """Local"""
+
+class NoTypeAnnotation2(HasTypeAnnotatedMember):
+ a = 1
+ """Local"""
+
diff --git a/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py b/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py
new file mode 100644
index 0000000..039fada
--- /dev/null
+++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py
@@ -0,0 +1,31 @@
+from functools import singledispatchmethod
+
+
+class Foo:
+ """docstring"""
+
+ @singledispatchmethod
+ @classmethod
+ def class_meth(cls, arg, kwarg=None):
+ """A class method for general use."""
+ pass
+
+ @class_meth.register(int)
+ @class_meth.register(float)
+ @classmethod
+ def _class_meth_int(cls, arg, kwarg=None):
+ """A class method for numbers."""
+ pass
+
+ @class_meth.register(str)
+ @classmethod
+ def _class_meth_str(cls, arg, kwarg=None):
+ """A class method for str."""
+ pass
+
+ @class_meth.register
+ @classmethod
+ def _class_meth_dict(cls, arg: dict, kwarg=None):
+ """A class method for dict."""
+ # This function tests for specifying type through annotations
+ pass
diff --git a/tests/roots/test-ext-doctest-skipif/conf.py b/tests/roots/test-ext-doctest-skipif/conf.py
index 6f54982..cd8f3eb 100644
--- a/tests/roots/test-ext-doctest-skipif/conf.py
+++ b/tests/roots/test-ext-doctest-skipif/conf.py
@@ -6,7 +6,7 @@ source_suffix = '.txt'
exclude_patterns = ['_build']
doctest_global_setup = '''
-from tests.test_ext_doctest import record
+from tests.test_extensions.test_ext_doctest import record
record('doctest_global_setup', 'body', True)
'''
diff --git a/tests/roots/test-ext-doctest/doctest.txt b/tests/roots/test-ext-doctest/doctest.txt
index 04780cf..0adcf74 100644
--- a/tests/roots/test-ext-doctest/doctest.txt
+++ b/tests/roots/test-ext-doctest/doctest.txt
@@ -139,7 +139,7 @@ Special directives
.. testcleanup:: *
- from tests import test_ext_doctest
+ from tests.test_extensions import test_ext_doctest
test_ext_doctest.cleanup_call()
non-ASCII result
diff --git a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py
index 43368de..c092860 100644
--- a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py
+++ b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py
@@ -8,9 +8,9 @@ from sphinx.transforms.post_transforms.images import ImageConverter
if False:
# For type annotation
- from typing import Any, Dict # NOQA
+ from typing import Any, Dict # NoQA
- from sphinx.application import Sphinx # NOQA
+ from sphinx.application import Sphinx # NoQA
class MyConverter(ImageConverter):
conversion_rules = [
diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst
index 58edb7a..63bccf0 100644
--- a/tests/roots/test-ext-intersphinx-role/index.rst
+++ b/tests/roots/test-ext-intersphinx-role/index.rst
@@ -39,6 +39,10 @@
- a class with explicit non-existing inventory, which also has upper-case in name:
:external+invNope:cpp:class:`foo::Bar`
+- An object type being mistakenly used instead of a role name:
+
+ - :external+inv:c:function:`CFunc`
+ - :external+inv:function:`CFunc`
- explicit title:
:external:cpp:type:`FoonsTitle <foons>`
diff --git a/tests/roots/test-ext-math-include/conf.py b/tests/roots/test-ext-math-include/conf.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/roots/test-ext-math-include/conf.py
diff --git a/tests/roots/test-ext-math-include/included.rst b/tests/roots/test-ext-math-include/included.rst
new file mode 100644
index 0000000..7fb746e
--- /dev/null
+++ b/tests/roots/test-ext-math-include/included.rst
@@ -0,0 +1,6 @@
+Title
+=====
+
+Some file including some maths.
+
+.. include:: math.rst \ No newline at end of file
diff --git a/tests/roots/test-ext-math-include/index.rst b/tests/roots/test-ext-math-include/index.rst
new file mode 100644
index 0000000..cc95338
--- /dev/null
+++ b/tests/roots/test-ext-math-include/index.rst
@@ -0,0 +1,7 @@
+Test Math
+=========
+
+.. toctree::
+ :numbered: 1
+
+ included
diff --git a/tests/roots/test-ext-math-include/math.rst b/tests/roots/test-ext-math-include/math.rst
new file mode 100644
index 0000000..a4266d0
--- /dev/null
+++ b/tests/roots/test-ext-math-include/math.rst
@@ -0,0 +1,4 @@
+:math:`1 + 1 = 2`
+=================
+
+Lorem ipsum. \ No newline at end of file
diff --git a/tests/roots/test-ext-napoleon-paramtype/conf.py b/tests/roots/test-ext-napoleon-paramtype/conf.py
new file mode 100644
index 0000000..34e2274
--- /dev/null
+++ b/tests/roots/test-ext-napoleon-paramtype/conf.py
@@ -0,0 +1,15 @@
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath('.'))
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.intersphinx'
+]
+
+# Python inventory is manually created in the test
+# in order to avoid creating a real HTTP connection
+intersphinx_mapping = {}
+intersphinx_cache_limit = 0
+intersphinx_disabled_reftypes = [] \ No newline at end of file
diff --git a/tests/roots/test-ext-napoleon-paramtype/index.rst b/tests/roots/test-ext-napoleon-paramtype/index.rst
new file mode 100644
index 0000000..5540897
--- /dev/null
+++ b/tests/roots/test-ext-napoleon-paramtype/index.rst
@@ -0,0 +1,8 @@
+test-ext-napoleon
+=================
+
+.. automodule:: pkg.bar
+ :members:
+
+.. automodule:: pkg.foo
+ :members:
diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/__init__.py b/tests/roots/test-ext-napoleon-paramtype/pkg/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/roots/test-ext-napoleon-paramtype/pkg/__init__.py
diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py b/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py
new file mode 100644
index 0000000..e1ae794
--- /dev/null
+++ b/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py
@@ -0,0 +1,10 @@
+class Bar:
+ """The bar."""
+ def list(self) -> None:
+ """A list method."""
+
+ @staticmethod
+ def int() -> float:
+ """An int method."""
+ return 1.0
+
diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py b/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py
new file mode 100644
index 0000000..6979f9e
--- /dev/null
+++ b/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py
@@ -0,0 +1,27 @@
+class Foo:
+ """The foo."""
+ def do(
+ self,
+ *,
+ keyword_paramtype,
+ keyword_kwtype,
+ kwarg_paramtype,
+ kwarg_kwtype,
+ kwparam_paramtype,
+ kwparam_kwtype,
+ ):
+ """Some method.
+
+ :keyword keyword_paramtype: some param
+ :paramtype keyword_paramtype: list[int]
+ :keyword keyword_kwtype: some param
+ :kwtype keyword_kwtype: list[int]
+ :kwarg kwarg_paramtype: some param
+ :paramtype kwarg_paramtype: list[int]
+ :kwarg kwarg_kwtype: some param
+ :kwtype kwarg_kwtype: list[int]
+ :kwparam kwparam_paramtype: some param
+ :paramtype kwparam_paramtype: list[int]
+ :kwparam kwparam_kwtype: some param
+ :kwtype kwparam_kwtype: list[int]
+ """
diff --git a/tests/roots/test-ext-viewcode/conf.py b/tests/roots/test-ext-viewcode/conf.py
index 5e07214..d6de1d9 100644
--- a/tests/roots/test-ext-viewcode/conf.py
+++ b/tests/roots/test-ext-viewcode/conf.py
@@ -15,10 +15,10 @@ if 'test_linkcode' in tags:
def linkcode_resolve(domain, info):
if domain == 'py':
fn = info['module'].replace('.', '/')
- return "http://foobar/source/%s.py" % fn
+ return "https://foobar/source/%s.py" % fn
elif domain == "js":
- return "http://foobar/js/" + info['fullname']
+ return "https://foobar/js/" + info['fullname']
elif domain in ("c", "cpp"):
- return f"http://foobar/{domain}/{''.join(info['names'])}"
+ return f"https://foobar/{domain}/{''.join(info['names'])}"
else:
raise AssertionError()
diff --git a/tests/roots/test-footnotes/index.rst b/tests/roots/test-footnotes/index.rst
index f2c5d0e..e4490ed 100644
--- a/tests/roots/test-footnotes/index.rst
+++ b/tests/roots/test-footnotes/index.rst
@@ -31,10 +31,10 @@ The section with a reference to [AuthorYear]_
* First footnote: [#]_
* Second footnote: [1]_
-* `Sphinx <http://sphinx-doc.org/>`_
+* `Sphinx <https://sphinx-doc.org/>`_
* Third footnote: [#]_
* Fourth footnote: [#named]_
-* `URL including tilde <http://sphinx-doc.org/~test/>`_
+* `URL including tilde <https://sphinx-doc.org/~test/>`_
* GitHub Page: `https://github.com/sphinx-doc/sphinx <https://github.com/sphinx-doc/sphinx>`_
* Mailing list: `sphinx-dev@googlegroups.com <mailto:sphinx-dev@googlegroups.com>`_
@@ -49,13 +49,13 @@ The section with a reference to [#]_
.. [#] Footnote in section
-`URL in term <http://sphinx-doc.org/>`_
+`URL in term <https://sphinx-doc.org/>`_
Description Description Description ...
Footnote in term [#]_
Description Description Description ...
- `Term in deflist <http://sphinx-doc.org/>`_
+ `Term in deflist <https://sphinx-doc.org/>`_
Description2
.. [#] Footnote in term
diff --git a/tests/roots/test-images/index.rst b/tests/roots/test-images/index.rst
index 14a2987..9b9aac1 100644
--- a/tests/roots/test-images/index.rst
+++ b/tests/roots/test-images/index.rst
@@ -23,7 +23,7 @@ test-image
:target: https://www.python.org/
.. a remote image
-.. image:: https://www.python.org/static/img/python-logo.png
+.. image:: http://localhost:7777/sphinx.png
.. non-exist remote image
-.. image:: https://www.google.com/NOT_EXIST.PNG
+.. image:: http://localhost:7777/NOT_EXIST.PNG
diff --git a/tests/roots/test-intl/definition_terms.txt b/tests/roots/test-intl/definition_terms.txt
index 4c56288..19d4fcb 100644
--- a/tests/roots/test-intl/definition_terms.txt
+++ b/tests/roots/test-intl/definition_terms.txt
@@ -6,7 +6,7 @@ i18n with definition terms
Some term
The corresponding definition
-Some *term* `with link <http://sphinx-doc.org/>`__
+Some *term* `with link <https://sphinx-doc.org/>`__
The corresponding definition #2
Some **term** with : classifier1 : classifier2
diff --git a/tests/roots/test-intl/external_links.txt b/tests/roots/test-intl/external_links.txt
index 1cecbee..2d0c063 100644
--- a/tests/roots/test-intl/external_links.txt
+++ b/tests/roots/test-intl/external_links.txt
@@ -8,12 +8,12 @@ External link to Python_.
Internal link to `i18n with external links`_.
-Inline link by `Sphinx Site <http://sphinx-doc.org>`_.
+Inline link by `Sphinx Site <https://sphinx-doc.org>`_.
Unnamed link__.
-.. _Python: http://python.org/index.html
-.. __: http://google.com
+.. _Python: https://python.org/index.html
+.. __: https://google.com
link target swapped translation
@@ -21,7 +21,7 @@ link target swapped translation
link to external1_ and external2_.
-link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.org>`_.
+link to `Sphinx Site <https://sphinx-doc.org>`_ and `Python Site <https://python.org>`_.
.. _external1: https://www.google.com/external1
.. _external2: https://www.google.com/external2
@@ -30,6 +30,6 @@ link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.o
Multiple references in the same line
=====================================
-Link to `Sphinx Site <http://sphinx-doc.org>`_, `Python Site <http://python.org>`_, Python_, Unnamed__ and `i18n with external links`_.
+Link to `Sphinx Site <https://sphinx-doc.org>`_, `Python Site <https://python.org>`_, Python_, Unnamed__ and `i18n with external links`_.
-.. __: http://google.com
+.. __: https://google.com
diff --git a/tests/roots/test-intl/raw.txt b/tests/roots/test-intl/raw.txt
index fe77f6c..1825941 100644
--- a/tests/roots/test-intl/raw.txt
+++ b/tests/roots/test-intl/raw.txt
@@ -4,5 +4,5 @@ Raw
.. raw:: html
- <iframe src="http://sphinx-doc.org"></iframe>
+ <iframe src="https://sphinx-doc.org"></iframe>
diff --git a/tests/roots/test-intl/refs.txt b/tests/roots/test-intl/refs.txt
index 4a094b2..ab52ef1 100644
--- a/tests/roots/test-intl/refs.txt
+++ b/tests/roots/test-intl/refs.txt
@@ -6,7 +6,7 @@ Translation Tips
.. _download Sphinx: https://pypi.org/project/Sphinx/
.. _Docutils site: https://docutils.sourceforge.io/
-.. _Sphinx site: http://sphinx-doc.org/
+.. _Sphinx site: https://sphinx-doc.org/
A-1. Here's how you can `download Sphinx`_.
diff --git a/tests/roots/test-intl/refs_inconsistency.txt b/tests/roots/test-intl/refs_inconsistency.txt
index b16623a..4840597 100644
--- a/tests/roots/test-intl/refs_inconsistency.txt
+++ b/tests/roots/test-intl/refs_inconsistency.txt
@@ -10,4 +10,4 @@ i18n with refs inconsistency
.. [#] This is a auto numbered footnote.
.. [ref2] This is a citation.
.. [100] This is a numbered footnote.
-.. _reference: http://www.example.com
+.. _reference: https://www.example.com
diff --git a/tests/roots/test-intl/versionchange.txt b/tests/roots/test-intl/versionchange.txt
index 4c57e14..7645342 100644
--- a/tests/roots/test-intl/versionchange.txt
+++ b/tests/roots/test-intl/versionchange.txt
@@ -14,3 +14,5 @@ i18n with versionchange
.. versionchanged:: 1.0
This is the *first* paragraph of versionchanged.
+
+.. versionremoved:: 1.0 This is the *first* paragraph of versionremoved.
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po b/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po
index 1752dd6..a19c9d1 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po
@@ -25,8 +25,8 @@ msgstr "SOME TERM"
msgid "The corresponding definition"
msgstr "THE CORRESPONDING DEFINITION"
-msgid "Some *term* `with link <http://sphinx-doc.org/>`__"
-msgstr "SOME *TERM* `WITH LINK <http://sphinx-doc.org/>`__"
+msgid "Some *term* `with link <https://sphinx-doc.org/>`__"
+msgstr "SOME *TERM* `WITH LINK <https://sphinx-doc.org/>`__"
msgid "The corresponding definition #2"
msgstr "THE CORRESPONDING DEFINITION #2"
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po b/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po
index 8c53abb..345dc95 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po
@@ -25,8 +25,8 @@ msgstr "EXTERNAL LINK TO Python_."
msgid "Internal link to `i18n with external links`_."
msgstr "`EXTERNAL LINKS`_ IS INTERNAL LINK."
-msgid "Inline link by `Sphinx Site <http://sphinx-doc.org>`_."
-msgstr "INLINE LINK BY `THE SPHINX SITE <http://sphinx-doc.org>`_."
+msgid "Inline link by `Sphinx Site <https://sphinx-doc.org>`_."
+msgstr "INLINE LINK BY `THE SPHINX SITE <https://sphinx-doc.org>`_."
msgid "Unnamed link__."
msgstr "UNNAMED LINK__."
@@ -37,11 +37,11 @@ msgstr "LINK TARGET SWAPPED TRANSLATION"
msgid "link to external1_ and external2_."
msgstr "LINK TO external2_ AND external1_."
-msgid "link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.org>`_."
-msgstr "LINK TO `THE PYTHON SITE <http://python.org>`_ AND `THE SPHINX SITE <http://sphinx-doc.org>`_."
+msgid "link to `Sphinx Site <https://sphinx-doc.org>`_ and `Python Site <https://python.org>`_."
+msgstr "LINK TO `THE PYTHON SITE <https://python.org>`_ AND `THE SPHINX SITE <https://sphinx-doc.org>`_."
msgid "Multiple references in the same line"
msgstr "MULTIPLE REFERENCES IN THE SAME LINE"
-msgid "Link to `Sphinx Site <http://sphinx-doc.org>`_, `Python Site <http://python.org>`_, Python_, Unnamed__ and `i18n with external links`_."
-msgstr "LINK TO `EXTERNAL LINKS`_, Python_, `THE SPHINX SITE <http://sphinx-doc.org>`_, UNNAMED__ AND `THE PYTHON SITE <http://python.org>`_."
+msgid "Link to `Sphinx Site <https://sphinx-doc.org>`_, `Python Site <https://python.org>`_, Python_, Unnamed__ and `i18n with external links`_."
+msgstr "LINK TO `EXTERNAL LINKS`_, Python_, `THE SPHINX SITE <https://sphinx-doc.org>`_, UNNAMED__ AND `THE PYTHON SITE <https://python.org>`_."
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/raw.po b/tests/roots/test-intl/xx/LC_MESSAGES/raw.po
index f2e8893..303f46b 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/raw.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/raw.po
@@ -16,6 +16,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-msgid "<iframe src=\"http://sphinx-doc.org\"></iframe>"
-msgstr "<iframe src=\"HTTP://SPHINX-DOC.ORG\"></iframe>"
+msgid "<iframe src=\"https://sphinx-doc.org\"></iframe>"
+msgstr "<iframe src=\"HTTPS://SPHINX-DOC.ORG\"></iframe>"
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po b/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po
index 5a8df38..b1d7865 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po
@@ -31,3 +31,5 @@ msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONADDED."
msgid "This is the *first* paragraph of versionchanged."
msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONCHANGED."
+msgid "This is the *first* paragraph of versionremoved."
+msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONREMOVED."
diff --git a/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py b/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py
index 0005bfa..3fc3628 100644
--- a/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py
+++ b/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py
@@ -1,3 +1,3 @@
exclude_patterns = ['_build']
linkcheck_anchors = True
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-anchors-ignore/conf.py b/tests/roots/test-linkcheck-anchors-ignore/conf.py
index 0005bfa..3fc3628 100644
--- a/tests/roots/test-linkcheck-anchors-ignore/conf.py
+++ b/tests/roots/test-linkcheck-anchors-ignore/conf.py
@@ -1,3 +1,3 @@
exclude_patterns = ['_build']
linkcheck_anchors = True
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-documents_exclude/conf.py b/tests/roots/test-linkcheck-documents_exclude/conf.py
index 52388f9..39fcdcb 100644
--- a/tests/roots/test-linkcheck-documents_exclude/conf.py
+++ b/tests/roots/test-linkcheck-documents_exclude/conf.py
@@ -3,4 +3,4 @@ linkcheck_exclude_documents = [
'^broken_link$',
'br[0-9]ken_link',
]
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-localserver-anchor/conf.py b/tests/roots/test-linkcheck-localserver-anchor/conf.py
index 0005bfa..3fc3628 100644
--- a/tests/roots/test-linkcheck-localserver-anchor/conf.py
+++ b/tests/roots/test-linkcheck-localserver-anchor/conf.py
@@ -1,3 +1,3 @@
exclude_patterns = ['_build']
linkcheck_anchors = True
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-localserver-https/conf.py b/tests/roots/test-linkcheck-localserver-https/conf.py
index a2ce01e..85cbe6f 100644
--- a/tests/roots/test-linkcheck-localserver-https/conf.py
+++ b/tests/roots/test-linkcheck-localserver-https/conf.py
@@ -1,2 +1,2 @@
exclude_patterns = ['_build']
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py b/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py
index a2ce01e..85cbe6f 100644
--- a/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py
+++ b/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py
@@ -1,2 +1,2 @@
exclude_patterns = ['_build']
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-localserver/conf.py b/tests/roots/test-linkcheck-localserver/conf.py
index a2ce01e..85cbe6f 100644
--- a/tests/roots/test-linkcheck-localserver/conf.py
+++ b/tests/roots/test-linkcheck-localserver/conf.py
@@ -1,2 +1,2 @@
exclude_patterns = ['_build']
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-raw-node/conf.py b/tests/roots/test-linkcheck-raw-node/conf.py
index a2ce01e..85cbe6f 100644
--- a/tests/roots/test-linkcheck-raw-node/conf.py
+++ b/tests/roots/test-linkcheck-raw-node/conf.py
@@ -1,2 +1,2 @@
exclude_patterns = ['_build']
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck-raw-node/index.rst b/tests/roots/test-linkcheck-raw-node/index.rst
deleted file mode 100644
index 76e26b5..0000000
--- a/tests/roots/test-linkcheck-raw-node/index.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-.. raw:: html
- :url: http://localhost:7777/
diff --git a/tests/roots/test-linkcheck-too-many-retries/conf.py b/tests/roots/test-linkcheck-too-many-retries/conf.py
index 0005bfa..3fc3628 100644
--- a/tests/roots/test-linkcheck-too-many-retries/conf.py
+++ b/tests/roots/test-linkcheck-too-many-retries/conf.py
@@ -1,3 +1,3 @@
exclude_patterns = ['_build']
linkcheck_anchors = True
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-linkcheck/conf.py b/tests/roots/test-linkcheck/conf.py
index 6ddb41a..7cb6a0d 100644
--- a/tests/roots/test-linkcheck/conf.py
+++ b/tests/roots/test-linkcheck/conf.py
@@ -1,4 +1,4 @@
root_doc = 'links'
exclude_patterns = ['_build']
linkcheck_anchors = True
-linkcheck_timeout = 0.05
+linkcheck_timeout = 0.25
diff --git a/tests/roots/test-manpage_url/index.rst b/tests/roots/test-manpage_url/index.rst
index 50d3b04..6761f97 100644
--- a/tests/roots/test-manpage_url/index.rst
+++ b/tests/roots/test-manpage_url/index.rst
@@ -1,3 +1,7 @@
- * :manpage:`man(1)`
- * :manpage:`ls.1`
- * :manpage:`sphinx`
+The :manpage:`cp(1)`
+--------------------
+* :manpage:`man(1)`
+* :manpage:`ls.1`
+* :manpage:`sphinx`
+* :manpage:`mailx(1) <bsd-mailx/mailx.1>`
+* :manpage:`!man(1)`
diff --git a/tests/roots/test-need-escaped/index.rst b/tests/roots/test-need-escaped/index.rst
index 9ef74e0..29a24fa 100644
--- a/tests/roots/test-need-escaped/index.rst
+++ b/tests/roots/test-need-escaped/index.rst
@@ -15,7 +15,7 @@ Contents:
foo
bar
- http://sphinx-doc.org/
+ https://sphinx-doc.org/
baz
qux
diff --git a/tests/roots/test-roles-download/index.rst b/tests/roots/test-roles-download/index.rst
index cdb075e..9d2c622 100644
--- a/tests/roots/test-roles-download/index.rst
+++ b/tests/roots/test-roles-download/index.rst
@@ -4,4 +4,4 @@ test-roles-download
* :download:`dummy.dat`
* :download:`another/dummy.dat`
* :download:`not_found.dat`
-* :download:`Sphinx logo <http://www.sphinx-doc.org/en/master/_static/sphinxheader.png>`
+* :download:`Sphinx logo <https://www.sphinx-doc.org/en/master/_static/sphinx-logo.svg>`
diff --git a/tests/roots/test-root/conf.py b/tests/roots/test-root/conf.py
index 154d4d1..a14ffaf 100644
--- a/tests/roots/test-root/conf.py
+++ b/tests/roots/test-root/conf.py
@@ -114,8 +114,8 @@ latex_elements = {
coverage_c_path = ['special/*.h']
coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'}
-extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue %s'),
- 'pyurl': ('http://python.org/%s', None)}
+extlinks = {'issue': ('https://bugs.python.org/issue%s', 'issue %s'),
+ 'pyurl': ('https://python.org/%s', None)}
# modify tags from conf.py
tags.add('confpytag')
diff --git a/tests/roots/test-root/images.txt b/tests/roots/test-root/images.txt
index 1dc591a..5a096dc 100644
--- a/tests/roots/test-root/images.txt
+++ b/tests/roots/test-root/images.txt
@@ -12,9 +12,6 @@ Sphinx image handling
.. an image with unspecified extension
.. image:: img.*
-.. a non-local image URI
-.. image:: https://www.python.org/static/img/python-logo.png
-
.. an image with subdir and unspecified extension
.. image:: subdir/simg.*
diff --git a/tests/roots/test-root/index.txt b/tests/roots/test-root/index.txt
index e39c958..6a37668 100644
--- a/tests/roots/test-root/index.txt
+++ b/tests/roots/test-root/index.txt
@@ -28,9 +28,9 @@ Contents:
lists
otherext
- http://sphinx-doc.org/
- Latest reference <http://sphinx-doc.org/latest/>
- Python <http://python.org/>
+ https://sphinx-doc.org/
+ Latest reference <https://sphinx-doc.org/latest/>
+ Python <https://python.org/>
Indices and tables
==================
diff --git a/tests/roots/test-root/markup.txt b/tests/roots/test-root/markup.txt
index b59a652..ff677eb 100644
--- a/tests/roots/test-root/markup.txt
+++ b/tests/roots/test-root/markup.txt
@@ -274,6 +274,9 @@ Version markup
.. deprecated:: 0.6
Boring stuff.
+.. versionremoved:: 0.6
+ Goodbye boring stuff.
+
.. versionadded:: 1.2
First paragraph of versionadded.
@@ -308,7 +311,7 @@ Reference lookup underscore: [Ref_1]_
.. seealso:: something, something else, something more
- `Google <http://www.google.com>`_
+ `Google <https://www.google.com>`_
For everything.
.. hlist::
diff --git a/tests/roots/test-toctree/index.rst b/tests/roots/test-toctree/index.rst
index adf1b84..d56f2f6 100644
--- a/tests/roots/test-toctree/index.rst
+++ b/tests/roots/test-toctree/index.rst
@@ -15,7 +15,7 @@ Contents:
foo
bar
- http://sphinx-doc.org/
+ https://sphinx-doc.org/
self
.. only:: html
@@ -44,8 +44,8 @@ This used to crash:
.. toctree::
:hidden:
- Latest reference <http://sphinx-doc.org/latest/>
- Python <http://python.org/>
+ Latest reference <https://sphinx-doc.org/latest/>
+ Python <https://python.org/>
Indices and tables
==================
diff --git a/tests/roots/test-versioning/insert_beginning.txt b/tests/roots/test-versioning/insert_beginning.txt
index 57102a7..9b6e723 100644
--- a/tests/roots/test-versioning/insert_beginning.txt
+++ b/tests/roots/test-versioning/insert_beginning.txt
@@ -1,7 +1,7 @@
Versioning test text
====================
-Apperantly inserting a paragraph at the beginning of a document caused
+Apparently inserting a paragraph at the beginning of a document caused
problems earlier so this document should be used to test that.
So the thing is I need some kind of text - not the lorem ipsum stuff, that
diff --git a/tests/test_addnodes.py b/tests/test_addnodes.py
index 184a696..aa99343 100644
--- a/tests/test_addnodes.py
+++ b/tests/test_addnodes.py
@@ -2,13 +2,18 @@
from __future__ import annotations
+from typing import TYPE_CHECKING
+
import pytest
from sphinx import addnodes
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+
@pytest.fixture()
-def sig_elements() -> set[type[addnodes.desc_sig_element]]:
+def sig_elements() -> Iterator[set[type[addnodes.desc_sig_element]]]:
"""Fixture returning the current ``addnodes.SIG_ELEMENTS`` set."""
original = addnodes.SIG_ELEMENTS.copy() # safe copy of the current nodes
yield {*addnodes.SIG_ELEMENTS} # temporary value to use during tests
@@ -17,7 +22,6 @@ def sig_elements() -> set[type[addnodes.desc_sig_element]]:
def test_desc_sig_element_nodes(sig_elements):
"""Test the registration of ``desc_sig_element`` subclasses."""
-
# expected desc_sig_* node classes (must be declared *after* reloading
# the module since otherwise the objects are not the correct ones)
EXPECTED_SIG_ELEMENTS = {
diff --git a/tests/test_application.py b/tests/test_application.py
index a0fe268..1fc49d6 100644
--- a/tests/test_application.py
+++ b/tests/test_application.py
@@ -1,9 +1,11 @@
"""Test the Sphinx class."""
+from __future__ import annotations
import shutil
import sys
from io import StringIO
from pathlib import Path
+from typing import TYPE_CHECKING
from unittest.mock import Mock
import pytest
@@ -11,11 +13,19 @@ from docutils import nodes
import sphinx.application
from sphinx.errors import ExtensionError
-from sphinx.testing.util import SphinxTestApp, strip_escseq
+from sphinx.testing.util import SphinxTestApp
from sphinx.util import logging
+from sphinx.util.console import strip_colors
+if TYPE_CHECKING:
+ import os
-def test_instantiation(tmp_path_factory, rootdir: str, monkeypatch):
+
+def test_instantiation(
+ tmp_path_factory: pytest.TempPathFactory,
+ rootdir: str | os.PathLike[str] | None,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
# Given
src_dir = tmp_path_factory.getbasetemp() / 'root'
@@ -70,13 +80,13 @@ def test_emit_with_nonascii_name_node(app, status, warning):
def test_extensions(app, status, warning):
app.setup_extension('shutil')
- warning = strip_escseq(warning.getvalue())
+ warning = strip_colors(warning.getvalue())
assert "extension 'shutil' has no setup() function" in warning
def test_extension_in_blacklist(app, status, warning):
app.setup_extension('sphinxjp.themecore')
- msg = strip_escseq(warning.getvalue())
+ msg = strip_colors(warning.getvalue())
assert msg.startswith("WARNING: the extension 'sphinxjp.themecore' was")
diff --git a/tests/test_build_html.py b/tests/test_build_html.py
deleted file mode 100644
index 07f101d..0000000
--- a/tests/test_build_html.py
+++ /dev/null
@@ -1,1841 +0,0 @@
-"""Test the HTML builder and check output against XPath."""
-
-import hashlib
-import os
-import posixpath
-import re
-from itertools import chain, cycle
-from pathlib import Path
-from unittest.mock import ANY, call, patch
-
-import pytest
-from html5lib import HTMLParser
-
-import sphinx.builders.html
-from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
-from sphinx.builders.html._assets import _file_checksum
-from sphinx.errors import ConfigError, ThemeError
-from sphinx.testing.util import strip_escseq
-from sphinx.util.inventory import InventoryFile
-
-FIGURE_CAPTION = ".//figure/figcaption/p"
-
-
-ENV_WARNINGS = """\
-%(root)s/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \
-WARNING: Explicit markup ends without a blank line; unexpected unindent.
-%(root)s/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \
-file '%(root)s/wrongenc.inc' seems to be wrong, try giving an :encoding: option
-%(root)s/index.rst:\\d+: WARNING: invalid single index entry ''
-%(root)s/index.rst:\\d+: WARNING: image file not readable: foo.png
-%(root)s/index.rst:\\d+: WARNING: download file not readable: %(root)s/nonexisting.png
-%(root)s/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \
-with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?'
-"""
-
-HTML_WARNINGS = ENV_WARNINGS + """\
-%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
-%(root)s/index.rst:\\d+: WARNING: citation not found: missing
-%(root)s/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\*
-%(root)s/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
-"""
-
-
-etree_cache = {}
-
-
-@pytest.fixture(scope='module')
-def cached_etree_parse():
- def parse(fname):
- if fname in etree_cache:
- return etree_cache[fname]
- with (fname).open('rb') as fp:
- etree = HTMLParser(namespaceHTMLElements=False).parse(fp)
- etree_cache.clear()
- etree_cache[fname] = etree
- return etree
- yield parse
- etree_cache.clear()
-
-
-def flat_dict(d):
- return chain.from_iterable(
- [
- zip(cycle([fname]), values)
- for fname, values in d.items()
- ],
- )
-
-
-def tail_check(check):
- rex = re.compile(check)
-
- def checker(nodes):
- for node in nodes:
- if node.tail and rex.search(node.tail):
- return True
- msg = f'{check!r} not found in tail of any nodes {nodes}'
- raise AssertionError(msg)
- return checker
-
-
-def check_xpath(etree, fname, path, check, be_found=True):
- nodes = list(etree.findall(path))
- if check is None:
- assert nodes == [], ('found any nodes matching xpath '
- '%r in file %s' % (path, fname))
- return
- else:
- assert nodes != [], ('did not find any node matching xpath '
- '%r in file %s' % (path, fname))
- if callable(check):
- check(nodes)
- elif not check:
- # only check for node presence
- pass
- else:
- def get_text(node):
- if node.text is not None:
- # the node has only one text
- return node.text
- else:
- # the node has tags and text; gather texts just under the node
- return ''.join(n.tail or '' for n in node)
-
- rex = re.compile(check)
- if be_found:
- if any(rex.search(get_text(node)) for node in nodes):
- return
- else:
- if all(not rex.search(get_text(node)) for node in nodes):
- return
-
- raise AssertionError('%r not found in any node matching '
- 'path %s in %s: %r' % (check, path, fname,
- [node.text for node in nodes]))
-
-
-@pytest.mark.sphinx('html', testroot='warnings')
-def test_html_warnings(app, warning):
- app.build()
- html_warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue()))
- html_warnings_exp = HTML_WARNINGS % {
- 'root': re.escape(app.srcdir.as_posix())}
- assert re.match(html_warnings_exp + '$', html_warnings), \
- "Warnings don't match:\n" + \
- '--- Expected (regex):\n' + html_warnings_exp + \
- '--- Got:\n' + html_warnings
-
-
-def test_html4_error(make_app, tmp_path):
- (tmp_path / 'conf.py').write_text('', encoding='utf-8')
- with pytest.raises(
- ConfigError,
- match='HTML 4 is no longer supported by Sphinx',
- ):
- make_app(
- buildername='html',
- srcdir=tmp_path,
- confoverrides={'html4_writer': True},
- )
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'images.html': [
- (".//img[@src='_images/img.png']", ''),
- (".//img[@src='_images/img1.png']", ''),
- (".//img[@src='_images/simg.png']", ''),
- (".//img[@src='_images/svgimg.svg']", ''),
- (".//a[@href='_sources/images.txt']", ''),
- ],
- 'subdir/images.html': [
- (".//img[@src='../_images/img1.png']", ''),
- (".//img[@src='../_images/rimg.png']", ''),
- ],
- 'subdir/includes.html': [
- (".//a[@class='reference download internal']", ''),
- (".//img[@src='../_images/img.png']", ''),
- (".//p", 'This is an include file.'),
- (".//pre/span", 'line 1'),
- (".//pre/span", 'line 2'),
- ],
- 'includes.html': [
- (".//pre", 'Max Strauß'),
- (".//a[@class='reference download internal']", ''),
- (".//pre/span", '"quotes"'),
- (".//pre/span", "'included'"),
- (".//pre/span[@class='s2']", 'üöä'),
- (".//div[@class='inc-pyobj1 highlight-text notranslate']//pre",
- r'^class Foo:\n pass\n\s*$'),
- (".//div[@class='inc-pyobj2 highlight-text notranslate']//pre",
- r'^ def baz\(\):\n pass\n\s*$'),
- (".//div[@class='inc-lines highlight-text notranslate']//pre",
- r'^class Foo:\n pass\nclass Bar:\n$'),
- (".//div[@class='inc-startend highlight-text notranslate']//pre",
- '^foo = "Including Unicode characters: üöä"\\n$'),
- (".//div[@class='inc-preappend highlight-text notranslate']//pre",
- r'(?m)^START CODE$'),
- (".//div[@class='inc-pyobj-dedent highlight-python notranslate']//span",
- r'def'),
- (".//div[@class='inc-tab3 highlight-text notranslate']//pre",
- r'-| |-'),
- (".//div[@class='inc-tab8 highlight-python notranslate']//pre/span",
- r'-| |-'),
- ],
- 'autodoc.html': [
- (".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''),
- (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'),
- (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'),
- (".//dd/p", r'Return spam\.'),
- ],
- 'extapi.html': [
- (".//strong", 'from class: Bar'),
- ],
- 'markup.html': [
- (".//title", 'set by title directive'),
- (".//p/em", 'Section author: Georg Brandl'),
- (".//p/em", 'Module author: Georg Brandl'),
- # created by the meta directive
- (".//meta[@name='author'][@content='Me']", ''),
- (".//meta[@name='keywords'][@content='docs, sphinx']", ''),
- # a label created by ``.. _label:``
- (".//div[@id='label']", ''),
- # code with standard code blocks
- (".//pre", '^some code$'),
- # an option list
- (".//span[@class='option']", '--help'),
- # admonitions
- (".//p[@class='admonition-title']", 'My Admonition'),
- (".//div[@class='admonition note']/p", 'Note text.'),
- (".//div[@class='admonition warning']/p", 'Warning text.'),
- # inline markup
- (".//li/p/strong", r'^command\\n$'),
- (".//li/p/strong", r'^program\\n$'),
- (".//li/p/em", r'^dfn\\n$'),
- (".//li/p/kbd", r'^kbd\\n$'),
- (".//li/p/span", 'File \N{TRIANGULAR BULLET} Close'),
- (".//li/p/code/span[@class='pre']", '^a/$'),
- (".//li/p/code/em/span[@class='pre']", '^varpart$'),
- (".//li/p/code/em/span[@class='pre']", '^i$'),
- (".//a[@href='https://peps.python.org/pep-0008/']"
- "[@class='pep reference external']/strong", 'PEP 8'),
- (".//a[@href='https://peps.python.org/pep-0008/']"
- "[@class='pep reference external']/strong",
- 'Python Enhancement Proposal #8'),
- (".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
- "[@class='rfc reference external']/strong", 'RFC 1'),
- (".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
- "[@class='rfc reference external']/strong", 'Request for Comments #1'),
- (".//a[@href='objects.html#envvar-HOME']"
- "[@class='reference internal']/code/span[@class='pre']", 'HOME'),
- (".//a[@href='#with']"
- "[@class='reference internal']/code/span[@class='pre']", '^with$'),
- (".//a[@href='#grammar-token-try_stmt']"
- "[@class='reference internal']/code/span", '^statement$'),
- (".//a[@href='#some-label'][@class='reference internal']/span", '^here$'),
- (".//a[@href='#some-label'][@class='reference internal']/span", '^there$'),
- (".//a[@href='subdir/includes.html']"
- "[@class='reference internal']/span", 'Including in subdir'),
- (".//a[@href='objects.html#cmdoption-python-c']"
- "[@class='reference internal']/code/span[@class='pre']", '-c'),
- # abbreviations
- (".//abbr[@title='abbreviation']", '^abbr$'),
- # version stuff
- (".//div[@class='versionadded']/p/span", 'New in version 0.6: '),
- (".//div[@class='versionadded']/p/span",
- tail_check('First paragraph of versionadded')),
- (".//div[@class='versionchanged']/p/span",
- tail_check('First paragraph of versionchanged')),
- (".//div[@class='versionchanged']/p",
- 'Second paragraph of versionchanged'),
- # footnote reference
- (".//a[@class='footnote-reference brackets']", r'1'),
- # created by reference lookup
- (".//a[@href='index.html#ref1']", ''),
- # ``seealso`` directive
- (".//div/p[@class='admonition-title']", 'See also'),
- # a ``hlist`` directive
- (".//table[@class='hlist']/tbody/tr/td/ul/li/p", '^This$'),
- # a ``centered`` directive
- (".//p[@class='centered']/strong", 'LICENSE'),
- # a glossary
- (".//dl/dt[@id='term-boson']", 'boson'),
- (".//dl/dt[@id='term-boson']/a", '¶'),
- # a production list
- (".//pre/strong", 'try_stmt'),
- (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'),
- # tests for ``only`` directive
- (".//p", 'A global substitution!'),
- (".//p", 'In HTML.'),
- (".//p", 'In both.'),
- (".//p", 'Always present'),
- # tests for ``any`` role
- (".//a[@href='#with']/span", 'headings'),
- (".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
- # tests for numeric labels
- (".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'),
- # tests for smartypants
- (".//li/p", 'Smart “quotes” in English ‘text’.'),
- (".//li/p", 'Smart — long and – short dashes.'),
- (".//li/p", 'Ellipsis…'),
- (".//li/p/code/span[@class='pre']", 'foo--"bar"...'),
- (".//p", 'Этот «абзац» должен использовать „русские“ кавычки.'),
- (".//p", 'Il dit : « C’est “super” ! »'),
- ],
- 'objects.html': [
- (".//dt[@id='mod.Cls.meth1']", ''),
- (".//dt[@id='errmod.Error']", ''),
- (".//dt/span[@class='sig-name descname']/span[@class='pre']", r'long\(parameter,'),
- (".//dt/span[@class='sig-name descname']/span[@class='pre']", r'list\)'),
- (".//dt/span[@class='sig-name descname']/span[@class='pre']", 'another'),
- (".//dt/span[@class='sig-name descname']/span[@class='pre']", 'one'),
- (".//a[@href='#mod.Cls'][@class='reference internal']", ''),
- (".//dl[@class='std userdesc']", ''),
- (".//dt[@id='userdesc-myobj']", ''),
- (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''),
- # docfields
- (".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'),
- (".//a[@class='reference internal'][@href='#Time']", 'Time'),
- (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'),
- # C references
- (".//span[@class='pre']", 'CFunction()'),
- (".//a[@href='#c.Sphinx_DoSomething']", ''),
- (".//a[@href='#c.SphinxStruct.member']", ''),
- (".//a[@href='#c.SPHINX_USE_PYTHON']", ''),
- (".//a[@href='#c.SphinxType']", ''),
- (".//a[@href='#c.sphinx_global']", ''),
- # test global TOC created by toctree()
- (".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']",
- 'Testing object descriptions'),
- (".//li[@class='toctree-l1']/a[@href='markup.html']",
- 'Testing various markup'),
- # test unknown field names
- (".//dt[@class='field-odd']", 'Field_name'),
- (".//dt[@class='field-even']", 'Field_name all lower'),
- (".//dt[@class='field-odd']", 'FIELD_NAME'),
- (".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'),
- (".//dt[@class='field-odd']", 'Field_Name'),
- (".//dt[@class='field-even']", 'Field_Name All Word Caps'),
- (".//dt[@class='field-odd']", 'Field_name'),
- (".//dt[@class='field-even']", 'Field_name First word cap'),
- (".//dt[@class='field-odd']", 'FIELd_name'),
- (".//dt[@class='field-even']", 'FIELd_name PARTial caps'),
- # custom sidebar
- (".//h4", 'Custom sidebar'),
- # docfields
- (".//dd[@class='field-odd']/p/strong", '^moo$'),
- (".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')),
- (".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'),
- (".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'),
- (".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')),
- # others
- (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
- 'perl'),
- (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
- '\\+p'),
- (".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span",
- '--ObjC\\+\\+'),
- (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span",
- '--plugin.option'),
- (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']"
- "/code/span",
- 'create-auth-token'),
- (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span",
- 'arg'),
- (".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span",
- '-j'),
- (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
- 'hg'),
- (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
- 'commit'),
- (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
- 'git'),
- (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
- 'commit'),
- (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
- '-p'),
- ],
- 'index.html': [
- (".//meta[@name='hc'][@content='hcval']", ''),
- (".//meta[@name='hc_co'][@content='hcval_co']", ''),
- (".//li[@class='toctree-l1']/a", 'Testing various markup'),
- (".//li[@class='toctree-l2']/a", 'Inline markup'),
- (".//title", 'Sphinx <Tests>'),
- (".//div[@class='footer']", 'copyright text credits'),
- (".//a[@href='http://python.org/']"
- "[@class='reference external']", ''),
- (".//li/p/a[@href='genindex.html']/span", 'Index'),
- (".//li/p/a[@href='py-modindex.html']/span", 'Module Index'),
- # custom sidebar only for contents
- (".//h4", 'Contents sidebar'),
- # custom JavaScript
- (".//script[@src='file://moo.js']", ''),
- # URL in contents
- (".//a[@class='reference external'][@href='http://sphinx-doc.org/']",
- 'http://sphinx-doc.org/'),
- (".//a[@class='reference external'][@href='http://sphinx-doc.org/latest/']",
- 'Latest reference'),
- # Indirect hyperlink targets across files
- (".//a[@href='markup.html#some-label'][@class='reference internal']/span",
- '^indirect hyperref$'),
- ],
- 'bom.html': [
- (".//title", " File with UTF-8 BOM"),
- ],
- 'extensions.html': [
- (".//a[@href='http://python.org/dev/']", "http://python.org/dev/"),
- (".//a[@href='http://bugs.python.org/issue1000']", "issue 1000"),
- (".//a[@href='http://bugs.python.org/issue1042']", "explicit caption"),
- ],
- 'genindex.html': [
- # index entries
- (".//a/strong", "Main"),
- (".//a/strong", "[1]"),
- (".//a/strong", "Other"),
- (".//a", "entry"),
- (".//li/a", "double"),
- ],
- 'otherext.html': [
- (".//h1", "Generated section"),
- (".//a[@href='_sources/otherext.foo.txt']", ''),
- ],
-}))
-@pytest.mark.sphinx('html', tags=['testtag'],
- confoverrides={'html_context.hckey_co': 'hcval_co'})
-@pytest.mark.test_params(shared_result='test_build_html_output')
-def test_html5_output(app, cached_etree_parse, fname, expect):
- app.build()
- print(app.outdir / fname)
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (".//div[@class='citation']/span", r'Ref1'),
- (".//div[@class='citation']/span", r'Ref_1'),
- ],
- 'footnote.html': [
- (".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"),
- (".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"),
- (".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"),
- (".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"),
- (".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"),
- (".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"),
- (".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"),
- (".//aside[@class='footnote brackets']/span/a[@href='#id1']", r"1"),
- (".//aside[@class='footnote brackets']/span/a[@href='#id2']", r"2"),
- (".//aside[@class='footnote brackets']/span/a[@href='#id3']", r"3"),
- (".//div[@class='citation']/span/a[@href='#id4']", r"bar"),
- (".//div[@class='citation']/span/a[@href='#id5']", r"baz_qux"),
- (".//aside[@class='footnote brackets']/span/a[@href='#id6']", r"4"),
- (".//aside[@class='footnote brackets']/span/a[@href='#id7']", r"5"),
- (".//aside[@class='footnote brackets']/span/a[@href='#id8']", r"6"),
- ],
-}))
-@pytest.mark.sphinx('html')
-@pytest.mark.test_params(shared_result='test_build_html_output_docutils18')
-def test_docutils_output(app, cached_etree_parse, fname, expect):
- app.build()
- print(app.outdir / fname)
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', parallel=2)
-def test_html_parallel(app):
- app.build()
-
-
-@pytest.mark.sphinx('html')
-@pytest.mark.test_params(shared_result='test_build_html_output')
-def test_html_download(app):
- app.build()
-
- # subdir/includes.html
- result = (app.outdir / 'subdir' / 'includes.html').read_text(encoding='utf8')
- pattern = ('<a class="reference download internal" download="" '
- 'href="../(_downloads/.*/img.png)">')
- matched = re.search(pattern, result)
- assert matched
- assert (app.outdir / matched.group(1)).exists()
- filename = matched.group(1)
-
- # includes.html
- result = (app.outdir / 'includes.html').read_text(encoding='utf8')
- pattern = ('<a class="reference download internal" download="" '
- 'href="(_downloads/.*/img.png)">')
- matched = re.search(pattern, result)
- assert matched
- assert (app.outdir / matched.group(1)).exists()
- assert matched.group(1) == filename
-
- pattern = ('<a class="reference download internal" download="" '
- 'href="(_downloads/.*/)(file_with_special_%23_chars.xyz)">')
- matched = re.search(pattern, result)
- assert matched
- assert (app.outdir / matched.group(1) / "file_with_special_#_chars.xyz").exists()
-
-
-@pytest.mark.sphinx('html', testroot='roles-download')
-def test_html_download_role(app, status, warning):
- app.build()
- digest = hashlib.md5(b'dummy.dat', usedforsecurity=False).hexdigest()
- assert (app.outdir / '_downloads' / digest / 'dummy.dat').exists()
- digest_another = hashlib.md5(b'another/dummy.dat', usedforsecurity=False).hexdigest()
- assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists()
-
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert (('<li><p><a class="reference download internal" download="" '
- 'href="_downloads/%s/dummy.dat">'
- '<code class="xref download docutils literal notranslate">'
- '<span class="pre">dummy.dat</span></code></a></p></li>' % digest)
- in content)
- assert (('<li><p><a class="reference download internal" download="" '
- 'href="_downloads/%s/dummy.dat">'
- '<code class="xref download docutils literal notranslate">'
- '<span class="pre">another/dummy.dat</span></code></a></p></li>' %
- digest_another) in content)
- assert ('<li><p><code class="xref download docutils literal notranslate">'
- '<span class="pre">not_found.dat</span></code></p></li>' in content)
- assert ('<li><p><a class="reference download external" download="" '
- 'href="http://www.sphinx-doc.org/en/master/_static/sphinxheader.png">'
- '<code class="xref download docutils literal notranslate">'
- '<span class="pre">Sphinx</span> <span class="pre">logo</span>'
- '</code></a></p></li>' in content)
-
-
-@pytest.mark.sphinx('html', testroot='build-html-translator')
-def test_html_translator(app):
- app.build()
- assert app.builder.docwriter.visitor.depart_with_node == 10
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
- (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
- (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
- (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
- ],
- 'foo.html': [
- (".//h1", 'Foo', True),
- (".//h2", 'Foo A', True),
- (".//h3", 'Foo A1', True),
- (".//h2", 'Foo B', True),
- (".//h3", 'Foo B1', True),
-
- (".//h1//span[@class='section-number']", '1. ', True),
- (".//h2//span[@class='section-number']", '1.1. ', True),
- (".//h3//span[@class='section-number']", '1.1.1. ', True),
- (".//h2//span[@class='section-number']", '1.2. ', True),
- (".//h3//span[@class='section-number']", '1.2.1. ', True),
-
- (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1. Foo A', True),
- (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1.1. Foo A1', True),
- (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2. Foo B', True),
- (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2.1. Foo B1', True),
- ],
- 'bar.html': [
- (".//h1", 'Bar', True),
- (".//h2", 'Bar A', True),
- (".//h2", 'Bar B', True),
- (".//h3", 'Bar B1', True),
- (".//h1//span[@class='section-number']", '2. ', True),
- (".//h2//span[@class='section-number']", '2.1. ', True),
- (".//h2//span[@class='section-number']", '2.2. ', True),
- (".//h3//span[@class='section-number']", '2.2.1. ', True),
- (".//div[@class='sphinxsidebarwrapper']//li/a", '2. Bar', True),
- (".//div[@class='sphinxsidebarwrapper']//li/a", '2.1. Bar A', True),
- (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2. Bar B', True),
- (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2.1. Bar B1', False),
- ],
- 'baz.html': [
- (".//h1", 'Baz A', True),
- (".//h1//span[@class='section-number']", '2.1.1. ', True),
- ],
-}))
-@pytest.mark.sphinx('html', testroot='tocdepth')
-@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
-def test_tocdepth(app, cached_etree_parse, fname, expect):
- app.build()
- # issue #1251
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
- (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
- (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
- (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
-
- # index.rst
- (".//h1", 'test-tocdepth', True),
-
- # foo.rst
- (".//h2", 'Foo', True),
- (".//h3", 'Foo A', True),
- (".//h4", 'Foo A1', True),
- (".//h3", 'Foo B', True),
- (".//h4", 'Foo B1', True),
- (".//h2//span[@class='section-number']", '1. ', True),
- (".//h3//span[@class='section-number']", '1.1. ', True),
- (".//h4//span[@class='section-number']", '1.1.1. ', True),
- (".//h3//span[@class='section-number']", '1.2. ', True),
- (".//h4//span[@class='section-number']", '1.2.1. ', True),
-
- # bar.rst
- (".//h2", 'Bar', True),
- (".//h3", 'Bar A', True),
- (".//h3", 'Bar B', True),
- (".//h4", 'Bar B1', True),
- (".//h2//span[@class='section-number']", '2. ', True),
- (".//h3//span[@class='section-number']", '2.1. ', True),
- (".//h3//span[@class='section-number']", '2.2. ', True),
- (".//h4//span[@class='section-number']", '2.2.1. ', True),
-
- # baz.rst
- (".//h4", 'Baz A', True),
- (".//h4//span[@class='section-number']", '2.1.1. ', True),
- ],
-}))
-@pytest.mark.sphinx('singlehtml', testroot='tocdepth')
-@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
-def test_tocdepth_singlehtml(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', testroot='numfig')
-@pytest.mark.test_params(shared_result='test_build_html_numfig')
-def test_numfig_disabled_warn(app, warning):
- app.build()
- warnings = warning.getvalue()
- assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' in warnings
- assert 'index.rst:56: WARNING: invalid numfig_format: invalid' not in warnings
- assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' not in warnings
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
- (".//table/caption/span[@class='caption-number']", None, True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", None, True),
- (".//li/p/code/span", '^fig1$', True),
- (".//li/p/code/span", '^Figure%s$', True),
- (".//li/p/code/span", '^table-1$', True),
- (".//li/p/code/span", '^Table:%s$', True),
- (".//li/p/code/span", '^CODE_1$', True),
- (".//li/p/code/span", '^Code-%s$', True),
- (".//li/p/a/span", '^Section 1$', True),
- (".//li/p/a/span", '^Section 2.1$', True),
- (".//li/p/code/span", '^Fig.{number}$', True),
- (".//li/p/a/span", '^Sect.1 Foo$', True),
- ],
- 'foo.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
- (".//table/caption/span[@class='caption-number']", None, True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", None, True),
- ],
- 'bar.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
- (".//table/caption/span[@class='caption-number']", None, True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", None, True),
- ],
- 'baz.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
- (".//table/caption/span[@class='caption-number']", None, True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", None, True),
- ],
-}))
-@pytest.mark.sphinx('html', testroot='numfig')
-@pytest.mark.test_params(shared_result='test_build_html_numfig')
-def test_numfig_disabled(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx(
- 'html', testroot='numfig',
- srcdir='test_numfig_without_numbered_toctree_warn',
- confoverrides={'numfig': True})
-def test_numfig_without_numbered_toctree_warn(app, warning):
- app.build()
- # remove :numbered: option
- index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
- index = re.sub(':numbered:.*', '', index)
- (app.srcdir / 'index.rst').write_text(index, encoding='utf8')
- app.build()
-
- warnings = warning.getvalue()
- assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
- assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
- assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
- assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 9 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 10 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 9 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 10 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 9 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 10 $', True),
- (".//li/p/a/span", '^Fig. 9$', True),
- (".//li/p/a/span", '^Figure6$', True),
- (".//li/p/a/span", '^Table 9$', True),
- (".//li/p/a/span", '^Table:6$', True),
- (".//li/p/a/span", '^Listing 9$', True),
- (".//li/p/a/span", '^Code-6$', True),
- (".//li/p/code/span", '^foo$', True),
- (".//li/p/code/span", '^bar_a$', True),
- (".//li/p/a/span", '^Fig.9 should be Fig.1$', True),
- (".//li/p/code/span", '^Sect.{number}$', True),
- ],
- 'foo.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 4 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 4 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 4 $', True),
- ],
- 'bar.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 5 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 7 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 8 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 5 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 7 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 8 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 5 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 7 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 8 $', True),
- ],
- 'baz.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 6 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 6 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 6 $', True),
- ],
-}))
-@pytest.mark.sphinx(
- 'html', testroot='numfig',
- srcdir='test_numfig_without_numbered_toctree',
- confoverrides={'numfig': True})
-def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, expect):
- # remove :numbered: option
- index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
- index = re.sub(':numbered:.*', '', index)
- (app.srcdir / 'index.rst').write_text(index, encoding='utf8')
-
- if not os.listdir(app.outdir):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
-@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
-def test_numfig_with_numbered_toctree_warn(app, warning):
- app.build()
- warnings = warning.getvalue()
- assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
- assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
- assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
- assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2 $', True),
- (".//li/p/a/span", '^Fig. 1$', True),
- (".//li/p/a/span", '^Figure2.2$', True),
- (".//li/p/a/span", '^Table 1$', True),
- (".//li/p/a/span", '^Table:2.2$', True),
- (".//li/p/a/span", '^Listing 1$', True),
- (".//li/p/a/span", '^Code-2.2$', True),
- (".//li/p/a/span", '^Section.1$', True),
- (".//li/p/a/span", '^Section.2.1$', True),
- (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
- (".//li/p/a/span", '^Sect.1 Foo$', True),
- ],
- 'foo.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.4 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.4 $', True),
- ],
- 'bar.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.4 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.4 $', True),
- ],
- 'baz.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.2 $', True),
- ],
-}))
-@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
-@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
-def test_numfig_with_numbered_toctree(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', testroot='numfig', confoverrides={
- 'numfig': True,
- 'numfig_format': {'figure': 'Figure:%s',
- 'table': 'Tab_%s',
- 'code-block': 'Code-%s',
- 'section': 'SECTION-%s'}})
-@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
-def test_numfig_with_prefix_warn(app, warning):
- app.build()
- warnings = warning.getvalue()
- assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
- assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
- assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
- assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-2 $', True),
- (".//li/p/a/span", '^Figure:1$', True),
- (".//li/p/a/span", '^Figure2.2$', True),
- (".//li/p/a/span", '^Tab_1$', True),
- (".//li/p/a/span", '^Table:2.2$', True),
- (".//li/p/a/span", '^Code-1$', True),
- (".//li/p/a/span", '^Code-2.2$', True),
- (".//li/p/a/span", '^SECTION-1$', True),
- (".//li/p/a/span", '^SECTION-2.1$', True),
- (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
- (".//li/p/a/span", '^Sect.1 Foo$', True),
- ],
- 'foo.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.2 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.4 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_1.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_1.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_1.3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_1.4 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-1.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-1.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-1.3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-1.4 $', True),
- ],
- 'bar.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.4 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_2.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_2.3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_2.4 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-2.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-2.3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-2.4 $', True),
- ],
- 'baz.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Tab_2.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Code-2.2 $', True),
- ],
-}))
-@pytest.mark.sphinx('html', testroot='numfig',
- confoverrides={'numfig': True,
- 'numfig_format': {'figure': 'Figure:%s',
- 'table': 'Tab_%s',
- 'code-block': 'Code-%s',
- 'section': 'SECTION-%s'}})
-@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
-def test_numfig_with_prefix(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', testroot='numfig',
- confoverrides={'numfig': True, 'numfig_secnum_depth': 2})
-@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
-def test_numfig_with_secnum_depth_warn(app, warning):
- app.build()
- warnings = warning.getvalue()
- assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
- assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
- assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
- assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2 $', True),
- (".//li/p/a/span", '^Fig. 1$', True),
- (".//li/p/a/span", '^Figure2.1.2$', True),
- (".//li/p/a/span", '^Table 1$', True),
- (".//li/p/a/span", '^Table:2.1.2$', True),
- (".//li/p/a/span", '^Listing 1$', True),
- (".//li/p/a/span", '^Code-2.1.2$', True),
- (".//li/p/a/span", '^Section.1$', True),
- (".//li/p/a/span", '^Section.2.1$', True),
- (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
- (".//li/p/a/span", '^Sect.1 Foo$', True),
- ],
- 'foo.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.2 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.1.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.1.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.2.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.1.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.1.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.2.1 $', True),
- ],
- 'bar.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.1.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.1.3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.2.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.1.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.1.3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.2.1 $', True),
- ],
- 'baz.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.1.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.1.2 $', True),
- ],
-}))
-@pytest.mark.sphinx('html', testroot='numfig',
- confoverrides={'numfig': True,
- 'numfig_secnum_depth': 2})
-@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
-def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2 $', True),
- (".//li/p/a/span", '^Fig. 1$', True),
- (".//li/p/a/span", '^Figure2.2$', True),
- (".//li/p/a/span", '^Table 1$', True),
- (".//li/p/a/span", '^Table:2.2$', True),
- (".//li/p/a/span", '^Listing 1$', True),
- (".//li/p/a/span", '^Code-2.2$', True),
- (".//li/p/a/span", '^Section.1$', True),
- (".//li/p/a/span", '^Section.2.1$', True),
- (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
- (".//li/p/a/span", '^Sect.1 Foo$', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 1.4 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 1.4 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.1 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.3 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.4 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.1 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.3 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.4 $', True),
- (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
- (".//table/caption/span[@class='caption-number']",
- '^Table 2.2 $', True),
- (".//div[@class='code-block-caption']/"
- "span[@class='caption-number']", '^Listing 2.2 $', True),
- ],
-}))
-@pytest.mark.sphinx('singlehtml', testroot='numfig', confoverrides={'numfig': True})
-@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
-def test_numfig_with_singlehtml(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 1", True),
- (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 2", True),
- (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 3", True),
- (".//div//span[@class='caption-number']", "No.1 ", True),
- (".//div//span[@class='caption-number']", "No.2 ", True),
- (".//li/p/a/span", 'Fig. 1', True),
- (".//li/p/a/span", 'Fig. 2', True),
- (".//li/p/a/span", 'Fig. 3', True),
- (".//li/p/a/span", 'No.1', True),
- (".//li/p/a/span", 'No.2', True),
- ],
-}))
-@pytest.mark.sphinx('html', testroot='add_enumerable_node',
- srcdir='test_enumerable_node')
-def test_enumerable_node(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', testroot='html_assets')
-def test_html_assets(app):
- app.builder.build_all()
-
- # exclude_path and its family
- assert not (app.outdir / 'static' / 'index.html').exists()
- assert not (app.outdir / 'extra' / 'index.html').exists()
-
- # html_static_path
- assert not (app.outdir / '_static' / '.htaccess').exists()
- assert not (app.outdir / '_static' / '.htpasswd').exists()
- assert (app.outdir / '_static' / 'API.html').exists()
- assert (app.outdir / '_static' / 'API.html').read_text(encoding='utf8') == 'Sphinx-1.4.4'
- assert (app.outdir / '_static' / 'css' / 'style.css').exists()
- assert (app.outdir / '_static' / 'js' / 'custom.js').exists()
- assert (app.outdir / '_static' / 'rimg.png').exists()
- assert not (app.outdir / '_static' / '_build' / 'index.html').exists()
- assert (app.outdir / '_static' / 'background.png').exists()
- assert not (app.outdir / '_static' / 'subdir' / '.htaccess').exists()
- assert not (app.outdir / '_static' / 'subdir' / '.htpasswd').exists()
-
- # html_extra_path
- assert (app.outdir / '.htaccess').exists()
- assert not (app.outdir / '.htpasswd').exists()
- assert (app.outdir / 'API.html_t').exists()
- assert (app.outdir / 'css/style.css').exists()
- assert (app.outdir / 'rimg.png').exists()
- assert not (app.outdir / '_build' / 'index.html').exists()
- assert (app.outdir / 'background.png').exists()
- assert (app.outdir / 'subdir' / '.htaccess').exists()
- assert not (app.outdir / 'subdir' / '.htpasswd').exists()
-
- # html_css_files
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content
- assert ('<link media="print" rel="stylesheet" title="title" type="text/css" '
- 'href="https://example.com/custom.css" />' in content)
-
- # html_js_files
- assert '<script src="_static/js/custom.js"></script>' in content
- assert ('<script async="async" src="https://example.com/script.js">'
- '</script>' in content)
-
-
-@pytest.mark.sphinx('html', testroot='html_assets')
-def test_assets_order(app, monkeypatch):
- monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '')
-
- app.add_css_file('normal.css')
- app.add_css_file('early.css', priority=100)
- app.add_css_file('late.css', priority=750)
- app.add_css_file('lazy.css', priority=900)
- app.add_js_file('normal.js')
- app.add_js_file('early.js', priority=100)
- app.add_js_file('late.js', priority=750)
- app.add_js_file('lazy.js', priority=900)
-
- app.builder.build_all()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- # css_files
- expected = [
- '_static/early.css',
- '_static/pygments.css',
- '_static/alabaster.css',
- 'https://example.com/custom.css',
- '_static/normal.css',
- '_static/late.css',
- '_static/css/style.css',
- '_static/lazy.css',
- ]
- pattern = '.*'.join(f'href="{re.escape(f)}"' for f in expected)
- assert re.search(pattern, content, re.DOTALL), content
-
- # js_files
- expected = [
- '_static/early.js',
- '_static/doctools.js',
- '_static/sphinx_highlight.js',
- 'https://example.com/script.js',
- '_static/normal.js',
- '_static/late.js',
- '_static/js/custom.js',
- '_static/lazy.js',
- ]
- pattern = '.*'.join(f'src="{re.escape(f)}"' for f in expected)
- assert re.search(pattern, content, re.DOTALL), content
-
-
-@pytest.mark.sphinx('html', testroot='html_file_checksum')
-def test_file_checksum(app):
- app.add_css_file('stylesheet-a.css')
- app.add_css_file('stylesheet-b.css')
- app.add_css_file('https://example.com/custom.css')
- app.add_js_file('script.js')
- app.add_js_file('empty.js')
- app.add_js_file('https://example.com/script.js')
-
- app.builder.build_all()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- # checksum for local files
- assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-a.css?v=e575b6df" />' in content
- assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-b.css?v=a2d5cc0f" />' in content
- assert '<script src="_static/script.js?v=48278d48"></script>' in content
-
- # empty files have no checksum
- assert '<script src="_static/empty.js"></script>' in content
-
- # no checksum for hyperlinks
- assert '<link rel="stylesheet" type="text/css" href="https://example.com/custom.css" />' in content
- assert '<script src="https://example.com/script.js"></script>' in content
-
-
-def test_file_checksum_query_string():
- with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
- _file_checksum(Path(), 'with_query_string.css?dead_parrots=1')
-
- with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
- _file_checksum(Path(), 'with_query_string.js?dead_parrots=1')
-
- with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
- _file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1')
-
- with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
- _file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1')
-
-
-@pytest.mark.sphinx('html', testroot='html_assets')
-def test_javscript_loading_method(app):
- app.add_js_file('normal.js')
- app.add_js_file('early.js', loading_method='async')
- app.add_js_file('late.js', loading_method='defer')
-
- app.builder.build_all()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- assert '<script src="_static/normal.js"></script>' in content
- assert '<script async="async" src="_static/early.js"></script>' in content
- assert '<script defer="defer" src="_static/late.js"></script>' in content
-
-
-@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False})
-def test_html_copy_source(app):
- app.builder.build_all()
- assert not (app.outdir / '_sources' / 'index.rst.txt').exists()
-
-
-@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.txt'})
-def test_html_sourcelink_suffix(app):
- app.builder.build_all()
- assert (app.outdir / '_sources' / 'index.rst.txt').exists()
-
-
-@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.rst'})
-def test_html_sourcelink_suffix_same(app):
- app.builder.build_all()
- assert (app.outdir / '_sources' / 'index.rst').exists()
-
-
-@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': ''})
-def test_html_sourcelink_suffix_empty(app):
- app.builder.build_all()
- assert (app.outdir / '_sources' / 'index.rst').exists()
-
-
-@pytest.mark.sphinx('html', testroot='html_entity')
-def test_html_entity(app):
- app.builder.build_all()
- valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'}
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
- for entity in re.findall(r'&([a-z]+);', content, re.M):
- assert entity not in valid_entities
-
-
-@pytest.mark.sphinx('html', testroot='basic')
-def test_html_inventory(app):
- app.builder.build_all()
-
- with app.outdir.joinpath('objects.inv').open('rb') as f:
- invdata = InventoryFile.load(f, 'https://www.google.com', posixpath.join)
-
- assert set(invdata.keys()) == {'std:label', 'std:doc'}
- assert set(invdata['std:label'].keys()) == {'modindex',
- 'py-modindex',
- 'genindex',
- 'search'}
- assert invdata['std:label']['modindex'] == ('Python',
- '',
- 'https://www.google.com/py-modindex.html',
- 'Module Index')
- assert invdata['std:label']['py-modindex'] == ('Python',
- '',
- 'https://www.google.com/py-modindex.html',
- 'Python Module Index')
- assert invdata['std:label']['genindex'] == ('Python',
- '',
- 'https://www.google.com/genindex.html',
- 'Index')
- assert invdata['std:label']['search'] == ('Python',
- '',
- 'https://www.google.com/search.html',
- 'Search Page')
- assert set(invdata['std:doc'].keys()) == {'index'}
- assert invdata['std:doc']['index'] == ('Python',
- '',
- 'https://www.google.com/index.html',
- 'The basic Sphinx documentation for testing')
-
-
-@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''})
-def test_html_anchor_for_figure(app):
- app.builder.build_all()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<figcaption>\n<p><span class="caption-text">The caption of pic</span>'
- '<a class="headerlink" href="#id1" title="Link to this image">¶</a></p>\n</figcaption>'
- in content)
-
-
-@pytest.mark.sphinx('html', testroot='directives-raw')
-def test_html_raw_directive(app, status, warning):
- app.builder.build_all()
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- # standard case
- assert 'standalone raw directive (HTML)' in result
- assert 'standalone raw directive (LaTeX)' not in result
-
- # with substitution
- assert '<p>HTML: abc def ghi</p>' in result
- assert '<p>LaTeX: abc ghi</p>' in result
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [
- (".//link[@href='_static/persistent.css']"
- "[@rel='stylesheet']", '', True),
- (".//link[@href='_static/default.css']"
- "[@rel='stylesheet']"
- "[@title='Default']", '', True),
- (".//link[@href='_static/alternate1.css']"
- "[@rel='alternate stylesheet']"
- "[@title='Alternate']", '', True),
- (".//link[@href='_static/alternate2.css']"
- "[@rel='alternate stylesheet']", '', True),
- (".//link[@href='_static/more_persistent.css']"
- "[@rel='stylesheet']", '', True),
- (".//link[@href='_static/more_default.css']"
- "[@rel='stylesheet']"
- "[@title='Default']", '', True),
- (".//link[@href='_static/more_alternate1.css']"
- "[@rel='alternate stylesheet']"
- "[@title='Alternate']", '', True),
- (".//link[@href='_static/more_alternate2.css']"
- "[@rel='alternate stylesheet']", '', True),
- ],
-}))
-@pytest.mark.sphinx('html', testroot='stylesheets')
-def test_alternate_stylesheets(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', testroot='html_style')
-def test_html_style(app, status, warning):
- app.build()
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert '<link rel="stylesheet" type="text/css" href="_static/default.css" />' in result
- assert ('<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />'
- not in result)
-
-
-@pytest.mark.sphinx('html', testroot='images')
-def test_html_remote_images(app, status, warning):
- app.builder.build_all()
-
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<img alt="https://www.python.org/static/img/python-logo.png" '
- 'src="https://www.python.org/static/img/python-logo.png" />' in result)
- assert not (app.outdir / 'python-logo.png').exists()
-
-
-@pytest.mark.sphinx('html', testroot='image-escape')
-def test_html_encoded_image(app, status, warning):
- app.builder.build_all()
-
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<img alt="_images/img_%231.png" src="_images/img_%231.png" />' in result)
- assert (app.outdir / '_images/img_#1.png').exists()
-
-
-@pytest.mark.sphinx('html', testroot='remote-logo')
-def test_html_remote_logo(app, status, warning):
- app.builder.build_all()
-
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo"/>' in result)
- assert ('<link rel="icon" href="https://www.python.org/static/favicon.ico"/>' in result)
- assert not (app.outdir / 'python-logo.png').exists()
-
-
-@pytest.mark.sphinx('html', testroot='local-logo')
-def test_html_local_logo(app, status, warning):
- app.builder.build_all()
-
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<img class="logo" src="_static/img.png" alt="Logo"/>' in result)
- assert (app.outdir / '_static/img.png').exists()
-
-
-@pytest.mark.sphinx('html', testroot='basic')
-def test_html_sidebar(app, status, warning):
- ctx = {}
-
- # default for alabaster
- app.builder.build_all()
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<div class="sphinxsidebar" role="navigation" '
- 'aria-label="main navigation">' in result)
- assert '<h1 class="logo"><a href="#">Python</a></h1>' in result
- assert '<h3>Navigation</h3>' in result
- assert '<h3>Related Topics</h3>' in result
- assert '<h3 id="searchlabel">Quick search</h3>' in result
-
- app.builder.add_sidebars('index', ctx)
- assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html',
- 'searchbox.html', 'donate.html']
-
- # only relations.html
- app.config.html_sidebars = {'**': ['relations.html']}
- app.builder.build_all()
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<div class="sphinxsidebar" role="navigation" '
- 'aria-label="main navigation">' in result)
- assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
- assert '<h3>Navigation</h3>' not in result
- assert '<h3>Related Topics</h3>' in result
- assert '<h3 id="searchlabel">Quick search</h3>' not in result
-
- app.builder.add_sidebars('index', ctx)
- assert ctx['sidebars'] == ['relations.html']
-
- # no sidebars
- app.config.html_sidebars = {'**': []}
- app.builder.build_all()
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<div class="sphinxsidebar" role="navigation" '
- 'aria-label="main navigation">' not in result)
- assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
- assert '<h3>Navigation</h3>' not in result
- assert '<h3>Related Topics</h3>' not in result
- assert '<h3 id="searchlabel">Quick search</h3>' not in result
-
- app.builder.add_sidebars('index', ctx)
- assert ctx['sidebars'] == []
-
-
-@pytest.mark.parametrize(("fname", "expect"), flat_dict({
- 'index.html': [(".//em/a[@href='https://example.com/man.1']", "", True),
- (".//em/a[@href='https://example.com/ls.1']", "", True),
- (".//em/a[@href='https://example.com/sphinx.']", "", True)],
-
-}))
-@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={
- 'manpages_url': 'https://example.com/{page}.{section}'})
-@pytest.mark.test_params(shared_result='test_build_html_manpage_url')
-def test_html_manpage(app, cached_etree_parse, fname, expect):
- app.build()
- check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
-
-
-@pytest.mark.sphinx('html', testroot='toctree-glob',
- confoverrides={'html_baseurl': 'https://example.com/'})
-def test_html_baseurl(app, status, warning):
- app.build()
-
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert '<link rel="canonical" href="https://example.com/index.html" />' in result
-
- result = (app.outdir / 'qux' / 'index.html').read_text(encoding='utf8')
- assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result
-
-
-@pytest.mark.sphinx('html', testroot='toctree-glob',
- confoverrides={'html_baseurl': 'https://example.com/subdir',
- 'html_file_suffix': '.htm'})
-def test_html_baseurl_and_html_file_suffix(app, status, warning):
- app.build()
-
- result = (app.outdir / 'index.htm').read_text(encoding='utf8')
- assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result
-
- result = (app.outdir / 'qux' / 'index.htm').read_text(encoding='utf8')
- assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result
-
-
-@pytest.mark.sphinx('html', testroot='basic')
-def test_default_html_math_renderer(app, status, warning):
- assert app.builder.math_renderer_name == 'mathjax'
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'extensions': ['sphinx.ext.mathjax']})
-def test_html_math_renderer_is_mathjax(app, status, warning):
- assert app.builder.math_renderer_name == 'mathjax'
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'extensions': ['sphinx.ext.imgmath']})
-def test_html_math_renderer_is_imgmath(app, status, warning):
- assert app.builder.math_renderer_name == 'imgmath'
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'extensions': ['sphinxcontrib.jsmath',
- 'sphinx.ext.imgmath']})
-def test_html_math_renderer_is_duplicated(make_app, app_params):
- args, kwargs = app_params
- with pytest.raises(
- ConfigError,
- match='Many math_renderers are registered. But no math_renderer is selected.',
- ):
- make_app(*args, **kwargs)
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'extensions': ['sphinx.ext.imgmath',
- 'sphinx.ext.mathjax']})
-def test_html_math_renderer_is_duplicated2(app, status, warning):
- # case of both mathjax and another math_renderer is loaded
- assert app.builder.math_renderer_name == 'imgmath' # The another one is chosen
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'extensions': ['sphinxcontrib.jsmath',
- 'sphinx.ext.imgmath'],
- 'html_math_renderer': 'imgmath'})
-def test_html_math_renderer_is_chosen(app, status, warning):
- assert app.builder.math_renderer_name == 'imgmath'
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'extensions': ['sphinxcontrib.jsmath',
- 'sphinx.ext.mathjax'],
- 'html_math_renderer': 'imgmath'})
-def test_html_math_renderer_is_mismatched(make_app, app_params):
- args, kwargs = app_params
- with pytest.raises(ConfigError, match="Unknown math_renderer 'imgmath' is given."):
- make_app(*args, **kwargs)
-
-
-@pytest.mark.sphinx('html', testroot='basic')
-def test_html_pygments_style_default(app):
- style = app.builder.highlighter.formatter_args.get('style')
- assert style.__name__ == 'Alabaster'
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'pygments_style': 'sphinx'})
-def test_html_pygments_style_manually(app):
- style = app.builder.highlighter.formatter_args.get('style')
- assert style.__name__ == 'SphinxStyle'
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'html_theme': 'classic'})
-def test_html_pygments_for_classic_theme(app):
- style = app.builder.highlighter.formatter_args.get('style')
- assert style.__name__ == 'SphinxStyle'
-
-
-@pytest.mark.sphinx('html', testroot='basic')
-def test_html_dark_pygments_style_default(app):
- assert app.builder.dark_highlighter is None
-
-
-@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path')
-def test_validate_html_extra_path(app):
- (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
- app.config.html_extra_path = [
- '/path/to/not_found', # not found
- '_static',
- app.outdir, # outdir
- app.outdir / '_static', # inside outdir
- ]
- validate_html_extra_path(app, app.config)
- assert app.config.html_extra_path == ['_static']
-
-
-@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path')
-def test_validate_html_static_path(app):
- (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
- app.config.html_static_path = [
- '/path/to/not_found', # not found
- '_static',
- app.outdir, # outdir
- app.outdir / '_static', # inside outdir
- ]
- validate_html_static_path(app, app.config)
- assert app.config.html_static_path == ['_static']
-
-
-@pytest.mark.sphinx(testroot='html_scaled_image_link')
-def test_html_scaled_image_link(app):
- app.build()
- context = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- # no scaled parameters
- assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context)
-
- # scaled_image_link
- assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">'
- '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" /></a>',
- context)
-
- # no-scaled-link class disables the feature
- assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"'
- ' src="_images/img.png" style="[^"]+" />',
- context)
-
-
-@pytest.mark.sphinx('html', testroot='reST-code-block',
- confoverrides={'html_codeblock_linenos_style': 'table'})
-def test_html_codeblock_linenos_style_table(app):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- assert ('<div class="linenodiv"><pre><span class="normal">1</span>\n'
- '<span class="normal">2</span>\n'
- '<span class="normal">3</span>\n'
- '<span class="normal">4</span></pre></div>') in content
-
-
-@pytest.mark.sphinx('html', testroot='reST-code-block',
- confoverrides={'html_codeblock_linenos_style': 'inline'})
-def test_html_codeblock_linenos_style_inline(app):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- assert '<span class="linenos">1</span>' in content
-
-
-@pytest.mark.sphinx('html', testroot='highlight_options')
-def test_highlight_options(app):
- subject = app.builder.highlighter
- with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
- app.build()
-
- call_args = highlight.call_args_list
- assert len(call_args) == 3
- assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
- location=ANY, opts={'default_option': True})
- assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
- location=ANY, opts={'python_option': True})
- assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
- location=ANY, opts={})
-
-
-@pytest.mark.sphinx('html', testroot='highlight_options',
- confoverrides={'highlight_options': {'default_option': True}})
-def test_highlight_options_old(app):
- subject = app.builder.highlighter
- with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
- app.build()
-
- call_args = highlight.call_args_list
- assert len(call_args) == 3
- assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
- location=ANY, opts={'default_option': True})
- assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
- location=ANY, opts={})
- assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
- location=ANY, opts={})
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'html_permalinks': False})
-def test_html_permalink_disable(app):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- assert '<h1>The basic Sphinx documentation for testing</h1>' in content
-
-
-@pytest.mark.sphinx('html', testroot='basic',
- confoverrides={'html_permalinks_icon': '<span>[PERMALINK]</span>'})
-def test_html_permalink_icon(app):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- assert ('<h1>The basic Sphinx documentation for testing<a class="headerlink" '
- 'href="#the-basic-sphinx-documentation-for-testing" '
- 'title="Link to this heading"><span>[PERMALINK]</span></a></h1>' in content)
-
-
-@pytest.mark.sphinx('html', testroot='html_signaturereturn_icon')
-def test_html_signaturereturn_icon(app):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- assert ('<span class="sig-return-icon">&#x2192;</span>' in content)
-
-
-@pytest.mark.sphinx('html', testroot='reST-code-role')
-def test_html_code_role(app):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
-
- common_content = (
- '<span class="k">def</span> <span class="nf">foo</span>'
- '<span class="p">(</span>'
- '<span class="mi">1</span> '
- '<span class="o">+</span> '
- '<span class="mi">2</span> '
- '<span class="o">+</span> '
- '<span class="kc">None</span> '
- '<span class="o">+</span> '
- '<span class="s2">&quot;abc&quot;</span>'
- '<span class="p">):</span> '
- '<span class="k">pass</span>')
- assert ('<p>Inline <code class="code highlight python docutils literal highlight-python">' +
- common_content + '</code> code block</p>') in content
- assert ('<div class="highlight-python notranslate">' +
- '<div class="highlight"><pre><span></span>' +
- common_content) in content
-
-
-@pytest.mark.sphinx('html', testroot='root',
- confoverrides={'option_emphasise_placeholders': True})
-def test_option_emphasise_placeholders(app, status, warning):
- app.build()
- content = (app.outdir / 'objects.html').read_text(encoding='utf8')
- assert '<em><span class="pre">TYPE</span></em>' in content
- assert '{TYPE}' not in content
- assert ('<em><span class="pre">WHERE</span></em>'
- '<span class="pre">-</span>'
- '<em><span class="pre">COUNT</span></em>' in content)
- assert '<span class="pre">{{value}}</span>' in content
- assert ('<span class="pre">--plugin.option</span></span>'
- '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content
-
-
-@pytest.mark.sphinx('html', testroot='root')
-def test_option_emphasise_placeholders_default(app, status, warning):
- app.build()
- content = (app.outdir / 'objects.html').read_text(encoding='utf8')
- assert '<span class="pre">={TYPE}</span>' in content
- assert '<span class="pre">={WHERE}-{COUNT}</span></span>' in content
- assert '<span class="pre">{client_name}</span>' in content
- assert ('<span class="pre">--plugin.option</span></span>'
- '<span class="sig-prename descclassname"></span>'
- '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content
-
-
-@pytest.mark.sphinx('html', testroot='root')
-def test_option_reference_with_value(app, status, warning):
- app.build()
- content = (app.outdir / 'objects.html').read_text(encoding='utf-8')
- assert ('<span class="pre">-mapi</span></span><span class="sig-prename descclassname">'
- '</span><a class="headerlink" href="#cmdoption-git-commit-mapi"') in content
- assert 'first option <a class="reference internal" href="#cmdoption-git-commit-mapi">' in content
- assert ('<a class="reference internal" href="#cmdoption-git-commit-mapi">'
- '<code class="xref std std-option docutils literal notranslate"><span class="pre">-mapi[=xxx]</span></code></a>') in content
- assert '<span class="pre">-mapi</span> <span class="pre">with_space</span>' in content
-
-
-@pytest.mark.sphinx('html', testroot='theming')
-def test_theme_options(app, status, warning):
- app.build()
-
- result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8')
- assert 'NAVIGATION_WITH_KEYS: false' in result
- assert 'ENABLE_SEARCH_SHORTCUTS: true' in result
-
-
-@pytest.mark.sphinx('html', testroot='theming',
- confoverrides={'html_theme_options.navigation_with_keys': True,
- 'html_theme_options.enable_search_shortcuts': False})
-def test_theme_options_with_override(app, status, warning):
- app.build()
-
- result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8')
- assert 'NAVIGATION_WITH_KEYS: true' in result
- assert 'ENABLE_SEARCH_SHORTCUTS: false' in result
-
-
-@pytest.mark.sphinx('html', testroot='build-html-theme-having-multiple-stylesheets')
-def test_theme_having_multiple_stylesheets(app):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf-8')
-
- assert '<link rel="stylesheet" type="text/css" href="_static/mytheme.css" />' in content
- assert '<link rel="stylesheet" type="text/css" href="_static/extra.css" />' in content
-
-
-@pytest.mark.sphinx('html', testroot='images')
-def test_copy_images(app, status, warning):
- app.build()
-
- images_dir = Path(app.outdir) / '_images'
- images = {image.name for image in images_dir.rglob('*')}
- assert images == {
- 'img.png',
- 'rimg.png',
- 'rimg1.png',
- 'svgimg.svg',
- 'testimäge.png',
- }
diff --git a/tests/test_builders/__init__.py b/tests/test_builders/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_builders/__init__.py
diff --git a/tests/test_builders/conftest.py b/tests/test_builders/conftest.py
new file mode 100644
index 0000000..1203d5d
--- /dev/null
+++ b/tests/test_builders/conftest.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+from sphinx.testing.util import etree_parse
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator
+ from pathlib import Path
+ from xml.etree.ElementTree import ElementTree
+
+_etree_cache: dict[Path, ElementTree] = {}
+
+
+def _parse(path: Path) -> ElementTree:
+ if path in _etree_cache:
+ return _etree_cache[path]
+
+ _etree_cache[path] = tree = etree_parse(path)
+ return tree
+
+
+@pytest.fixture(scope='package')
+def cached_etree_parse() -> Iterator[Callable[[Path], ElementTree]]:
+ yield _parse
+ _etree_cache.clear()
diff --git a/tests/test_build.py b/tests/test_builders/test_build.py
index ed4bc43..3f6d12c 100644
--- a/tests/test_build.py
+++ b/tests/test_builders/test_build.py
@@ -2,13 +2,17 @@
import os
import shutil
+from contextlib import contextmanager
from unittest import mock
import pytest
from docutils import nodes
+from sphinx.cmd.build import build_main
from sphinx.errors import SphinxError
+from tests.utils import TESTS_ROOT
+
def request_session_head(url, **kwargs):
response = mock.Mock()
@@ -60,12 +64,12 @@ def test_root_doc_not_found(tmp_path, make_app):
app = make_app('dummy', srcdir=tmp_path)
with pytest.raises(SphinxError):
- app.builder.build_all() # no index.rst
+ app.build(force_all=True) # no index.rst
@pytest.mark.sphinx(buildername='text', testroot='circular')
def test_circular_toctree(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
warnings = warning.getvalue()
assert (
'circular toctree references detected, ignoring: '
@@ -77,7 +81,7 @@ def test_circular_toctree(app, status, warning):
@pytest.mark.sphinx(buildername='text', testroot='numbered-circular')
def test_numbered_circular_toctree(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
warnings = warning.getvalue()
assert (
'circular toctree references detected, ignoring: '
@@ -89,7 +93,7 @@ def test_numbered_circular_toctree(app, status, warning):
@pytest.mark.sphinx(buildername='dummy', testroot='images')
def test_image_glob(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
# index.rst
doctree = app.env.get_doctree('index')
@@ -133,3 +137,29 @@ def test_image_glob(app, status, warning):
assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf',
'image/svg+xml': 'subdir/svgimg.svg'}
assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*'
+
+
+@contextmanager
+def force_colors():
+ forcecolor = os.environ.get('FORCE_COLOR', None)
+
+ try:
+ os.environ['FORCE_COLOR'] = '1'
+ yield
+ finally:
+ if forcecolor is None:
+ os.environ.pop('FORCE_COLOR', None)
+ else:
+ os.environ['FORCE_COLOR'] = forcecolor
+
+
+def test_log_no_ansi_colors(tmp_path):
+ with force_colors():
+ wfile = tmp_path / 'warnings.txt'
+ srcdir = TESTS_ROOT / 'roots' / 'test-nitpicky-warnings'
+ argv = list(map(str, ['-b', 'html', srcdir, tmp_path, '-n', '-w', wfile]))
+ retcode = build_main(argv)
+ assert retcode == 0
+
+ content = wfile.read_text(encoding='utf8')
+ assert '\x1b[91m' not in content
diff --git a/tests/test_build_changes.py b/tests/test_builders/test_build_changes.py
index b340c8d..b537b87 100644
--- a/tests/test_build_changes.py
+++ b/tests/test_builders/test_build_changes.py
@@ -9,7 +9,7 @@ def test_build(app):
# TODO: Use better checking of html content
htmltext = (app.outdir / 'changes.html').read_text(encoding='utf8')
- assert 'New in version 0.6: Some funny stuff.' in htmltext
+ assert 'Added in version 0.6: Some funny stuff.' in htmltext
assert 'Changed in version 0.6: Even more funny stuff.' in htmltext
assert 'Deprecated since version 0.6: Boring stuff.' in htmltext
diff --git a/tests/test_build_dirhtml.py b/tests/test_builders/test_build_dirhtml.py
index dc5ab86..dc5ab86 100644
--- a/tests/test_build_dirhtml.py
+++ b/tests/test_builders/test_build_dirhtml.py
diff --git a/tests/test_build_epub.py b/tests/test_builders/test_build_epub.py
index 7f5b815..6829f22 100644
--- a/tests/test_build_epub.py
+++ b/tests/test_builders/test_build_epub.py
@@ -22,6 +22,7 @@ def runnable(command):
class EPUBElementTree:
"""Test helper for content.opf and toc.ncx"""
+
namespaces = {
'idpf': 'http://www.idpf.org/2007/opf',
'dc': 'http://purl.org/dc/elements/1.1/',
@@ -60,7 +61,7 @@ class EPUBElementTree:
@pytest.mark.sphinx('epub', testroot='basic')
def test_build_epub(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'mimetype').read_text(encoding='utf8') == 'application/epub+zip'
assert (app.outdir / 'META-INF' / 'container.xml').exists()
@@ -277,7 +278,7 @@ def test_escaped_toc(app):
@pytest.mark.sphinx('epub', testroot='basic')
def test_epub_writing_mode(app):
# horizontal (default)
- app.builder.build_all()
+ app.build(force_all=True)
# horizontal / page-progression-direction
opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text(encoding='utf8'))
@@ -323,7 +324,7 @@ def test_epub_anchor_id(app):
@pytest.mark.sphinx('epub', testroot='html_assets')
def test_epub_assets(app):
- app.builder.build_all()
+ app.build(force_all=True)
# epub_sytlesheets (same as html_css_files)
content = (app.outdir / 'index.xhtml').read_text(encoding='utf8')
@@ -336,7 +337,7 @@ def test_epub_assets(app):
@pytest.mark.sphinx('epub', testroot='html_assets',
confoverrides={'epub_css_files': ['css/epub.css']})
def test_epub_css_files(app):
- app.builder.build_all()
+ app.build(force_all=True)
# epub_css_files
content = (app.outdir / 'index.xhtml').read_text(encoding='utf8')
@@ -361,13 +362,13 @@ def test_html_download_role(app, status, warning):
'<span class="pre">not_found.dat</span></code></p></li>' in content)
assert ('<li><p><code class="xref download docutils literal notranslate">'
'<span class="pre">Sphinx</span> <span class="pre">logo</span></code>'
- '<span class="link-target"> [http://www.sphinx-doc.org/en/master'
- '/_static/sphinxheader.png]</span></p></li>' in content)
+ '<span class="link-target"> [https://www.sphinx-doc.org/en/master'
+ '/_static/sphinx-logo.svg]</span></p></li>' in content)
@pytest.mark.sphinx('epub', testroot='toctree-duplicated')
def test_duplicated_toctree_entry(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert 'WARNING: duplicated ToC entry found: foo.xhtml' in warning.getvalue()
@@ -377,16 +378,21 @@ def test_duplicated_toctree_entry(app, status, warning):
def test_run_epubcheck(app):
app.build()
+ if not runnable(['java', '-version']):
+ pytest.skip("Unable to run Java; skipping test")
+
epubcheck = os.environ.get('EPUBCHECK_PATH', '/usr/share/java/epubcheck.jar')
- if runnable(['java', '-version']) and os.path.exists(epubcheck):
- try:
- subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'],
- capture_output=True, check=True)
- except CalledProcessError as exc:
- print(exc.stdout.decode('utf-8'))
- print(exc.stderr.decode('utf-8'))
- msg = f'epubcheck exited with return code {exc.returncode}'
- raise AssertionError(msg) from exc
+ if not os.path.exists(epubcheck):
+ pytest.skip("Could not find epubcheck; skipping test")
+
+ try:
+ subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'],
+ capture_output=True, check=True)
+ except CalledProcessError as exc:
+ print(exc.stdout.decode('utf-8'))
+ print(exc.stderr.decode('utf-8'))
+ msg = f'epubcheck exited with return code {exc.returncode}'
+ raise AssertionError(msg) from exc
def test_xml_name_pattern_check():
diff --git a/tests/test_build_gettext.py b/tests/test_builders/test_build_gettext.py
index 6d9154e..ddc6d30 100644
--- a/tests/test_build_gettext.py
+++ b/tests/test_builders/test_build_gettext.py
@@ -47,7 +47,7 @@ def test_Catalog_duplicated_message():
@pytest.mark.sphinx('gettext', srcdir='root-gettext')
def test_build_gettext(app):
# Generic build; should fail only when the builder is horribly broken.
- app.builder.build_all()
+ app.build(force_all=True)
# Do messages end up in the correct location?
# top-level documents end up in a message catalog
@@ -62,7 +62,7 @@ def test_build_gettext(app):
@pytest.mark.sphinx('gettext', srcdir='root-gettext')
def test_msgfmt(app):
- app.builder.build_all()
+ app.build(force_all=True)
(app.outdir / 'en' / 'LC_MESSAGES').mkdir(parents=True, exist_ok=True)
with chdir(app.outdir):
@@ -102,7 +102,7 @@ def test_msgfmt(app):
confoverrides={'gettext_compact': False})
def test_gettext_index_entries(app):
# regression test for #976
- app.builder.build(['index_entries'])
+ app.build(filenames=[app.srcdir / 'index_entries.txt'])
pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8')
msg_ids = list(filter(None, map(msgid_getter, pot.splitlines())))
@@ -131,7 +131,7 @@ def test_gettext_index_entries(app):
def test_gettext_disable_index_entries(app):
# regression test for #976
app.env._pickled_doctree_cache.clear() # clear cache
- app.builder.build(['index_entries'])
+ app.build(filenames=[app.srcdir / 'index_entries.txt'])
pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8')
msg_ids = list(filter(None, map(msgid_getter, pot.splitlines())))
@@ -147,7 +147,7 @@ def test_gettext_disable_index_entries(app):
@pytest.mark.sphinx('gettext', testroot='intl', srcdir='gettext')
def test_gettext_template(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'sphinx.pot').is_file()
@@ -158,7 +158,7 @@ def test_gettext_template(app):
@pytest.mark.sphinx('gettext', testroot='gettext-template')
def test_gettext_template_msgid_order_in_sphinxpot(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'sphinx.pot').is_file()
result = (app.outdir / 'sphinx.pot').read_text(encoding='utf8')
@@ -175,7 +175,7 @@ def test_gettext_template_msgid_order_in_sphinxpot(app):
'gettext', srcdir='root-gettext',
confoverrides={'gettext_compact': 'documentation'})
def test_build_single_pot(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'documentation.pot').is_file()
@@ -196,7 +196,7 @@ def test_build_single_pot(app):
confoverrides={'gettext_compact': False,
'gettext_additional_targets': ['image']})
def test_gettext_prolog_epilog_substitution(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'prolog_epilog_substitution.pot').is_file()
pot = (app.outdir / 'prolog_epilog_substitution.pot').read_text(encoding='utf8')
@@ -223,7 +223,7 @@ def test_gettext_prolog_epilog_substitution(app):
'gettext_additional_targets': ['image']})
def test_gettext_prolog_epilog_substitution_excluded(app):
# regression test for #9428
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'prolog_epilog_substitution_excluded.pot').is_file()
pot = (app.outdir / 'prolog_epilog_substitution_excluded.pot').read_text(encoding='utf8')
diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py
new file mode 100644
index 0000000..1fa3ba4
--- /dev/null
+++ b/tests/test_builders/test_build_html.py
@@ -0,0 +1,378 @@
+"""Test the HTML builder and check output against XPath."""
+
+import os
+import posixpath
+import re
+
+import pytest
+
+from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
+from sphinx.deprecation import RemovedInSphinx80Warning
+from sphinx.errors import ConfigError
+from sphinx.util.console import strip_colors
+from sphinx.util.inventory import InventoryFile
+
+from tests.test_builders.xpath_data import FIGURE_CAPTION
+from tests.test_builders.xpath_util import check_xpath
+
+
+def test_html4_error(make_app, tmp_path):
+ (tmp_path / 'conf.py').write_text('', encoding='utf-8')
+ with pytest.raises(
+ ConfigError,
+ match='HTML 4 is no longer supported by Sphinx',
+ ):
+ make_app(
+ buildername='html',
+ srcdir=tmp_path,
+ confoverrides={'html4_writer': True},
+ )
+
+
+@pytest.mark.parametrize(("fname", "path", "check"), [
+ ('index.html', ".//div[@class='citation']/span", r'Ref1'),
+ ('index.html', ".//div[@class='citation']/span", r'Ref_1'),
+
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"),
+ ('footnote.html', ".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"),
+ ('footnote.html', ".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id1']", r"1"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id2']", r"2"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id3']", r"3"),
+ ('footnote.html', ".//div[@class='citation']/span/a[@href='#id4']", r"bar"),
+ ('footnote.html', ".//div[@class='citation']/span/a[@href='#id5']", r"baz_qux"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id6']", r"4"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id7']", r"5"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id8']", r"6"),
+])
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_build_html_output_docutils18')
+def test_docutils_output(app, cached_etree_parse, fname, path, check):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check)
+
+
+@pytest.mark.sphinx('html', parallel=2)
+def test_html_parallel(app):
+ app.build()
+
+
+@pytest.mark.sphinx('html', testroot='build-html-translator')
+def test_html_translator(app):
+ app.build()
+ assert app.builder.docwriter.visitor.depart_with_node == 10
+
+
+@pytest.mark.parametrize("expect", [
+ (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 1", True),
+ (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 2", True),
+ (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 3", True),
+ (".//div//span[@class='caption-number']", "No.1 ", True),
+ (".//div//span[@class='caption-number']", "No.2 ", True),
+ (".//li/p/a/span", 'Fig. 1', True),
+ (".//li/p/a/span", 'Fig. 2', True),
+ (".//li/p/a/span", 'Fig. 3', True),
+ (".//li/p/a/span", 'No.1', True),
+ (".//li/p/a/span", 'No.2', True),
+])
+@pytest.mark.sphinx('html', testroot='add_enumerable_node',
+ srcdir='test_enumerable_node')
+def test_enumerable_node(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False})
+def test_html_copy_source(app):
+ app.build(force_all=True)
+ assert not (app.outdir / '_sources' / 'index.rst.txt').exists()
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.txt'})
+def test_html_sourcelink_suffix(app):
+ app.build(force_all=True)
+ assert (app.outdir / '_sources' / 'index.rst.txt').exists()
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.rst'})
+def test_html_sourcelink_suffix_same(app):
+ app.build(force_all=True)
+ assert (app.outdir / '_sources' / 'index.rst').exists()
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': ''})
+def test_html_sourcelink_suffix_empty(app):
+ app.build(force_all=True)
+ assert (app.outdir / '_sources' / 'index.rst').exists()
+
+
+@pytest.mark.sphinx('html', testroot='html_entity')
+def test_html_entity(app):
+ app.build(force_all=True)
+ valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'}
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ for entity in re.findall(r'&([a-z]+);', content, re.MULTILINE):
+ assert entity not in valid_entities
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_inventory(app):
+ app.build(force_all=True)
+
+ with app.outdir.joinpath('objects.inv').open('rb') as f:
+ invdata = InventoryFile.load(f, 'https://www.google.com', posixpath.join)
+
+ assert set(invdata.keys()) == {'std:label', 'std:doc'}
+ assert set(invdata['std:label'].keys()) == {'modindex',
+ 'py-modindex',
+ 'genindex',
+ 'search'}
+ assert invdata['std:label']['modindex'] == ('Python',
+ '',
+ 'https://www.google.com/py-modindex.html',
+ 'Module Index')
+ assert invdata['std:label']['py-modindex'] == ('Python',
+ '',
+ 'https://www.google.com/py-modindex.html',
+ 'Python Module Index')
+ assert invdata['std:label']['genindex'] == ('Python',
+ '',
+ 'https://www.google.com/genindex.html',
+ 'Index')
+ assert invdata['std:label']['search'] == ('Python',
+ '',
+ 'https://www.google.com/search.html',
+ 'Search Page')
+ assert set(invdata['std:doc'].keys()) == {'index'}
+ assert invdata['std:doc']['index'] == ('Python',
+ '',
+ 'https://www.google.com/index.html',
+ 'The basic Sphinx documentation for testing')
+
+
+@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''})
+def test_html_anchor_for_figure(app):
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<figcaption>\n<p><span class="caption-text">The caption of pic</span>'
+ '<a class="headerlink" href="#id1" title="Link to this image">¶</a></p>\n</figcaption>'
+ in content)
+
+
+@pytest.mark.sphinx('html', testroot='directives-raw')
+def test_html_raw_directive(app, status, warning):
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # standard case
+ assert 'standalone raw directive (HTML)' in result
+ assert 'standalone raw directive (LaTeX)' not in result
+
+ # with substitution
+ assert '<p>HTML: abc def ghi</p>' in result
+ assert '<p>LaTeX: abc ghi</p>' in result
+
+
+@pytest.mark.parametrize("expect", [
+ (".//link[@href='_static/persistent.css']"
+ "[@rel='stylesheet']", '', True),
+ (".//link[@href='_static/default.css']"
+ "[@rel='stylesheet']"
+ "[@title='Default']", '', True),
+ (".//link[@href='_static/alternate1.css']"
+ "[@rel='alternate stylesheet']"
+ "[@title='Alternate']", '', True),
+ (".//link[@href='_static/alternate2.css']"
+ "[@rel='alternate stylesheet']", '', True),
+ (".//link[@href='_static/more_persistent.css']"
+ "[@rel='stylesheet']", '', True),
+ (".//link[@href='_static/more_default.css']"
+ "[@rel='stylesheet']"
+ "[@title='Default']", '', True),
+ (".//link[@href='_static/more_alternate1.css']"
+ "[@rel='alternate stylesheet']"
+ "[@title='Alternate']", '', True),
+ (".//link[@href='_static/more_alternate2.css']"
+ "[@rel='alternate stylesheet']", '', True),
+])
+@pytest.mark.sphinx('html', testroot='stylesheets')
+def test_alternate_stylesheets(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
+
+
+@pytest.mark.sphinx('html', testroot='html_style')
+def test_html_style(app, status, warning):
+ app.build()
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="stylesheet" type="text/css" href="_static/default.css" />' in result
+ assert ('<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />'
+ not in result)
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_sidebar(app, status, warning):
+ ctx = {}
+
+ # default for alabaster
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<div class="sphinxsidebar" role="navigation" '
+ 'aria-label="main navigation">' in result)
+ assert '<h1 class="logo"><a href="#">Python</a></h1>' in result
+ assert '<h3>Navigation</h3>' in result
+ assert '<h3>Related Topics</h3>' in result
+ assert '<h3 id="searchlabel">Quick search</h3>' in result
+
+ app.builder.add_sidebars('index', ctx)
+ assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html',
+ 'searchbox.html', 'donate.html']
+
+ # only relations.html
+ app.config.html_sidebars = {'**': ['relations.html']}
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<div class="sphinxsidebar" role="navigation" '
+ 'aria-label="main navigation">' in result)
+ assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
+ assert '<h3>Navigation</h3>' not in result
+ assert '<h3>Related Topics</h3>' in result
+ assert '<h3 id="searchlabel">Quick search</h3>' not in result
+
+ app.builder.add_sidebars('index', ctx)
+ assert ctx['sidebars'] == ['relations.html']
+
+ # no sidebars
+ app.config.html_sidebars = {'**': []}
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<div class="sphinxsidebar" role="navigation" '
+ 'aria-label="main navigation">' not in result)
+ assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
+ assert '<h3>Navigation</h3>' not in result
+ assert '<h3>Related Topics</h3>' not in result
+ assert '<h3 id="searchlabel">Quick search</h3>' not in result
+
+ app.builder.add_sidebars('index', ctx)
+ assert ctx['sidebars'] == []
+
+
+@pytest.mark.parametrize(("fname", "expect"), [
+ ('index.html', (".//h1/em/a[@href='https://example.com/cp.1']", '', True)),
+ ('index.html', (".//em/a[@href='https://example.com/man.1']", '', True)),
+ ('index.html', (".//em/a[@href='https://example.com/ls.1']", '', True)),
+ ('index.html', (".//em/a[@href='https://example.com/sphinx.']", '', True)),
+])
+@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={
+ 'manpages_url': 'https://example.com/{page}.{section}'})
+@pytest.mark.test_params(shared_result='test_build_html_manpage_url')
+def test_html_manpage(app, cached_etree_parse, fname, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
+
+
+@pytest.mark.sphinx('html', testroot='toctree-glob',
+ confoverrides={'html_baseurl': 'https://example.com/'})
+def test_html_baseurl(app, status, warning):
+ app.build()
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/index.html" />' in result
+
+ result = (app.outdir / 'qux' / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result
+
+
+@pytest.mark.sphinx('html', testroot='toctree-glob',
+ confoverrides={'html_baseurl': 'https://example.com/subdir',
+ 'html_file_suffix': '.htm'})
+def test_html_baseurl_and_html_file_suffix(app, status, warning):
+ app.build()
+
+ result = (app.outdir / 'index.htm').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result
+
+ result = (app.outdir / 'qux' / 'index.htm').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result
+
+
+@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path')
+def test_validate_html_extra_path(app):
+ (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
+ app.config.html_extra_path = [
+ '/path/to/not_found', # not found
+ '_static',
+ app.outdir, # outdir
+ app.outdir / '_static', # inside outdir
+ ]
+ with pytest.warns(RemovedInSphinx80Warning, match='Use "pathlib.Path" or "os.fspath" instead'):
+ validate_html_extra_path(app, app.config)
+ assert app.config.html_extra_path == ['_static']
+
+
+@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path')
+def test_validate_html_static_path(app):
+ (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
+ app.config.html_static_path = [
+ '/path/to/not_found', # not found
+ '_static',
+ app.outdir, # outdir
+ app.outdir / '_static', # inside outdir
+ ]
+ with pytest.warns(RemovedInSphinx80Warning, match='Use "pathlib.Path" or "os.fspath" instead'):
+ validate_html_static_path(app, app.config)
+ assert app.config.html_static_path == ['_static']
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'html_permalinks': False})
+def test_html_permalink_disable(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert '<h1>The basic Sphinx documentation for testing</h1>' in content
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'html_permalinks_icon': '<span>[PERMALINK]</span>'})
+def test_html_permalink_icon(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert ('<h1>The basic Sphinx documentation for testing<a class="headerlink" '
+ 'href="#the-basic-sphinx-documentation-for-testing" '
+ 'title="Link to this heading"><span>[PERMALINK]</span></a></h1>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='html_signaturereturn_icon')
+def test_html_signaturereturn_icon(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert ('<span class="sig-return-icon">&#x2192;</span>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='root', srcdir=os.urandom(4).hex())
+def test_html_remove_sources_before_write_gh_issue_10786(app, warning):
+ # see: https://github.com/sphinx-doc/sphinx/issues/10786
+ target = app.srcdir / 'img.png'
+
+ def handler(app):
+ assert target.exists()
+ target.unlink()
+ return []
+
+ app.connect('html-collect-pages', handler)
+ assert target.exists()
+ app.build()
+ assert not target.exists()
+
+ ws = strip_colors(warning.getvalue()).splitlines()
+ assert len(ws) >= 1
+
+ file = os.fsdecode(target)
+ assert f'WARNING: cannot copy image file {file!r}: {file!s} does not exist' == ws[-1]
diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py
new file mode 100644
index 0000000..ece6f49
--- /dev/null
+++ b/tests/test_builders/test_build_html_5_output.py
@@ -0,0 +1,276 @@
+"""Test the HTML builder and check output against XPath."""
+
+import re
+
+import pytest
+
+from tests.test_builders.xpath_util import check_xpath
+
+
+def tail_check(check):
+ rex = re.compile(check)
+
+ def checker(nodes):
+ for node in nodes:
+ if node.tail and rex.search(node.tail):
+ return True
+ msg = f'{check!r} not found in tail of any nodes {nodes}'
+ raise AssertionError(msg)
+ return checker
+
+
+@pytest.mark.parametrize(("fname", "path", "check"), [
+ ('images.html', ".//img[@src='_images/img.png']", ''),
+ ('images.html', ".//img[@src='_images/img1.png']", ''),
+ ('images.html', ".//img[@src='_images/simg.png']", ''),
+ ('images.html', ".//img[@src='_images/svgimg.svg']", ''),
+ ('images.html', ".//a[@href='_sources/images.txt']", ''),
+
+ ('subdir/images.html', ".//img[@src='../_images/img1.png']", ''),
+ ('subdir/images.html', ".//img[@src='../_images/rimg.png']", ''),
+
+ ('subdir/includes.html', ".//a[@class='reference download internal']", ''),
+ ('subdir/includes.html', ".//img[@src='../_images/img.png']", ''),
+ ('subdir/includes.html', ".//p", 'This is an include file.'),
+ ('subdir/includes.html', ".//pre/span", 'line 1'),
+ ('subdir/includes.html', ".//pre/span", 'line 2'),
+
+ ('includes.html', ".//pre", 'Max Strauß'),
+ ('includes.html', ".//a[@class='reference download internal']", ''),
+ ('includes.html', ".//pre/span", '"quotes"'),
+ ('includes.html', ".//pre/span", "'included'"),
+ ('includes.html', ".//pre/span[@class='s2']", 'üöä'),
+ ('includes.html', ".//div[@class='inc-pyobj1 highlight-text notranslate']//pre",
+ r'^class Foo:\n pass\n\s*$'),
+ ('includes.html', ".//div[@class='inc-pyobj2 highlight-text notranslate']//pre",
+ r'^ def baz\(\):\n pass\n\s*$'),
+ ('includes.html', ".//div[@class='inc-lines highlight-text notranslate']//pre",
+ r'^class Foo:\n pass\nclass Bar:\n$'),
+ ('includes.html', ".//div[@class='inc-startend highlight-text notranslate']//pre",
+ '^foo = "Including Unicode characters: üöä"\\n$'),
+ ('includes.html', ".//div[@class='inc-preappend highlight-text notranslate']//pre",
+ r'(?m)^START CODE$'),
+ ('includes.html', ".//div[@class='inc-pyobj-dedent highlight-python notranslate']//span",
+ r'def'),
+ ('includes.html', ".//div[@class='inc-tab3 highlight-text notranslate']//pre",
+ r'-| |-'),
+ ('includes.html', ".//div[@class='inc-tab8 highlight-python notranslate']//pre/span",
+ r'-| |-'),
+
+ ('autodoc.html', ".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''),
+ ('autodoc.html', ".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'),
+ ('autodoc.html', ".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'),
+ ('autodoc.html', ".//dd/p", r'Return spam\.'),
+
+ ('extapi.html', ".//strong", 'from class: Bar'),
+
+ ('markup.html', ".//title", 'set by title directive'),
+ ('markup.html', ".//p/em", 'Section author: Georg Brandl'),
+ ('markup.html', ".//p/em", 'Module author: Georg Brandl'),
+ # created by the meta directive
+ ('markup.html', ".//meta[@name='author'][@content='Me']", ''),
+ ('markup.html', ".//meta[@name='keywords'][@content='docs, sphinx']", ''),
+ # a label created by ``.. _label:``
+ ('markup.html', ".//div[@id='label']", ''),
+ # code with standard code blocks
+ ('markup.html', ".//pre", '^some code$'),
+ # an option list
+ ('markup.html', ".//span[@class='option']", '--help'),
+ # admonitions
+ ('markup.html', ".//p[@class='admonition-title']", 'My Admonition'),
+ ('markup.html', ".//div[@class='admonition note']/p", 'Note text.'),
+ ('markup.html', ".//div[@class='admonition warning']/p", 'Warning text.'),
+ # inline markup
+ ('markup.html', ".//li/p/strong", r'^command\\n$'),
+ ('markup.html', ".//li/p/strong", r'^program\\n$'),
+ ('markup.html', ".//li/p/em", r'^dfn\\n$'),
+ ('markup.html', ".//li/p/kbd", r'^kbd\\n$'),
+ ('markup.html', ".//li/p/span", 'File \N{TRIANGULAR BULLET} Close'),
+ ('markup.html', ".//li/p/code/span[@class='pre']", '^a/$'),
+ ('markup.html', ".//li/p/code/em/span[@class='pre']", '^varpart$'),
+ ('markup.html', ".//li/p/code/em/span[@class='pre']", '^i$'),
+ ('markup.html', ".//a[@href='https://peps.python.org/pep-0008/']"
+ "[@class='pep reference external']/strong", 'PEP 8'),
+ ('markup.html', ".//a[@href='https://peps.python.org/pep-0008/']"
+ "[@class='pep reference external']/strong",
+ 'Python Enhancement Proposal #8'),
+ ('markup.html', ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
+ "[@class='rfc reference external']/strong", 'RFC 1'),
+ ('markup.html', ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
+ "[@class='rfc reference external']/strong", 'Request for Comments #1'),
+ ('markup.html', ".//a[@href='objects.html#envvar-HOME']"
+ "[@class='reference internal']/code/span[@class='pre']", 'HOME'),
+ ('markup.html', ".//a[@href='#with']"
+ "[@class='reference internal']/code/span[@class='pre']", '^with$'),
+ ('markup.html', ".//a[@href='#grammar-token-try_stmt']"
+ "[@class='reference internal']/code/span", '^statement$'),
+ ('markup.html', ".//a[@href='#some-label'][@class='reference internal']/span", '^here$'),
+ ('markup.html', ".//a[@href='#some-label'][@class='reference internal']/span", '^there$'),
+ ('markup.html', ".//a[@href='subdir/includes.html']"
+ "[@class='reference internal']/span", 'Including in subdir'),
+ ('markup.html', ".//a[@href='objects.html#cmdoption-python-c']"
+ "[@class='reference internal']/code/span[@class='pre']", '-c'),
+ # abbreviations
+ ('markup.html', ".//abbr[@title='abbreviation']", '^abbr$'),
+ # version stuff
+ ('markup.html', ".//div[@class='versionadded']/p/span", 'Added in version 0.6: '),
+ ('markup.html', ".//div[@class='versionadded']/p/span",
+ tail_check('First paragraph of versionadded')),
+ ('markup.html', ".//div[@class='versionchanged']/p/span",
+ tail_check('First paragraph of versionchanged')),
+ ('markup.html', ".//div[@class='versionchanged']/p",
+ 'Second paragraph of versionchanged'),
+ ('markup.html', ".//div[@class='versionremoved']/p/span", 'Removed in version 0.6: '),
+ # footnote reference
+ ('markup.html', ".//a[@class='footnote-reference brackets']", r'1'),
+ # created by reference lookup
+ ('markup.html', ".//a[@href='index.html#ref1']", ''),
+ # ``seealso`` directive
+ ('markup.html', ".//div/p[@class='admonition-title']", 'See also'),
+ # a ``hlist`` directive
+ ('markup.html', ".//table[@class='hlist']/tr/td/ul/li/p", '^This$'),
+ # a ``centered`` directive
+ ('markup.html', ".//p[@class='centered']/strong", 'LICENSE'),
+ # a glossary
+ ('markup.html', ".//dl/dt[@id='term-boson']", 'boson'),
+ ('markup.html', ".//dl/dt[@id='term-boson']/a", '¶'),
+ # a production list
+ ('markup.html', ".//pre/strong", 'try_stmt'),
+ ('markup.html', ".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'),
+ # tests for ``only`` directive
+ ('markup.html', ".//p", 'A global substitution!'),
+ ('markup.html', ".//p", 'In HTML.'),
+ ('markup.html', ".//p", 'In both.'),
+ ('markup.html', ".//p", 'Always present'),
+ # tests for ``any`` role
+ ('markup.html', ".//a[@href='#with']/span", 'headings'),
+ ('markup.html', ".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
+ # tests for numeric labels
+ ('markup.html', ".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'),
+ # tests for smartypants
+ ('markup.html', ".//li/p", 'Smart “quotes” in English ‘text’.'),
+ ('markup.html', ".//li/p", 'Smart — long and – short dashes.'),
+ ('markup.html', ".//li/p", 'Ellipsis…'),
+ ('markup.html', ".//li/p/code/span[@class='pre']", 'foo--"bar"...'),
+ ('markup.html', ".//p", 'Этот «абзац» должен использовать „русские“ кавычки.'),
+ ('markup.html', ".//p", 'Il dit : « C’est “super” ! »'),
+
+ ('objects.html', ".//dt[@id='mod.Cls.meth1']", ''),
+ ('objects.html', ".//dt[@id='errmod.Error']", ''),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", r'long\(parameter,'),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", r'list\)'),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", 'another'),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", 'one'),
+ ('objects.html', ".//a[@href='#mod.Cls'][@class='reference internal']", ''),
+ ('objects.html', ".//dl[@class='std userdesc']", ''),
+ ('objects.html', ".//dt[@id='userdesc-myobj']", ''),
+ ('objects.html', ".//a[@href='#userdesc-myobj'][@class='reference internal']", ''),
+ # docfields
+ ('objects.html', ".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#Time']", 'Time'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'),
+ # C references
+ ('objects.html', ".//span[@class='pre']", 'CFunction()'),
+ ('objects.html', ".//a[@href='#c.Sphinx_DoSomething']", ''),
+ ('objects.html', ".//a[@href='#c.SphinxStruct.member']", ''),
+ ('objects.html', ".//a[@href='#c.SPHINX_USE_PYTHON']", ''),
+ ('objects.html', ".//a[@href='#c.SphinxType']", ''),
+ ('objects.html', ".//a[@href='#c.sphinx_global']", ''),
+ # test global TOC created by toctree()
+ ('objects.html', ".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']",
+ 'Testing object descriptions'),
+ ('objects.html', ".//li[@class='toctree-l1']/a[@href='markup.html']",
+ 'Testing various markup'),
+ # test unknown field names
+ ('objects.html', ".//dt[@class='field-odd']", 'Field_name'),
+ ('objects.html', ".//dt[@class='field-even']", 'Field_name all lower'),
+ ('objects.html', ".//dt[@class='field-odd']", 'FIELD_NAME'),
+ ('objects.html', ".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'),
+ ('objects.html', ".//dt[@class='field-odd']", 'Field_Name'),
+ ('objects.html', ".//dt[@class='field-even']", 'Field_Name All Word Caps'),
+ # ('objects.html', ".//dt[@class='field-odd']", 'Field_name'), (duplicate)
+ ('objects.html', ".//dt[@class='field-even']", 'Field_name First word cap'),
+ ('objects.html', ".//dt[@class='field-odd']", 'FIELd_name'),
+ ('objects.html', ".//dt[@class='field-even']", 'FIELd_name PARTial caps'),
+ # custom sidebar
+ ('objects.html', ".//h4", 'Custom sidebar'),
+ # docfields
+ ('objects.html', ".//dd[@class='field-odd']/p/strong", '^moo$'),
+ ('objects.html', ".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')),
+ ('objects.html', ".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'),
+ ('objects.html', ".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'),
+ ('objects.html', ".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')),
+ # others
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
+ 'perl'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
+ '\\+p'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span",
+ '--ObjC\\+\\+'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span",
+ '--plugin.option'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']"
+ "/code/span",
+ 'create-auth-token'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span",
+ 'arg'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span",
+ '-j'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
+ 'hg'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
+ 'commit'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
+ 'git'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
+ 'commit'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
+ '-p'),
+
+ ('index.html', ".//meta[@name='hc'][@content='hcval']", ''),
+ ('index.html', ".//meta[@name='hc_co'][@content='hcval_co']", ''),
+ ('index.html', ".//li[@class='toctree-l1']/a", 'Testing various markup'),
+ ('index.html', ".//li[@class='toctree-l2']/a", 'Inline markup'),
+ ('index.html', ".//title", 'Sphinx <Tests>'),
+ ('index.html', ".//div[@class='footer']", 'copyright text credits'),
+ ('index.html', ".//a[@href='https://python.org/']"
+ "[@class='reference external']", ''),
+ ('index.html', ".//li/p/a[@href='genindex.html']/span", 'Index'),
+ ('index.html', ".//li/p/a[@href='py-modindex.html']/span", 'Module Index'),
+ # custom sidebar only for contents
+ ('index.html', ".//h4", 'Contents sidebar'),
+ # custom JavaScript
+ ('index.html', ".//script[@src='file://moo.js']", ''),
+ # URL in contents
+ ('index.html', ".//a[@class='reference external'][@href='https://sphinx-doc.org/']",
+ 'https://sphinx-doc.org/'),
+ ('index.html', ".//a[@class='reference external'][@href='https://sphinx-doc.org/latest/']",
+ 'Latest reference'),
+ # Indirect hyperlink targets across files
+ ('index.html', ".//a[@href='markup.html#some-label'][@class='reference internal']/span",
+ '^indirect hyperref$'),
+
+ ('bom.html', ".//title", " File with UTF-8 BOM"),
+
+ ('extensions.html', ".//a[@href='https://python.org/dev/']", "https://python.org/dev/"),
+ ('extensions.html', ".//a[@href='https://bugs.python.org/issue1000']", "issue 1000"),
+ ('extensions.html', ".//a[@href='https://bugs.python.org/issue1042']", "explicit caption"),
+
+ # index entries
+ ('genindex.html', ".//a/strong", "Main"),
+ ('genindex.html', ".//a/strong", "[1]"),
+ ('genindex.html', ".//a/strong", "Other"),
+ ('genindex.html', ".//a", "entry"),
+ ('genindex.html', ".//li/a", "double"),
+
+ ('otherext.html', ".//h1", "Generated section"),
+ ('otherext.html', ".//a[@href='_sources/otherext.foo.txt']", ''),
+
+ ('search.html', ".//meta[@name='robots'][@content='noindex']", ''),
+])
+@pytest.mark.sphinx('html', tags=['testtag'],
+ confoverrides={'html_context.hckey_co': 'hcval_co'})
+@pytest.mark.test_params(shared_result='test_build_html_output')
+def test_html5_output(app, cached_etree_parse, fname, path, check):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check)
diff --git a/tests/test_builders/test_build_html_assets.py b/tests/test_builders/test_build_html_assets.py
new file mode 100644
index 0000000..fc7a987
--- /dev/null
+++ b/tests/test_builders/test_build_html_assets.py
@@ -0,0 +1,152 @@
+"""Test the HTML builder and check output against XPath."""
+
+import re
+from pathlib import Path
+
+import pytest
+
+import sphinx.builders.html
+from sphinx.builders.html._assets import _file_checksum
+from sphinx.errors import ThemeError
+
+
+@pytest.mark.sphinx('html', testroot='html_assets')
+def test_html_assets(app):
+ app.build(force_all=True)
+
+ # exclude_path and its family
+ assert not (app.outdir / 'static' / 'index.html').exists()
+ assert not (app.outdir / 'extra' / 'index.html').exists()
+
+ # html_static_path
+ assert not (app.outdir / '_static' / '.htaccess').exists()
+ assert not (app.outdir / '_static' / '.htpasswd').exists()
+ assert (app.outdir / '_static' / 'API.html').exists()
+ assert (app.outdir / '_static' / 'API.html').read_text(encoding='utf8') == 'Sphinx-1.4.4'
+ assert (app.outdir / '_static' / 'css' / 'style.css').exists()
+ assert (app.outdir / '_static' / 'js' / 'custom.js').exists()
+ assert (app.outdir / '_static' / 'rimg.png').exists()
+ assert not (app.outdir / '_static' / '_build' / 'index.html').exists()
+ assert (app.outdir / '_static' / 'background.png').exists()
+ assert not (app.outdir / '_static' / 'subdir' / '.htaccess').exists()
+ assert not (app.outdir / '_static' / 'subdir' / '.htpasswd').exists()
+
+ # html_extra_path
+ assert (app.outdir / '.htaccess').exists()
+ assert not (app.outdir / '.htpasswd').exists()
+ assert (app.outdir / 'API.html_t').exists()
+ assert (app.outdir / 'css/style.css').exists()
+ assert (app.outdir / 'rimg.png').exists()
+ assert not (app.outdir / '_build' / 'index.html').exists()
+ assert (app.outdir / 'background.png').exists()
+ assert (app.outdir / 'subdir' / '.htaccess').exists()
+ assert not (app.outdir / 'subdir' / '.htpasswd').exists()
+
+ # html_css_files
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content
+ assert ('<link media="print" rel="stylesheet" title="title" type="text/css" '
+ 'href="https://example.com/custom.css" />' in content)
+
+ # html_js_files
+ assert '<script src="_static/js/custom.js"></script>' in content
+ assert ('<script async="async" src="https://example.com/script.js">'
+ '</script>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='html_assets')
+def test_assets_order(app, monkeypatch):
+ monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '')
+
+ app.add_css_file('normal.css')
+ app.add_css_file('early.css', priority=100)
+ app.add_css_file('late.css', priority=750)
+ app.add_css_file('lazy.css', priority=900)
+ app.add_js_file('normal.js')
+ app.add_js_file('early.js', priority=100)
+ app.add_js_file('late.js', priority=750)
+ app.add_js_file('lazy.js', priority=900)
+
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # css_files
+ expected = [
+ '_static/early.css',
+ '_static/pygments.css',
+ '_static/alabaster.css',
+ 'https://example.com/custom.css',
+ '_static/normal.css',
+ '_static/late.css',
+ '_static/css/style.css',
+ '_static/lazy.css',
+ ]
+ pattern = '.*'.join(f'href="{re.escape(f)}"' for f in expected)
+ assert re.search(pattern, content, re.DOTALL), content
+
+ # js_files
+ expected = [
+ '_static/early.js',
+ '_static/doctools.js',
+ '_static/sphinx_highlight.js',
+ 'https://example.com/script.js',
+ '_static/normal.js',
+ '_static/late.js',
+ '_static/js/custom.js',
+ '_static/lazy.js',
+ ]
+ pattern = '.*'.join(f'src="{re.escape(f)}"' for f in expected)
+ assert re.search(pattern, content, re.DOTALL), content
+
+
+@pytest.mark.sphinx('html', testroot='html_file_checksum')
+def test_file_checksum(app):
+ app.add_css_file('stylesheet-a.css')
+ app.add_css_file('stylesheet-b.css')
+ app.add_css_file('https://example.com/custom.css')
+ app.add_js_file('script.js')
+ app.add_js_file('empty.js')
+ app.add_js_file('https://example.com/script.js')
+
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # checksum for local files
+ assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-a.css?v=e575b6df" />' in content
+ assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-b.css?v=a2d5cc0f" />' in content
+ assert '<script src="_static/script.js?v=48278d48"></script>' in content
+
+ # empty files have no checksum
+ assert '<script src="_static/empty.js"></script>' in content
+
+ # no checksum for hyperlinks
+ assert '<link rel="stylesheet" type="text/css" href="https://example.com/custom.css" />' in content
+ assert '<script src="https://example.com/script.js"></script>' in content
+
+
+def test_file_checksum_query_string():
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path(), 'with_query_string.css?dead_parrots=1')
+
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path(), 'with_query_string.js?dead_parrots=1')
+
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1')
+
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1')
+
+
+@pytest.mark.sphinx('html', testroot='html_assets')
+def test_javscript_loading_method(app):
+ app.add_js_file('normal.js')
+ app.add_js_file('early.js', loading_method='async')
+ app.add_js_file('late.js', loading_method='defer')
+
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert '<script src="_static/normal.js"></script>' in content
+ assert '<script async="async" src="_static/early.js"></script>' in content
+ assert '<script defer="defer" src="_static/late.js"></script>' in content
diff --git a/tests/test_builders/test_build_html_code.py b/tests/test_builders/test_build_html_code.py
new file mode 100644
index 0000000..f07eb97
--- /dev/null
+++ b/tests/test_builders/test_build_html_code.py
@@ -0,0 +1,46 @@
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='reST-code-block',
+ confoverrides={'html_codeblock_linenos_style': 'table'})
+def test_html_codeblock_linenos_style_table(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert ('<div class="linenodiv"><pre><span class="normal">1</span>\n'
+ '<span class="normal">2</span>\n'
+ '<span class="normal">3</span>\n'
+ '<span class="normal">4</span></pre></div>') in content
+
+
+@pytest.mark.sphinx('html', testroot='reST-code-block',
+ confoverrides={'html_codeblock_linenos_style': 'inline'})
+def test_html_codeblock_linenos_style_inline(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert '<span class="linenos">1</span>' in content
+
+
+@pytest.mark.sphinx('html', testroot='reST-code-role')
+def test_html_code_role(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ common_content = (
+ '<span class="k">def</span> <span class="nf">foo</span>'
+ '<span class="p">(</span>'
+ '<span class="mi">1</span> '
+ '<span class="o">+</span> '
+ '<span class="mi">2</span> '
+ '<span class="o">+</span> '
+ '<span class="kc">None</span> '
+ '<span class="o">+</span> '
+ '<span class="s2">&quot;abc&quot;</span>'
+ '<span class="p">):</span> '
+ '<span class="k">pass</span>')
+ assert ('<p>Inline <code class="code highlight python docutils literal highlight-python">' +
+ common_content + '</code> code block</p>') in content
+ assert ('<div class="highlight-python notranslate">' +
+ '<div class="highlight"><pre><span></span>' +
+ common_content) in content
diff --git a/tests/test_builders/test_build_html_download.py b/tests/test_builders/test_build_html_download.py
new file mode 100644
index 0000000..1201c66
--- /dev/null
+++ b/tests/test_builders/test_build_html_download.py
@@ -0,0 +1,62 @@
+import hashlib
+import re
+
+import pytest
+
+
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_build_html_output')
+def test_html_download(app):
+ app.build()
+
+ # subdir/includes.html
+ result = (app.outdir / 'subdir' / 'includes.html').read_text(encoding='utf8')
+ pattern = ('<a class="reference download internal" download="" '
+ 'href="../(_downloads/.*/img.png)">')
+ matched = re.search(pattern, result)
+ assert matched
+ assert (app.outdir / matched.group(1)).exists()
+ filename = matched.group(1)
+
+ # includes.html
+ result = (app.outdir / 'includes.html').read_text(encoding='utf8')
+ pattern = ('<a class="reference download internal" download="" '
+ 'href="(_downloads/.*/img.png)">')
+ matched = re.search(pattern, result)
+ assert matched
+ assert (app.outdir / matched.group(1)).exists()
+ assert matched.group(1) == filename
+
+ pattern = ('<a class="reference download internal" download="" '
+ 'href="(_downloads/.*/)(file_with_special_%23_chars.xyz)">')
+ matched = re.search(pattern, result)
+ assert matched
+ assert (app.outdir / matched.group(1) / "file_with_special_#_chars.xyz").exists()
+
+
+@pytest.mark.sphinx('html', testroot='roles-download')
+def test_html_download_role(app, status, warning):
+ app.build()
+ digest = hashlib.md5(b'dummy.dat', usedforsecurity=False).hexdigest()
+ assert (app.outdir / '_downloads' / digest / 'dummy.dat').exists()
+ digest_another = hashlib.md5(b'another/dummy.dat', usedforsecurity=False).hexdigest()
+ assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists()
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert (('<li><p><a class="reference download internal" download="" '
+ 'href="_downloads/%s/dummy.dat">'
+ '<code class="xref download docutils literal notranslate">'
+ '<span class="pre">dummy.dat</span></code></a></p></li>' % digest)
+ in content)
+ assert (('<li><p><a class="reference download internal" download="" '
+ 'href="_downloads/%s/dummy.dat">'
+ '<code class="xref download docutils literal notranslate">'
+ '<span class="pre">another/dummy.dat</span></code></a></p></li>' %
+ digest_another) in content)
+ assert ('<li><p><code class="xref download docutils literal notranslate">'
+ '<span class="pre">not_found.dat</span></code></p></li>' in content)
+ assert ('<li><p><a class="reference download external" download="" '
+ 'href="https://www.sphinx-doc.org/en/master/_static/sphinx-logo.svg">'
+ '<code class="xref download docutils literal notranslate">'
+ '<span class="pre">Sphinx</span> <span class="pre">logo</span>'
+ '</code></a></p></li>' in content)
diff --git a/tests/test_builders/test_build_html_highlight.py b/tests/test_builders/test_build_html_highlight.py
new file mode 100644
index 0000000..aee1ece
--- /dev/null
+++ b/tests/test_builders/test_build_html_highlight.py
@@ -0,0 +1,61 @@
+from unittest.mock import ANY, call, patch
+
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_pygments_style_default(app):
+ style = app.builder.highlighter.formatter_args.get('style')
+ assert style.__name__ == 'Alabaster'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'pygments_style': 'sphinx'})
+def test_html_pygments_style_manually(app):
+ style = app.builder.highlighter.formatter_args.get('style')
+ assert style.__name__ == 'SphinxStyle'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'html_theme': 'classic'})
+def test_html_pygments_for_classic_theme(app):
+ style = app.builder.highlighter.formatter_args.get('style')
+ assert style.__name__ == 'SphinxStyle'
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_dark_pygments_style_default(app):
+ assert app.builder.dark_highlighter is None
+
+
+@pytest.mark.sphinx('html', testroot='highlight_options')
+def test_highlight_options(app):
+ subject = app.builder.highlighter
+ with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
+ app.build()
+
+ call_args = highlight.call_args_list
+ assert len(call_args) == 3
+ assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
+ location=ANY, opts={'default_option': True})
+ assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
+ location=ANY, opts={'python_option': True})
+ assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
+ location=ANY, opts={})
+
+
+@pytest.mark.sphinx('html', testroot='highlight_options',
+ confoverrides={'highlight_options': {'default_option': True}})
+def test_highlight_options_old(app):
+ subject = app.builder.highlighter
+ with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
+ app.build()
+
+ call_args = highlight.call_args_list
+ assert len(call_args) == 3
+ assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
+ location=ANY, opts={'default_option': True})
+ assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
+ location=ANY, opts={})
+ assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
+ location=ANY, opts={})
diff --git a/tests/test_builders/test_build_html_image.py b/tests/test_builders/test_build_html_image.py
new file mode 100644
index 0000000..08ed618
--- /dev/null
+++ b/tests/test_builders/test_build_html_image.py
@@ -0,0 +1,80 @@
+import re
+from pathlib import Path
+
+import docutils
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='images')
+def test_html_remote_images(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img alt="http://localhost:7777/sphinx.png" '
+ 'src="http://localhost:7777/sphinx.png" />' in result)
+ assert not (app.outdir / 'sphinx.png').exists()
+
+
+@pytest.mark.sphinx('html', testroot='image-escape')
+def test_html_encoded_image(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img alt="_images/img_%231.png" src="_images/img_%231.png" />' in result)
+ assert (app.outdir / '_images/img_#1.png').exists()
+
+
+@pytest.mark.sphinx('html', testroot='remote-logo')
+def test_html_remote_logo(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo"/>' in result)
+ assert ('<link rel="icon" href="https://www.python.org/static/favicon.ico"/>' in result)
+ assert not (app.outdir / 'python-logo.png').exists()
+
+
+@pytest.mark.sphinx('html', testroot='local-logo')
+def test_html_local_logo(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img class="logo" src="_static/img.png" alt="Logo"/>' in result)
+ assert (app.outdir / '_static/img.png').exists()
+
+
+@pytest.mark.sphinx(testroot='html_scaled_image_link')
+def test_html_scaled_image_link(app):
+ app.build()
+ context = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # no scaled parameters
+ assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context)
+
+ # scaled_image_link
+ # Docutils 0.21 adds a newline before the closing </a> tag
+ closing_space = "\n" if docutils.__version_info__[:2] >= (0, 21) else ""
+ assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">'
+ '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" />'
+ f'{closing_space}</a>',
+ context)
+
+ # no-scaled-link class disables the feature
+ assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"'
+ ' src="_images/img.png" style="[^"]+" />',
+ context)
+
+
+@pytest.mark.sphinx('html', testroot='images')
+def test_copy_images(app, status, warning):
+ app.build()
+
+ images_dir = Path(app.outdir) / '_images'
+ images = {image.name for image in images_dir.rglob('*')}
+ assert images == {
+ 'img.png',
+ 'rimg.png',
+ 'rimg1.png',
+ 'svgimg.svg',
+ 'testimäge.png',
+ }
diff --git a/tests/test_builders/test_build_html_maths.py b/tests/test_builders/test_build_html_maths.py
new file mode 100644
index 0000000..900846b
--- /dev/null
+++ b/tests/test_builders/test_build_html_maths.py
@@ -0,0 +1,58 @@
+import pytest
+
+from sphinx.errors import ConfigError
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_default_html_math_renderer(app, status, warning):
+ assert app.builder.math_renderer_name == 'mathjax'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinx.ext.mathjax']})
+def test_html_math_renderer_is_mathjax(app, status, warning):
+ assert app.builder.math_renderer_name == 'mathjax'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinx.ext.imgmath']})
+def test_html_math_renderer_is_imgmath(app, status, warning):
+ assert app.builder.math_renderer_name == 'imgmath'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinxcontrib.jsmath',
+ 'sphinx.ext.imgmath']})
+def test_html_math_renderer_is_duplicated(make_app, app_params):
+ args, kwargs = app_params
+ with pytest.raises(
+ ConfigError,
+ match='Many math_renderers are registered. But no math_renderer is selected.',
+ ):
+ make_app(*args, **kwargs)
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinx.ext.imgmath',
+ 'sphinx.ext.mathjax']})
+def test_html_math_renderer_is_duplicated2(app, status, warning):
+ # case of both mathjax and another math_renderer is loaded
+ assert app.builder.math_renderer_name == 'imgmath' # The another one is chosen
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinxcontrib.jsmath',
+ 'sphinx.ext.imgmath'],
+ 'html_math_renderer': 'imgmath'})
+def test_html_math_renderer_is_chosen(app, status, warning):
+ assert app.builder.math_renderer_name == 'imgmath'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinxcontrib.jsmath',
+ 'sphinx.ext.mathjax'],
+ 'html_math_renderer': 'imgmath'})
+def test_html_math_renderer_is_mismatched(make_app, app_params):
+ args, kwargs = app_params
+ with pytest.raises(ConfigError, match="Unknown math_renderer 'imgmath' is given."):
+ make_app(*args, **kwargs)
diff --git a/tests/test_builders/test_build_html_numfig.py b/tests/test_builders/test_build_html_numfig.py
new file mode 100644
index 0000000..62e68cb
--- /dev/null
+++ b/tests/test_builders/test_build_html_numfig.py
@@ -0,0 +1,487 @@
+"""Test the HTML builder and check output against XPath."""
+
+import os
+import re
+
+import pytest
+
+from tests.test_builders.xpath_data import FIGURE_CAPTION
+from tests.test_builders.xpath_util import check_xpath
+
+
+@pytest.mark.sphinx('html', testroot='numfig')
+@pytest.mark.test_params(shared_result='test_build_html_numfig')
+def test_numfig_disabled_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' not in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' not in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('index.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+ ('index.html', ".//li/p/code/span", '^fig1$', True),
+ ('index.html', ".//li/p/code/span", '^Figure%s$', True),
+ ('index.html', ".//li/p/code/span", '^table-1$', True),
+ ('index.html', ".//li/p/code/span", '^Table:%s$', True),
+ ('index.html', ".//li/p/code/span", '^CODE_1$', True),
+ ('index.html', ".//li/p/code/span", '^Code-%s$', True),
+ ('index.html', ".//li/p/a/span", '^Section 1$', True),
+ ('index.html', ".//li/p/a/span", '^Section 2.1$', True),
+ ('index.html', ".//li/p/code/span", '^Fig.{number}$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+])
+@pytest.mark.sphinx('html', testroot='numfig')
+@pytest.mark.test_params(shared_result='test_build_html_numfig')
+def test_numfig_disabled(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx(
+ 'html', testroot='numfig',
+ srcdir='test_numfig_without_numbered_toctree_warn',
+ confoverrides={'numfig': True})
+def test_numfig_without_numbered_toctree_warn(app, warning):
+ app.build()
+ # remove :numbered: option
+ index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
+ index = re.sub(':numbered:.*', '', index)
+ (app.srcdir / 'index.rst').write_text(index, encoding='utf8')
+ app.build()
+
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 9 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 10 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 9 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 10 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 9 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 10 $', True),
+ ('index.html', ".//li/p/a/span", '^Fig. 9$', True),
+ ('index.html', ".//li/p/a/span", '^Figure6$', True),
+ ('index.html', ".//li/p/a/span", '^Table 9$', True),
+ ('index.html', ".//li/p/a/span", '^Table:6$', True),
+ ('index.html', ".//li/p/a/span", '^Listing 9$', True),
+ ('index.html', ".//li/p/a/span", '^Code-6$', True),
+ ('index.html', ".//li/p/code/span", '^foo$', True),
+ ('index.html', ".//li/p/code/span", '^bar_a$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.9 should be Fig.1$', True),
+ ('index.html', ".//li/p/code/span", '^Sect.{number}$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 3 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 4 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 3 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 4 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 3 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 4 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 5 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 7 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 8 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 5 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 7 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 8 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 5 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 7 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 8 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 6 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 6 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 6 $', True),
+])
+@pytest.mark.sphinx(
+ 'html', testroot='numfig',
+ srcdir='test_numfig_without_numbered_toctree',
+ confoverrides={'numfig': True})
+def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, path, check, be_found):
+ # remove :numbered: option
+ index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
+ index = re.sub(':numbered:.*', '', index)
+ (app.srcdir / 'index.rst').write_text(index, encoding='utf8')
+
+ if not os.listdir(app.outdir):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
+def test_numfig_with_numbered_toctree_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ ('index.html', ".//li/p/a/span", '^Fig. 1$', True),
+ ('index.html', ".//li/p/a/span", '^Figure2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Table 1$', True),
+ ('index.html', ".//li/p/a/span", '^Table:2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Listing 1$', True),
+ ('index.html', ".//li/p/a/span", '^Code-2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Section.1$', True),
+ ('index.html', ".//li/p/a/span", '^Section.2.1$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.3 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.4 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.3 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.4 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.3 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.4 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.3 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.4 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.2 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.2 $', True),
+])
+@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
+def test_numfig_with_numbered_toctree(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx('html', testroot='numfig', confoverrides={
+ 'numfig': True,
+ 'numfig_format': {'figure': 'Figure:%s',
+ 'table': 'Tab_%s',
+ 'code-block': 'Code-%s',
+ 'section': 'SECTION-%s'}})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
+def test_numfig_with_prefix_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2 $', True),
+ ('index.html', ".//li/p/a/span", '^Figure:1$', True),
+ ('index.html', ".//li/p/a/span", '^Figure2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Tab_1$', True),
+ ('index.html', ".//li/p/a/span", '^Table:2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Code-1$', True),
+ ('index.html', ".//li/p/a/span", '^Code-2.2$', True),
+ ('index.html', ".//li/p/a/span", '^SECTION-1$', True),
+ ('index.html', ".//li/p/a/span", '^SECTION-2.1$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.3 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.4 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.3 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.4 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.3 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.4 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.1 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.3 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.4 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.3 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.4 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.3 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.4 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.2 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.2 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.2 $', True),
+])
+@pytest.mark.sphinx('html', testroot='numfig',
+ confoverrides={'numfig': True,
+ 'numfig_format': {'figure': 'Figure:%s',
+ 'table': 'Tab_%s',
+ 'code-block': 'Code-%s',
+ 'section': 'SECTION-%s'}})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
+def test_numfig_with_prefix(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx('html', testroot='numfig',
+ confoverrides={'numfig': True, 'numfig_secnum_depth': 2})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
+def test_numfig_with_secnum_depth_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ ('index.html', ".//li/p/a/span", '^Fig. 1$', True),
+ ('index.html', ".//li/p/a/span", '^Figure2.1.2$', True),
+ ('index.html', ".//li/p/a/span", '^Table 1$', True),
+ ('index.html', ".//li/p/a/span", '^Table:2.1.2$', True),
+ ('index.html', ".//li/p/a/span", '^Listing 1$', True),
+ ('index.html', ".//li/p/a/span", '^Code-2.1.2$', True),
+ ('index.html', ".//li/p/a/span", '^Section.1$', True),
+ ('index.html', ".//li/p/a/span", '^Section.2.1$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1.2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.2.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1.2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.2.1 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.1 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.3 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1.3 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.2.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1.3 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.2.1 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.2 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1.2 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1.2 $', True),
+])
+@pytest.mark.sphinx('html', testroot='numfig',
+ confoverrides={'numfig': True,
+ 'numfig_secnum_depth': 2})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
+def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.parametrize("expect", [
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ (".//li/p/a/span", '^Fig. 1$', True),
+ (".//li/p/a/span", '^Figure2.2$', True),
+ (".//li/p/a/span", '^Table 1$', True),
+ (".//li/p/a/span", '^Table:2.2$', True),
+ (".//li/p/a/span", '^Listing 1$', True),
+ (".//li/p/a/span", '^Code-2.2$', True),
+ (".//li/p/a/span", '^Section.1$', True),
+ (".//li/p/a/span", '^Section.2.1$', True),
+ (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ (".//li/p/a/span", '^Sect.1 Foo$', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.1 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.2 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.3 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.4 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.2 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.3 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.4 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.1 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.3 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.4 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.3 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.4 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.2 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.2 $', True),
+])
+@pytest.mark.sphinx('singlehtml', testroot='numfig', confoverrides={'numfig': True})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
+def test_numfig_with_singlehtml(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
diff --git a/tests/test_builders/test_build_html_tocdepth.py b/tests/test_builders/test_build_html_tocdepth.py
new file mode 100644
index 0000000..4d99995
--- /dev/null
+++ b/tests/test_builders/test_build_html_tocdepth.py
@@ -0,0 +1,94 @@
+"""Test the HTML builder and check output against XPath."""
+
+import pytest
+
+from tests.test_builders.xpath_util import check_xpath
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', ".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
+ ('index.html', ".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
+ ('index.html', ".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
+ ('index.html', ".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
+
+ ('foo.html', ".//h1", 'Foo', True),
+ ('foo.html', ".//h2", 'Foo A', True),
+ ('foo.html', ".//h3", 'Foo A1', True),
+ ('foo.html', ".//h2", 'Foo B', True),
+ ('foo.html', ".//h3", 'Foo B1', True),
+
+ ('foo.html', ".//h1//span[@class='section-number']", '1. ', True),
+ ('foo.html', ".//h2//span[@class='section-number']", '1.1. ', True),
+ ('foo.html', ".//h3//span[@class='section-number']", '1.1.1. ', True),
+ ('foo.html', ".//h2//span[@class='section-number']", '1.2. ', True),
+ ('foo.html', ".//h3//span[@class='section-number']", '1.2.1. ', True),
+
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.1. Foo A', True),
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.1.1. Foo A1', True),
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.2. Foo B', True),
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.2.1. Foo B1', True),
+
+ ('bar.html', ".//h1", 'Bar', True),
+ ('bar.html', ".//h2", 'Bar A', True),
+ ('bar.html', ".//h2", 'Bar B', True),
+ ('bar.html', ".//h3", 'Bar B1', True),
+ ('bar.html', ".//h1//span[@class='section-number']", '2. ', True),
+ ('bar.html', ".//h2//span[@class='section-number']", '2.1. ', True),
+ ('bar.html', ".//h2//span[@class='section-number']", '2.2. ', True),
+ ('bar.html', ".//h3//span[@class='section-number']", '2.2.1. ', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2. Bar', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.1. Bar A', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.2. Bar B', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.2.1. Bar B1', False),
+
+ ('baz.html', ".//h1", 'Baz A', True),
+ ('baz.html', ".//h1//span[@class='section-number']", '2.1.1. ', True),
+])
+@pytest.mark.sphinx('html', testroot='tocdepth')
+@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
+def test_tocdepth(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ # issue #1251
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.parametrize("expect", [
+ (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
+ (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
+ (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
+ (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
+
+ # index.rst
+ (".//h1", 'test-tocdepth', True),
+
+ # foo.rst
+ (".//h2", 'Foo', True),
+ (".//h3", 'Foo A', True),
+ (".//h4", 'Foo A1', True),
+ (".//h3", 'Foo B', True),
+ (".//h4", 'Foo B1', True),
+ (".//h2//span[@class='section-number']", '1. ', True),
+ (".//h3//span[@class='section-number']", '1.1. ', True),
+ (".//h4//span[@class='section-number']", '1.1.1. ', True),
+ (".//h3//span[@class='section-number']", '1.2. ', True),
+ (".//h4//span[@class='section-number']", '1.2.1. ', True),
+
+ # bar.rst
+ (".//h2", 'Bar', True),
+ (".//h3", 'Bar A', True),
+ (".//h3", 'Bar B', True),
+ (".//h4", 'Bar B1', True),
+ (".//h2//span[@class='section-number']", '2. ', True),
+ (".//h3//span[@class='section-number']", '2.1. ', True),
+ (".//h3//span[@class='section-number']", '2.2. ', True),
+ (".//h4//span[@class='section-number']", '2.2.1. ', True),
+
+ # baz.rst
+ (".//h4", 'Baz A', True),
+ (".//h4//span[@class='section-number']", '2.1.1. ', True),
+])
+@pytest.mark.sphinx('singlehtml', testroot='tocdepth')
+@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
+def test_tocdepth_singlehtml(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
diff --git a/tests/test_build_latex.py b/tests/test_builders/test_build_latex.py
index e37a97e..0776c74 100644
--- a/tests/test_build_latex.py
+++ b/tests/test_builders/test_build_latex.py
@@ -1,9 +1,9 @@
"""Test the build process with LaTeX builder with the test root."""
+import http.server
import os
import re
import subprocess
-from itertools import chain, product
from pathlib import Path
from shutil import copyfile
from subprocess import CalledProcessError
@@ -15,36 +15,26 @@ from sphinx.config import Config
from sphinx.errors import SphinxError
from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping
from sphinx.ext.intersphinx import setup as intersphinx_setup
-from sphinx.testing.util import strip_escseq
from sphinx.util.osutil import ensuredir
from sphinx.writers.latex import LaTeXTranslator
-from .test_build_html import ENV_WARNINGS
+from tests.utils import http_server
try:
from contextlib import chdir
except ImportError:
from sphinx.util.osutil import _chdir as chdir
-LATEX_ENGINES = ['pdflatex', 'lualatex', 'xelatex']
-DOCCLASSES = ['manual', 'howto']
STYLEFILES = ['article.cls', 'fancyhdr.sty', 'titlesec.sty', 'amsmath.sty',
'framed.sty', 'color.sty', 'fancyvrb.sty',
'fncychap.sty', 'geometry.sty', 'kvoptions.sty', 'hyperref.sty',
'booktabs.sty']
-LATEX_WARNINGS = ENV_WARNINGS + """\
-%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
-%(root)s/index.rst:\\d+: WARNING: citation not found: missing
-%(root)s/index.rst:\\d+: WARNING: a suitable image for latex builder not found: foo.\\*
-%(root)s/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
-"""
-
# only run latex if all needed packages are there
def kpsetest(*filenames):
try:
- subprocess.run(['kpsewhich'] + list(filenames), capture_output=True, check=True)
+ subprocess.run(['kpsewhich', *list(filenames)], capture_output=True, check=True)
return True
except (OSError, CalledProcessError):
return False # command not found or exit with non-zero
@@ -64,7 +54,7 @@ def compile_latex_document(app, filename='python.tex', docclass='manual'):
args = [app.config.latex_engine,
'--halt-on-error',
'--interaction=nonstopmode',
- '-output-directory=%s' % latex_outputdir,
+ f'-output-directory={latex_outputdir}',
filename]
subprocess.run(args, capture_output=True, check=True)
except OSError as exc: # most likely the latex executable was not found
@@ -92,6 +82,28 @@ def skip_if_stylefiles_notfound(testfunc):
return testfunc
+class RemoteImageHandler(http.server.BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def do_GET(self):
+ content, content_type = None, None
+ if self.path == "/sphinx.png":
+ with open("tests/roots/test-local-logo/images/img.png", "rb") as f:
+ content = f.read()
+ content_type = "image/png"
+
+ if content:
+ self.send_response(200, "OK")
+ self.send_header("Content-Length", str(len(content)))
+ self.send_header("Content-Type", content_type)
+ self.end_headers()
+ self.wfile.write(content)
+ else:
+ self.send_response(404, "Not Found")
+ self.send_header("Content-Length", "0")
+ self.end_headers()
+
+
@skip_if_requested
@skip_if_stylefiles_notfound
@pytest.mark.parametrize(
@@ -99,13 +111,17 @@ def skip_if_stylefiles_notfound(testfunc):
# Only running test with `python_maximum_signature_line_length` not None with last
# LaTeX engine to reduce testing time, as if this configuration does not fail with
# one engine, it's almost impossible it would fail with another.
- chain(
- product(LATEX_ENGINES[:-1], DOCCLASSES, [None]),
- product([LATEX_ENGINES[-1]], DOCCLASSES, [1]),
- ),
+ [
+ ('pdflatex', 'manual', None),
+ ('pdflatex', 'howto', None),
+ ('lualatex', 'manual', None),
+ ('lualatex', 'howto', None),
+ ('xelatex', 'manual', 1),
+ ('xelatex', 'howto', 1),
+ ],
)
@pytest.mark.sphinx('latex', freshenv=True)
-def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_signature_line_length):
+def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_length):
app.config.python_maximum_signature_line_length = python_maximum_signature_line_length
app.config.intersphinx_mapping = {
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
@@ -121,7 +137,8 @@ def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_
load_mappings(app)
app.builder.init()
LaTeXTranslator.ignore_missing_images = True
- app.builder.build_all()
+ with http_server(RemoteImageHandler):
+ app.build(force_all=True)
# file from latex_additional_files
assert (app.outdir / 'svgimg.svg').is_file()
@@ -131,7 +148,7 @@ def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_
@pytest.mark.sphinx('latex')
def test_writer(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'sphinxtests.tex').read_text(encoding='utf8')
assert ('\\begin{sphinxfigure-in-table}\n\\centering\n\\capstart\n'
@@ -165,7 +182,7 @@ def test_writer(app, status, warning):
'\\sphinxAtStartPar\n'
'something, something else, something more\n'
'\\begin{description}\n'
- '\\sphinxlineitem{\\sphinxhref{http://www.google.com}{Google}}\n'
+ '\\sphinxlineitem{\\sphinxhref{https://www.google.com}{Google}}\n'
'\\sphinxAtStartPar\n'
'For everything.\n'
'\n'
@@ -173,22 +190,9 @@ def test_writer(app, status, warning):
'\n\n\\end{sphinxseealso}\n\n' in result)
-@pytest.mark.sphinx('latex', testroot='warnings', freshenv=True)
-def test_latex_warnings(app, status, warning):
- app.builder.build_all()
-
- warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue()))
- warnings_exp = LATEX_WARNINGS % {
- 'root': re.escape(app.srcdir.as_posix())}
- assert re.match(warnings_exp + '$', warnings), \
- "Warnings don't match:\n" + \
- '--- Expected (regex):\n' + warnings_exp + \
- '--- Got:\n' + warnings
-
-
@pytest.mark.sphinx('latex', testroot='basic')
def test_latex_basic(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -203,7 +207,7 @@ def test_latex_basic(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')],
})
def test_latex_basic_manual(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{report}' in result
@@ -215,7 +219,7 @@ def test_latex_basic_manual(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')],
})
def test_latex_basic_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{article}' in result
@@ -228,7 +232,7 @@ def test_latex_basic_howto(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')],
})
def test_latex_basic_manual_ja(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{ujbook}' in result
@@ -241,7 +245,7 @@ def test_latex_basic_manual_ja(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')],
})
def test_latex_basic_howto_ja(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{ujreport}' in result
@@ -250,7 +254,7 @@ def test_latex_basic_howto_ja(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-theme')
def test_latex_theme(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{book}' in result
@@ -261,7 +265,7 @@ def test_latex_theme(app, status, warning):
confoverrides={'latex_elements': {'papersize': 'b5paper',
'pointsize': '9pt'}})
def test_latex_theme_papersize(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{book}' in result
@@ -272,7 +276,7 @@ def test_latex_theme_papersize(app, status, warning):
confoverrides={'latex_theme_options': {'papersize': 'b5paper',
'pointsize': '9pt'}})
def test_latex_theme_options(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{book}' in result
@@ -281,7 +285,7 @@ def test_latex_theme_options(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'zh'})
def test_latex_additional_settings_for_language_code(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -291,7 +295,7 @@ def test_latex_additional_settings_for_language_code(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'el'})
def test_latex_additional_settings_for_greek(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -302,7 +306,7 @@ def test_latex_additional_settings_for_greek(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-title')
def test_latex_title_after_admonitions(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -313,7 +317,7 @@ def test_latex_title_after_admonitions(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic',
confoverrides={'release': '1.0_0'})
def test_latex_release(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -325,7 +329,7 @@ def test_latex_release(app, status, warning):
@pytest.mark.sphinx('latex', testroot='numfig',
confoverrides={'numfig': True})
def test_numref(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -367,7 +371,7 @@ def test_numref(app, status, warning):
'code-block': 'Code-%s',
'section': 'SECTION-%s'}})
def test_numref_with_prefix1(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -415,7 +419,7 @@ def test_numref_with_prefix1(app, status, warning):
'code-block': 'Code-%s | ',
'section': 'SECTION_%s_'}})
def test_numref_with_prefix2(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -491,7 +495,7 @@ def test_numref_with_language_ja(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-numfig')
def test_latex_obey_numfig_is_false(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage{sphinx}' in result
@@ -504,7 +508,7 @@ def test_latex_obey_numfig_is_false(app, status, warning):
'latex', testroot='latex-numfig',
confoverrides={'numfig': True, 'numfig_secnum_depth': 0})
def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage[,nonumfigreset,mathnumfig]{sphinx}' in result
@@ -517,7 +521,7 @@ def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning):
'latex', testroot='latex-numfig',
confoverrides={'numfig': True, 'numfig_secnum_depth': 2})
def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage[,numfigreset=2,mathnumfig]{sphinx}' in result
@@ -530,7 +534,7 @@ def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning):
'latex', testroot='latex-numfig',
confoverrides={'numfig': True, 'math_numfig': False})
def test_latex_obey_numfig_but_math_numfig_false(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage[,numfigreset=1]{sphinx}' in result
@@ -543,7 +547,7 @@ def test_latex_obey_numfig_but_math_numfig_false(app, status, warning):
def test_latex_add_latex_package(app, status, warning):
app.add_latex_package('foo')
app.add_latex_package('bar', 'baz')
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
assert '\\usepackage{foo}' in result
assert '\\usepackage[baz]{bar}' in result
@@ -551,7 +555,7 @@ def test_latex_add_latex_package(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-babel')
def test_babel_with_no_language_settings(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -576,7 +580,7 @@ def test_babel_with_no_language_settings(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'de'})
def test_babel_with_language_de(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -601,7 +605,7 @@ def test_babel_with_language_de(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'ru'})
def test_babel_with_language_ru(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -626,7 +630,7 @@ def test_babel_with_language_ru(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'tr'})
def test_babel_with_language_tr(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -651,7 +655,7 @@ def test_babel_with_language_tr(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'ja'})
def test_babel_with_language_ja(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -675,7 +679,7 @@ def test_babel_with_language_ja(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'unknown'})
def test_babel_with_unknown_language(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -702,7 +706,7 @@ def test_babel_with_unknown_language(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'de', 'latex_engine': 'lualatex'})
def test_polyglossia_with_language_de(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -728,7 +732,7 @@ def test_polyglossia_with_language_de(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'de-1901', 'latex_engine': 'lualatex'})
def test_polyglossia_with_language_de_1901(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -752,7 +756,7 @@ def test_polyglossia_with_language_de_1901(app, status, warning):
@pytest.mark.sphinx('latex')
def test_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'sphinxtests.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -781,7 +785,7 @@ def test_footnote(app, status, warning):
@pytest.mark.sphinx('latex', testroot='footnotes')
def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -821,7 +825,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
@pytest.mark.sphinx('latex', testroot='footnotes')
def test_footnote_referred_multiple_times(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -843,7 +847,7 @@ def test_footnote_referred_multiple_times(app, status, warning):
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'inline'})
def test_latex_show_urls_is_inline(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -866,29 +870,29 @@ def test_latex_show_urls_is_inline(app, status, warning):
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}\n') in result
- assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx} (http://sphinx\\sphinxhyphen{}doc.org/)' in result
+ assert '\\sphinxhref{https://sphinx-doc.org/}{Sphinx} (https://sphinx\\sphinxhyphen{}doc.org/)' in result
assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n'
'\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n'
'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result
assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n'
'Fourth\n%\n\\end{footnote}\n') in result
- assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde} '
- '(http://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term} '
- '(http://sphinx\\sphinxhyphen{}doc.org/)}\n'
+ assert ('\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/)}\n'
'\\sphinxAtStartPar\nDescription' in result)
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' in result)
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term} '
- '(http://sphinx\\sphinxhyphen{}doc.org/)}\n'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/)}\n'
'\\sphinxAtStartPar\nDescription' in result)
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n'
'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces '
'\n\\sphinxAtStartPar\nDescription') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist} '
- '(http://sphinx\\sphinxhyphen{}doc.org/)}'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/)}'
'\n\\sphinxAtStartPar\nDescription') in result
assert '\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
@@ -900,7 +904,7 @@ def test_latex_show_urls_is_inline(app, status, warning):
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'footnote'})
def test_latex_show_urls_is_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -922,9 +926,9 @@ def test_latex_show_urls_is_footnote(app, status, warning):
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result
- assert ('\\sphinxhref{http://sphinx-doc.org/}{Sphinx}'
+ assert ('\\sphinxhref{https://sphinx-doc.org/}{Sphinx}'
'%\n\\begin{footnote}[4]\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n\\end{footnote}') in result
+ '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n\\end{footnote}') in result
assert ('Third footnote: %\n\\begin{footnote}[6]\\sphinxAtStartFootnote\n'
'Third \\sphinxfootnotemark[7]\n%\n\\end{footnote}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n'
@@ -932,25 +936,25 @@ def test_latex_show_urls_is_footnote(app, status, warning):
'\\end{footnotetext}\\ignorespaces') in result
assert ('Fourth footnote: %\n\\begin{footnote}[8]\\sphinxAtStartFootnote\n'
'Fourth\n%\n\\end{footnote}\n') in result
- assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}'
+ assert ('\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde}'
'%\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}'
+ '\\sphinxnolinkurl{https://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}'
'{URL in term}\\sphinxfootnotemark[10]}%\n'
'\\begin{footnotetext}[10]'
'\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n'
+ '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n'
'\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[12]}%\n'
'\\begin{footnotetext}[12]'
'\\sphinxAtStartFootnote\n'
'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces '
'\n\\sphinxAtStartPar\nDescription') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist}'
'\\sphinxfootnotemark[11]}%\n'
'\\begin{footnotetext}[11]'
'\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n'
+ '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n'
'\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result)
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
@@ -962,7 +966,7 @@ def test_latex_show_urls_is_footnote(app, status, warning):
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'no'})
def test_latex_show_urls_is_no(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -984,21 +988,21 @@ def test_latex_show_urls_is_no(app, status, warning):
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result
- assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' in result
+ assert '\\sphinxhref{https://sphinx-doc.org/}{Sphinx}' in result
assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n'
'\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n'
'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result
assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n'
'Fourth\n%\n\\end{footnote}\n') in result
- assert '\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}' in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term}}\n'
+ assert '\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde}' in result
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term}}\n'
'\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n'
'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces '
'\n\\sphinxAtStartPar\nDescription') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}}'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist}}'
'\n\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result)
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
@@ -1009,7 +1013,7 @@ def test_latex_show_urls_is_no(app, status, warning):
@pytest.mark.sphinx(
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'footnote',
- 'rst_prolog': '.. |URL| replace:: `text <http://www.example.com/>`__'})
+ 'rst_prolog': '.. |URL| replace:: `text <https://www.example.com/>`__'})
def test_latex_show_urls_footnote_and_substitutions(app, status, warning):
# hyperlinks in substitutions should not effect to make footnotes (refs: #4784)
test_latex_show_urls_is_footnote(app, status, warning)
@@ -1017,7 +1021,7 @@ def test_latex_show_urls_footnote_and_substitutions(app, status, warning):
@pytest.mark.sphinx('latex', testroot='image-in-section')
def test_image_in_section(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1035,12 +1039,12 @@ def test_image_in_section(app, status, warning):
confoverrides={'latex_logo': 'notfound.jpg'})
def test_latex_logo_if_not_found(app, status, warning):
with pytest.raises(SphinxError):
- app.builder.build_all()
+ app.build(force_all=True)
@pytest.mark.sphinx('latex', testroot='toctree-maxdepth')
def test_toctree_maxdepth_manual(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1057,7 +1061,7 @@ def test_toctree_maxdepth_manual(app, status, warning):
'Georg Brandl', 'howto'),
]})
def test_toctree_maxdepth_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1071,7 +1075,7 @@ def test_toctree_maxdepth_howto(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'root_doc': 'foo'})
def test_toctree_not_found(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1085,7 +1089,7 @@ def test_toctree_not_found(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'root_doc': 'bar'})
def test_toctree_without_maxdepth(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1098,7 +1102,7 @@ def test_toctree_without_maxdepth(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'root_doc': 'qux'})
def test_toctree_with_deeper_maxdepth(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1111,7 +1115,7 @@ def test_toctree_with_deeper_maxdepth(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': None})
def test_latex_toplevel_sectioning_is_None(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1123,7 +1127,7 @@ def test_latex_toplevel_sectioning_is_None(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': 'part'})
def test_latex_toplevel_sectioning_is_part(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1141,7 +1145,7 @@ def test_latex_toplevel_sectioning_is_part(app, status, warning):
'Georg Brandl', 'howto'),
]})
def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1155,7 +1159,7 @@ def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': 'chapter'})
def test_latex_toplevel_sectioning_is_chapter(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1171,7 +1175,7 @@ def test_latex_toplevel_sectioning_is_chapter(app, status, warning):
'Georg Brandl', 'howto'),
]})
def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1183,7 +1187,7 @@ def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': 'section'})
def test_latex_toplevel_sectioning_is_section(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1194,7 +1198,7 @@ def test_latex_toplevel_sectioning_is_section(app, status, warning):
@skip_if_stylefiles_notfound
@pytest.mark.sphinx('latex', testroot='maxlistdepth')
def test_maxlistdepth_at_ten(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1206,7 +1210,7 @@ def test_maxlistdepth_at_ten(app, status, warning):
confoverrides={'latex_table_style': []})
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_tabulars(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
tables = {}
for chap in re.split(r'\\(?:section|chapter){', result)[1:]:
@@ -1277,7 +1281,7 @@ def test_latex_table_tabulars(app, status, warning):
confoverrides={'latex_table_style': []})
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_longtable(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
tables = {}
for chap in re.split(r'\\(?:section|chapter){', result)[1:]:
@@ -1338,7 +1342,7 @@ def test_latex_table_longtable(app, status, warning):
confoverrides={'latex_table_style': []})
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_complex_tables(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
tables = {}
for chap in re.split(r'\\(?:section|renewcommand){', result)[1:]:
@@ -1368,7 +1372,7 @@ def test_latex_table_complex_tables(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table')
def test_latex_table_with_booktabs_and_colorrows(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert r'\PassOptionsToPackage{booktabs}{sphinx}' in result
assert r'\PassOptionsToPackage{colorrows}{sphinx}' in result
@@ -1384,7 +1388,7 @@ def test_latex_table_with_booktabs_and_colorrows(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table',
confoverrides={'templates_path': ['_mytemplates/latex']})
def test_latex_table_custom_template_caseA(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert 'SALUT LES COPAINS' in result
@@ -1392,7 +1396,7 @@ def test_latex_table_custom_template_caseA(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table',
confoverrides={'templates_path': ['_mytemplates']})
def test_latex_table_custom_template_caseB(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert 'SALUT LES COPAINS' not in result
@@ -1400,14 +1404,14 @@ def test_latex_table_custom_template_caseB(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table')
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_custom_template_caseC(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert 'SALUT LES COPAINS' not in result
@pytest.mark.sphinx('latex', testroot='directives-raw')
def test_latex_raw_directive(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
# standard case
@@ -1422,18 +1426,19 @@ def test_latex_raw_directive(app, status, warning):
@pytest.mark.sphinx('latex', testroot='images')
def test_latex_images(app, status, warning):
- app.builder.build_all()
+ with http_server(RemoteImageHandler, port=7777):
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
# images are copied
- assert '\\sphinxincludegraphics{{python-logo}.png}' in result
- assert (app.outdir / 'python-logo.png').exists()
+ assert '\\sphinxincludegraphics{{sphinx}.png}' in result
+ assert (app.outdir / 'sphinx.png').exists()
# not found images
assert '\\sphinxincludegraphics{{NOT_EXIST}.PNG}' not in result
assert ('WARNING: Could not fetch remote image: '
- 'https://www.google.com/NOT_EXIST.PNG [404]' in warning.getvalue())
+ 'http://localhost:7777/NOT_EXIST.PNG [404]' in warning.getvalue())
# an image having target
assert ('\\sphinxhref{https://www.sphinx-doc.org/}'
@@ -1446,7 +1451,7 @@ def test_latex_images(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-index')
def test_latex_index(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('A \\index{famous@\\spxentry{famous}}famous '
@@ -1460,7 +1465,7 @@ def test_latex_index(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-equations')
def test_latex_equations(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
expected = (app.srcdir / 'expects' / 'latex-equations.tex').read_text(encoding='utf8').strip()
@@ -1470,7 +1475,7 @@ def test_latex_equations(app, status, warning):
@pytest.mark.sphinx('latex', testroot='image-in-parsed-literal')
def test_latex_image_in_parsed_literal(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('{\\sphinxunactivateextrasandspace \\raisebox{-0.5\\height}'
@@ -1480,7 +1485,7 @@ def test_latex_image_in_parsed_literal(app, status, warning):
@pytest.mark.sphinx('latex', testroot='nested-enumerated-list')
def test_latex_nested_enumerated_list(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n'
@@ -1497,7 +1502,7 @@ def test_latex_nested_enumerated_list(app, status, warning):
@pytest.mark.sphinx('latex', testroot='footnotes')
def test_latex_thebibliography(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
@@ -1510,7 +1515,7 @@ def test_latex_thebibliography(app, status, warning):
@pytest.mark.sphinx('latex', testroot='glossary')
def test_latex_glossary(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert (r'\sphinxlineitem{ähnlich\index{ähnlich@\spxentry{ähnlich}|spxpagem}'
@@ -1534,7 +1539,7 @@ def test_latex_glossary(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-labels')
def test_latex_labels(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
@@ -1583,7 +1588,7 @@ def test_latex_labels(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-figure-in-admonition')
def test_latex_figure_in_admonition(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert r'\begin{figure}[H]' in result
@@ -1594,7 +1599,6 @@ def test_default_latex_documents():
config = Config({'root_doc': 'index',
'project': 'STASI™ Documentation',
'author': "Wolfgang Schäuble & G'Beckstein."})
- config.init_values()
config.add('latex_engine', None, True, None)
config.add('latex_theme', 'manual', True, None)
expected = [('index', 'stasi.tex', 'STASI™ Documentation',
@@ -1606,7 +1610,7 @@ def test_default_latex_documents():
@skip_if_stylefiles_notfound
@pytest.mark.sphinx('latex', testroot='latex-includegraphics')
def test_includegraphics_oversized(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
print(status.getvalue())
print(warning.getvalue())
compile_latex_document(app)
@@ -1614,7 +1618,7 @@ def test_includegraphics_oversized(app, status, warning):
@pytest.mark.sphinx('latex', testroot='index_on_title')
def test_index_on_title(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('\\chapter{Test for index in top level title}\n'
'\\label{\\detokenize{contents:test-for-index-in-top-level-title}}'
@@ -1625,7 +1629,7 @@ def test_index_on_title(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-unicode',
confoverrides={'latex_engine': 'pdflatex'})
def test_texescape_for_non_unicode_supported_engine(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert 'script small e: e' in result
@@ -1637,7 +1641,7 @@ def test_texescape_for_non_unicode_supported_engine(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-unicode',
confoverrides={'latex_engine': 'xelatex'})
def test_texescape_for_unicode_supported_engine(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert 'script small e: e' in result
@@ -1649,20 +1653,20 @@ def test_texescape_for_unicode_supported_engine(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic',
confoverrides={'latex_elements': {'extrapackages': r'\usepackage{foo}'}})
def test_latex_elements_extrapackages(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
assert r'\usepackage{foo}' in result
@pytest.mark.sphinx('latex', testroot='nested-tables')
def test_latex_nested_tables(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert warning.getvalue() == ''
@pytest.mark.sphinx('latex', testroot='latex-container')
def test_latex_container(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert r'\begin{sphinxuseclass}{classname}' in result
assert r'\end{sphinxuseclass}' in result
@@ -1704,7 +1708,7 @@ def test_copy_images(app, status, warning):
image.name for image in test_dir.rglob('*')
if image.suffix in {'.gif', '.pdf', '.png', '.svg'}
}
- images.discard('python-logo.png')
+ images.discard('sphinx.png')
assert images == {
'img.pdf',
'rimg.png',
@@ -1745,7 +1749,7 @@ def test_duplicated_labels_before_module(app, status, warning):
@pytest.mark.sphinx('latex', testroot='domain-py-python_maximum_signature_line_length',
confoverrides={'python_maximum_signature_line_length': 23})
def test_one_parameter_per_line(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
# TODO: should these asserts check presence or absence of a final \sphinxparamcomma?
diff --git a/tests/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py
index 38a0bd1..c8d8515 100644
--- a/tests/test_build_linkcheck.py
+++ b/tests/test_builders/test_build_linkcheck.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import http.server
import json
import re
import sys
@@ -10,10 +9,11 @@ import textwrap
import time
import wsgiref.handlers
from base64 import b64encode
-from os import path
+from http.server import BaseHTTPRequestHandler
from queue import Queue
from unittest import mock
+import docutils
import pytest
from urllib3.poolmanager import PoolManager
@@ -23,18 +23,18 @@ from sphinx.builders.linkcheck import (
Hyperlink,
HyperlinkAvailabilityCheckWorker,
RateLimit,
+ compile_linkcheck_allowed_redirects,
)
-from sphinx.testing.util import strip_escseq
+from sphinx.deprecation import RemovedInSphinx80Warning
from sphinx.util import requests
from sphinx.util.console import strip_colors
-from .utils import CERT_FILE, http_server, https_server
+from tests.utils import CERT_FILE, serve_application
ts_re = re.compile(r".*\[(?P<ts>.*)\].*")
-SPHINX_DOCS_INDEX = path.abspath(path.join(__file__, "..", "roots", "test-linkcheck", "sphinx-docs-index.html"))
-class DefaultsHandler(http.server.BaseHTTPRequestHandler):
+class DefaultsHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -102,7 +102,7 @@ class ConnectionMeasurement:
@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
def test_defaults(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler) as address:
with ConnectionMeasurement() as m:
app.build()
assert m.connection_count <= 5
@@ -115,9 +115,9 @@ def test_defaults(app):
assert "Anchor 'top' not found" in content
assert "Anchor 'does-not-exist' not found" in content
# images should fail
- assert "Not Found for url: http://localhost:7777/image.png" in content
- assert "Not Found for url: http://localhost:7777/image2.png" in content
- # looking for local file should fail
+ assert f"Not Found for url: http://{address}/image.png" in content
+ assert f"Not Found for url: http://{address}/image2.png" in content
+ # looking for missing local file should fail
assert "[broken] path/to/notfound" in content
assert len(content.splitlines()) == 5
@@ -135,35 +135,42 @@ def test_defaults(app):
# the output order of the rows is not stable
# due to possible variance in network latency
rowsby = {row["uri"]: row for row in rows}
- assert rowsby["http://localhost:7777#!bar"] == {
+ # looking for local file that exists should succeed
+ assert rowsby["conf.py"]["status"] == "working"
+ assert rowsby[f"http://{address}#!bar"] == {
'filename': 'links.rst',
'lineno': 5,
'status': 'working',
'code': 0,
- 'uri': 'http://localhost:7777#!bar',
+ 'uri': f'http://{address}#!bar',
'info': '',
}
- assert rowsby['http://localhost:7777/image2.png'] == {
- 'filename': 'links.rst',
- 'lineno': 13,
- 'status': 'broken',
- 'code': 0,
- 'uri': 'http://localhost:7777/image2.png',
- 'info': '404 Client Error: Not Found for url: http://localhost:7777/image2.png',
- }
+
+ def _missing_resource(filename: str, lineno: int):
+ return {
+ 'filename': 'links.rst',
+ 'lineno': lineno,
+ 'status': 'broken',
+ 'code': 0,
+ 'uri': f'http://{address}/{filename}',
+ 'info': f'404 Client Error: Not Found for url: http://{address}/{filename}',
+ }
+ accurate_linenumbers = docutils.__version_info__[:2] >= (0, 21)
+ image2_lineno = 12 if accurate_linenumbers else 13
+ assert rowsby[f'http://{address}/image2.png'] == _missing_resource("image2.png", image2_lineno)
# looking for '#top' and '#does-not-exist' not found should fail
- assert rowsby["http://localhost:7777/#top"]["info"] == "Anchor 'top' not found"
- assert rowsby["http://localhost:7777/#top"]["status"] == "broken"
- assert rowsby["http://localhost:7777#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found"
+ assert rowsby[f"http://{address}/#top"]["info"] == "Anchor 'top' not found"
+ assert rowsby[f"http://{address}/#top"]["status"] == "broken"
+ assert rowsby[f"http://{address}#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found"
# images should fail
- assert "Not Found for url: http://localhost:7777/image.png" in rowsby["http://localhost:7777/image.png"]["info"]
+ assert f"Not Found for url: http://{address}/image.png" in rowsby[f"http://{address}/image.png"]["info"]
# anchor should be found
- assert rowsby['http://localhost:7777/anchor.html#found'] == {
+ assert rowsby[f'http://{address}/anchor.html#found'] == {
'filename': 'links.rst',
'lineno': 14,
'status': 'working',
'code': 0,
- 'uri': 'http://localhost:7777/anchor.html#found',
+ 'uri': f'http://{address}/anchor.html#found',
'info': '',
}
@@ -172,7 +179,7 @@ def test_defaults(app):
'linkcheck', testroot='linkcheck', freshenv=True,
confoverrides={'linkcheck_anchors': False})
def test_check_link_response_only(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler) as address:
app.build()
# JSON output
@@ -181,12 +188,12 @@ def test_check_link_response_only(app):
rows = [json.loads(x) for x in content.splitlines()]
rowsby = {row["uri"]: row for row in rows}
- assert rowsby["http://localhost:7777/#top"]["status"] == "working"
+ assert rowsby[f"http://{address}/#top"]["status"] == "working"
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-too-many-retries', freshenv=True)
def test_too_many_retries(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler) as address:
app.build()
# Text output
@@ -210,12 +217,20 @@ def test_too_many_retries(app):
assert row['lineno'] == 1
assert row['status'] == 'broken'
assert row['code'] == 0
- assert row['uri'] == 'https://localhost:7777/doesnotexist'
+ assert row['uri'] == f'https://{address}/doesnotexist'
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-raw-node', freshenv=True)
def test_raw_node(app):
- with http_server(OKHandler):
+ with serve_application(app, OKHandler) as address:
+ # write an index file that contains a link back to this webserver's root
+ # URL. docutils will replace the raw node with the contents retrieved..
+ # ..and then the linkchecker will check that the root URL is available.
+ index = (app.srcdir / "index.rst")
+ index.write_text(
+ ".. raw:: 'html'\n"
+ " :url: http://{address}/".format(address=address),
+ )
app.build()
# JSON output
@@ -231,7 +246,7 @@ def test_raw_node(app):
'lineno': 1,
'status': 'working',
'code': 0,
- 'uri': 'http://localhost:7777/',
+ 'uri': f'http://{address}/', # the received rST contains a link to its' own URL
'info': '',
}
@@ -240,7 +255,7 @@ def test_raw_node(app):
'linkcheck', testroot='linkcheck-anchors-ignore', freshenv=True,
confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"]})
def test_anchors_ignored(app):
- with http_server(OKHandler):
+ with serve_application(app, OKHandler):
app.build()
assert (app.outdir / 'output.txt').exists()
@@ -250,7 +265,7 @@ def test_anchors_ignored(app):
assert not content
-class AnchorsIgnoreForUrlHandler(http.server.BaseHTTPRequestHandler):
+class AnchorsIgnoreForUrlHandler(BaseHTTPRequestHandler):
def do_HEAD(self):
if self.path in {'/valid', '/ignored'}:
self.send_response(200, "OK")
@@ -266,14 +281,13 @@ class AnchorsIgnoreForUrlHandler(http.server.BaseHTTPRequestHandler):
self.wfile.write(b"no anchor but page exists\n")
-@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True,
- confoverrides={'linkcheck_anchors_ignore_for_url': [
- 'http://localhost:7777/ignored', # existing page
- 'http://localhost:7777/invalid', # unknown page
- ]})
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True)
def test_anchors_ignored_for_url(app):
- with http_server(AnchorsIgnoreForUrlHandler):
+ with serve_application(app, AnchorsIgnoreForUrlHandler) as address:
+ app.config.linkcheck_anchors_ignore_for_url = [ # type: ignore[attr-defined]
+ f'http://{address}/ignored', # existing page
+ f'http://{address}/invalid', # unknown page
+ ]
app.build()
assert (app.outdir / 'output.txt').exists()
@@ -288,41 +302,41 @@ def test_anchors_ignored_for_url(app):
# the order the threads are processing the links
rows = {r['uri']: {'status': r['status'], 'info': r['info']} for r in data}
- assert rows['http://localhost:7777/valid']['status'] == 'working'
- assert rows['http://localhost:7777/valid#valid-anchor']['status'] == 'working'
- assert rows['http://localhost:7777/valid#invalid-anchor'] == {
+ assert rows[f'http://{address}/valid']['status'] == 'working'
+ assert rows[f'http://{address}/valid#valid-anchor']['status'] == 'working'
+ assert rows[f'http://{address}/valid#invalid-anchor'] == {
'status': 'broken',
'info': "Anchor 'invalid-anchor' not found",
}
- assert rows['http://localhost:7777/ignored']['status'] == 'working'
- assert rows['http://localhost:7777/ignored#invalid-anchor']['status'] == 'working'
+ assert rows[f'http://{address}/ignored']['status'] == 'working'
+ assert rows[f'http://{address}/ignored#invalid-anchor']['status'] == 'working'
- assert rows['http://localhost:7777/invalid'] == {
+ assert rows[f'http://{address}/invalid'] == {
'status': 'broken',
- 'info': '404 Client Error: Not Found for url: http://localhost:7777/invalid',
+ 'info': f'404 Client Error: Not Found for url: http://{address}/invalid',
}
- assert rows['http://localhost:7777/invalid#anchor'] == {
+ assert rows[f'http://{address}/invalid#anchor'] == {
'status': 'broken',
- 'info': '404 Client Error: Not Found for url: http://localhost:7777/invalid',
+ 'info': f'404 Client Error: Not Found for url: http://{address}/invalid',
}
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True)
def test_raises_for_invalid_status(app):
- class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler):
+ class InternalServerErrorHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
self.send_error(500, "Internal Server Error")
- with http_server(InternalServerErrorHandler):
+ with serve_application(app, InternalServerErrorHandler) as address:
app.build()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert content == (
- "index.rst:1: [broken] http://localhost:7777/#anchor: "
+ f"index.rst:1: [broken] http://{address}/#anchor: "
"500 Server Error: Internal Server Error "
- "for url: http://localhost:7777/\n"
+ f"for url: http://{address}/\n"
)
@@ -338,13 +352,17 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True):
expected_token = b64encode(":".join(valid_credentials).encode()).decode("utf-8")
del valid_credentials
- class CustomHandler(http.server.BaseHTTPRequestHandler):
+ class CustomHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def authenticated(method):
def method_if_authenticated(self):
- if (expected_token is None
- or self.headers["Authorization"] == f"Basic {expected_token}"):
+ if expected_token is None:
+ return method(self)
+ elif not self.headers["Authorization"]:
+ self.send_response(401, "Unauthorized")
+ self.end_headers()
+ elif self.headers["Authorization"] == f"Basic {expected_token}":
return method(self)
else:
self.send_response(403, "Forbidden")
@@ -370,15 +388,14 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True):
return CustomHandler
-@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_auth': [
- (r'^$', ('no', 'match')),
- (r'^http://localhost:7777/$', ('user1', 'password')),
- (r'.*local.*', ('user2', 'hunter2')),
- ]})
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_auth_header_uses_first_match(app):
- with http_server(custom_handler(valid_credentials=("user1", "password"))):
+ with serve_application(app, custom_handler(valid_credentials=("user1", "password"))) as address:
+ app.config.linkcheck_auth = [ # type: ignore[attr-defined]
+ (r'^$', ('no', 'match')),
+ (fr'^http://{re.escape(address)}/$', ('user1', 'password')),
+ (r'.*local.*', ('user2', 'hunter2')),
+ ]
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -387,41 +404,51 @@ def test_auth_header_uses_first_match(app):
assert content["status"] == "working"
+@pytest.mark.filterwarnings('ignore::sphinx.deprecation.RemovedInSphinx80Warning')
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]})
-def test_auth_header_no_match(app):
- with http_server(custom_handler(valid_credentials=("user1", "password"))):
+ confoverrides={'linkcheck_allow_unauthorized': False})
+def test_unauthorized_broken(app):
+ with serve_application(app, custom_handler(valid_credentials=("user1", "password"))):
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
content = json.load(fp)
- # TODO: should this test's webserver return HTTP 401 here?
- # https://github.com/sphinx-doc/sphinx/issues/11433
- assert content["info"] == "403 Client Error: Forbidden for url: http://localhost:7777/"
+ assert content["info"] == "unauthorized"
assert content["status"] == "broken"
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_request_headers': {
- "http://localhost:7777/": {
- "Accept": "text/html",
- },
- "*": {
- "X-Secret": "open sesami",
- },
- }})
+ confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]})
+def test_auth_header_no_match(app):
+ with (
+ serve_application(app, custom_handler(valid_credentials=("user1", "password"))),
+ pytest.warns(RemovedInSphinx80Warning, match='linkcheck builder encountered an HTTP 401'),
+ ):
+ app.build()
+
+ with open(app.outdir / "output.json", encoding="utf-8") as fp:
+ content = json.load(fp)
+
+ # This link is considered working based on the default linkcheck_allow_unauthorized=true
+ assert content["info"] == "unauthorized"
+ assert content["status"] == "working"
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_linkcheck_request_headers(app):
def check_headers(self):
if "X-Secret" in self.headers:
return False
- if self.headers["Accept"] != "text/html":
- return False
- return True
+ return self.headers["Accept"] == "text/html"
- with http_server(custom_handler(success_criteria=check_headers)):
+ with serve_application(app, custom_handler(success_criteria=check_headers)) as address:
+ app.config.linkcheck_request_headers = { # type: ignore[attr-defined]
+ f"http://{address}/": {"Accept": "text/html"},
+ "*": {"X-Secret": "open sesami"},
+ }
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -430,21 +457,18 @@ def test_linkcheck_request_headers(app):
assert content["status"] == "working"
-@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_request_headers': {
- "http://localhost:7777": {"Accept": "application/json"},
- "*": {"X-Secret": "open sesami"},
- }})
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_linkcheck_request_headers_no_slash(app):
def check_headers(self):
if "X-Secret" in self.headers:
return False
- if self.headers["Accept"] != "application/json":
- return False
- return True
+ return self.headers["Accept"] == "application/json"
- with http_server(custom_handler(success_criteria=check_headers)):
+ with serve_application(app, custom_handler(success_criteria=check_headers)) as address:
+ app.config.linkcheck_request_headers = { # type: ignore[attr-defined]
+ f"http://{address}": {"Accept": "application/json"},
+ "*": {"X-Secret": "open sesami"},
+ }
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -463,11 +487,9 @@ def test_linkcheck_request_headers_default(app):
def check_headers(self):
if self.headers["X-Secret"] != "open sesami":
return False
- if self.headers["Accept"] == "application/json":
- return False
- return True
+ return self.headers["Accept"] != "application/json"
- with http_server(custom_handler(success_criteria=check_headers)):
+ with serve_application(app, custom_handler(success_criteria=check_headers)):
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -477,7 +499,7 @@ def test_linkcheck_request_headers_default(app):
def make_redirect_handler(*, support_head):
- class RedirectOnceHandler(http.server.BaseHTTPRequestHandler):
+ class RedirectOnceHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -493,7 +515,7 @@ def make_redirect_handler(*, support_head):
self.send_response(204, "No content")
else:
self.send_response(302, "Found")
- self.send_header("Location", "http://localhost:7777/?redirected=1")
+ self.send_header("Location", "/?redirected=1")
self.send_header("Content-Length", "0")
self.end_headers()
@@ -506,13 +528,13 @@ def make_redirect_handler(*, support_head):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_follows_redirects_on_HEAD(app, capsys, warning):
- with http_server(make_redirect_handler(support_head=True)):
+ with serve_application(app, make_redirect_handler(support_head=True)) as address:
app.build()
stdout, stderr = capsys.readouterr()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert content == (
"index.rst:1: [redirected with Found] "
- "http://localhost:7777/ to http://localhost:7777/?redirected=1\n"
+ f"http://{address}/ to http://{address}/?redirected=1\n"
)
assert stderr == textwrap.dedent(
"""\
@@ -525,13 +547,13 @@ def test_follows_redirects_on_HEAD(app, capsys, warning):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_follows_redirects_on_GET(app, capsys, warning):
- with http_server(make_redirect_handler(support_head=False)):
+ with serve_application(app, make_redirect_handler(support_head=False)) as address:
app.build()
stdout, stderr = capsys.readouterr()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert content == (
"index.rst:1: [redirected with Found] "
- "http://localhost:7777/ to http://localhost:7777/?redirected=1\n"
+ f"http://{address}/ to http://{address}/?redirected=1\n"
)
assert stderr == textwrap.dedent(
"""\
@@ -543,35 +565,34 @@ def test_follows_redirects_on_GET(app, capsys, warning):
assert warning.getvalue() == ''
-@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects',
- freshenv=True, confoverrides={
- 'linkcheck_allowed_redirects': {'http://localhost:7777/.*1': '.*'},
- })
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects')
def test_linkcheck_allowed_redirects(app, warning):
- with http_server(make_redirect_handler(support_head=False)):
+ with serve_application(app, make_redirect_handler(support_head=False)) as address:
+ app.config.linkcheck_allowed_redirects = {f'http://{address}/.*1': '.*'} # type: ignore[attr-defined]
+ compile_linkcheck_allowed_redirects(app, app.config)
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
- rows = [json.loads(l) for l in fp.readlines()]
+ rows = [json.loads(l) for l in fp]
assert len(rows) == 2
records = {row["uri"]: row for row in rows}
- assert records["http://localhost:7777/path1"]["status"] == "working"
- assert records["http://localhost:7777/path2"] == {
+ assert records[f"http://{address}/path1"]["status"] == "working"
+ assert records[f"http://{address}/path2"] == {
'filename': 'index.rst',
'lineno': 3,
'status': 'redirected',
'code': 302,
- 'uri': 'http://localhost:7777/path2',
- 'info': 'http://localhost:7777/?redirected=1',
+ 'uri': f'http://{address}/path2',
+ 'info': f'http://{address}/?redirected=1',
}
- assert ("index.rst:3: WARNING: redirect http://localhost:7777/path2 - with Found to "
- "http://localhost:7777/?redirected=1\n" in strip_escseq(warning.getvalue()))
+ assert (f"index.rst:3: WARNING: redirect http://{address}/path2 - with Found to "
+ f"http://{address}/?redirected=1\n" in strip_colors(warning.getvalue()))
assert len(warning.getvalue().splitlines()) == 1
-class OKHandler(http.server.BaseHTTPRequestHandler):
+class OKHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -591,7 +612,7 @@ class OKHandler(http.server.BaseHTTPRequestHandler):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_invalid_ssl(get_request, app):
# Link indicates SSL should be used (https) but the server does not handle it.
- with http_server(OKHandler):
+ with serve_application(app, OKHandler) as address:
app.build()
assert not get_request.called
@@ -600,13 +621,13 @@ def test_invalid_ssl(get_request, app):
assert content["status"] == "broken"
assert content["filename"] == "index.rst"
assert content["lineno"] == 1
- assert content["uri"] == "https://localhost:7777/"
+ assert content["uri"] == f"https://{address}/"
assert "SSLError" in content["info"]
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_fails(app):
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -614,14 +635,14 @@ def test_connect_to_selfsigned_fails(app):
assert content["status"] == "broken"
assert content["filename"] == "index.rst"
assert content["lineno"] == 1
- assert content["uri"] == "https://localhost:7777/"
+ assert content["uri"] == f"https://{address}/"
assert "[SSL: CERTIFICATE_VERIFY_FAILED]" in content["info"]
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_with_tls_verify_false(app):
app.config.tls_verify = False
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -631,7 +652,7 @@ def test_connect_to_selfsigned_with_tls_verify_false(app):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "",
}
@@ -639,7 +660,7 @@ def test_connect_to_selfsigned_with_tls_verify_false(app):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_with_tls_cacerts(app):
app.config.tls_cacerts = CERT_FILE
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -649,7 +670,7 @@ def test_connect_to_selfsigned_with_tls_cacerts(app):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "",
}
@@ -657,7 +678,7 @@ def test_connect_to_selfsigned_with_tls_cacerts(app):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app):
monkeypatch.setenv("REQUESTS_CA_BUNDLE", CERT_FILE)
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -667,7 +688,7 @@ def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "",
}
@@ -675,7 +696,7 @@ def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_nonexistent_cert_file(app):
app.config.tls_cacerts = "does/not/exist"
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -685,17 +706,17 @@ def test_connect_to_selfsigned_nonexistent_cert_file(app):
"status": "broken",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "Could not find a suitable TLS CA certificate bundle, invalid path: does/not/exist",
}
-class InfiniteRedirectOnHeadHandler(http.server.BaseHTTPRequestHandler):
+class InfiniteRedirectOnHeadHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
self.send_response(302, "Found")
- self.send_header("Location", "http://localhost:7777/")
+ self.send_header("Location", "/")
self.send_header("Content-Length", "0")
self.end_headers()
@@ -714,7 +735,7 @@ def test_TooManyRedirects_on_HEAD(app, monkeypatch):
monkeypatch.setattr(requests.sessions, "DEFAULT_REDIRECT_LIMIT", 5)
- with http_server(InfiniteRedirectOnHeadHandler):
+ with serve_application(app, InfiniteRedirectOnHeadHandler) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -724,13 +745,13 @@ def test_TooManyRedirects_on_HEAD(app, monkeypatch):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
def make_retry_after_handler(responses):
- class RetryAfterHandler(http.server.BaseHTTPRequestHandler):
+ class RetryAfterHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -750,9 +771,11 @@ def make_retry_after_handler(responses):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_retry_after_int_delay(app, capsys, status):
- with http_server(make_retry_after_handler([(429, "0"), (200, None)])), \
- mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0), \
- mock.patch("sphinx.builders.linkcheck.QUEUE_POLL_SECS", 0.01):
+ with (
+ serve_application(app, make_retry_after_handler([(429, "0"), (200, None)])) as address,
+ mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0),
+ mock.patch("sphinx.builders.linkcheck.QUEUE_POLL_SECS", 0.01),
+ ):
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
assert json.loads(content) == {
@@ -760,10 +783,10 @@ def test_too_many_requests_retry_after_int_delay(app, capsys, status):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
- rate_limit_log = "-rate limited- http://localhost:7777/ | sleeping...\n"
+ rate_limit_log = f"-rate limited- http://{address}/ | sleeping...\n"
assert rate_limit_log in strip_colors(status.getvalue())
_stdout, stderr = capsys.readouterr()
assert stderr == textwrap.dedent(
@@ -787,7 +810,7 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys):
m.setattr(sphinx.util.http_date, '_GMT_OFFSET',
float(time.localtime().tm_gmtoff))
- with http_server(make_retry_after_handler([(429, retry_after), (200, None)])):
+ with serve_application(app, make_retry_after_handler([(429, retry_after), (200, None)])) as address:
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
@@ -796,7 +819,7 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
_stdout, stderr = capsys.readouterr()
@@ -810,8 +833,10 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_retry_after_without_header(app, capsys):
- with http_server(make_retry_after_handler([(429, None), (200, None)])), \
- mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0):
+ with (
+ serve_application(app, make_retry_after_handler([(429, None), (200, None)])) as address,
+ mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0),
+ ):
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
assert json.loads(content) == {
@@ -819,7 +844,7 @@ def test_too_many_requests_retry_after_without_header(app, capsys):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
_stdout, stderr = capsys.readouterr()
@@ -831,10 +856,36 @@ def test_too_many_requests_retry_after_without_header(app, capsys):
)
+@pytest.mark.sphinx(
+ 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
+ confoverrides={
+ 'linkcheck_report_timeouts_as_broken': False,
+ 'linkcheck_timeout': 0.01,
+ }
+)
+def test_requests_timeout(app):
+ class DelayedResponseHandler(BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def do_GET(self):
+ time.sleep(0.2) # wait before sending any response data
+ self.send_response(200, "OK")
+ self.send_header("Content-Length", "0")
+ self.end_headers()
+
+ with serve_application(app, DelayedResponseHandler):
+ app.build()
+
+ with open(app.outdir / "output.json", encoding="utf-8") as fp:
+ content = json.load(fp)
+
+ assert content["status"] == "timeout"
+
+
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_user_timeout(app):
app.config.linkcheck_rate_limit_timeout = 0.0
- with http_server(make_retry_after_handler([(429, None)])):
+ with serve_application(app, make_retry_after_handler([(429, None)])) as address:
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
assert json.loads(content) == {
@@ -842,8 +893,8 @@ def test_too_many_requests_user_timeout(app):
"lineno": 1,
"status": "broken",
"code": 0,
- "uri": "http://localhost:7777/",
- "info": "429 Client Error: Too Many Requests for url: http://localhost:7777/",
+ "uri": f'http://{address}/',
+ "info": f"429 Client Error: Too Many Requests for url: http://{address}/",
}
@@ -901,14 +952,15 @@ def test_connection_contention(get_adapter, app, capsys):
import socket
socket.setdefaulttimeout(5)
- # Place a workload into the linkcheck queue
- link_count = 10
- rqueue, wqueue = Queue(), Queue()
- for _ in range(link_count):
- wqueue.put(CheckRequest(0, Hyperlink("http://localhost:7777", "test", "test.rst", 1)))
-
# Create parallel consumer threads
- with http_server(make_redirect_handler(support_head=True)):
+ with serve_application(app, make_redirect_handler(support_head=True)) as address:
+
+ # Place a workload into the linkcheck queue
+ link_count = 10
+ rqueue, wqueue = Queue(), Queue()
+ for _ in range(link_count):
+ wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", "test.rst", 1)))
+
begin, checked = time.time(), []
threads = [
HyperlinkAvailabilityCheckWorker(
@@ -932,7 +984,7 @@ def test_connection_contention(get_adapter, app, capsys):
assert "TimeoutError" not in stderr
-class ConnectionResetHandler(http.server.BaseHTTPRequestHandler):
+class ConnectionResetHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -946,7 +998,7 @@ class ConnectionResetHandler(http.server.BaseHTTPRequestHandler):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_get_after_head_raises_connection_error(app):
- with http_server(ConnectionResetHandler):
+ with serve_application(app, ConnectionResetHandler) as address:
app.build()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert not content
@@ -956,34 +1008,33 @@ def test_get_after_head_raises_connection_error(app):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-documents_exclude', freshenv=True)
def test_linkcheck_exclude_documents(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler):
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
content = [json.loads(record) for record in fp]
- assert content == [
- {
- 'filename': 'broken_link.rst',
- 'lineno': 4,
- 'status': 'ignored',
- 'code': 0,
- 'uri': 'https://www.sphinx-doc.org/this-is-a-broken-link',
- 'info': 'broken_link matched ^broken_link$ from linkcheck_exclude_documents',
- },
- {
- 'filename': 'br0ken_link.rst',
- 'lineno': 4,
- 'status': 'ignored',
- 'code': 0,
- 'uri': 'https://www.sphinx-doc.org/this-is-another-broken-link',
- 'info': 'br0ken_link matched br[0-9]ken_link from linkcheck_exclude_documents',
- },
- ]
+ assert len(content) == 2
+ assert {
+ 'filename': 'broken_link.rst',
+ 'lineno': 4,
+ 'status': 'ignored',
+ 'code': 0,
+ 'uri': 'https://www.sphinx-doc.org/this-is-a-broken-link',
+ 'info': 'broken_link matched ^broken_link$ from linkcheck_exclude_documents',
+ } in content
+ assert {
+ 'filename': 'br0ken_link.rst',
+ 'lineno': 4,
+ 'status': 'ignored',
+ 'code': 0,
+ 'uri': 'https://www.sphinx-doc.org/this-is-another-broken-link',
+ 'info': 'br0ken_link matched br[0-9]ken_link from linkcheck_exclude_documents',
+ } in content
diff --git a/tests/test_build_manpage.py b/tests/test_builders/test_build_manpage.py
index e765644..7172281 100644
--- a/tests/test_build_manpage.py
+++ b/tests/test_builders/test_build_manpage.py
@@ -9,7 +9,7 @@ from sphinx.config import Config
@pytest.mark.sphinx('man')
def test_all(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'sphinxtests.1').exists()
content = (app.outdir / 'sphinxtests.1').read_text(encoding='utf8')
@@ -34,7 +34,7 @@ def test_all(app, status, warning):
@pytest.mark.sphinx('man', testroot='basic',
confoverrides={'man_pages': [('index', 'title', None, [], 1)]})
def test_man_pages_empty_description(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'title.1').read_text(encoding='utf8')
assert r'title \-' not in content
@@ -49,7 +49,7 @@ def test_man_make_section_directory(app, status, warning):
@pytest.mark.sphinx('man', testroot='directive-code')
def test_captioned_code_block(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'python.1').read_text(encoding='utf8')
if docutils.__version_info__[:2] < (0, 21):
@@ -92,7 +92,6 @@ def test_default_man_pages():
config = Config({'project': 'STASI™ Documentation',
'author': "Wolfgang Schäuble & G'Beckstein",
'release': '1.0'})
- config.init_values()
expected = [('index', 'stasi', 'STASI™ Documentation 1.0',
["Wolfgang Schäuble & G'Beckstein"], 1)]
assert default_man_pages(config) == expected
diff --git a/tests/test_build_texinfo.py b/tests/test_builders/test_build_texinfo.py
index 9964382..f9effb2 100644
--- a/tests/test_build_texinfo.py
+++ b/tests/test_builders/test_build_texinfo.py
@@ -1,6 +1,5 @@
"""Test the build process with Texinfo builder with the test root."""
-import os
import re
import subprocess
from pathlib import Path
@@ -11,37 +10,14 @@ import pytest
from sphinx.builders.texinfo import default_texinfo_documents
from sphinx.config import Config
-from sphinx.testing.util import strip_escseq
from sphinx.util.docutils import new_document
from sphinx.writers.texinfo import TexinfoTranslator
-from .test_build_html import ENV_WARNINGS
-
-TEXINFO_WARNINGS = ENV_WARNINGS + """\
-%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
-%(root)s/index.rst:\\d+: WARNING: citation not found: missing
-%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: foo.\\*
-%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: \
-\\['application/pdf', 'image/svg\\+xml'\\] \\(svgimg.\\*\\)
-"""
-
-
-@pytest.mark.sphinx('texinfo', testroot='warnings', freshenv=True)
-def test_texinfo_warnings(app, status, warning):
- app.builder.build_all()
- warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue()))
- warnings_exp = TEXINFO_WARNINGS % {
- 'root': re.escape(app.srcdir.as_posix())}
- assert re.match(warnings_exp + '$', warnings), \
- "Warnings don't match:\n" + \
- '--- Expected (regex):\n' + warnings_exp + \
- '--- Got:\n' + warnings
-
@pytest.mark.sphinx('texinfo')
def test_texinfo(app, status, warning):
TexinfoTranslator.ignore_missing_images = True
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8')
assert ('@anchor{markup doc}@anchor{11}'
'@anchor{markup id1}@anchor{12}'
@@ -71,7 +47,7 @@ def test_texinfo_rubric(app, status, warning):
@pytest.mark.sphinx('texinfo', testroot='markup-citation')
def test_texinfo_citation(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'python.texi').read_text(encoding='utf8')
assert 'This is a citation ref; @ref{1,,[CITE1]} and @ref{2,,[CITE2]}.' in output
@@ -84,7 +60,6 @@ def test_texinfo_citation(app, status, warning):
def test_default_texinfo_documents():
config = Config({'project': 'STASI™ Documentation',
'author': "Wolfgang Schäuble & G'Beckstein"})
- config.init_values()
expected = [('index', 'stasi', 'STASI™ Documentation',
"Wolfgang Schäuble & G'Beckstein", 'stasi',
'One line description of project', 'Miscellaneous')]
@@ -110,7 +85,7 @@ def test_texinfo_escape_id(app, status, warning):
@pytest.mark.sphinx('texinfo', testroot='footnotes')
def test_texinfo_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'python.texi').read_text(encoding='utf8')
assert 'First footnote: @footnote{\nFirst\n}' in output
@@ -118,13 +93,13 @@ def test_texinfo_footnote(app, status, warning):
@pytest.mark.sphinx('texinfo')
def test_texinfo_xrefs(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8')
assert re.search(r'@ref{\w+,,--plugin\.option}', output)
# Now rebuild it without xrefs
app.config.texinfo_cross_references = False
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8')
assert not re.search(r'@ref{\w+,,--plugin\.option}', output)
assert 'Link to perl +p, --ObjC++, --plugin.option, create-auth-token, arg and -j' in output
diff --git a/tests/test_build_text.py b/tests/test_builders/test_build_text.py
index 4a53be3..6dc0d03 100644
--- a/tests/test_build_text.py
+++ b/tests/test_builders/test_build_text.py
@@ -17,7 +17,7 @@ def with_text_app(*args, **kw):
@with_text_app()
def test_maxwitdh_with_prefix(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'maxwidth.txt').read_text(encoding='utf8')
lines = result.splitlines()
@@ -40,7 +40,7 @@ def test_maxwitdh_with_prefix(app, status, warning):
@with_text_app()
def test_lineblock(app, status, warning):
# regression test for #1109: need empty line after line block
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'lineblock.txt').read_text(encoding='utf8')
expect = (
"* one\n"
@@ -55,7 +55,7 @@ def test_lineblock(app, status, warning):
@with_text_app()
def test_nonascii_title_line(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'nonascii_title.txt').read_text(encoding='utf8')
expect_underline = '*********'
result_underline = result.splitlines()[1].strip()
@@ -64,7 +64,7 @@ def test_nonascii_title_line(app, status, warning):
@with_text_app()
def test_nonascii_table(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'nonascii_table.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
line_widths = [column_width(line) for line in lines]
@@ -73,7 +73,7 @@ def test_nonascii_table(app, status, warning):
@with_text_app()
def test_nonascii_maxwidth(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'nonascii_maxwidth.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
line_widths = [column_width(line) for line in lines]
@@ -117,7 +117,7 @@ def test_table_cell():
@with_text_app()
def test_table_with_empty_cell(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -131,7 +131,7 @@ def test_table_with_empty_cell(app, status, warning):
@with_text_app()
def test_table_with_rowspan(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_rowspan.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -145,7 +145,7 @@ def test_table_with_rowspan(app, status, warning):
@with_text_app()
def test_table_with_colspan(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_colspan.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -159,7 +159,7 @@ def test_table_with_colspan(app, status, warning):
@with_text_app()
def test_table_with_colspan_left(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_colspan_left.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -173,7 +173,7 @@ def test_table_with_colspan_left(app, status, warning):
@with_text_app()
def test_table_with_colspan_and_rowspan(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_colspan_and_rowspan.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert result
@@ -188,7 +188,7 @@ def test_table_with_colspan_and_rowspan(app, status, warning):
@with_text_app()
def test_list_items_in_admonition(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'listitems.txt').read_text(encoding='utf8')
lines = [line.rstrip() for line in result.splitlines()]
assert lines[0] == "See also:"
@@ -200,7 +200,7 @@ def test_list_items_in_admonition(app, status, warning):
@with_text_app()
def test_secnums(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
index = (app.outdir / 'index.txt').read_text(encoding='utf8')
lines = index.splitlines()
assert lines[0] == "* 1. Section A"
@@ -226,7 +226,7 @@ def test_secnums(app, status, warning):
assert doc2 == expect
app.config.text_secnumber_suffix = " "
- app.builder.build_all()
+ app.build(force_all=True)
index = (app.outdir / 'index.txt').read_text(encoding='utf8')
lines = index.splitlines()
assert lines[0] == "* 1 Section A"
@@ -252,7 +252,7 @@ def test_secnums(app, status, warning):
assert doc2 == expect
app.config.text_add_secnumbers = False
- app.builder.build_all()
+ app.build(force_all=True)
index = (app.outdir / 'index.txt').read_text(encoding='utf8')
lines = index.splitlines()
assert lines[0] == "* Section A"
diff --git a/tests/test_builders/test_build_warnings.py b/tests/test_builders/test_build_warnings.py
new file mode 100644
index 0000000..eeb7e9d
--- /dev/null
+++ b/tests/test_builders/test_build_warnings.py
@@ -0,0 +1,89 @@
+import os
+import re
+import sys
+
+import pytest
+
+from sphinx.util.console import strip_colors
+
+ENV_WARNINGS = """\
+{root}/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \
+WARNING: Explicit markup ends without a blank line; unexpected unindent.
+{root}/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \
+file '{root}/wrongenc.inc' seems to be wrong, try giving an :encoding: option
+{root}/index.rst:\\d+: WARNING: invalid single index entry ''
+{root}/index.rst:\\d+: WARNING: image file not readable: foo.png
+{root}/index.rst:\\d+: WARNING: download file not readable: {root}/nonexisting.png
+{root}/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \
+with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?'
+"""
+
+HTML_WARNINGS = ENV_WARNINGS + """\
+{root}/index.rst:\\d+: WARNING: unknown option: '&option'
+{root}/index.rst:\\d+: WARNING: citation not found: missing
+{root}/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\*
+{root}/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
+"""
+
+LATEX_WARNINGS = ENV_WARNINGS + """\
+{root}/index.rst:\\d+: WARNING: unknown option: '&option'
+{root}/index.rst:\\d+: WARNING: citation not found: missing
+{root}/index.rst:\\d+: WARNING: a suitable image for latex builder not found: foo.\\*
+{root}/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
+"""
+
+TEXINFO_WARNINGS = ENV_WARNINGS + """\
+{root}/index.rst:\\d+: WARNING: unknown option: '&option'
+{root}/index.rst:\\d+: WARNING: citation not found: missing
+{root}/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: foo.\\*
+{root}/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: \
+\\['application/pdf', 'image/svg\\+xml'\\] \\(svgimg.\\*\\)
+"""
+
+
+def _check_warnings(expected_warnings: str, warning: str) -> None:
+ warnings = strip_colors(re.sub(re.escape(os.sep) + '{1,2}', '/', warning))
+ assert re.match(f'{expected_warnings}$', warnings), (
+ "Warnings don't match:\n"
+ + f'--- Expected (regex):\n{expected_warnings}\n'
+ + f'--- Got:\n{warnings}'
+ )
+ sys.modules.pop('autodoc_fodder', None)
+
+
+@pytest.mark.sphinx('html', testroot='warnings', freshenv=True)
+def test_html_warnings(app, warning):
+ app.build(force_all=True)
+ warnings_exp = HTML_WARNINGS.format(root=re.escape(app.srcdir.as_posix()))
+ _check_warnings(warnings_exp, warning.getvalue())
+
+
+@pytest.mark.sphinx('latex', testroot='warnings', freshenv=True)
+def test_latex_warnings(app, warning):
+ app.build(force_all=True)
+ warnings_exp = LATEX_WARNINGS.format(root=re.escape(app.srcdir.as_posix()))
+ _check_warnings(warnings_exp, warning.getvalue())
+
+
+@pytest.mark.sphinx('texinfo', testroot='warnings', freshenv=True)
+def test_texinfo_warnings(app, warning):
+ app.build(force_all=True)
+ warnings_exp = TEXINFO_WARNINGS.format(root=re.escape(app.srcdir.as_posix()))
+ _check_warnings(warnings_exp, warning.getvalue())
+
+
+def test_uncacheable_config_warning(make_app, tmp_path):
+ """Test that an unpickleable config value raises a warning."""
+ tmp_path.joinpath('conf.py').write_text("""\
+my_config = lambda: None
+show_warning_types = True
+def setup(app):
+ app.add_config_value('my_config', None, 'env')
+ """, encoding='utf-8')
+ tmp_path.joinpath('index.rst').write_text('Test\n====\n', encoding='utf-8')
+ app = make_app(srcdir=tmp_path)
+ app.build()
+ assert strip_colors(app.warning.getvalue()).strip() == (
+ "WARNING: cannot cache unpickable configuration value: 'my_config' "
+ "(because it contains a function, class, or module object) [config.cache]"
+ )
diff --git a/tests/test_builder.py b/tests/test_builders/test_builder.py
index 1ff8aea..ee946a5 100644
--- a/tests/test_builder.py
+++ b/tests/test_builders/test_builder.py
@@ -1,4 +1,7 @@
"""Test the Builder class."""
+
+import sys
+
import pytest
@@ -37,3 +40,5 @@ def test_incremental_reading_for_missing_files(app):
# "index" is listed up to updated because it contains references
# to nonexisting downloadable or image files
assert set(updated) == {'index'}
+
+ sys.modules.pop('autodoc_fodder', None)
diff --git a/tests/test_builders/xpath_data.py b/tests/test_builders/xpath_data.py
new file mode 100644
index 0000000..30f8e07
--- /dev/null
+++ b/tests/test_builders/xpath_data.py
@@ -0,0 +1,8 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Final
+
+FIGURE_CAPTION: Final[str] = ".//figure/figcaption/p"
diff --git a/tests/test_builders/xpath_util.py b/tests/test_builders/xpath_util.py
new file mode 100644
index 0000000..7525c19
--- /dev/null
+++ b/tests/test_builders/xpath_util.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+import re
+import textwrap
+from typing import TYPE_CHECKING
+from xml.etree.ElementTree import tostring
+
+if TYPE_CHECKING:
+ import os
+ from collections.abc import Callable, Iterable, Sequence
+ from xml.etree.ElementTree import Element, ElementTree
+
+
+def _get_text(node: Element) -> str:
+ if node.text is not None:
+ # the node has only one text
+ return node.text
+
+ # the node has tags and text; gather texts just under the node
+ return ''.join(n.tail or '' for n in node)
+
+
+def _prettify(nodes: Iterable[Element]) -> str:
+ def pformat(node: Element) -> str:
+ return tostring(node, encoding='unicode', method='html')
+
+ return ''.join(f'(i={index}) {pformat(node)}\n' for index, node in enumerate(nodes))
+
+
+def check_xpath(
+ etree: ElementTree,
+ filename: str | os.PathLike[str],
+ xpath: str,
+ check: str | re.Pattern[str] | Callable[[Sequence[Element]], None] | None,
+ be_found: bool = True,
+ *,
+ min_count: int = 1,
+) -> None:
+ """Check that one or more nodes satisfy a predicate.
+
+ :param etree: The element tree.
+ :param filename: The element tree source name (for errors only).
+ :param xpath: An XPath expression to use.
+ :param check: Optional regular expression or a predicate the nodes must validate.
+ :param be_found: If false, negate the predicate.
+ :param min_count: Minimum number of nodes expected to satisfy the predicate.
+
+ * If *check* is empty (``''``), only the minimum count is checked.
+ * If *check* is ``None``, no node should satisfy the XPath expression.
+ """
+ nodes = etree.findall(xpath)
+ assert isinstance(nodes, list)
+
+ if check is None:
+ # use == to have a nice pytest diff
+ assert nodes == [], f'found nodes matching xpath {xpath!r} in file {filename}'
+ return
+
+ assert len(nodes) >= min_count, (f'expecting at least {min_count} node(s) '
+ f'to satisfy {xpath!r} in file {filename}')
+
+ if check == '':
+ return
+
+ if callable(check):
+ check(nodes)
+ return
+
+ rex = re.compile(check)
+ if be_found:
+ if any(rex.search(_get_text(node)) for node in nodes):
+ return
+ else:
+ if all(not rex.search(_get_text(node)) for node in nodes):
+ return
+
+ ctx = textwrap.indent(_prettify(nodes), ' ' * 2)
+ msg = f'{check!r} not found in any node matching {xpath!r} in file {filename}:\n{ctx}'
+ raise AssertionError(msg)
diff --git a/tests/test_config/__init__.py b/tests/test_config/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_config/__init__.py
diff --git a/tests/test_config.py b/tests/test_config/test_config.py
index 0be0a58..e1cb1b0 100644
--- a/tests/test_config.py
+++ b/tests/test_config/test_config.py
@@ -1,15 +1,81 @@
"""Test the sphinx.config.Config class."""
+from __future__ import annotations
+import pickle
import time
+from collections import Counter
from pathlib import Path
+from typing import TYPE_CHECKING, Any
from unittest import mock
import pytest
import sphinx
-from sphinx.config import ENUM, Config, check_confval_types
+from sphinx.builders.gettext import _gettext_compact_validator
+from sphinx.config import (
+ ENUM,
+ Config,
+ _Opt,
+ check_confval_types,
+ correct_copyright_year,
+ is_serializable,
+)
+from sphinx.deprecation import RemovedInSphinx90Warning
from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+ from typing import Union
+
+ CircularList = list[Union[int, 'CircularList']]
+ CircularDict = dict[str, Union[int, 'CircularDict']]
+
+
+def check_is_serializable(subject: object, *, circular: bool) -> None:
+ assert is_serializable(subject)
+
+ if circular:
+ class UselessGuard(frozenset[int]):
+ def __or__(self, other: object, /) -> UselessGuard:
+ # do nothing
+ return self
+
+ def union(self, *args: Iterable[object]) -> UselessGuard:
+ # do nothing
+ return self
+
+ # check that without recursive guards, a recursion error occurs
+ with pytest.raises(RecursionError):
+ assert is_serializable(subject, _seen=UselessGuard())
+
+
+def test_is_serializable() -> None:
+ subject = [1, [2, {3, 'a'}], {'x': {'y': frozenset((4, 5))}}]
+ check_is_serializable(subject, circular=False)
+
+ a, b = [1], [2] # type: (CircularList, CircularList)
+ a.append(b)
+ b.append(a)
+ check_is_serializable(a, circular=True)
+ check_is_serializable(b, circular=True)
+
+ x: CircularDict = {'a': 1, 'b': {'c': 1}}
+ x['b'] = x
+ check_is_serializable(x, circular=True)
+
+
+def test_config_opt_deprecated(recwarn):
+ opt = _Opt('default', '', ())
+
+ with pytest.warns(RemovedInSphinx90Warning):
+ default, rebuild, valid_types = opt
+
+ with pytest.warns(RemovedInSphinx90Warning):
+ _ = opt[0]
+
+ with pytest.warns(RemovedInSphinx90Warning):
+ _ = list(opt)
+
@pytest.mark.sphinx(testroot='config', confoverrides={
'root_doc': 'root',
@@ -30,7 +96,7 @@ def test_core_config(app, status, warning):
assert cfg.modindex_common_prefix == ['path1', 'path2']
# simple default values
- assert 'locale_dirs' not in cfg.__dict__
+ assert 'locale_dirs' in cfg.__dict__
assert cfg.locale_dirs == ['locales']
assert cfg.trim_footnote_reference_space is False
@@ -71,6 +137,161 @@ def test_config_not_found(tmp_path):
Config.read(tmp_path)
+@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL)))
+def test_config_pickle_protocol(tmp_path, protocol: int):
+ config = Config()
+
+ pickled_config = pickle.loads(pickle.dumps(config, protocol))
+
+ assert list(config._options) == list(pickled_config._options)
+ assert repr(config) == repr(pickled_config)
+
+
+def test_config_pickle_circular_reference_in_list():
+ a, b = [1], [2] # type: (CircularList, CircularList)
+ a.append(b)
+ b.append(a)
+
+ check_is_serializable(a, circular=True)
+ check_is_serializable(b, circular=True)
+
+ config = Config()
+ config.add('a', [], '', types=list)
+ config.add('b', [], '', types=list)
+ config.a, config.b = a, b
+
+ actual = pickle.loads(pickle.dumps(config))
+ assert isinstance(actual.a, list)
+ check_is_serializable(actual.a, circular=True)
+
+ assert isinstance(actual.b, list)
+ check_is_serializable(actual.b, circular=True)
+
+ assert actual.a[0] == 1
+ assert actual.a[1][0] == 2
+ assert actual.a[1][1][0] == 1
+ assert actual.a[1][1][1][0] == 2
+
+ assert actual.b[0] == 2
+ assert actual.b[1][0] == 1
+ assert actual.b[1][1][0] == 2
+ assert actual.b[1][1][1][0] == 1
+
+ assert len(actual.a) == 2
+ assert len(actual.a[1]) == 2
+ assert len(actual.a[1][1]) == 2
+ assert len(actual.a[1][1][1]) == 2
+ assert len(actual.a[1][1][1][1]) == 2
+
+ assert len(actual.b) == 2
+ assert len(actual.b[1]) == 2
+ assert len(actual.b[1][1]) == 2
+ assert len(actual.b[1][1][1]) == 2
+ assert len(actual.b[1][1][1][1]) == 2
+
+ def check(
+ u: list[list[object] | int],
+ v: list[list[object] | int],
+ *,
+ counter: Counter[type, int] | None = None,
+ guard: frozenset[int] = frozenset(),
+ ) -> Counter[type, int]:
+ counter = Counter() if counter is None else counter
+
+ if id(u) in guard and id(v) in guard:
+ return counter
+
+ if isinstance(u, int):
+ assert v.__class__ is u.__class__
+ assert u == v
+ counter[type(u)] += 1
+ return counter
+
+ assert isinstance(u, list)
+ assert v.__class__ is u.__class__
+ assert len(u) == len(v)
+
+ for u_i, v_i in zip(u, v):
+ counter[type(u)] += 1
+ check(u_i, v_i, counter=counter, guard=guard | {id(u), id(v)})
+
+ return counter
+
+ counter = check(actual.a, a)
+ # check(actual.a, a)
+ # check(actual.a[0], a[0]) -> ++counter[dict]
+ # ++counter[int] (a[0] is an int)
+ # check(actual.a[1], a[1]) -> ++counter[dict]
+ # check(actual.a[1][0], a[1][0]) -> ++counter[dict]
+ # ++counter[int] (a[1][0] is an int)
+ # check(actual.a[1][1], a[1][1]) -> ++counter[dict]
+ # recursive guard since a[1][1] == a
+ assert counter[type(a[0])] == 2
+ assert counter[type(a[1])] == 4
+
+ # same logic as above
+ counter = check(actual.b, b)
+ assert counter[type(b[0])] == 2
+ assert counter[type(b[1])] == 4
+
+
+def test_config_pickle_circular_reference_in_dict():
+ x: CircularDict = {'a': 1, 'b': {'c': 1}}
+ x['b'] = x
+ check_is_serializable(x, circular=True)
+
+ config = Config()
+ config.add('x', [], '', types=dict)
+ config.x = x
+
+ actual = pickle.loads(pickle.dumps(config))
+ check_is_serializable(actual.x, circular=True)
+ assert isinstance(actual.x, dict)
+
+ assert actual.x['a'] == 1
+ assert actual.x['b']['a'] == 1
+
+ assert len(actual.x) == 2
+ assert len(actual.x['b']) == 2
+ assert len(actual.x['b']['b']) == 2
+
+ def check(
+ u: dict[str, dict[str, object] | int],
+ v: dict[str, dict[str, object] | int],
+ *,
+ counter: Counter[type, int] | None = None,
+ guard: frozenset[int] = frozenset(),
+ ) -> Counter:
+ counter = Counter() if counter is None else counter
+
+ if id(u) in guard and id(v) in guard:
+ return counter
+
+ if isinstance(u, int):
+ assert v.__class__ is u.__class__
+ assert u == v
+ counter[type(u)] += 1
+ return counter
+
+ assert isinstance(u, dict)
+ assert v.__class__ is u.__class__
+ assert len(u) == len(v)
+
+ for u_i, v_i in zip(u, v):
+ counter[type(u)] += 1
+ check(u[u_i], v[v_i], counter=counter, guard=guard | {id(u), id(v)})
+ return counter
+
+ counters = check(actual.x, x, counter=Counter())
+ # check(actual.x, x)
+ # check(actual.x['a'], x['a']) -> ++counter[dict]
+ # ++counter[int] (x['a'] is an int)
+ # check(actual.x['b'], x['b']) -> ++counter[dict]
+ # recursive guard since x['b'] == x
+ assert counters[type(x['a'])] == 1
+ assert counters[type(x['b'])] == 2
+
+
def test_extension_values():
config = Config()
@@ -104,7 +325,6 @@ def test_overrides():
config.add('value6', {'default': 0}, 'env', ())
config.add('value7', None, 'env', ())
config.add('value8', [], 'env', ())
- config.init_values()
assert config.value1 == '1'
assert config.value2 == 999
@@ -123,7 +343,6 @@ def test_overrides_boolean():
config.add('value1', None, 'env', [bool])
config.add('value2', None, 'env', [bool])
config.add('value3', True, 'env', ())
- config.init_values()
assert config.value1 is True
assert config.value2 is False
@@ -131,6 +350,37 @@ def test_overrides_boolean():
@mock.patch("sphinx.config.logger")
+def test_overrides_dict_str(logger):
+ config = Config({}, {'spam': 'lobster'})
+
+ config.add('spam', {'ham': 'eggs'}, 'env', {dict, str})
+
+ assert config.spam == {'ham': 'eggs'}
+
+ # assert len(caplog.records) == 1
+ # msg = caplog.messages[0]
+ assert logger.method_calls
+ msg = str(logger.method_calls[0].args[1])
+ assert msg == ("cannot override dictionary config setting 'spam', "
+ "ignoring (use 'spam.key=value' to set individual elements)")
+
+
+def test_callable_defer():
+ config = Config()
+ config.add('alias', lambda c: c.master_doc, '', str)
+
+ assert config.master_doc == 'index'
+ assert config.alias == 'index'
+
+ config.master_doc = 'contents'
+ assert config.alias == 'contents'
+
+ config.master_doc = 'master_doc'
+ config.alias = 'spam'
+ assert config.alias == 'spam'
+
+
+@mock.patch("sphinx.config.logger")
def test_errors_warnings(logger, tmp_path):
# test the error for syntax errors in the config file
(tmp_path / 'conf.py').write_text('project = \n', encoding='ascii')
@@ -141,7 +391,6 @@ def test_errors_warnings(logger, tmp_path):
# test the automatic conversion of 2.x only code in configs
(tmp_path / 'conf.py').write_text('project = u"Jägermeister"\n', encoding='utf8')
cfg = Config.read(tmp_path, {}, None)
- cfg.init_values()
assert cfg.project == 'Jägermeister'
assert logger.called is False
@@ -193,7 +442,6 @@ def test_config_eol(logger, tmp_path):
for eol in (b'\n', b'\r\n'):
configfile.write_bytes(b'project = "spam"' + eol)
cfg = Config.read(tmp_path, {}, None)
- cfg.init_values()
assert cfg.project == 'spam'
assert logger.called is False
@@ -248,7 +496,6 @@ TYPECHECK_WARNINGS = [
def test_check_types(logger, name, default, annotation, actual, warned):
config = Config({name: actual})
config.add(name, default, 'env', annotation or ())
- config.init_values()
check_confval_types(None, config)
assert logger.warning.called == warned
@@ -257,9 +504,9 @@ TYPECHECK_WARNING_MESSAGES = [
('value1', 'string', [str], ['foo', 'bar'],
"The config value `value1' has type `list'; expected `str'."),
('value1', 'string', [str, int], ['foo', 'bar'],
- "The config value `value1' has type `list'; expected `str' or `int'."),
+ "The config value `value1' has type `list'; expected `int' or `str'."),
('value1', 'string', [str, int, tuple], ['foo', 'bar'],
- "The config value `value1' has type `list'; expected `str', `int', or `tuple'."),
+ "The config value `value1' has type `list'; expected `int', `str', or `tuple'."),
]
@@ -268,7 +515,6 @@ TYPECHECK_WARNING_MESSAGES = [
def test_conf_warning_message(logger, name, default, annotation, actual, message):
config = Config({name: actual})
config.add(name, default, False, annotation or ())
- config.init_values()
check_confval_types(None, config)
assert logger.warning.called
assert logger.warning.call_args[0][0] == message
@@ -278,7 +524,6 @@ def test_conf_warning_message(logger, name, default, annotation, actual, message
def test_check_enum(logger):
config = Config()
config.add('value', 'default', False, ENUM('default', 'one', 'two'))
- config.init_values()
check_confval_types(None, config)
logger.warning.assert_not_called() # not warned
@@ -287,7 +532,6 @@ def test_check_enum(logger):
def test_check_enum_failed(logger):
config = Config({'value': 'invalid'})
config.add('value', 'default', False, ENUM('default', 'one', 'two'))
- config.init_values()
check_confval_types(None, config)
assert logger.warning.called
@@ -296,7 +540,6 @@ def test_check_enum_failed(logger):
def test_check_enum_for_list(logger):
config = Config({'value': ['one', 'two']})
config.add('value', 'default', False, ENUM('default', 'one', 'two'))
- config.init_values()
check_confval_types(None, config)
logger.warning.assert_not_called() # not warned
@@ -305,11 +548,18 @@ def test_check_enum_for_list(logger):
def test_check_enum_for_list_failed(logger):
config = Config({'value': ['one', 'two', 'invalid']})
config.add('value', 'default', False, ENUM('default', 'one', 'two'))
- config.init_values()
check_confval_types(None, config)
assert logger.warning.called
+@mock.patch("sphinx.config.logger")
+def test_check_any(logger):
+ config = Config({'value': None})
+ config.add('value', 'default', '', Any)
+ check_confval_types(None, config)
+ logger.warning.assert_not_called() # not warned
+
+
nitpick_warnings = [
"WARNING: py:const reference target not found: prefix.anything.postfix",
"WARNING: py:class reference target not found: prefix.anything",
@@ -320,7 +570,7 @@ nitpick_warnings = [
@pytest.mark.sphinx(testroot='nitpicky-warnings')
def test_nitpick_base(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
warning = warning.getvalue().strip().split('\n')
assert len(warning) == len(nitpick_warnings)
@@ -337,7 +587,7 @@ def test_nitpick_base(app, status, warning):
},
})
def test_nitpick_ignore(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert not len(warning.getvalue().strip())
@@ -348,7 +598,7 @@ def test_nitpick_ignore(app, status, warning):
],
})
def test_nitpick_ignore_regex1(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert not len(warning.getvalue().strip())
@@ -359,7 +609,7 @@ def test_nitpick_ignore_regex1(app, status, warning):
],
})
def test_nitpick_ignore_regex2(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert not len(warning.getvalue().strip())
@@ -376,7 +626,7 @@ def test_nitpick_ignore_regex2(app, status, warning):
],
})
def test_nitpick_ignore_regex_fullmatch(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
warning = warning.getvalue().strip().split('\n')
assert len(warning) == len(nitpick_warnings)
@@ -386,13 +636,11 @@ def test_nitpick_ignore_regex_fullmatch(app, status, warning):
def test_conf_py_language_none(tmp_path):
"""Regression test for #10474."""
-
# Given a conf.py file with language = None
(tmp_path / 'conf.py').write_text("language = None", encoding='utf-8')
# When we load conf.py into a Config object
cfg = Config.read(tmp_path, {}, None)
- cfg.init_values()
# Then the language is coerced to English
assert cfg.language == "en"
@@ -401,7 +649,6 @@ def test_conf_py_language_none(tmp_path):
@mock.patch("sphinx.config.logger")
def test_conf_py_language_none_warning(logger, tmp_path):
"""Regression test for #10474."""
-
# Given a conf.py file with language = None
(tmp_path / 'conf.py').write_text("language = None", encoding='utf-8')
@@ -418,13 +665,11 @@ def test_conf_py_language_none_warning(logger, tmp_path):
def test_conf_py_no_language(tmp_path):
"""Regression test for #10474."""
-
# Given a conf.py file with no language attribute
(tmp_path / 'conf.py').write_text("", encoding='utf-8')
# When we load conf.py into a Config object
cfg = Config.read(tmp_path, {}, None)
- cfg.init_values()
# Then the language is coerced to English
assert cfg.language == "en"
@@ -432,13 +677,11 @@ def test_conf_py_no_language(tmp_path):
def test_conf_py_nitpick_ignore_list(tmp_path):
"""Regression test for #11355."""
-
# Given a conf.py file with no language attribute
(tmp_path / 'conf.py').write_text("", encoding='utf-8')
# When we load conf.py into a Config object
cfg = Config.read(tmp_path, {}, None)
- cfg.init_values()
# Then the default nitpick_ignore[_regex] is an empty list
assert cfg.nitpick_ignore == []
@@ -465,7 +708,7 @@ def source_date_year(request, monkeypatch):
@pytest.mark.sphinx(testroot='copyright-multiline')
def test_multi_line_copyright(source_date_year, app, monkeypatch):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf-8')
@@ -515,3 +758,48 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch):
f' \n'
f' &#169; Copyright 2022-{source_date_year}, Eve.'
) in content
+
+
+@pytest.mark.parametrize(('conf_copyright', 'expected_copyright'), [
+ ('1970', '{current_year}'),
+ # https://github.com/sphinx-doc/sphinx/issues/11913
+ ('1970-1990', '1970-{current_year}'),
+ ('1970-1990 Alice', '1970-{current_year} Alice'),
+])
+def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_year):
+ config = Config({}, {'copyright': conf_copyright})
+ correct_copyright_year(_app=None, config=config)
+ actual_copyright = config['copyright']
+
+ if source_date_year is None:
+ expected_copyright = conf_copyright
+ else:
+ expected_copyright = expected_copyright.format(current_year=source_date_year)
+ assert actual_copyright == expected_copyright
+
+
+def test_gettext_compact_command_line_true():
+ config = Config({}, {'gettext_compact': '1'})
+ config.add('gettext_compact', True, '', {bool, str})
+ _gettext_compact_validator(..., config)
+
+ # regression test for #8549 (-D gettext_compact=1)
+ assert config.gettext_compact is True
+
+
+def test_gettext_compact_command_line_false():
+ config = Config({}, {'gettext_compact': '0'})
+ config.add('gettext_compact', True, '', {bool, str})
+ _gettext_compact_validator(..., config)
+
+ # regression test for #8549 (-D gettext_compact=0)
+ assert config.gettext_compact is False
+
+
+def test_gettext_compact_command_line_str():
+ config = Config({}, {'gettext_compact': 'spam'})
+ config.add('gettext_compact', True, '', {bool, str})
+ _gettext_compact_validator(..., config)
+
+ # regression test for #8549 (-D gettext_compact=spam)
+ assert config.gettext_compact == 'spam'
diff --git a/tests/test_correct_year.py b/tests/test_config/test_correct_year.py
index 4ef77a6..4ef77a6 100644
--- a/tests/test_correct_year.py
+++ b/tests/test_config/test_correct_year.py
diff --git a/tests/test_directives/__init__.py b/tests/test_directives/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_directives/__init__.py
diff --git a/tests/test_directive_code.py b/tests/test_directives/test_directive_code.py
index df7de57..2783d8f 100644
--- a/tests/test_directive_code.py
+++ b/tests/test_directives/test_directive_code.py
@@ -295,7 +295,7 @@ def test_LiteralIncludeReader_diff(testroot, literal_inc_path):
@pytest.mark.sphinx('xml', testroot='directive-code')
def test_code_block(app, status, warning):
- app.builder.build('index')
+ app.build(filenames=[app.srcdir / 'index.rst'])
et = etree_parse(app.outdir / 'index.xml')
secs = et.findall('./section/section')
code_block = secs[0].findall('literal_block')
@@ -311,13 +311,13 @@ def test_code_block(app, status, warning):
@pytest.mark.sphinx('html', testroot='directive-code')
def test_force_option(app, status, warning):
- app.builder.build(['force'])
+ app.build(filenames=[app.srcdir / 'force.rst'])
assert 'force.rst' not in warning.getvalue()
@pytest.mark.sphinx('html', testroot='directive-code')
def test_code_block_caption_html(app, status, warning):
- app.builder.build(['caption'])
+ app.build(filenames=[app.srcdir / 'caption.rst'])
html = (app.outdir / 'caption.html').read_text(encoding='utf8')
caption = ('<div class="code-block-caption">'
'<span class="caption-number">Listing 1 </span>'
@@ -329,7 +329,7 @@ def test_code_block_caption_html(app, status, warning):
@pytest.mark.sphinx('latex', testroot='directive-code')
def test_code_block_caption_latex(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
latex = (app.outdir / 'python.tex').read_text(encoding='utf8')
caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstyleemphasis{test} rb}'
label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id1}}}'
@@ -342,7 +342,7 @@ def test_code_block_caption_latex(app, status, warning):
@pytest.mark.sphinx('latex', testroot='directive-code')
def test_code_block_namedlink_latex(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
latex = (app.outdir / 'python.tex').read_text(encoding='utf8')
label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-rb}}}'
link1 = '\\hyperref[\\detokenize{caption:name-test-rb}]'\
@@ -359,7 +359,7 @@ def test_code_block_namedlink_latex(app, status, warning):
@pytest.mark.sphinx('latex', testroot='directive-code')
def test_code_block_emphasize_latex(app, status, warning):
- app.builder.build(['emphasize'])
+ app.build(filenames=[app.srcdir / 'emphasize.rst'])
latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n')
includes = '\\fvset{hllines={, 5, 6, 13, 14, 15, 24, 25, 26,}}%\n'
assert includes in latex
@@ -369,7 +369,7 @@ def test_code_block_emphasize_latex(app, status, warning):
@pytest.mark.sphinx('xml', testroot='directive-code')
def test_literal_include(app, status, warning):
- app.builder.build(['index'])
+ app.build(filenames=[app.srcdir / 'index.rst'])
et = etree_parse(app.outdir / 'index.xml')
secs = et.findall('./section/section')
literal_include = secs[1].findall('literal_block')
@@ -381,7 +381,7 @@ def test_literal_include(app, status, warning):
@pytest.mark.sphinx('xml', testroot='directive-code')
def test_literal_include_block_start_with_comment_or_brank(app, status, warning):
- app.builder.build(['python'])
+ app.build(filenames=[app.srcdir / 'python.rst'])
et = etree_parse(app.outdir / 'python.xml')
secs = et.findall('./section/section')
literal_include = secs[0].findall('literal_block')
@@ -405,7 +405,7 @@ def test_literal_include_block_start_with_comment_or_brank(app, status, warning)
@pytest.mark.sphinx('html', testroot='directive-code')
def test_literal_include_linenos(app, status, warning):
- app.builder.build(['linenos'])
+ app.build(filenames=[app.srcdir / 'linenos.rst'])
html = (app.outdir / 'linenos.html').read_text(encoding='utf8')
# :linenos:
@@ -423,7 +423,7 @@ def test_literal_include_linenos(app, status, warning):
@pytest.mark.sphinx('latex', testroot='directive-code')
def test_literalinclude_file_whole_of_emptyline(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n')
includes = (
'\\begin{sphinxVerbatim}'
@@ -437,7 +437,7 @@ def test_literalinclude_file_whole_of_emptyline(app, status, warning):
@pytest.mark.sphinx('html', testroot='directive-code')
def test_literalinclude_caption_html(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
html = (app.outdir / 'caption.html').read_text(encoding='utf8')
caption = ('<div class="code-block-caption">'
'<span class="caption-number">Listing 2 </span>'
@@ -449,7 +449,7 @@ def test_literalinclude_caption_html(app, status, warning):
@pytest.mark.sphinx('latex', testroot='directive-code')
def test_literalinclude_caption_latex(app, status, warning):
- app.builder.build('index')
+ app.build(filenames='index')
latex = (app.outdir / 'python.tex').read_text(encoding='utf8')
caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstylestrong{test} py}'
label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id2}}}'
@@ -462,7 +462,7 @@ def test_literalinclude_caption_latex(app, status, warning):
@pytest.mark.sphinx('latex', testroot='directive-code')
def test_literalinclude_namedlink_latex(app, status, warning):
- app.builder.build('index')
+ app.build(filenames='index')
latex = (app.outdir / 'python.tex').read_text(encoding='utf8')
label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-py}}}'
link1 = '\\hyperref[\\detokenize{caption:name-test-py}]'\
@@ -479,7 +479,7 @@ def test_literalinclude_namedlink_latex(app, status, warning):
@pytest.mark.sphinx('xml', testroot='directive-code')
def test_literalinclude_classes(app, status, warning):
- app.builder.build(['classes'])
+ app.build(filenames=[app.srcdir / 'classes.rst'])
et = etree_parse(app.outdir / 'classes.xml')
secs = et.findall('./section/section')
@@ -496,7 +496,7 @@ def test_literalinclude_classes(app, status, warning):
@pytest.mark.sphinx('xml', testroot='directive-code')
def test_literalinclude_pydecorators(app, status, warning):
- app.builder.build(['py-decorators'])
+ app.build(filenames=[app.srcdir / 'py-decorators.rst'])
et = etree_parse(app.outdir / 'py-decorators.xml')
secs = et.findall('./section/section')
@@ -537,7 +537,7 @@ def test_literalinclude_pydecorators(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='directive-code')
def test_code_block_highlighted(app, status, warning):
- app.builder.build(['highlight'])
+ app.build(filenames=[app.srcdir / 'highlight.rst'])
doctree = app.env.get_doctree('highlight')
codeblocks = list(doctree.findall(nodes.literal_block))
@@ -549,7 +549,7 @@ def test_code_block_highlighted(app, status, warning):
@pytest.mark.sphinx('html', testroot='directive-code')
def test_linenothreshold(app, status, warning):
- app.builder.build(['linenothreshold'])
+ app.build(filenames=[app.srcdir / 'linenothreshold.rst'])
html = (app.outdir / 'linenothreshold.html').read_text(encoding='utf8')
# code-block using linenothreshold
@@ -570,7 +570,7 @@ def test_linenothreshold(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='directive-code')
def test_code_block_dedent(app, status, warning):
- app.builder.build(['dedent'])
+ app.build(filenames=[app.srcdir / 'dedent.rst'])
doctree = app.env.get_doctree('dedent')
codeblocks = list(doctree.findall(nodes.literal_block))
# Note: comparison string should not have newlines at the beginning or end
diff --git a/tests/test_directive_object_description.py b/tests/test_directives/test_directive_object_description.py
index f2c9f9d..f2c9f9d 100644
--- a/tests/test_directive_object_description.py
+++ b/tests/test_directives/test_directive_object_description.py
diff --git a/tests/test_directive_only.py b/tests/test_directives/test_directive_only.py
index 2e9ea63..bf03c7b 100644
--- a/tests/test_directive_only.py
+++ b/tests/test_directives/test_directive_only.py
@@ -34,7 +34,7 @@ def test_sectioning(app, status, warning):
'Unnumbered section: %r' % subsect[0]
testsects(prefix + str(i + 1) + '.', subsect, indent + 4)
- app.builder.build(['only'])
+ app.build(filenames=[app.srcdir / 'only.rst'])
doctree = app.env.get_doctree('only')
app.env.apply_post_transforms(doctree, 'only')
@@ -43,4 +43,4 @@ def test_sectioning(app, status, warning):
for i, s in enumerate(parts):
testsects(str(i + 1) + '.', s, 4)
assert len(parts) == 4, 'Expected 4 document level headings, got:\n%s' % \
- '\n'.join([p[0] for p in parts])
+ '\n'.join(p[0] for p in parts)
diff --git a/tests/test_directives/test_directive_option.py b/tests/test_directives/test_directive_option.py
new file mode 100644
index 0000000..76448cd
--- /dev/null
+++ b/tests/test_directives/test_directive_option.py
@@ -0,0 +1,40 @@
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='root',
+ confoverrides={'option_emphasise_placeholders': True})
+def test_option_emphasise_placeholders(app, status, warning):
+ app.build()
+ content = (app.outdir / 'objects.html').read_text(encoding='utf8')
+ assert '<em><span class="pre">TYPE</span></em>' in content
+ assert '{TYPE}' not in content
+ assert ('<em><span class="pre">WHERE</span></em>'
+ '<span class="pre">-</span>'
+ '<em><span class="pre">COUNT</span></em>' in content)
+ assert '<span class="pre">{{value}}</span>' in content
+ assert ('<span class="pre">--plugin.option</span></span>'
+ '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content
+
+
+@pytest.mark.sphinx('html', testroot='root')
+def test_option_emphasise_placeholders_default(app, status, warning):
+ app.build()
+ content = (app.outdir / 'objects.html').read_text(encoding='utf8')
+ assert '<span class="pre">={TYPE}</span>' in content
+ assert '<span class="pre">={WHERE}-{COUNT}</span></span>' in content
+ assert '<span class="pre">{client_name}</span>' in content
+ assert ('<span class="pre">--plugin.option</span></span>'
+ '<span class="sig-prename descclassname"></span>'
+ '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content
+
+
+@pytest.mark.sphinx('html', testroot='root')
+def test_option_reference_with_value(app, status, warning):
+ app.build()
+ content = (app.outdir / 'objects.html').read_text(encoding='utf-8')
+ assert ('<span class="pre">-mapi</span></span><span class="sig-prename descclassname">'
+ '</span><a class="headerlink" href="#cmdoption-git-commit-mapi"') in content
+ assert 'first option <a class="reference internal" href="#cmdoption-git-commit-mapi">' in content
+ assert ('<a class="reference internal" href="#cmdoption-git-commit-mapi">'
+ '<code class="xref std std-option docutils literal notranslate"><span class="pre">-mapi[=xxx]</span></code></a>') in content
+ assert '<span class="pre">-mapi</span> <span class="pre">with_space</span>' in content
diff --git a/tests/test_directive_other.py b/tests/test_directives/test_directive_other.py
index 1feb251..1feb251 100644
--- a/tests/test_directive_other.py
+++ b/tests/test_directives/test_directive_other.py
diff --git a/tests/test_directive_patch.py b/tests/test_directives/test_directive_patch.py
index f4eb8f9..f4eb8f9 100644
--- a/tests/test_directive_patch.py
+++ b/tests/test_directives/test_directive_patch.py
diff --git a/tests/test_directives_no_typesetting.py b/tests/test_directives/test_directives_no_typesetting.py
index fd101fb..fd101fb 100644
--- a/tests/test_directives_no_typesetting.py
+++ b/tests/test_directives/test_directives_no_typesetting.py
diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py
deleted file mode 100644
index c5a044b..0000000
--- a/tests/test_domain_py.py
+++ /dev/null
@@ -1,2123 +0,0 @@
-"""Tests the Python Domain"""
-
-from __future__ import annotations
-
-import re
-from unittest.mock import Mock
-
-import docutils.utils
-import pytest
-from docutils import nodes
-
-from sphinx import addnodes
-from sphinx.addnodes import (
- desc,
- desc_addname,
- desc_annotation,
- desc_content,
- desc_name,
- desc_optional,
- desc_parameter,
- desc_parameterlist,
- desc_returns,
- desc_sig_keyword,
- desc_sig_literal_number,
- desc_sig_literal_string,
- desc_sig_name,
- desc_sig_operator,
- desc_sig_punctuation,
- desc_sig_space,
- desc_signature,
- desc_type_parameter,
- desc_type_parameter_list,
- pending_xref,
-)
-from sphinx.domains import IndexEntry
-from sphinx.domains.python import (
- PythonDomain,
- PythonModuleIndex,
- _parse_annotation,
- _pseudo_parse_arglist,
- py_sig_re,
-)
-from sphinx.testing import restructuredtext
-from sphinx.testing.util import assert_node
-from sphinx.writers.text import STDINDENT
-
-
-def parse(sig):
- m = py_sig_re.match(sig)
- if m is None:
- raise ValueError
- name_prefix, tp_list, name, arglist, retann = m.groups()
- signode = addnodes.desc_signature(sig, '')
- _pseudo_parse_arglist(signode, arglist)
- return signode.astext()
-
-
-def test_function_signatures():
- rv = parse('func(a=1) -> int object')
- assert rv == '(a=1)'
-
- rv = parse('func(a=1, [b=None])')
- assert rv == '(a=1, [b=None])'
-
- rv = parse('func(a=1[, b=None])')
- assert rv == '(a=1, [b=None])'
-
- rv = parse("compile(source : string, filename, symbol='file')")
- assert rv == "(source : string, filename, symbol='file')"
-
- rv = parse('func(a=[], [b=None])')
- assert rv == '(a=[], [b=None])'
-
- rv = parse('func(a=[][, b=None])')
- assert rv == '(a=[], [b=None])'
-
-
-@pytest.mark.sphinx('dummy', testroot='domain-py')
-def test_domain_py_xrefs(app, status, warning):
- """Domain objects have correct prefixes when looking up xrefs"""
- app.builder.build_all()
-
- def assert_refnode(node, module_name, class_name, target, reftype=None,
- domain='py'):
- attributes = {
- 'refdomain': domain,
- 'reftarget': target,
- }
- if reftype is not None:
- attributes['reftype'] = reftype
- if module_name is not False:
- attributes['py:module'] = module_name
- if class_name is not False:
- attributes['py:class'] = class_name
- assert_node(node, **attributes)
-
- doctree = app.env.get_doctree('roles')
- refnodes = list(doctree.findall(pending_xref))
- assert_refnode(refnodes[0], None, None, 'TopLevel', 'class')
- assert_refnode(refnodes[1], None, None, 'top_level', 'meth')
- assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth')
- assert_refnode(refnodes[3], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth')
- assert_refnode(refnodes[4], None, 'NestedParentA', 'child_2', 'meth')
- assert_refnode(refnodes[5], False, 'NestedParentA', 'any_child', domain='')
- assert_refnode(refnodes[6], None, 'NestedParentA', 'NestedChildA', 'class')
- assert_refnode(refnodes[7], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth')
- assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA',
- 'NestedParentA.child_1', 'meth')
- assert_refnode(refnodes[9], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth')
- assert_refnode(refnodes[10], None, 'NestedParentB', 'child_1', 'meth')
- assert_refnode(refnodes[11], None, 'NestedParentB', 'NestedParentB', 'class')
- assert_refnode(refnodes[12], None, None, 'NestedParentA.NestedChildA', 'class')
- assert len(refnodes) == 13
-
- doctree = app.env.get_doctree('module')
- refnodes = list(doctree.findall(pending_xref))
- assert_refnode(refnodes[0], 'module_a.submodule', None,
- 'ModTopLevel', 'class')
- assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel',
- 'mod_child_1', 'meth')
- assert_refnode(refnodes[2], 'module_a.submodule', 'ModTopLevel',
- 'ModTopLevel.mod_child_1', 'meth')
- assert_refnode(refnodes[3], 'module_a.submodule', 'ModTopLevel',
- 'mod_child_2', 'meth')
- assert_refnode(refnodes[4], 'module_a.submodule', 'ModTopLevel',
- 'module_a.submodule.ModTopLevel.mod_child_1', 'meth')
- assert_refnode(refnodes[5], 'module_a.submodule', 'ModTopLevel',
- 'prop', 'attr')
- assert_refnode(refnodes[6], 'module_a.submodule', 'ModTopLevel',
- 'prop', 'meth')
- assert_refnode(refnodes[7], 'module_b.submodule', None,
- 'ModTopLevel', 'class')
- assert_refnode(refnodes[8], 'module_b.submodule', 'ModTopLevel',
- 'ModNoModule', 'class')
- assert_refnode(refnodes[9], False, False, 'int', 'class')
- assert_refnode(refnodes[10], False, False, 'tuple', 'class')
- assert_refnode(refnodes[11], False, False, 'str', 'class')
- assert_refnode(refnodes[12], False, False, 'float', 'class')
- assert_refnode(refnodes[13], False, False, 'list', 'class')
- assert_refnode(refnodes[14], False, False, 'ModTopLevel', 'class')
- assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std')
- assert len(refnodes) == 16
-
- doctree = app.env.get_doctree('module_option')
- refnodes = list(doctree.findall(pending_xref))
- print(refnodes)
- print(refnodes[0])
- print(refnodes[1])
- assert_refnode(refnodes[0], 'test.extra', 'B', 'foo', 'meth')
- assert_refnode(refnodes[1], 'test.extra', 'B', 'foo', 'meth')
- assert len(refnodes) == 2
-
-
-@pytest.mark.sphinx('html', testroot='domain-py')
-def test_domain_py_xrefs_abbreviations(app, status, warning):
- app.builder.build_all()
-
- content = (app.outdir / 'abbr.html').read_text(encoding='utf8')
- assert re.search(r'normal: <a .* href="module.html#module_a.submodule.ModTopLevel.'
- r'mod_child_1" .*><.*>module_a.submodule.ModTopLevel.mod_child_1\(\)'
- r'<.*></a>',
- content)
- assert re.search(r'relative: <a .* href="module.html#module_a.submodule.ModTopLevel.'
- r'mod_child_1" .*><.*>ModTopLevel.mod_child_1\(\)<.*></a>',
- content)
- assert re.search(r'short name: <a .* href="module.html#module_a.submodule.ModTopLevel.'
- r'mod_child_1" .*><.*>mod_child_1\(\)<.*></a>',
- content)
- assert re.search(r'relative \+ short name: <a .* href="module.html#module_a.submodule.'
- r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>',
- content)
- assert re.search(r'short name \+ relative: <a .* href="module.html#module_a.submodule.'
- r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>',
- content)
-
-
-@pytest.mark.sphinx('dummy', testroot='domain-py')
-def test_domain_py_objects(app, status, warning):
- app.builder.build_all()
-
- modules = app.env.domains['py'].data['modules']
- objects = app.env.domains['py'].data['objects']
-
- assert 'module_a.submodule' in modules
- assert 'module_a.submodule' in objects
- assert 'module_b.submodule' in modules
- assert 'module_b.submodule' in objects
-
- assert objects['module_a.submodule.ModTopLevel'][2] == 'class'
- assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method'
- assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method'
- assert 'ModTopLevel.ModNoModule' not in objects
- assert objects['ModNoModule'][2] == 'class'
- assert objects['module_b.submodule.ModTopLevel'][2] == 'class'
-
- assert objects['TopLevel'][2] == 'class'
- assert objects['top_level'][2] == 'method'
- assert objects['NestedParentA'][2] == 'class'
- assert objects['NestedParentA.child_1'][2] == 'method'
- assert objects['NestedParentA.any_child'][2] == 'method'
- assert objects['NestedParentA.NestedChildA'][2] == 'class'
- assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'method'
- assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'method'
- assert objects['NestedParentA.child_2'][2] == 'method'
- assert objects['NestedParentB'][2] == 'class'
- assert objects['NestedParentB.child_1'][2] == 'method'
-
-
-@pytest.mark.sphinx('html', testroot='domain-py')
-def test_resolve_xref_for_properties(app, status, warning):
- app.builder.build_all()
-
- content = (app.outdir / 'module.html').read_text(encoding='utf8')
- assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"'
- ' title="module_a.submodule.ModTopLevel.prop">'
- '<code class="xref py py-attr docutils literal notranslate"><span class="pre">'
- 'prop</span> <span class="pre">attribute</span></code></a>' in content)
- assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"'
- ' title="module_a.submodule.ModTopLevel.prop">'
- '<code class="xref py py-meth docutils literal notranslate"><span class="pre">'
- 'prop</span> <span class="pre">method</span></code></a>' in content)
- assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"'
- ' title="module_a.submodule.ModTopLevel.prop">'
- '<code class="xref py py-attr docutils literal notranslate"><span class="pre">'
- 'prop</span> <span class="pre">attribute</span></code></a>' in content)
-
-
-@pytest.mark.sphinx('dummy', testroot='domain-py')
-def test_domain_py_find_obj(app, status, warning):
-
- def find_obj(modname, prefix, obj_name, obj_type, searchmode=0):
- return app.env.domains['py'].find_obj(
- app.env, modname, prefix, obj_name, obj_type, searchmode)
-
- app.builder.build_all()
-
- assert (find_obj(None, None, 'NONEXISTANT', 'class') == [])
- assert (find_obj(None, None, 'NestedParentA', 'class') ==
- [('NestedParentA', ('roles', 'NestedParentA', 'class', False))])
- assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
- [('NestedParentA.NestedChildA',
- ('roles', 'NestedParentA.NestedChildA', 'class', False))])
- assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') ==
- [('NestedParentA.NestedChildA',
- ('roles', 'NestedParentA.NestedChildA', 'class', False))])
- assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') ==
- [('NestedParentA.NestedChildA.subchild_1',
- ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
- assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') ==
- [('NestedParentA.NestedChildA.subchild_1',
- ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
- assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') ==
- [('NestedParentA.NestedChildA.subchild_1',
- ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
-
-
-@pytest.mark.sphinx('html', testroot='domain-py', freshenv=True)
-def test_domain_py_canonical(app, status, warning):
- app.builder.build_all()
-
- content = (app.outdir / 'canonical.html').read_text(encoding='utf8')
- assert ('<a class="reference internal" href="#canonical.Foo" title="canonical.Foo">'
- '<code class="xref py py-class docutils literal notranslate">'
- '<span class="pre">Foo</span></code></a>' in content)
- assert warning.getvalue() == ''
-
-
-def test_get_full_qualified_name():
- env = Mock(domaindata={})
- domain = PythonDomain(env)
-
- # non-python references
- node = nodes.reference()
- assert domain.get_full_qualified_name(node) is None
-
- # simple reference
- node = nodes.reference(reftarget='func')
- assert domain.get_full_qualified_name(node) == 'func'
-
- # with py:module context
- kwargs = {'py:module': 'module1'}
- node = nodes.reference(reftarget='func', **kwargs)
- assert domain.get_full_qualified_name(node) == 'module1.func'
-
- # with py:class context
- kwargs = {'py:class': 'Class'}
- node = nodes.reference(reftarget='func', **kwargs)
- assert domain.get_full_qualified_name(node) == 'Class.func'
-
- # with both py:module and py:class context
- kwargs = {'py:module': 'module1', 'py:class': 'Class'}
- node = nodes.reference(reftarget='func', **kwargs)
- assert domain.get_full_qualified_name(node) == 'module1.Class.func'
-
-
-def test_parse_annotation(app):
- doctree = _parse_annotation("int", app.env)
- assert_node(doctree, ([pending_xref, "int"],))
- assert_node(doctree[0], pending_xref, refdomain="py", reftype="class", reftarget="int")
-
- doctree = _parse_annotation("List[int]", app.env)
- assert_node(doctree, ([pending_xref, "List"],
- [desc_sig_punctuation, "["],
- [pending_xref, "int"],
- [desc_sig_punctuation, "]"]))
-
- doctree = _parse_annotation("Tuple[int, int]", app.env)
- assert_node(doctree, ([pending_xref, "Tuple"],
- [desc_sig_punctuation, "["],
- [pending_xref, "int"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [pending_xref, "int"],
- [desc_sig_punctuation, "]"]))
-
- doctree = _parse_annotation("Tuple[()]", app.env)
- assert_node(doctree, ([pending_xref, "Tuple"],
- [desc_sig_punctuation, "["],
- [desc_sig_punctuation, "("],
- [desc_sig_punctuation, ")"],
- [desc_sig_punctuation, "]"]))
-
- doctree = _parse_annotation("Tuple[int, ...]", app.env)
- assert_node(doctree, ([pending_xref, "Tuple"],
- [desc_sig_punctuation, "["],
- [pending_xref, "int"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [desc_sig_punctuation, "..."],
- [desc_sig_punctuation, "]"]))
-
- doctree = _parse_annotation("Callable[[int, int], int]", app.env)
- assert_node(doctree, ([pending_xref, "Callable"],
- [desc_sig_punctuation, "["],
- [desc_sig_punctuation, "["],
- [pending_xref, "int"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [pending_xref, "int"],
- [desc_sig_punctuation, "]"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [pending_xref, "int"],
- [desc_sig_punctuation, "]"]))
-
- doctree = _parse_annotation("Callable[[], int]", app.env)
- assert_node(doctree, ([pending_xref, "Callable"],
- [desc_sig_punctuation, "["],
- [desc_sig_punctuation, "["],
- [desc_sig_punctuation, "]"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [pending_xref, "int"],
- [desc_sig_punctuation, "]"]))
-
- doctree = _parse_annotation("List[None]", app.env)
- assert_node(doctree, ([pending_xref, "List"],
- [desc_sig_punctuation, "["],
- [pending_xref, "None"],
- [desc_sig_punctuation, "]"]))
-
- # None type makes an object-reference (not a class reference)
- doctree = _parse_annotation("None", app.env)
- assert_node(doctree, ([pending_xref, "None"],))
- assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="None")
-
- # Literal type makes an object-reference (not a class reference)
- doctree = _parse_annotation("typing.Literal['a', 'b']", app.env)
- assert_node(doctree, ([pending_xref, "Literal"],
- [desc_sig_punctuation, "["],
- [desc_sig_literal_string, "'a'"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [desc_sig_literal_string, "'b'"],
- [desc_sig_punctuation, "]"]))
- assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Literal")
-
-
-def test_parse_annotation_suppress(app):
- doctree = _parse_annotation("~typing.Dict[str, str]", app.env)
- assert_node(doctree, ([pending_xref, "Dict"],
- [desc_sig_punctuation, "["],
- [pending_xref, "str"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [pending_xref, "str"],
- [desc_sig_punctuation, "]"]))
- assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Dict")
-
-
-def test_parse_annotation_Literal(app):
- doctree = _parse_annotation("Literal[True, False]", app.env)
- assert_node(doctree, ([pending_xref, "Literal"],
- [desc_sig_punctuation, "["],
- [desc_sig_keyword, "True"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [desc_sig_keyword, "False"],
- [desc_sig_punctuation, "]"]))
-
- doctree = _parse_annotation("typing.Literal[0, 1, 'abc']", app.env)
- assert_node(doctree, ([pending_xref, "Literal"],
- [desc_sig_punctuation, "["],
- [desc_sig_literal_number, "0"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [desc_sig_literal_number, "1"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [desc_sig_literal_string, "'abc'"],
- [desc_sig_punctuation, "]"]))
-
-
-def test_pyfunction_signature(app):
- text = ".. py:function:: hello(name: str) -> str"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"])],
- desc_content)]))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, desc_parameter, ([desc_sig_name, "name"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"])])
-
-
-def test_pyfunction_signature_full(app):
- text = (".. py:function:: hello(a: str, b = 1, *args: str, "
- "c: bool = True, d: tuple = (1, 2), **kwargs: str) -> str")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"])],
- desc_content)]))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [desc_sig_name, pending_xref, "str"])],
- [desc_parameter, ([desc_sig_name, "b"],
- [desc_sig_operator, "="],
- [nodes.inline, "1"])],
- [desc_parameter, ([desc_sig_operator, "*"],
- [desc_sig_name, "args"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [desc_sig_name, pending_xref, "str"])],
- [desc_parameter, ([desc_sig_name, "c"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [desc_sig_name, pending_xref, "bool"],
- desc_sig_space,
- [desc_sig_operator, "="],
- desc_sig_space,
- [nodes.inline, "True"])],
- [desc_parameter, ([desc_sig_name, "d"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [desc_sig_name, pending_xref, "tuple"],
- desc_sig_space,
- [desc_sig_operator, "="],
- desc_sig_space,
- [nodes.inline, "(1, 2)"])],
- [desc_parameter, ([desc_sig_operator, "**"],
- [desc_sig_name, "kwargs"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [desc_sig_name, pending_xref, "str"])])])
- # case: separator at head
- text = ".. py:function:: hello(*, a)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, nodes.inline, "*"],
- [desc_parameter, desc_sig_name, "a"])])
-
- # case: separator in the middle
- text = ".. py:function:: hello(a, /, b, *, c)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"],
- [desc_parameter, desc_sig_operator, "/"],
- [desc_parameter, desc_sig_name, "b"],
- [desc_parameter, desc_sig_operator, "*"],
- [desc_parameter, desc_sig_name, "c"])])
-
- # case: separator in the middle (2)
- text = ".. py:function:: hello(a, /, *, b)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"],
- [desc_parameter, desc_sig_operator, "/"],
- [desc_parameter, desc_sig_operator, "*"],
- [desc_parameter, desc_sig_name, "b"])])
-
- # case: separator at tail
- text = ".. py:function:: hello(a, /)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"],
- [desc_parameter, desc_sig_operator, "/"])])
-
-
-def test_pyfunction_with_unary_operators(app):
- text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"],
- [desc_sig_operator, "="],
- [nodes.inline, "+1"])],
- [desc_parameter, ([desc_sig_name, "bacon"],
- [desc_sig_operator, "="],
- [nodes.inline, "-1"])],
- [desc_parameter, ([desc_sig_name, "sausage"],
- [desc_sig_operator, "="],
- [nodes.inline, "~1"])],
- [desc_parameter, ([desc_sig_name, "spam"],
- [desc_sig_operator, "="],
- [nodes.inline, "not spam"])])])
-
-
-def test_pyfunction_with_binary_operators(app):
- text = ".. py:function:: menu(spam=2**64)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"],
- [desc_sig_operator, "="],
- [nodes.inline, "2**64"])])])
-
-
-def test_pyfunction_with_number_literals(app):
- text = ".. py:function:: hello(age=0x10, height=1_6_0)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"],
- [desc_sig_operator, "="],
- [nodes.inline, "0x10"])],
- [desc_parameter, ([desc_sig_name, "height"],
- [desc_sig_operator, "="],
- [nodes.inline, "1_6_0"])])])
-
-
-def test_pyfunction_with_union_type_operator(app):
- text = ".. py:function:: hello(age: int | None)"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0][1],
- [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [desc_sig_name, ([pending_xref, "int"],
- desc_sig_space,
- [desc_sig_punctuation, "|"],
- desc_sig_space,
- [pending_xref, "None"])])])])
-
-
-def test_optional_pyfunction_signature(app):
- text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "compile"],
- desc_parameterlist,
- [desc_returns, pending_xref, "ast object"])],
- desc_content)]))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1],
- ([desc_parameter, ([desc_sig_name, "source"])],
- [desc_optional, ([desc_parameter, ([desc_sig_name, "filename"])],
- [desc_optional, desc_parameter, ([desc_sig_name, "symbol"])])]))
-
-
-def test_pyexception_signature(app):
- text = ".. py:exception:: builtins.IOError"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ('exception', desc_sig_space)],
- [desc_addname, "builtins."],
- [desc_name, "IOError"])],
- desc_content)]))
- assert_node(doctree[1], desc, desctype="exception",
- domain="py", objtype="exception", no_index=False)
-
-
-def test_pydata_signature(app):
- text = (".. py:data:: version\n"
- " :type: int\n"
- " :value: 1\n")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "version"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "int"])],
- [desc_annotation, (
- desc_sig_space,
- [desc_sig_punctuation, '='],
- desc_sig_space,
- "1")],
- )],
- desc_content)]))
- assert_node(doctree[1], addnodes.desc, desctype="data",
- domain="py", objtype="data", no_index=False)
-
-
-def test_pydata_signature_old(app):
- text = (".. py:data:: version\n"
- " :annotation: = 1\n")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "version"],
- [desc_annotation, (desc_sig_space,
- "= 1")])],
- desc_content)]))
- assert_node(doctree[1], addnodes.desc, desctype="data",
- domain="py", objtype="data", no_index=False)
-
-
-def test_pydata_with_union_type_operator(app):
- text = (".. py:data:: version\n"
- " :type: int | str")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree[1][0],
- ([desc_name, "version"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "int"],
- desc_sig_space,
- [desc_sig_punctuation, "|"],
- desc_sig_space,
- [pending_xref, "str"])]))
-
-
-def test_pyobject_prefix(app):
- text = (".. py:class:: Foo\n"
- "\n"
- " .. py:method:: Foo.say\n"
- " .. py:method:: FooBar.say")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)],
- [desc_name, "Foo"])],
- [desc_content, (addnodes.index,
- desc,
- addnodes.index,
- desc)])]))
- assert doctree[1][1][1].astext().strip() == 'say()' # prefix is stripped
- assert doctree[1][1][3].astext().strip() == 'FooBar.say()' # not stripped
-
-
-def test_pydata(app):
- text = (".. py:module:: example\n"
- ".. py:data:: var\n"
- " :type: int\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- addnodes.index,
- nodes.target,
- [desc, ([desc_signature, ([desc_addname, "example."],
- [desc_name, "var"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "int"])])],
- [desc_content, ()])]))
- assert_node(doctree[3][0][2][2], pending_xref, **{"py:module": "example"})
- assert 'example.var' in domain.objects
- assert domain.objects['example.var'] == ('index', 'example.var', 'data', False)
-
-
-def test_pyfunction(app):
- text = (".. py:function:: func1\n"
- ".. py:module:: example\n"
- ".. py:function:: func2\n"
- " :async:\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "func1"],
- [desc_parameterlist, ()])],
- [desc_content, ()])],
- addnodes.index,
- addnodes.index,
- nodes.target,
- [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'async'],
- desc_sig_space)],
- [desc_addname, "example."],
- [desc_name, "func2"],
- [desc_parameterlist, ()])],
- [desc_content, ()])]))
- assert_node(doctree[0], addnodes.index,
- entries=[('pair', 'built-in function; func1()', 'func1', '', None)])
- assert_node(doctree[2], addnodes.index,
- entries=[('pair', 'module; example', 'module-example', '', None)])
- assert_node(doctree[3], addnodes.index,
- entries=[('single', 'func2() (in module example)', 'example.func2', '', None)])
-
- assert 'func1' in domain.objects
- assert domain.objects['func1'] == ('index', 'func1', 'function', False)
- assert 'example.func2' in domain.objects
- assert domain.objects['example.func2'] == ('index', 'example.func2', 'function', False)
-
-
-def test_pyclass_options(app):
- text = (".. py:class:: Class1\n"
- ".. py:class:: Class2\n"
- " :final:\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_name, "Class1"])],
- [desc_content, ()])],
- addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("final",
- desc_sig_space,
- "class",
- desc_sig_space)],
- [desc_name, "Class2"])],
- [desc_content, ()])]))
-
- # class
- assert_node(doctree[0], addnodes.index,
- entries=[('single', 'Class1 (built-in class)', 'Class1', '', None)])
- assert 'Class1' in domain.objects
- assert domain.objects['Class1'] == ('index', 'Class1', 'class', False)
-
- # :final:
- assert_node(doctree[2], addnodes.index,
- entries=[('single', 'Class2 (built-in class)', 'Class2', '', None)])
- assert 'Class2' in domain.objects
- assert domain.objects['Class2'] == ('index', 'Class2', 'class', False)
-
-
-def test_pymethod_options(app):
- text = (".. py:class:: Class\n"
- "\n"
- " .. py:method:: meth1\n"
- " .. py:method:: meth2\n"
- " :classmethod:\n"
- " .. py:method:: meth3\n"
- " :staticmethod:\n"
- " .. py:method:: meth4\n"
- " :async:\n"
- " .. py:method:: meth5\n"
- " :abstractmethod:\n"
- " .. py:method:: meth6\n"
- " :final:\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_name, "Class"])],
- [desc_content, (addnodes.index,
- desc,
- addnodes.index,
- desc,
- addnodes.index,
- desc,
- addnodes.index,
- desc,
- addnodes.index,
- desc,
- addnodes.index,
- desc)])]))
-
- # method
- assert_node(doctree[1][1][0], addnodes.index,
- entries=[('single', 'meth1() (Class method)', 'Class.meth1', '', None)])
- assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth1' in domain.objects
- assert domain.objects['Class.meth1'] == ('index', 'Class.meth1', 'method', False)
-
- # :classmethod:
- assert_node(doctree[1][1][2], addnodes.index,
- entries=[('single', 'meth2() (Class class method)', 'Class.meth2', '', None)])
- assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)],
- [desc_name, "meth2"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth2' in domain.objects
- assert domain.objects['Class.meth2'] == ('index', 'Class.meth2', 'method', False)
-
- # :staticmethod:
- assert_node(doctree[1][1][4], addnodes.index,
- entries=[('single', 'meth3() (Class static method)', 'Class.meth3', '', None)])
- assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)],
- [desc_name, "meth3"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth3' in domain.objects
- assert domain.objects['Class.meth3'] == ('index', 'Class.meth3', 'method', False)
-
- # :async:
- assert_node(doctree[1][1][6], addnodes.index,
- entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)])
- assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, ("async", desc_sig_space)],
- [desc_name, "meth4"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth4' in domain.objects
- assert domain.objects['Class.meth4'] == ('index', 'Class.meth4', 'method', False)
-
- # :abstractmethod:
- assert_node(doctree[1][1][8], addnodes.index,
- entries=[('single', 'meth5() (Class method)', 'Class.meth5', '', None)])
- assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space)],
- [desc_name, "meth5"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth5' in domain.objects
- assert domain.objects['Class.meth5'] == ('index', 'Class.meth5', 'method', False)
-
- # :final:
- assert_node(doctree[1][1][10], addnodes.index,
- entries=[('single', 'meth6() (Class method)', 'Class.meth6', '', None)])
- assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, ("final", desc_sig_space)],
- [desc_name, "meth6"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth6' in domain.objects
- assert domain.objects['Class.meth6'] == ('index', 'Class.meth6', 'method', False)
-
-
-def test_pyclassmethod(app):
- text = (".. py:class:: Class\n"
- "\n"
- " .. py:classmethod:: meth\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_name, "Class"])],
- [desc_content, (addnodes.index,
- desc)])]))
- assert_node(doctree[1][1][0], addnodes.index,
- entries=[('single', 'meth() (Class class method)', 'Class.meth', '', None)])
- assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)],
- [desc_name, "meth"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth' in domain.objects
- assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False)
-
-
-def test_pystaticmethod(app):
- text = (".. py:class:: Class\n"
- "\n"
- " .. py:staticmethod:: meth\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_name, "Class"])],
- [desc_content, (addnodes.index,
- desc)])]))
- assert_node(doctree[1][1][0], addnodes.index,
- entries=[('single', 'meth() (Class static method)', 'Class.meth', '', None)])
- assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)],
- [desc_name, "meth"],
- [desc_parameterlist, ()])],
- [desc_content, ()]))
- assert 'Class.meth' in domain.objects
- assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False)
-
-
-def test_pyattribute(app):
- text = (".. py:class:: Class\n"
- "\n"
- " .. py:attribute:: attr\n"
- " :type: Optional[str]\n"
- " :value: ''\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_name, "Class"])],
- [desc_content, (addnodes.index,
- desc)])]))
- assert_node(doctree[1][1][0], addnodes.index,
- entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)])
- assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "str"],
- desc_sig_space,
- [desc_sig_punctuation, "|"],
- desc_sig_space,
- [pending_xref, "None"])],
- [desc_annotation, (desc_sig_space,
- [desc_sig_punctuation, '='],
- desc_sig_space,
- "''")],
- )],
- [desc_content, ()]))
- assert_node(doctree[1][1][1][0][1][2], pending_xref, **{"py:class": "Class"})
- assert_node(doctree[1][1][1][0][1][6], pending_xref, **{"py:class": "Class"})
- assert 'Class.attr' in domain.objects
- assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False)
-
-
-def test_pyproperty(app):
- text = (".. py:class:: Class\n"
- "\n"
- " .. py:property:: prop1\n"
- " :abstractmethod:\n"
- " :type: str\n"
- "\n"
- " .. py:property:: prop2\n"
- " :classmethod:\n"
- " :type: str\n")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_name, "Class"])],
- [desc_content, (addnodes.index,
- desc,
- addnodes.index,
- desc)])]))
- assert_node(doctree[1][1][0], addnodes.index,
- entries=[('single', 'prop1 (Class property)', 'Class.prop1', '', None)])
- assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space,
- "property", desc_sig_space)],
- [desc_name, "prop1"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "str"])])],
- [desc_content, ()]))
- assert_node(doctree[1][1][2], addnodes.index,
- entries=[('single', 'prop2 (Class property)', 'Class.prop2', '', None)])
- assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("class", desc_sig_space,
- "property", desc_sig_space)],
- [desc_name, "prop2"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "str"])])],
- [desc_content, ()]))
- assert 'Class.prop1' in domain.objects
- assert domain.objects['Class.prop1'] == ('index', 'Class.prop1', 'property', False)
- assert 'Class.prop2' in domain.objects
- assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False)
-
-
-def test_pydecorator_signature(app):
- text = ".. py:decorator:: deco"
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_addname, "@"],
- [desc_name, "deco"])],
- desc_content)]))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
-
- assert 'deco' in domain.objects
- assert domain.objects['deco'] == ('index', 'deco', 'function', False)
-
-
-def test_pydecoratormethod_signature(app):
- text = ".. py:decoratormethod:: deco"
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_addname, "@"],
- [desc_name, "deco"])],
- desc_content)]))
- assert_node(doctree[1], addnodes.desc, desctype="method",
- domain="py", objtype="method", no_index=False)
-
- assert 'deco' in domain.objects
- assert domain.objects['deco'] == ('index', 'deco', 'method', False)
-
-
-def test_canonical(app):
- text = (".. py:class:: io.StringIO\n"
- " :canonical: _io.StringIO")
- domain = app.env.get_domain('py')
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_addname, "io."],
- [desc_name, "StringIO"])],
- desc_content)]))
- assert 'io.StringIO' in domain.objects
- assert domain.objects['io.StringIO'] == ('index', 'io.StringIO', 'class', False)
- assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', True)
-
-
-def test_canonical_definition_overrides(app, warning):
- text = (".. py:class:: io.StringIO\n"
- " :canonical: _io.StringIO\n"
- ".. py:class:: _io.StringIO\n")
- restructuredtext.parse(app, text)
- assert warning.getvalue() == ""
-
- domain = app.env.get_domain('py')
- assert domain.objects['_io.StringIO'] == ('index', 'id0', 'class', False)
-
-
-def test_canonical_definition_skip(app, warning):
- text = (".. py:class:: _io.StringIO\n"
- ".. py:class:: io.StringIO\n"
- " :canonical: _io.StringIO\n")
-
- restructuredtext.parse(app, text)
- assert warning.getvalue() == ""
-
- domain = app.env.get_domain('py')
- assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', False)
-
-
-def test_canonical_duplicated(app, warning):
- text = (".. py:class:: mypackage.StringIO\n"
- " :canonical: _io.StringIO\n"
- ".. py:class:: io.StringIO\n"
- " :canonical: _io.StringIO\n")
-
- restructuredtext.parse(app, text)
- assert warning.getvalue() != ""
-
-
-def test_info_field_list(app):
- text = (".. py:module:: example\n"
- ".. py:class:: Class\n"
- "\n"
- " :meta blah: this meta-field must not show up in the toc-tree\n"
- " :param str name: blah blah\n"
- " :meta another meta field:\n"
- " :param age: blah blah\n"
- " :type age: int\n"
- " :param items: blah blah\n"
- " :type items: Tuple[str, ...]\n"
- " :param Dict[str, str] params: blah blah\n")
- doctree = restructuredtext.parse(app, text)
- print(doctree)
-
- assert_node(doctree, (addnodes.index,
- addnodes.index,
- nodes.target,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_addname, "example."],
- [desc_name, "Class"])],
- [desc_content, nodes.field_list, nodes.field])]))
- assert_node(doctree[3][1][0][0],
- ([nodes.field_name, "Parameters"],
- [nodes.field_body, nodes.bullet_list, ([nodes.list_item, nodes.paragraph],
- [nodes.list_item, nodes.paragraph],
- [nodes.list_item, nodes.paragraph],
- [nodes.list_item, nodes.paragraph])]))
-
- # :param str name:
- assert_node(doctree[3][1][0][0][1][0][0][0],
- ([addnodes.literal_strong, "name"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "str"],
- ")",
- " -- ",
- "blah blah"))
- assert_node(doctree[3][1][0][0][1][0][0][0][2], pending_xref,
- refdomain="py", reftype="class", reftarget="str",
- **{"py:module": "example", "py:class": "Class"})
-
- # :param age: + :type age:
- assert_node(doctree[3][1][0][0][1][0][1][0],
- ([addnodes.literal_strong, "age"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "int"],
- ")",
- " -- ",
- "blah blah"))
- assert_node(doctree[3][1][0][0][1][0][1][0][2], pending_xref,
- refdomain="py", reftype="class", reftarget="int",
- **{"py:module": "example", "py:class": "Class"})
-
- # :param items: + :type items:
- assert_node(doctree[3][1][0][0][1][0][2][0],
- ([addnodes.literal_strong, "items"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "Tuple"],
- [addnodes.literal_emphasis, "["],
- [pending_xref, addnodes.literal_emphasis, "str"],
- [addnodes.literal_emphasis, ", "],
- [addnodes.literal_emphasis, "..."],
- [addnodes.literal_emphasis, "]"],
- ")",
- " -- ",
- "blah blah"))
- assert_node(doctree[3][1][0][0][1][0][2][0][2], pending_xref,
- refdomain="py", reftype="class", reftarget="Tuple",
- **{"py:module": "example", "py:class": "Class"})
- assert_node(doctree[3][1][0][0][1][0][2][0][4], pending_xref,
- refdomain="py", reftype="class", reftarget="str",
- **{"py:module": "example", "py:class": "Class"})
-
- # :param Dict[str, str] params:
- assert_node(doctree[3][1][0][0][1][0][3][0],
- ([addnodes.literal_strong, "params"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "Dict"],
- [addnodes.literal_emphasis, "["],
- [pending_xref, addnodes.literal_emphasis, "str"],
- [addnodes.literal_emphasis, ", "],
- [pending_xref, addnodes.literal_emphasis, "str"],
- [addnodes.literal_emphasis, "]"],
- ")",
- " -- ",
- "blah blah"))
- assert_node(doctree[3][1][0][0][1][0][3][0][2], pending_xref,
- refdomain="py", reftype="class", reftarget="Dict",
- **{"py:module": "example", "py:class": "Class"})
- assert_node(doctree[3][1][0][0][1][0][3][0][4], pending_xref,
- refdomain="py", reftype="class", reftarget="str",
- **{"py:module": "example", "py:class": "Class"})
- assert_node(doctree[3][1][0][0][1][0][3][0][6], pending_xref,
- refdomain="py", reftype="class", reftarget="str",
- **{"py:module": "example", "py:class": "Class"})
-
-
-def test_info_field_list_piped_type(app):
- text = (".. py:module:: example\n"
- ".. py:class:: Class\n"
- "\n"
- " :param age: blah blah\n"
- " :type age: int | str\n")
- doctree = restructuredtext.parse(app, text)
-
- assert_node(doctree,
- (addnodes.index,
- addnodes.index,
- nodes.target,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_addname, "example."],
- [desc_name, "Class"])],
- [desc_content, nodes.field_list, nodes.field, (nodes.field_name,
- nodes.field_body)])]))
- assert_node(doctree[3][1][0][0][1],
- ([nodes.paragraph, ([addnodes.literal_strong, "age"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "int"],
- [addnodes.literal_emphasis, " | "],
- [pending_xref, addnodes.literal_emphasis, "str"],
- ")",
- " -- ",
- "blah blah")],))
- assert_node(doctree[3][1][0][0][1][0][2], pending_xref,
- refdomain="py", reftype="class", reftarget="int",
- **{"py:module": "example", "py:class": "Class"})
- assert_node(doctree[3][1][0][0][1][0][4], pending_xref,
- refdomain="py", reftype="class", reftarget="str",
- **{"py:module": "example", "py:class": "Class"})
-
-
-def test_info_field_list_Literal(app):
- text = (".. py:module:: example\n"
- ".. py:class:: Class\n"
- "\n"
- " :param age: blah blah\n"
- " :type age: Literal['foo', 'bar', 'baz']\n")
- doctree = restructuredtext.parse(app, text)
-
- assert_node(doctree,
- (addnodes.index,
- addnodes.index,
- nodes.target,
- [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
- [desc_addname, "example."],
- [desc_name, "Class"])],
- [desc_content, nodes.field_list, nodes.field, (nodes.field_name,
- nodes.field_body)])]))
- assert_node(doctree[3][1][0][0][1],
- ([nodes.paragraph, ([addnodes.literal_strong, "age"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "Literal"],
- [addnodes.literal_emphasis, "["],
- [addnodes.literal_emphasis, "'foo'"],
- [addnodes.literal_emphasis, ", "],
- [addnodes.literal_emphasis, "'bar'"],
- [addnodes.literal_emphasis, ", "],
- [addnodes.literal_emphasis, "'baz'"],
- [addnodes.literal_emphasis, "]"],
- ")",
- " -- ",
- "blah blah")],))
- assert_node(doctree[3][1][0][0][1][0][2], pending_xref,
- refdomain="py", reftype="class", reftarget="Literal",
- **{"py:module": "example", "py:class": "Class"})
-
-
-def test_info_field_list_var(app):
- text = (".. py:class:: Class\n"
- "\n"
- " :var int attr: blah blah\n")
- doctree = restructuredtext.parse(app, text)
-
- assert_node(doctree, (addnodes.index,
- [desc, (desc_signature,
- [desc_content, nodes.field_list, nodes.field])]))
- assert_node(doctree[1][1][0][0], ([nodes.field_name, "Variables"],
- [nodes.field_body, nodes.paragraph]))
-
- # :var int attr:
- assert_node(doctree[1][1][0][0][1][0],
- ([addnodes.literal_strong, "attr"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "int"],
- ")",
- " -- ",
- "blah blah"))
- assert_node(doctree[1][1][0][0][1][0][2], pending_xref,
- refdomain="py", reftype="class", reftarget="int", **{"py:class": "Class"})
-
-
-def test_info_field_list_napoleon_deliminator_of(app):
- text = (".. py:module:: example\n"
- ".. py:class:: Class\n"
- "\n"
- " :param list_str_var: example description.\n"
- " :type list_str_var: list of str\n"
- " :param tuple_int_var: example description.\n"
- " :type tuple_int_var: tuple of tuple of int\n"
- )
- doctree = restructuredtext.parse(app, text)
-
- # :param list of str list_str_var:
- assert_node(doctree[3][1][0][0][1][0][0][0],
- ([addnodes.literal_strong, "list_str_var"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "list"],
- [addnodes.literal_emphasis, " of "],
- [pending_xref, addnodes.literal_emphasis, "str"],
- ")",
- " -- ",
- "example description."))
-
- # :param tuple of tuple of int tuple_int_var:
- assert_node(doctree[3][1][0][0][1][0][1][0],
- ([addnodes.literal_strong, "tuple_int_var"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "tuple"],
- [addnodes.literal_emphasis, " of "],
- [pending_xref, addnodes.literal_emphasis, "tuple"],
- [addnodes.literal_emphasis, " of "],
- [pending_xref, addnodes.literal_emphasis, "int"],
- ")",
- " -- ",
- "example description."))
-
-
-def test_info_field_list_napoleon_deliminator_or(app):
- text = (".. py:module:: example\n"
- ".. py:class:: Class\n"
- "\n"
- " :param bool_str_var: example description.\n"
- " :type bool_str_var: bool or str\n"
- " :param str_float_int_var: example description.\n"
- " :type str_float_int_var: str or float or int\n"
- )
- doctree = restructuredtext.parse(app, text)
-
- # :param bool or str bool_str_var:
- assert_node(doctree[3][1][0][0][1][0][0][0],
- ([addnodes.literal_strong, "bool_str_var"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "bool"],
- [addnodes.literal_emphasis, " or "],
- [pending_xref, addnodes.literal_emphasis, "str"],
- ")",
- " -- ",
- "example description."))
-
- # :param str or float or int str_float_int_var:
- assert_node(doctree[3][1][0][0][1][0][1][0],
- ([addnodes.literal_strong, "str_float_int_var"],
- " (",
- [pending_xref, addnodes.literal_emphasis, "str"],
- [addnodes.literal_emphasis, " or "],
- [pending_xref, addnodes.literal_emphasis, "float"],
- [addnodes.literal_emphasis, " or "],
- [pending_xref, addnodes.literal_emphasis, "int"],
- ")",
- " -- ",
- "example description."))
-
-
-def test_type_field(app):
- text = (".. py:data:: var1\n"
- " :type: .int\n"
- ".. py:data:: var2\n"
- " :type: ~builtins.int\n"
- ".. py:data:: var3\n"
- " :type: typing.Optional[typing.Tuple[int, typing.Any]]\n")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "var1"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "int"])])],
- [desc_content, ()])],
- addnodes.index,
- [desc, ([desc_signature, ([desc_name, "var2"],
- [desc_annotation, ([desc_sig_punctuation, ':'],
- desc_sig_space,
- [pending_xref, "int"])])],
- [desc_content, ()])],
- addnodes.index,
- [desc, ([desc_signature, ([desc_name, "var3"],
- [desc_annotation, ([desc_sig_punctuation, ":"],
- desc_sig_space,
- [pending_xref, "Optional"],
- [desc_sig_punctuation, "["],
- [pending_xref, "Tuple"],
- [desc_sig_punctuation, "["],
- [pending_xref, "int"],
- [desc_sig_punctuation, ","],
- desc_sig_space,
- [pending_xref, "Any"],
- [desc_sig_punctuation, "]"],
- [desc_sig_punctuation, "]"])])],
- [desc_content, ()])]))
- assert_node(doctree[1][0][1][2], pending_xref, reftarget='int', refspecific=True)
- assert_node(doctree[3][0][1][2], pending_xref, reftarget='builtins.int', refspecific=False)
- assert_node(doctree[5][0][1][2], pending_xref, reftarget='typing.Optional', refspecific=False)
- assert_node(doctree[5][0][1][4], pending_xref, reftarget='typing.Tuple', refspecific=False)
- assert_node(doctree[5][0][1][6], pending_xref, reftarget='int', refspecific=False)
- assert_node(doctree[5][0][1][9], pending_xref, reftarget='typing.Any', refspecific=False)
-
-
-@pytest.mark.sphinx(freshenv=True)
-def test_module_index(app):
- text = (".. py:module:: docutils\n"
- ".. py:module:: sphinx\n"
- ".. py:module:: sphinx.config\n"
- ".. py:module:: sphinx.builders\n"
- ".. py:module:: sphinx.builders.html\n"
- ".. py:module:: sphinx_intl\n")
- restructuredtext.parse(app, text)
- index = PythonModuleIndex(app.env.get_domain('py'))
- assert index.generate() == (
- [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
- ('s', [IndexEntry('sphinx', 1, 'index', 'module-sphinx', '', '', ''),
- IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx.builders', '', '', ''),
- IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', ''),
- IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', ''),
- IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])],
- False,
- )
-
-
-@pytest.mark.sphinx(freshenv=True)
-def test_module_index_submodule(app):
- text = ".. py:module:: sphinx.config\n"
- restructuredtext.parse(app, text)
- index = PythonModuleIndex(app.env.get_domain('py'))
- assert index.generate() == (
- [('s', [IndexEntry('sphinx', 1, '', '', '', '', ''),
- IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '')])],
- False,
- )
-
-
-@pytest.mark.sphinx(freshenv=True)
-def test_module_index_not_collapsed(app):
- text = (".. py:module:: docutils\n"
- ".. py:module:: sphinx\n")
- restructuredtext.parse(app, text)
- index = PythonModuleIndex(app.env.get_domain('py'))
- assert index.generate() == (
- [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
- ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', '')])],
- True,
- )
-
-
-@pytest.mark.sphinx(freshenv=True, confoverrides={'modindex_common_prefix': ['sphinx.']})
-def test_modindex_common_prefix(app):
- text = (".. py:module:: docutils\n"
- ".. py:module:: sphinx\n"
- ".. py:module:: sphinx.config\n"
- ".. py:module:: sphinx.builders\n"
- ".. py:module:: sphinx.builders.html\n"
- ".. py:module:: sphinx_intl\n")
- restructuredtext.parse(app, text)
- index = PythonModuleIndex(app.env.get_domain('py'))
- assert index.generate() == (
- [('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx.builders', '', '', ''),
- IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', '')]),
- ('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '')]),
- ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
- ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''),
- IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])],
- True,
- )
-
-
-def test_no_index_entry(app):
- text = (".. py:function:: f()\n"
- ".. py:function:: g()\n"
- " :no-index-entry:\n")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index, desc, addnodes.index, desc))
- assert_node(doctree[0], addnodes.index, entries=[('pair', 'built-in function; f()', 'f', '', None)])
- assert_node(doctree[2], addnodes.index, entries=[])
-
- text = (".. py:class:: f\n"
- ".. py:class:: g\n"
- " :no-index-entry:\n")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (addnodes.index, desc, addnodes.index, desc))
- assert_node(doctree[0], addnodes.index, entries=[('single', 'f (built-in class)', 'f', '', None)])
- assert_node(doctree[2], addnodes.index, entries=[])
-
-
-@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names')
-def test_python_python_use_unqualified_type_names(app, status, warning):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
- '<span class="pre">Name</span></a></span>' in content)
- assert '<span class="n"><span class="pre">foo.Age</span></span>' in content
- assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" '
- 'title="foo.Name"><em>Name</em></a>) – blah blah</p>' in content)
- assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content
-
-
-@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names',
- confoverrides={'python_use_unqualified_type_names': False})
-def test_python_python_use_unqualified_type_names_disabled(app, status, warning):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
- '<span class="pre">foo.Name</span></a></span>' in content)
- assert '<span class="n"><span class="pre">foo.Age</span></span>' in content
- assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" '
- 'title="foo.Name"><em>foo.Name</em></a>) – blah blah</p>' in content)
- assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content
-
-
-@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning')
-def test_warn_missing_reference(app, status, warning):
- app.build()
- assert "index.rst:6: WARNING: undefined label: 'no-label'" in warning.getvalue()
- assert ("index.rst:6: WARNING: Failed to create a cross reference. "
- "A title or caption not found: 'existing-label'") in warning.getvalue()
-
-
-@pytest.mark.sphinx(confoverrides={'nitpicky': True})
-@pytest.mark.parametrize('include_options', [True, False])
-def test_signature_line_number(app, include_options):
- text = (".. py:function:: foo(bar : string)\n" +
- (" :no-index-entry:\n" if include_options else ""))
- 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 == 1
-
-
-@pytest.mark.sphinx('html', confoverrides={
- 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
-})
-def test_pyfunction_signature_with_python_maximum_signature_line_length_equal(app):
- text = ".. py:function:: hello(name: str) -> str"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"],
- )],
- desc_content,
- )],
- ))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
- [desc_sig_name, "name"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"],
- )])
- assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
-
-
-@pytest.mark.sphinx('html', confoverrides={
- 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
-})
-def test_pyfunction_signature_with_python_maximum_signature_line_length_force_single(app):
- text = (".. py:function:: hello(names: str) -> str\n"
- " :single-line-parameter-list:")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"],
- )],
- desc_content,
- )],
- ))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
- [desc_sig_name, "names"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"],
- )])
- assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
-
-
-@pytest.mark.sphinx('html', confoverrides={
- 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
-})
-def test_pyfunction_signature_with_python_maximum_signature_line_length_break(app):
- text = ".. py:function:: hello(names: str) -> str"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"],
- )],
- desc_content,
- )],
- ))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
- [desc_sig_name, "names"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"],
- )])
- assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True)
-
-
-@pytest.mark.sphinx('html', confoverrides={
- 'maximum_signature_line_length': len("hello(name: str) -> str"),
-})
-def test_pyfunction_signature_with_maximum_signature_line_length_equal(app):
- text = ".. py:function:: hello(name: str) -> str"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"],
- )],
- desc_content,
- )],
- ))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
- [desc_sig_name, "name"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"],
- )])
- assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
-
-
-@pytest.mark.sphinx('html', confoverrides={
- 'maximum_signature_line_length': len("hello(name: str) -> str"),
-})
-def test_pyfunction_signature_with_maximum_signature_line_length_force_single(app):
- text = (".. py:function:: hello(names: str) -> str\n"
- " :single-line-parameter-list:")
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"],
- )],
- desc_content,
- )],
- ))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
- [desc_sig_name, "names"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"],
- )])
- assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
-
-
-@pytest.mark.sphinx('html', confoverrides={
- 'maximum_signature_line_length': len("hello(name: str) -> str"),
-})
-def test_pyfunction_signature_with_maximum_signature_line_length_break(app):
- text = ".. py:function:: hello(names: str) -> str"
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"],
- )],
- desc_content,
- )],
- ))
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
- [desc_sig_name, "names"],
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"],
- )])
- assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True)
-
-
-@pytest.mark.sphinx(
- 'html',
- confoverrides={
- 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
- 'maximum_signature_line_length': 1,
- },
-)
-def test_python_maximum_signature_line_length_overrides_global(app):
- text = ".. py:function:: hello(name: str) -> str"
- doctree = restructuredtext.parse(app, text)
- expected_doctree = (addnodes.index,
- [desc, ([desc_signature, ([desc_name, "hello"],
- desc_parameterlist,
- [desc_returns, pending_xref, "str"])],
- desc_content)])
- assert_node(doctree, expected_doctree)
- assert_node(doctree[1], addnodes.desc, desctype="function",
- domain="py", objtype="function", no_index=False)
- signame_node = [desc_sig_name, "name"]
- expected_sig = [desc_parameterlist, desc_parameter, (signame_node,
- [desc_sig_punctuation, ":"],
- desc_sig_space,
- [nodes.inline, pending_xref, "str"])]
- assert_node(doctree[1][0][1], expected_sig)
- assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
-
-
-@pytest.mark.sphinx(
- 'html', testroot='domain-py-python_maximum_signature_line_length',
-)
-def test_domain_py_python_maximum_signature_line_length_in_html(app, status, warning):
- app.build()
- content = (app.outdir / 'index.html').read_text(encoding='utf8')
- expected_parameter_list_hello = """\
-
-<dl>
-<dd>\
-<em class="sig-param">\
-<span class="n"><span class="pre">name</span></span>\
-<span class="p"><span class="pre">:</span></span>\
-<span class="w"> </span>\
-<span class="n"><span class="pre">str</span></span>\
-</em>,\
-</dd>
-</dl>
-
-<span class="sig-paren">)</span> \
-<span class="sig-return">\
-<span class="sig-return-icon">&#x2192;</span> \
-<span class="sig-return-typehint"><span class="pre">str</span></span>\
-</span>\
-<a class="headerlink" href="#hello" title="Link to this definition">¶</a>\
-</dt>\
-"""
- assert expected_parameter_list_hello in content
-
- param_line_fmt = '<dd>{}</dd>\n'
- param_name_fmt = (
- '<em class="sig-param"><span class="n"><span class="pre">{}</span></span></em>'
- )
- optional_fmt = '<span class="optional">{}</span>'
-
- expected_a = param_line_fmt.format(
- optional_fmt.format("[") + param_name_fmt.format("a") + "," + optional_fmt.format("["),
- )
- assert expected_a in content
-
- expected_b = param_line_fmt.format(
- param_name_fmt.format("b") + "," + optional_fmt.format("]") + optional_fmt.format("]"),
- )
- assert expected_b in content
-
- expected_c = param_line_fmt.format(param_name_fmt.format("c") + ",")
- assert expected_c in content
-
- expected_d = param_line_fmt.format(param_name_fmt.format("d") + optional_fmt.format("[") + ",")
- assert expected_d in content
-
- expected_e = param_line_fmt.format(param_name_fmt.format("e") + ",")
- assert expected_e in content
-
- expected_f = param_line_fmt.format(param_name_fmt.format("f") + "," + optional_fmt.format("]"))
- assert expected_f in content
-
- expected_parameter_list_foo = """\
-
-<dl>
-{}{}{}{}{}{}</dl>
-
-<span class="sig-paren">)</span>\
-<a class="headerlink" href="#foo" title="Link to this definition">¶</a>\
-</dt>\
-""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f)
- assert expected_parameter_list_foo in content
-
-
-@pytest.mark.sphinx(
- 'text', testroot='domain-py-python_maximum_signature_line_length',
-)
-def test_domain_py_python_maximum_signature_line_length_in_text(app, status, warning):
- app.build()
- content = (app.outdir / 'index.txt').read_text(encoding='utf8')
- param_line_fmt = STDINDENT * " " + "{}\n"
-
- expected_parameter_list_hello = "(\n{}) -> str".format(param_line_fmt.format("name: str,"))
-
- assert expected_parameter_list_hello in content
-
- expected_a = param_line_fmt.format("[a,[")
- assert expected_a in content
-
- expected_b = param_line_fmt.format("b,]]")
- assert expected_b in content
-
- expected_c = param_line_fmt.format("c,")
- assert expected_c in content
-
- expected_d = param_line_fmt.format("d[,")
- assert expected_d in content
-
- expected_e = param_line_fmt.format("e,")
- assert expected_e in content
-
- expected_f = param_line_fmt.format("f,]")
- assert expected_f in content
-
- expected_parameter_list_foo = "(\n{}{}{}{}{}{})".format(
- expected_a, expected_b, expected_c, expected_d, expected_e, expected_f,
- )
- assert expected_parameter_list_foo in content
-
-
-def test_module_content_line_number(app):
- text = (".. py:module:: foo\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
-
-
-@pytest.mark.sphinx(freshenv=True, confoverrides={'python_display_short_literal_types': True})
-def test_short_literal_types(app):
- text = """\
-.. py:function:: literal_ints(x: Literal[1, 2, 3] = 1) -> None
-.. py:function:: literal_union(x: Union[Literal["a"], Literal["b"], Literal["c"]]) -> None
-"""
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, 'literal_ints'],
- [desc_parameterlist, (
- [desc_parameter, (
- [desc_sig_name, 'x'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, (
- [desc_sig_literal_number, '1'],
- desc_sig_space,
- [desc_sig_punctuation, '|'],
- desc_sig_space,
- [desc_sig_literal_number, '2'],
- desc_sig_space,
- [desc_sig_punctuation, '|'],
- desc_sig_space,
- [desc_sig_literal_number, '3'],
- )],
- desc_sig_space,
- [desc_sig_operator, '='],
- desc_sig_space,
- [nodes.inline, '1'],
- )],
- )],
- [desc_returns, pending_xref, 'None'],
- )],
- [desc_content, ()],
- )],
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, 'literal_union'],
- [desc_parameterlist, (
- [desc_parameter, (
- [desc_sig_name, 'x'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, (
- [desc_sig_literal_string, "'a'"],
- desc_sig_space,
- [desc_sig_punctuation, '|'],
- desc_sig_space,
- [desc_sig_literal_string, "'b'"],
- desc_sig_space,
- [desc_sig_punctuation, '|'],
- desc_sig_space,
- [desc_sig_literal_string, "'c'"],
- )],
- )],
- )],
- [desc_returns, pending_xref, 'None'],
- )],
- [desc_content, ()],
- )],
- ))
-
-
-def test_function_pep_695(app):
- text = """.. py:function:: func[\
- S,\
- T: int,\
- U: (int, str),\
- R: int | int,\
- A: int | Annotated[int, ctype("char")],\
- *V,\
- **P\
- ]
- """
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_name, 'func'],
- [desc_type_parameter_list, (
- [desc_type_parameter, ([desc_sig_name, 'S'])],
- [desc_type_parameter, (
- [desc_sig_name, 'T'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, ([pending_xref, 'int'])],
- )],
- [desc_type_parameter, (
- [desc_sig_name, 'U'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_punctuation, '('],
- [desc_sig_name, (
- [pending_xref, 'int'],
- [desc_sig_punctuation, ','],
- desc_sig_space,
- [pending_xref, 'str'],
- )],
- [desc_sig_punctuation, ')'],
- )],
- [desc_type_parameter, (
- [desc_sig_name, 'R'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, (
- [pending_xref, 'int'],
- desc_sig_space,
- [desc_sig_punctuation, '|'],
- desc_sig_space,
- [pending_xref, 'int'],
- )],
- )],
- [desc_type_parameter, (
- [desc_sig_name, 'A'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, ([pending_xref, 'int | Annotated[int, ctype("char")]'])],
- )],
- [desc_type_parameter, (
- [desc_sig_operator, '*'],
- [desc_sig_name, 'V'],
- )],
- [desc_type_parameter, (
- [desc_sig_operator, '**'],
- [desc_sig_name, 'P'],
- )],
- )],
- [desc_parameterlist, ()],
- )],
- [desc_content, ()],
- )],
- ))
-
-
-def test_class_def_pep_695(app):
- # Non-concrete unbound generics are allowed at runtime but type checkers
- # should fail (https://peps.python.org/pep-0695/#type-parameter-scopes)
- text = """.. py:class:: Class[S: Sequence[T], T, KT, VT](Dict[KT, VT])"""
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_annotation, ('class', desc_sig_space)],
- [desc_name, 'Class'],
- [desc_type_parameter_list, (
- [desc_type_parameter, (
- [desc_sig_name, 'S'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, (
- [pending_xref, 'Sequence'],
- [desc_sig_punctuation, '['],
- [pending_xref, 'T'],
- [desc_sig_punctuation, ']'],
- )],
- )],
- [desc_type_parameter, ([desc_sig_name, 'T'])],
- [desc_type_parameter, ([desc_sig_name, 'KT'])],
- [desc_type_parameter, ([desc_sig_name, 'VT'])],
- )],
- [desc_parameterlist, ([desc_parameter, 'Dict[KT, VT]'])],
- )],
- [desc_content, ()],
- )],
- ))
-
-
-def test_class_def_pep_696(app):
- # test default values for type variables without using PEP 696 AST parser
- text = """.. py:class:: Class[\
- T, KT, VT,\
- J: int,\
- K = list,\
- S: str = str,\
- L: (T, tuple[T, ...], collections.abc.Iterable[T]) = set[T],\
- Q: collections.abc.Mapping[KT, VT] = dict[KT, VT],\
- *V = *tuple[*Ts, bool],\
- **P = [int, Annotated[int, ValueRange(3, 10), ctype("char")]]\
- ](Other[T, KT, VT, J, S, L, Q, *V, **P])
- """
- doctree = restructuredtext.parse(app, text)
- assert_node(doctree, (
- addnodes.index,
- [desc, (
- [desc_signature, (
- [desc_annotation, ('class', desc_sig_space)],
- [desc_name, 'Class'],
- [desc_type_parameter_list, (
- [desc_type_parameter, ([desc_sig_name, 'T'])],
- [desc_type_parameter, ([desc_sig_name, 'KT'])],
- [desc_type_parameter, ([desc_sig_name, 'VT'])],
- # J: int
- [desc_type_parameter, (
- [desc_sig_name, 'J'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, ([pending_xref, 'int'])],
- )],
- # K = list
- [desc_type_parameter, (
- [desc_sig_name, 'K'],
- desc_sig_space,
- [desc_sig_operator, '='],
- desc_sig_space,
- [nodes.inline, 'list'],
- )],
- # S: str = str
- [desc_type_parameter, (
- [desc_sig_name, 'S'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, ([pending_xref, 'str'])],
- desc_sig_space,
- [desc_sig_operator, '='],
- desc_sig_space,
- [nodes.inline, 'str'],
- )],
- [desc_type_parameter, (
- [desc_sig_name, 'L'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_punctuation, '('],
- [desc_sig_name, (
- # T
- [pending_xref, 'T'],
- [desc_sig_punctuation, ','],
- desc_sig_space,
- # tuple[T, ...]
- [pending_xref, 'tuple'],
- [desc_sig_punctuation, '['],
- [pending_xref, 'T'],
- [desc_sig_punctuation, ','],
- desc_sig_space,
- [desc_sig_punctuation, '...'],
- [desc_sig_punctuation, ']'],
- [desc_sig_punctuation, ','],
- desc_sig_space,
- # collections.abc.Iterable[T]
- [pending_xref, 'collections.abc.Iterable'],
- [desc_sig_punctuation, '['],
- [pending_xref, 'T'],
- [desc_sig_punctuation, ']'],
- )],
- [desc_sig_punctuation, ')'],
- desc_sig_space,
- [desc_sig_operator, '='],
- desc_sig_space,
- [nodes.inline, 'set[T]'],
- )],
- [desc_type_parameter, (
- [desc_sig_name, 'Q'],
- [desc_sig_punctuation, ':'],
- desc_sig_space,
- [desc_sig_name, (
- [pending_xref, 'collections.abc.Mapping'],
- [desc_sig_punctuation, '['],
- [pending_xref, 'KT'],
- [desc_sig_punctuation, ','],
- desc_sig_space,
- [pending_xref, 'VT'],
- [desc_sig_punctuation, ']'],
- )],
- desc_sig_space,
- [desc_sig_operator, '='],
- desc_sig_space,
- [nodes.inline, 'dict[KT, VT]'],
- )],
- [desc_type_parameter, (
- [desc_sig_operator, '*'],
- [desc_sig_name, 'V'],
- desc_sig_space,
- [desc_sig_operator, '='],
- desc_sig_space,
- [nodes.inline, '*tuple[*Ts, bool]'],
- )],
- [desc_type_parameter, (
- [desc_sig_operator, '**'],
- [desc_sig_name, 'P'],
- desc_sig_space,
- [desc_sig_operator, '='],
- desc_sig_space,
- [nodes.inline, '[int, Annotated[int, ValueRange(3, 10), ctype("char")]]'],
- )],
- )],
- [desc_parameterlist, (
- [desc_parameter, 'Other[T, KT, VT, J, S, L, Q, *V, **P]'],
- )],
- )],
- [desc_content, ()],
- )],
- ))
-
-
-@pytest.mark.parametrize(('tp_list', 'tptext'), [
- ('[T:int]', '[T: int]'),
- ('[T:*Ts]', '[T: *Ts]'),
- ('[T:int|(*Ts)]', '[T: int | (*Ts)]'),
- ('[T:(*Ts)|int]', '[T: (*Ts) | int]'),
- ('[T:(int|(*Ts))]', '[T: (int | (*Ts))]'),
- ('[T:((*Ts)|int)]', '[T: ((*Ts) | int)]'),
- ('[T:Annotated[int,ctype("char")]]', '[T: Annotated[int, ctype("char")]]'),
-])
-def test_pep_695_and_pep_696_whitespaces_in_bound(app, tp_list, tptext):
- text = f'.. py:function:: f{tp_list}()'
- doctree = restructuredtext.parse(app, text)
- assert doctree.astext() == f'\n\nf{tptext}()\n\n'
-
-
-@pytest.mark.parametrize(('tp_list', 'tptext'), [
- ('[T:(int,str)]', '[T: (int, str)]'),
- ('[T:(int|str,*Ts)]', '[T: (int | str, *Ts)]'),
-])
-def test_pep_695_and_pep_696_whitespaces_in_constraints(app, tp_list, tptext):
- text = f'.. py:function:: f{tp_list}()'
- doctree = restructuredtext.parse(app, text)
- assert doctree.astext() == f'\n\nf{tptext}()\n\n'
-
-
-@pytest.mark.parametrize(('tp_list', 'tptext'), [
- ('[T=int]', '[T = int]'),
- ('[T:int=int]', '[T: int = int]'),
- ('[*V=*Ts]', '[*V = *Ts]'),
- ('[*V=(*Ts)]', '[*V = (*Ts)]'),
- ('[*V=*tuple[str,...]]', '[*V = *tuple[str, ...]]'),
- ('[*V=*tuple[*Ts,...]]', '[*V = *tuple[*Ts, ...]]'),
- ('[*V=*tuple[int,*Ts]]', '[*V = *tuple[int, *Ts]]'),
- ('[*V=*tuple[*Ts,int]]', '[*V = *tuple[*Ts, int]]'),
- ('[**P=[int,*Ts]]', '[**P = [int, *Ts]]'),
- ('[**P=[int, int*3]]', '[**P = [int, int * 3]]'),
- ('[**P=[int, *Ts*3]]', '[**P = [int, *Ts * 3]]'),
- ('[**P=[int,A[int,ctype("char")]]]', '[**P = [int, A[int, ctype("char")]]]'),
-])
-def test_pep_695_and_pep_696_whitespaces_in_default(app, tp_list, tptext):
- text = f'.. py:function:: f{tp_list}()'
- doctree = restructuredtext.parse(app, text)
- assert doctree.astext() == f'\n\nf{tptext}()\n\n'
diff --git a/tests/test_domains/__init__.py b/tests/test_domains/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_domains/__init__.py
diff --git a/tests/test_domain_c.py b/tests/test_domains/test_domain_c.py
index 6582a0c..a8a92cb 100644
--- a/tests/test_domain_c.py
+++ b/tests/test_domains/test_domain_c.py
@@ -19,17 +19,13 @@ from sphinx.addnodes import (
desc_signature_line,
pending_xref,
)
-from sphinx.domains.c import (
- DefinitionError,
- DefinitionParser,
- Symbol,
- _id_prefix,
- _macroKeywords,
- _max_id,
-)
+from sphinx.domains.c._ids import _id_prefix, _macroKeywords, _max_id
+from sphinx.domains.c._parser import DefinitionParser
+from sphinx.domains.c._symbol import Symbol
from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping
from sphinx.testing import restructuredtext
from sphinx.testing.util import assert_node
+from sphinx.util.cfamily import DefinitionError
from sphinx.writers.text import STDINDENT
@@ -66,7 +62,7 @@ def _check(name, input, idDict, output, key, asTextOutput):
ast = parse(name, inputActual)
res = str(ast)
if res != outputAst:
- print("")
+ print()
print("Input: ", input)
print("Result: ", res)
print("Expected: ", outputAst)
@@ -79,7 +75,7 @@ def _check(name, input, idDict, output, key, asTextOutput):
ast.describe_signature(signode, 'lastIsName', symbol, options={})
resAsText = parentNode.astext()
if resAsText != outputAsText:
- print("")
+ print()
print("Input: ", input)
print("astext(): ", resAsText)
print("Expected: ", outputAsText)
@@ -138,7 +134,7 @@ def test_domain_c_ast_expressions():
output = expr
res = str(ast)
if res != output:
- print("")
+ print()
print("Input: ", input)
print("Result: ", res)
print("Expected: ", output)
@@ -146,7 +142,7 @@ def test_domain_c_ast_expressions():
displayString = ast.get_display_string()
if res != displayString:
# note: if the expression contains an anon name then this will trigger a falsely
- print("")
+ print()
print("Input: ", expr)
print("Result: ", res)
print("Display: ", displayString)
@@ -658,14 +654,14 @@ def extract_role_links(app, filename):
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_domain_c_build(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "index")
assert len(ws) == 0
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_domain_c_build_namespace(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "namespace")
assert len(ws) == 0
t = (app.outdir / "namespace.html").read_text(encoding='utf8')
@@ -675,7 +671,7 @@ def test_domain_c_build_namespace(app, status, warning):
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_domain_c_build_anon_dup_decl(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "anon-dup-decl")
assert len(ws) == 2
assert "WARNING: c:identifier reference target not found: @a" in ws[0]
@@ -704,7 +700,7 @@ def test_domain_c_build_semicolon(app, warning):
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_domain_c_build_function_param_target(app, warning):
# the anchor for function parameters should be the function
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "function_param_target")
assert len(ws) == 0
entries = extract_role_links(app, "function_param_target.html")
@@ -716,14 +712,14 @@ def test_domain_c_build_function_param_target(app, warning):
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_domain_c_build_ns_lookup(app, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "ns_lookup")
assert len(ws) == 0
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_domain_c_build_field_role(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "field-role")
assert len(ws) == 0
@@ -753,7 +749,7 @@ def test_domain_c_build_intersphinx(tmp_path, app, status, warning):
.. c:type:: _type
.. c:function:: void _functionParam(int param)
-""" # noqa: F841
+""" # NoQA: F841
inv_file = tmp_path / 'inventory'
inv_file.write_bytes(b'''\
# Sphinx inventory version 2
@@ -773,7 +769,7 @@ _struct c:struct 1 index.html#c.$ -
_type c:type 1 index.html#c.$ -
_union c:union 1 index.html#c.$ -
_var c:member 1 index.html#c.$ -
-''')) # noqa: W291
+''')) # NoQA: W291
app.config.intersphinx_mapping = {
'https://localhost/intersphinx/c/': str(inv_file),
}
@@ -782,7 +778,7 @@ _var c:member 1 index.html#c.$ -
normalize_intersphinx_mapping(app, app.config)
load_mappings(app)
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "index")
assert len(ws) == 0
diff --git a/tests/test_domain_cpp.py b/tests/test_domains/test_domain_cpp.py
index dcc2b0f..abd0f82 100644
--- a/tests/test_domain_cpp.py
+++ b/tests/test_domains/test_domain_cpp.py
@@ -20,17 +20,13 @@ from sphinx.addnodes import (
desc_signature_line,
pending_xref,
)
-from sphinx.domains.cpp import (
- DefinitionError,
- DefinitionParser,
- NoOldIdError,
- Symbol,
- _id_prefix,
- _max_id,
-)
+from sphinx.domains.cpp._ids import _id_prefix, _max_id
+from sphinx.domains.cpp._parser import DefinitionParser
+from sphinx.domains.cpp._symbol import Symbol
from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping
from sphinx.testing import restructuredtext
from sphinx.testing.util import assert_node
+from sphinx.util.cfamily import DefinitionError, NoOldIdError
from sphinx.writers.text import STDINDENT
@@ -67,7 +63,7 @@ def _check(name, input, idDict, output, key, asTextOutput):
ast = parse(name, inputActual)
res = str(ast)
if res != outputAst:
- print("")
+ print()
print("Input: ", input)
print("Result: ", res)
print("Expected: ", outputAst)
@@ -80,7 +76,7 @@ def _check(name, input, idDict, output, key, asTextOutput):
ast.describe_signature(signode, 'lastIsName', symbol, options={})
resAsText = parentNode.astext()
if resAsText != outputAsText:
- print("")
+ print()
print("Input: ", input)
print("astext(): ", resAsText)
print("Expected: ", outputAsText)
@@ -129,7 +125,7 @@ def check(name, input, idDict, output=None, key=None, asTextOutput=None):
@pytest.mark.parametrize(('type_', 'id_v2'),
- sphinx.domains.cpp._id_fundamental_v2.items())
+ sphinx.domains.cpp._ids._id_fundamental_v2.items())
def test_domain_cpp_ast_fundamental_types(type_, id_v2):
# see https://en.cppreference.com/w/cpp/language/types
def make_id_v1():
@@ -184,14 +180,14 @@ def test_domain_cpp_ast_expressions():
ast = parser.parse_expression()
res = str(ast)
if res != expr:
- print("")
+ print()
print("Input: ", expr)
print("Result: ", res)
raise DefinitionError
displayString = ast.get_display_string()
if res != displayString:
# note: if the expression contains an anon name then this will trigger a falsely
- print("")
+ print()
print("Input: ", expr)
print("Result: ", res)
print("Display: ", displayString)
@@ -1116,7 +1112,7 @@ def filter_warnings(warning, file):
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_domain_cpp_build_multi_decl_lookup(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "lookup-key-overload")
assert len(ws) == 0
@@ -1126,7 +1122,7 @@ def test_domain_cpp_build_multi_decl_lookup(app, status, warning):
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_domain_cpp_build_warn_template_param_qualified_name(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "warn-template-param-qualified-name")
assert len(ws) == 2
assert "WARNING: cpp:type reference target not found: T::typeWarn" in ws[0]
@@ -1135,14 +1131,14 @@ def test_domain_cpp_build_warn_template_param_qualified_name(app, status, warnin
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_domain_cpp_build_backslash_ok_true(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "backslash")
assert len(ws) == 0
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_domain_cpp_build_semicolon(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "semicolon")
assert len(ws) == 0
@@ -1150,7 +1146,7 @@ def test_domain_cpp_build_semicolon(app, status, warning):
@pytest.mark.sphinx(testroot='domain-cpp',
confoverrides={'nitpicky': True, 'strip_signature_backslash': True})
def test_domain_cpp_build_backslash_ok_false(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "backslash")
assert len(ws) == 1
assert "WARNING: Parsing of expression failed. Using fallback parser." in ws[0]
@@ -1158,7 +1154,7 @@ def test_domain_cpp_build_backslash_ok_false(app, status, warning):
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_domain_cpp_build_anon_dup_decl(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "anon-dup-decl")
assert len(ws) == 2
assert "WARNING: cpp:identifier reference target not found: @a" in ws[0]
@@ -1167,7 +1163,7 @@ def test_domain_cpp_build_anon_dup_decl(app, status, warning):
@pytest.mark.sphinx(testroot='domain-cpp')
def test_domain_cpp_build_misuse_of_roles(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "roles-targets-ok")
assert len(ws) == 0
@@ -1215,7 +1211,7 @@ def test_domain_cpp_build_misuse_of_roles(app, status, warning):
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'add_function_parentheses': True})
def test_domain_cpp_build_with_add_function_parentheses_is_True(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
def check(spec, text, file):
pattern = '<li><p>%s<a .*?><code .*?><span .*?>%s</span></code></a></p></li>' % spec
@@ -1256,7 +1252,7 @@ def test_domain_cpp_build_with_add_function_parentheses_is_True(app, status, war
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'add_function_parentheses': False})
def test_domain_cpp_build_with_add_function_parentheses_is_False(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
def check(spec, text, file):
pattern = '<li><p>%s<a .*?><code .*?><span .*?>%s</span></code></a></p></li>' % spec
@@ -1297,7 +1293,7 @@ def test_domain_cpp_build_with_add_function_parentheses_is_False(app, status, wa
@pytest.mark.sphinx(testroot='domain-cpp')
def test_domain_cpp_build_xref_consistency(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
test = 'xref_consistency.html'
output = (app.outdir / test).read_text(encoding='utf8')
@@ -1361,11 +1357,24 @@ not found in `{test}`
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_domain_cpp_build_field_role(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "field-role")
assert len(ws) == 0
+@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
+def test_domain_cpp_build_operator_lookup(app, status, warning):
+ app.builder.build_all()
+ ws = filter_warnings(warning, "operator-lookup")
+ assert len(ws) == 5
+ # TODO: the first one should not happen
+ assert ":10: WARNING: cpp:identifier reference target not found: _lit" in ws[0]
+ assert ":18: WARNING: cpp:func reference target not found: int h" in ws[1]
+ assert ":19: WARNING: cpp:func reference target not found: int operator+(bool, bool)" in ws[2]
+ assert ":20: WARNING: cpp:func reference target not found: int operator\"\"_udl" in ws[3]
+ assert ":21: WARNING: cpp:func reference target not found: operator bool" in ws[4]
+
+
@pytest.mark.sphinx(testroot='domain-cpp-intersphinx', confoverrides={'nitpicky': True})
def test_domain_cpp_build_intersphinx(tmp_path, app, status, warning):
origSource = """\
@@ -1388,7 +1397,7 @@ def test_domain_cpp_build_intersphinx(tmp_path, app, status, warning):
.. cpp:enum-class:: _enumClass
.. cpp:function:: void _functionParam(int param)
.. cpp:function:: template<typename TParam> void _templateParam()
-""" # noqa: F841
+""" # NoQA: F841
inv_file = tmp_path / 'inventory'
inv_file.write_bytes(b'''\
# Sphinx inventory version 2
@@ -1415,7 +1424,7 @@ _templateParam::TParam cpp:templateParam 1 index.html#_CPPv4I0E14_templateParamv
_type cpp:type 1 index.html#_CPPv45$ -
_union cpp:union 1 index.html#_CPPv46$ -
_var cpp:member 1 index.html#_CPPv44$ -
-''')) # noqa: W291
+''')) # NoQA: W291
app.config.intersphinx_mapping = {
'https://localhost/intersphinx/cpp/': str(inv_file),
}
@@ -1424,7 +1433,7 @@ _var cpp:member 1 index.html#_CPPv44$ -
normalize_intersphinx_mapping(app, app.config)
load_mappings(app)
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(warning, "index")
assert len(ws) == 0
diff --git a/tests/test_domain_js.py b/tests/test_domains/test_domain_js.py
index bf4c3fe..995a440 100644
--- a/tests/test_domain_js.py
+++ b/tests/test_domains/test_domain_js.py
@@ -28,7 +28,7 @@ from sphinx.writers.text import STDINDENT
@pytest.mark.sphinx('dummy', testroot='domain-js')
def test_domain_js_xrefs(app, status, warning):
"""Domain objects have correct prefixes when looking up xrefs"""
- app.builder.build_all()
+ app.build(force_all=True)
def assert_refnode(node, mod_name, prefix, target, reftype=None,
domain='js'):
@@ -83,7 +83,7 @@ def test_domain_js_xrefs(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='domain-js')
def test_domain_js_objects(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
modules = app.env.domains['js'].data['modules']
objects = app.env.domains['js'].data['objects']
@@ -118,7 +118,7 @@ def test_domain_js_find_obj(app, status, warning):
return app.env.domains['js'].find_obj(
app.env, mod_name, prefix, obj_name, obj_type, searchmode)
- app.builder.build_all()
+ app.build(force_all=True)
assert (find_obj(None, None, 'NONEXISTANT', 'class') == (None, None))
assert (find_obj(None, None, 'NestedParentA', 'class') ==
diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py
new file mode 100644
index 0000000..e653c80
--- /dev/null
+++ b/tests/test_domains/test_domain_py.py
@@ -0,0 +1,1015 @@
+"""Tests the Python Domain"""
+
+from __future__ import annotations
+
+import re
+from unittest.mock import Mock
+
+import docutils.utils
+import pytest
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.addnodes import (
+ desc,
+ desc_annotation,
+ desc_content,
+ desc_name,
+ desc_parameter,
+ desc_parameterlist,
+ desc_returns,
+ desc_sig_keyword,
+ desc_sig_literal_number,
+ desc_sig_literal_string,
+ desc_sig_name,
+ desc_sig_operator,
+ desc_sig_punctuation,
+ desc_sig_space,
+ desc_signature,
+ desc_type_parameter,
+ desc_type_parameter_list,
+ pending_xref,
+)
+from sphinx.domains import IndexEntry
+from sphinx.domains.python import PythonDomain, PythonModuleIndex
+from sphinx.domains.python._annotations import _parse_annotation, _pseudo_parse_arglist
+from sphinx.domains.python._object import py_sig_re
+from sphinx.testing import restructuredtext
+from sphinx.testing.util import assert_node
+from sphinx.writers.text import STDINDENT
+
+
+def parse(sig):
+ m = py_sig_re.match(sig)
+ if m is None:
+ raise ValueError
+ name_prefix, tp_list, name, arglist, retann = m.groups()
+ signode = addnodes.desc_signature(sig, '')
+ _pseudo_parse_arglist(signode, arglist)
+ return signode.astext()
+
+
+def test_function_signatures():
+ rv = parse('func(a=1) -> int object')
+ assert rv == '(a=1)'
+
+ rv = parse('func(a=1, [b=None])')
+ assert rv == '(a=1, [b=None])'
+
+ rv = parse('func(a=1[, b=None])')
+ assert rv == '(a=1, [b=None])'
+
+ rv = parse("compile(source : string, filename, symbol='file')")
+ assert rv == "(source : string, filename, symbol='file')"
+
+ rv = parse('func(a=[], [b=None])')
+ assert rv == '(a=[], [b=None])'
+
+ rv = parse('func(a=[][, b=None])')
+ assert rv == '(a=[], [b=None])'
+
+
+@pytest.mark.sphinx('dummy', testroot='domain-py')
+def test_domain_py_xrefs(app, status, warning):
+ """Domain objects have correct prefixes when looking up xrefs"""
+ app.build(force_all=True)
+
+ def assert_refnode(node, module_name, class_name, target, reftype=None,
+ domain='py'):
+ attributes = {
+ 'refdomain': domain,
+ 'reftarget': target,
+ }
+ if reftype is not None:
+ attributes['reftype'] = reftype
+ if module_name is not False:
+ attributes['py:module'] = module_name
+ if class_name is not False:
+ attributes['py:class'] = class_name
+ assert_node(node, **attributes)
+
+ doctree = app.env.get_doctree('roles')
+ refnodes = list(doctree.findall(pending_xref))
+ assert_refnode(refnodes[0], None, None, 'TopLevel', 'class')
+ assert_refnode(refnodes[1], None, None, 'top_level', 'meth')
+ assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth')
+ assert_refnode(refnodes[3], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth')
+ assert_refnode(refnodes[4], None, 'NestedParentA', 'child_2', 'meth')
+ assert_refnode(refnodes[5], False, 'NestedParentA', 'any_child', domain='')
+ assert_refnode(refnodes[6], None, 'NestedParentA', 'NestedChildA', 'class')
+ assert_refnode(refnodes[7], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth')
+ assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA',
+ 'NestedParentA.child_1', 'meth')
+ assert_refnode(refnodes[9], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth')
+ assert_refnode(refnodes[10], None, 'NestedParentB', 'child_1', 'meth')
+ assert_refnode(refnodes[11], None, 'NestedParentB', 'NestedParentB', 'class')
+ assert_refnode(refnodes[12], None, None, 'NestedParentA.NestedChildA', 'class')
+ assert len(refnodes) == 13
+
+ doctree = app.env.get_doctree('module')
+ refnodes = list(doctree.findall(pending_xref))
+ assert_refnode(refnodes[0], 'module_a.submodule', None,
+ 'ModTopLevel', 'class')
+ assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel',
+ 'mod_child_1', 'meth')
+ assert_refnode(refnodes[2], 'module_a.submodule', 'ModTopLevel',
+ 'ModTopLevel.mod_child_1', 'meth')
+ assert_refnode(refnodes[3], 'module_a.submodule', 'ModTopLevel',
+ 'mod_child_2', 'meth')
+ assert_refnode(refnodes[4], 'module_a.submodule', 'ModTopLevel',
+ 'module_a.submodule.ModTopLevel.mod_child_1', 'meth')
+ assert_refnode(refnodes[5], 'module_a.submodule', 'ModTopLevel',
+ 'prop', 'attr')
+ assert_refnode(refnodes[6], 'module_a.submodule', 'ModTopLevel',
+ 'prop', 'meth')
+ assert_refnode(refnodes[7], 'module_b.submodule', None,
+ 'ModTopLevel', 'class')
+ assert_refnode(refnodes[8], 'module_b.submodule', 'ModTopLevel',
+ 'ModNoModule', 'class')
+ assert_refnode(refnodes[9], False, False, 'int', 'class')
+ assert_refnode(refnodes[10], False, False, 'tuple', 'class')
+ assert_refnode(refnodes[11], False, False, 'str', 'class')
+ assert_refnode(refnodes[12], False, False, 'float', 'class')
+ assert_refnode(refnodes[13], False, False, 'list', 'class')
+ assert_refnode(refnodes[14], False, False, 'ModTopLevel', 'class')
+ assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std')
+ assert_refnode(refnodes[16], False, False, 'typing.Literal', 'obj', domain='py')
+ assert_refnode(refnodes[17], False, False, 'typing.Literal', 'obj', domain='py')
+ assert len(refnodes) == 18
+
+ doctree = app.env.get_doctree('module_option')
+ refnodes = list(doctree.findall(pending_xref))
+ print(refnodes)
+ print(refnodes[0])
+ print(refnodes[1])
+ assert_refnode(refnodes[0], 'test.extra', 'B', 'foo', 'meth')
+ assert_refnode(refnodes[1], 'test.extra', 'B', 'foo', 'meth')
+ assert len(refnodes) == 2
+
+
+@pytest.mark.sphinx('html', testroot='domain-py')
+def test_domain_py_xrefs_abbreviations(app, status, warning):
+ app.build(force_all=True)
+
+ content = (app.outdir / 'abbr.html').read_text(encoding='utf8')
+ assert re.search(r'normal: <a .* href="module.html#module_a.submodule.ModTopLevel.'
+ r'mod_child_1" .*><.*>module_a.submodule.ModTopLevel.mod_child_1\(\)'
+ r'<.*></a>',
+ content)
+ assert re.search(r'relative: <a .* href="module.html#module_a.submodule.ModTopLevel.'
+ r'mod_child_1" .*><.*>ModTopLevel.mod_child_1\(\)<.*></a>',
+ content)
+ assert re.search(r'short name: <a .* href="module.html#module_a.submodule.ModTopLevel.'
+ r'mod_child_1" .*><.*>mod_child_1\(\)<.*></a>',
+ content)
+ assert re.search(r'relative \+ short name: <a .* href="module.html#module_a.submodule.'
+ r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>',
+ content)
+ assert re.search(r'short name \+ relative: <a .* href="module.html#module_a.submodule.'
+ r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>',
+ content)
+
+
+@pytest.mark.sphinx('dummy', testroot='domain-py')
+def test_domain_py_objects(app, status, warning):
+ app.build(force_all=True)
+
+ modules = app.env.domains['py'].data['modules']
+ objects = app.env.domains['py'].data['objects']
+
+ assert 'module_a.submodule' in modules
+ assert 'module_a.submodule' in objects
+ assert 'module_b.submodule' in modules
+ assert 'module_b.submodule' in objects
+
+ assert objects['module_a.submodule.ModTopLevel'][2] == 'class'
+ assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method'
+ assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method'
+ assert 'ModTopLevel.ModNoModule' not in objects
+ assert objects['ModNoModule'][2] == 'class'
+ assert objects['module_b.submodule.ModTopLevel'][2] == 'class'
+
+ assert objects['TopLevel'][2] == 'class'
+ assert objects['top_level'][2] == 'method'
+ assert objects['NestedParentA'][2] == 'class'
+ assert objects['NestedParentA.child_1'][2] == 'method'
+ assert objects['NestedParentA.any_child'][2] == 'method'
+ assert objects['NestedParentA.NestedChildA'][2] == 'class'
+ assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'method'
+ assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'method'
+ assert objects['NestedParentA.child_2'][2] == 'method'
+ assert objects['NestedParentB'][2] == 'class'
+ assert objects['NestedParentB.child_1'][2] == 'method'
+
+
+@pytest.mark.sphinx('html', testroot='domain-py')
+def test_resolve_xref_for_properties(app, status, warning):
+ app.build(force_all=True)
+
+ content = (app.outdir / 'module.html').read_text(encoding='utf8')
+ assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"'
+ ' title="module_a.submodule.ModTopLevel.prop">'
+ '<code class="xref py py-attr docutils literal notranslate"><span class="pre">'
+ 'prop</span> <span class="pre">attribute</span></code></a>' in content)
+ assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"'
+ ' title="module_a.submodule.ModTopLevel.prop">'
+ '<code class="xref py py-meth docutils literal notranslate"><span class="pre">'
+ 'prop</span> <span class="pre">method</span></code></a>' in content)
+ assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"'
+ ' title="module_a.submodule.ModTopLevel.prop">'
+ '<code class="xref py py-attr docutils literal notranslate"><span class="pre">'
+ 'prop</span> <span class="pre">attribute</span></code></a>' in content)
+
+
+@pytest.mark.sphinx('dummy', testroot='domain-py')
+def test_domain_py_find_obj(app, status, warning):
+
+ def find_obj(modname, prefix, obj_name, obj_type, searchmode=0):
+ return app.env.domains['py'].find_obj(
+ app.env, modname, prefix, obj_name, obj_type, searchmode)
+
+ app.build(force_all=True)
+
+ assert (find_obj(None, None, 'NONEXISTANT', 'class') == [])
+ assert (find_obj(None, None, 'NestedParentA', 'class') ==
+ [('NestedParentA', ('roles', 'NestedParentA', 'class', False))])
+ assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
+ [('NestedParentA.NestedChildA',
+ ('roles', 'NestedParentA.NestedChildA', 'class', False))])
+ assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') ==
+ [('NestedParentA.NestedChildA',
+ ('roles', 'NestedParentA.NestedChildA', 'class', False))])
+ assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') ==
+ [('NestedParentA.NestedChildA.subchild_1',
+ ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
+ assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') ==
+ [('NestedParentA.NestedChildA.subchild_1',
+ ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
+ assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') ==
+ [('NestedParentA.NestedChildA.subchild_1',
+ ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
+
+
+def test_get_full_qualified_name():
+ env = Mock(domaindata={})
+ domain = PythonDomain(env)
+
+ # non-python references
+ node = nodes.reference()
+ assert domain.get_full_qualified_name(node) is None
+
+ # simple reference
+ node = nodes.reference(reftarget='func')
+ assert domain.get_full_qualified_name(node) == 'func'
+
+ # with py:module context
+ kwargs = {'py:module': 'module1'}
+ node = nodes.reference(reftarget='func', **kwargs)
+ assert domain.get_full_qualified_name(node) == 'module1.func'
+
+ # with py:class context
+ kwargs = {'py:class': 'Class'}
+ node = nodes.reference(reftarget='func', **kwargs)
+ assert domain.get_full_qualified_name(node) == 'Class.func'
+
+ # with both py:module and py:class context
+ kwargs = {'py:module': 'module1', 'py:class': 'Class'}
+ node = nodes.reference(reftarget='func', **kwargs)
+ assert domain.get_full_qualified_name(node) == 'module1.Class.func'
+
+
+def test_parse_annotation(app):
+ doctree = _parse_annotation("int", app.env)
+ assert_node(doctree, ([pending_xref, "int"],))
+ assert_node(doctree[0], pending_xref, refdomain="py", reftype="class", reftarget="int")
+
+ doctree = _parse_annotation("List[int]", app.env)
+ assert_node(doctree, ([pending_xref, "List"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "int"],
+ [desc_sig_punctuation, "]"]))
+
+ doctree = _parse_annotation("Tuple[int, int]", app.env)
+ assert_node(doctree, ([pending_xref, "Tuple"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "int"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [pending_xref, "int"],
+ [desc_sig_punctuation, "]"]))
+
+ doctree = _parse_annotation("Tuple[()]", app.env)
+ assert_node(doctree, ([pending_xref, "Tuple"],
+ [desc_sig_punctuation, "["],
+ [desc_sig_punctuation, "("],
+ [desc_sig_punctuation, ")"],
+ [desc_sig_punctuation, "]"]))
+
+ doctree = _parse_annotation("Tuple[int, ...]", app.env)
+ assert_node(doctree, ([pending_xref, "Tuple"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "int"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [desc_sig_punctuation, "..."],
+ [desc_sig_punctuation, "]"]))
+
+ doctree = _parse_annotation("Callable[[int, int], int]", app.env)
+ assert_node(doctree, ([pending_xref, "Callable"],
+ [desc_sig_punctuation, "["],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "int"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [pending_xref, "int"],
+ [desc_sig_punctuation, "]"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [pending_xref, "int"],
+ [desc_sig_punctuation, "]"]))
+
+ doctree = _parse_annotation("Callable[[], int]", app.env)
+ assert_node(doctree, ([pending_xref, "Callable"],
+ [desc_sig_punctuation, "["],
+ [desc_sig_punctuation, "["],
+ [desc_sig_punctuation, "]"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [pending_xref, "int"],
+ [desc_sig_punctuation, "]"]))
+
+ doctree = _parse_annotation("List[None]", app.env)
+ assert_node(doctree, ([pending_xref, "List"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "None"],
+ [desc_sig_punctuation, "]"]))
+
+ # None type makes an object-reference (not a class reference)
+ doctree = _parse_annotation("None", app.env)
+ assert_node(doctree, ([pending_xref, "None"],))
+ assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="None")
+
+ # Literal type makes an object-reference (not a class reference)
+ doctree = _parse_annotation("typing.Literal['a', 'b']", app.env)
+ assert_node(doctree, ([pending_xref, "Literal"],
+ [desc_sig_punctuation, "["],
+ [desc_sig_literal_string, "'a'"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [desc_sig_literal_string, "'b'"],
+ [desc_sig_punctuation, "]"]))
+ assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Literal")
+
+
+def test_parse_annotation_suppress(app):
+ doctree = _parse_annotation("~typing.Dict[str, str]", app.env)
+ assert_node(doctree, ([pending_xref, "Dict"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "str"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [pending_xref, "str"],
+ [desc_sig_punctuation, "]"]))
+ assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Dict")
+
+
+def test_parse_annotation_Literal(app):
+ doctree = _parse_annotation("Literal[True, False]", app.env)
+ assert_node(doctree, ([pending_xref, "Literal"],
+ [desc_sig_punctuation, "["],
+ [desc_sig_keyword, "True"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [desc_sig_keyword, "False"],
+ [desc_sig_punctuation, "]"]))
+
+ doctree = _parse_annotation("typing.Literal[0, 1, 'abc']", app.env)
+ assert_node(doctree, ([pending_xref, "Literal"],
+ [desc_sig_punctuation, "["],
+ [desc_sig_literal_number, "0"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [desc_sig_literal_number, "1"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [desc_sig_literal_string, "'abc'"],
+ [desc_sig_punctuation, "]"]))
+
+
+@pytest.mark.sphinx(freshenv=True)
+def test_module_index(app):
+ text = (".. py:module:: docutils\n"
+ ".. py:module:: sphinx\n"
+ ".. py:module:: sphinx.config\n"
+ ".. py:module:: sphinx.builders\n"
+ ".. py:module:: sphinx.builders.html\n"
+ ".. py:module:: sphinx_intl\n")
+ restructuredtext.parse(app, text)
+ index = PythonModuleIndex(app.env.get_domain('py'))
+ assert index.generate() == (
+ [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
+ ('s', [IndexEntry('sphinx', 1, 'index', 'module-sphinx', '', '', ''),
+ IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx.builders', '', '', ''),
+ IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', ''),
+ IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', ''),
+ IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])],
+ False,
+ )
+
+
+@pytest.mark.sphinx(freshenv=True)
+def test_module_index_submodule(app):
+ text = ".. py:module:: sphinx.config\n"
+ restructuredtext.parse(app, text)
+ index = PythonModuleIndex(app.env.get_domain('py'))
+ assert index.generate() == (
+ [('s', [IndexEntry('sphinx', 1, '', '', '', '', ''),
+ IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '')])],
+ False,
+ )
+
+
+@pytest.mark.sphinx(freshenv=True)
+def test_module_index_not_collapsed(app):
+ text = (".. py:module:: docutils\n"
+ ".. py:module:: sphinx\n")
+ restructuredtext.parse(app, text)
+ index = PythonModuleIndex(app.env.get_domain('py'))
+ assert index.generate() == (
+ [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
+ ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', '')])],
+ True,
+ )
+
+
+@pytest.mark.sphinx(freshenv=True, confoverrides={'modindex_common_prefix': ['sphinx.']})
+def test_modindex_common_prefix(app):
+ text = (".. py:module:: docutils\n"
+ ".. py:module:: sphinx\n"
+ ".. py:module:: sphinx.config\n"
+ ".. py:module:: sphinx.builders\n"
+ ".. py:module:: sphinx.builders.html\n"
+ ".. py:module:: sphinx_intl\n")
+ restructuredtext.parse(app, text)
+ index = PythonModuleIndex(app.env.get_domain('py'))
+ assert index.generate() == (
+ [('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx.builders', '', '', ''),
+ IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', '')]),
+ ('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '')]),
+ ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
+ ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''),
+ IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])],
+ True,
+ )
+
+
+def test_no_index_entry(app):
+ text = (".. py:function:: f()\n"
+ ".. py:function:: g()\n"
+ " :no-index-entry:\n")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index, desc, addnodes.index, desc))
+ assert_node(doctree[0], addnodes.index, entries=[('pair', 'built-in function; f()', 'f', '', None)])
+ assert_node(doctree[2], addnodes.index, entries=[])
+
+ text = (".. py:class:: f\n"
+ ".. py:class:: g\n"
+ " :no-index-entry:\n")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index, desc, addnodes.index, desc))
+ assert_node(doctree[0], addnodes.index, entries=[('single', 'f (built-in class)', 'f', '', None)])
+ assert_node(doctree[2], addnodes.index, entries=[])
+
+
+@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names')
+def test_python_python_use_unqualified_type_names(app, status, warning):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
+ '<span class="pre">Name</span></a></span>' in content)
+ assert '<span class="n"><span class="pre">foo.Age</span></span>' in content
+ assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" '
+ 'title="foo.Name"><em>Name</em></a>) – blah blah</p>' in content)
+ assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content
+
+
+@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names',
+ confoverrides={'python_use_unqualified_type_names': False})
+def test_python_python_use_unqualified_type_names_disabled(app, status, warning):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
+ '<span class="pre">foo.Name</span></a></span>' in content)
+ assert '<span class="n"><span class="pre">foo.Age</span></span>' in content
+ assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" '
+ 'title="foo.Name"><em>foo.Name</em></a>) – blah blah</p>' in content)
+ assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content
+
+
+@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning')
+def test_warn_missing_reference(app, status, warning):
+ app.build()
+ assert "index.rst:6: WARNING: undefined label: 'no-label'" in warning.getvalue()
+ assert ("index.rst:6: WARNING: Failed to create a cross reference. "
+ "A title or caption not found: 'existing-label'") in warning.getvalue()
+
+
+@pytest.mark.sphinx(confoverrides={'nitpicky': True})
+@pytest.mark.parametrize('include_options', [True, False])
+def test_signature_line_number(app, include_options):
+ text = (".. py:function:: foo(bar : string)\n" +
+ (" :no-index-entry:\n" if include_options else ""))
+ 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 == 1
+
+
+@pytest.mark.sphinx(
+ 'html',
+ confoverrides={
+ 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
+ 'maximum_signature_line_length': 1,
+ },
+)
+def test_python_maximum_signature_line_length_overrides_global(app):
+ text = ".. py:function:: hello(name: str) -> str"
+ doctree = restructuredtext.parse(app, text)
+ expected_doctree = (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"])],
+ desc_content)])
+ assert_node(doctree, expected_doctree)
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ signame_node = [desc_sig_name, "name"]
+ expected_sig = [desc_parameterlist, desc_parameter, (signame_node,
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"])]
+ assert_node(doctree[1][0][1], expected_sig)
+ assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
+
+
+@pytest.mark.sphinx(
+ 'html', testroot='domain-py-python_maximum_signature_line_length',
+)
+def test_domain_py_python_maximum_signature_line_length_in_html(app, status, warning):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ expected_parameter_list_hello = """\
+
+<dl>
+<dd>\
+<em class="sig-param">\
+<span class="n"><span class="pre">name</span></span>\
+<span class="p"><span class="pre">:</span></span>\
+<span class="w"> </span>\
+<span class="n"><span class="pre">str</span></span>\
+</em>,\
+</dd>
+</dl>
+
+<span class="sig-paren">)</span> \
+<span class="sig-return">\
+<span class="sig-return-icon">&#x2192;</span> \
+<span class="sig-return-typehint"><span class="pre">str</span></span>\
+</span>\
+<a class="headerlink" href="#hello" title="Link to this definition">¶</a>\
+</dt>\
+"""
+ assert expected_parameter_list_hello in content
+
+ param_line_fmt = '<dd>{}</dd>\n'
+ param_name_fmt = (
+ '<em class="sig-param"><span class="n"><span class="pre">{}</span></span></em>'
+ )
+ optional_fmt = '<span class="optional">{}</span>'
+
+ expected_a = param_line_fmt.format(
+ optional_fmt.format("[") + param_name_fmt.format("a") + "," + optional_fmt.format("["),
+ )
+ assert expected_a in content
+
+ expected_b = param_line_fmt.format(
+ param_name_fmt.format("b") + "," + optional_fmt.format("]") + optional_fmt.format("]"),
+ )
+ assert expected_b in content
+
+ expected_c = param_line_fmt.format(param_name_fmt.format("c") + ",")
+ assert expected_c in content
+
+ expected_d = param_line_fmt.format(param_name_fmt.format("d") + optional_fmt.format("[") + ",")
+ assert expected_d in content
+
+ expected_e = param_line_fmt.format(param_name_fmt.format("e") + ",")
+ assert expected_e in content
+
+ expected_f = param_line_fmt.format(param_name_fmt.format("f") + "," + optional_fmt.format("]"))
+ assert expected_f in content
+
+ expected_parameter_list_foo = """\
+
+<dl>
+{}{}{}{}{}{}</dl>
+
+<span class="sig-paren">)</span>\
+<a class="headerlink" href="#foo" title="Link to this definition">¶</a>\
+</dt>\
+""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f)
+ assert expected_parameter_list_foo in content
+
+
+@pytest.mark.sphinx(
+ 'text', testroot='domain-py-python_maximum_signature_line_length',
+)
+def test_domain_py_python_maximum_signature_line_length_in_text(app, status, warning):
+ app.build()
+ content = (app.outdir / 'index.txt').read_text(encoding='utf8')
+ param_line_fmt = STDINDENT * " " + "{}\n"
+
+ expected_parameter_list_hello = "(\n{}) -> str".format(param_line_fmt.format("name: str,"))
+
+ assert expected_parameter_list_hello in content
+
+ expected_a = param_line_fmt.format("[a,[")
+ assert expected_a in content
+
+ expected_b = param_line_fmt.format("b,]]")
+ assert expected_b in content
+
+ expected_c = param_line_fmt.format("c,")
+ assert expected_c in content
+
+ expected_d = param_line_fmt.format("d[,")
+ assert expected_d in content
+
+ expected_e = param_line_fmt.format("e,")
+ assert expected_e in content
+
+ expected_f = param_line_fmt.format("f,]")
+ assert expected_f in content
+
+ expected_parameter_list_foo = "(\n{}{}{}{}{}{})".format(
+ expected_a, expected_b, expected_c, expected_d, expected_e, expected_f,
+ )
+ assert expected_parameter_list_foo in content
+
+
+def test_module_content_line_number(app):
+ text = (".. py:module:: foo\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
+
+
+@pytest.mark.sphinx(freshenv=True, confoverrides={'python_display_short_literal_types': True})
+def test_short_literal_types(app):
+ text = """\
+.. py:function:: literal_ints(x: Literal[1, 2, 3] = 1) -> None
+.. py:function:: literal_union(x: Union[Literal["a"], Literal["b"], Literal["c"]]) -> None
+"""
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, 'literal_ints'],
+ [desc_parameterlist, (
+ [desc_parameter, (
+ [desc_sig_name, 'x'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, (
+ [desc_sig_literal_number, '1'],
+ desc_sig_space,
+ [desc_sig_punctuation, '|'],
+ desc_sig_space,
+ [desc_sig_literal_number, '2'],
+ desc_sig_space,
+ [desc_sig_punctuation, '|'],
+ desc_sig_space,
+ [desc_sig_literal_number, '3'],
+ )],
+ desc_sig_space,
+ [desc_sig_operator, '='],
+ desc_sig_space,
+ [nodes.inline, '1'],
+ )],
+ )],
+ [desc_returns, pending_xref, 'None'],
+ )],
+ [desc_content, ()],
+ )],
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, 'literal_union'],
+ [desc_parameterlist, (
+ [desc_parameter, (
+ [desc_sig_name, 'x'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, (
+ [desc_sig_literal_string, "'a'"],
+ desc_sig_space,
+ [desc_sig_punctuation, '|'],
+ desc_sig_space,
+ [desc_sig_literal_string, "'b'"],
+ desc_sig_space,
+ [desc_sig_punctuation, '|'],
+ desc_sig_space,
+ [desc_sig_literal_string, "'c'"],
+ )],
+ )],
+ )],
+ [desc_returns, pending_xref, 'None'],
+ )],
+ [desc_content, ()],
+ )],
+ ))
+
+
+def test_function_pep_695(app):
+ text = """.. py:function:: func[\
+ S,\
+ T: int,\
+ U: (int, str),\
+ R: int | int,\
+ A: int | Annotated[int, ctype("char")],\
+ *V,\
+ **P\
+ ]
+ """
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, 'func'],
+ [desc_type_parameter_list, (
+ [desc_type_parameter, ([desc_sig_name, 'S'])],
+ [desc_type_parameter, (
+ [desc_sig_name, 'T'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, ([pending_xref, 'int'])],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_name, 'U'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_punctuation, '('],
+ [desc_sig_name, (
+ [pending_xref, 'int'],
+ [desc_sig_punctuation, ','],
+ desc_sig_space,
+ [pending_xref, 'str'],
+ )],
+ [desc_sig_punctuation, ')'],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_name, 'R'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, (
+ [pending_xref, 'int'],
+ desc_sig_space,
+ [desc_sig_punctuation, '|'],
+ desc_sig_space,
+ [pending_xref, 'int'],
+ )],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_name, 'A'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, ([pending_xref, 'int | Annotated[int, ctype("char")]'])],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_operator, '*'],
+ [desc_sig_name, 'V'],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_operator, '**'],
+ [desc_sig_name, 'P'],
+ )],
+ )],
+ [desc_parameterlist, ()],
+ )],
+ [desc_content, ()],
+ )],
+ ))
+
+
+def test_class_def_pep_695(app):
+ # Non-concrete unbound generics are allowed at runtime but type checkers
+ # should fail (https://peps.python.org/pep-0695/#type-parameter-scopes)
+ text = """.. py:class:: Class[S: Sequence[T], T, KT, VT](Dict[KT, VT])"""
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_annotation, ('class', desc_sig_space)],
+ [desc_name, 'Class'],
+ [desc_type_parameter_list, (
+ [desc_type_parameter, (
+ [desc_sig_name, 'S'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, (
+ [pending_xref, 'Sequence'],
+ [desc_sig_punctuation, '['],
+ [pending_xref, 'T'],
+ [desc_sig_punctuation, ']'],
+ )],
+ )],
+ [desc_type_parameter, ([desc_sig_name, 'T'])],
+ [desc_type_parameter, ([desc_sig_name, 'KT'])],
+ [desc_type_parameter, ([desc_sig_name, 'VT'])],
+ )],
+ [desc_parameterlist, ([desc_parameter, 'Dict[KT, VT]'])],
+ )],
+ [desc_content, ()],
+ )],
+ ))
+
+
+def test_class_def_pep_696(app):
+ # test default values for type variables without using PEP 696 AST parser
+ text = """.. py:class:: Class[\
+ T, KT, VT,\
+ J: int,\
+ K = list,\
+ S: str = str,\
+ L: (T, tuple[T, ...], collections.abc.Iterable[T]) = set[T],\
+ Q: collections.abc.Mapping[KT, VT] = dict[KT, VT],\
+ *V = *tuple[*Ts, bool],\
+ **P = [int, Annotated[int, ValueRange(3, 10), ctype("char")]]\
+ ](Other[T, KT, VT, J, S, L, Q, *V, **P])
+ """
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_annotation, ('class', desc_sig_space)],
+ [desc_name, 'Class'],
+ [desc_type_parameter_list, (
+ [desc_type_parameter, ([desc_sig_name, 'T'])],
+ [desc_type_parameter, ([desc_sig_name, 'KT'])],
+ [desc_type_parameter, ([desc_sig_name, 'VT'])],
+ # J: int
+ [desc_type_parameter, (
+ [desc_sig_name, 'J'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, ([pending_xref, 'int'])],
+ )],
+ # K = list
+ [desc_type_parameter, (
+ [desc_sig_name, 'K'],
+ desc_sig_space,
+ [desc_sig_operator, '='],
+ desc_sig_space,
+ [nodes.inline, 'list'],
+ )],
+ # S: str = str
+ [desc_type_parameter, (
+ [desc_sig_name, 'S'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, ([pending_xref, 'str'])],
+ desc_sig_space,
+ [desc_sig_operator, '='],
+ desc_sig_space,
+ [nodes.inline, 'str'],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_name, 'L'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_punctuation, '('],
+ [desc_sig_name, (
+ # T
+ [pending_xref, 'T'],
+ [desc_sig_punctuation, ','],
+ desc_sig_space,
+ # tuple[T, ...]
+ [pending_xref, 'tuple'],
+ [desc_sig_punctuation, '['],
+ [pending_xref, 'T'],
+ [desc_sig_punctuation, ','],
+ desc_sig_space,
+ [desc_sig_punctuation, '...'],
+ [desc_sig_punctuation, ']'],
+ [desc_sig_punctuation, ','],
+ desc_sig_space,
+ # collections.abc.Iterable[T]
+ [pending_xref, 'collections.abc.Iterable'],
+ [desc_sig_punctuation, '['],
+ [pending_xref, 'T'],
+ [desc_sig_punctuation, ']'],
+ )],
+ [desc_sig_punctuation, ')'],
+ desc_sig_space,
+ [desc_sig_operator, '='],
+ desc_sig_space,
+ [nodes.inline, 'set[T]'],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_name, 'Q'],
+ [desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [desc_sig_name, (
+ [pending_xref, 'collections.abc.Mapping'],
+ [desc_sig_punctuation, '['],
+ [pending_xref, 'KT'],
+ [desc_sig_punctuation, ','],
+ desc_sig_space,
+ [pending_xref, 'VT'],
+ [desc_sig_punctuation, ']'],
+ )],
+ desc_sig_space,
+ [desc_sig_operator, '='],
+ desc_sig_space,
+ [nodes.inline, 'dict[KT, VT]'],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_operator, '*'],
+ [desc_sig_name, 'V'],
+ desc_sig_space,
+ [desc_sig_operator, '='],
+ desc_sig_space,
+ [nodes.inline, '*tuple[*Ts, bool]'],
+ )],
+ [desc_type_parameter, (
+ [desc_sig_operator, '**'],
+ [desc_sig_name, 'P'],
+ desc_sig_space,
+ [desc_sig_operator, '='],
+ desc_sig_space,
+ [nodes.inline, '[int, Annotated[int, ValueRange(3, 10), ctype("char")]]'],
+ )],
+ )],
+ [desc_parameterlist, (
+ [desc_parameter, 'Other[T, KT, VT, J, S, L, Q, *V, **P]'],
+ )],
+ )],
+ [desc_content, ()],
+ )],
+ ))
+
+
+@pytest.mark.parametrize(('tp_list', 'tptext'), [
+ ('[T:int]', '[T: int]'),
+ ('[T:*Ts]', '[T: *Ts]'),
+ ('[T:int|(*Ts)]', '[T: int | (*Ts)]'),
+ ('[T:(*Ts)|int]', '[T: (*Ts) | int]'),
+ ('[T:(int|(*Ts))]', '[T: (int | (*Ts))]'),
+ ('[T:((*Ts)|int)]', '[T: ((*Ts) | int)]'),
+ ('[T:Annotated[int,ctype("char")]]', '[T: Annotated[int, ctype("char")]]'),
+])
+def test_pep_695_and_pep_696_whitespaces_in_bound(app, tp_list, tptext):
+ text = f'.. py:function:: f{tp_list}()'
+ doctree = restructuredtext.parse(app, text)
+ assert doctree.astext() == f'\n\nf{tptext}()\n\n'
+
+
+@pytest.mark.parametrize(('tp_list', 'tptext'), [
+ ('[T:(int,str)]', '[T: (int, str)]'),
+ ('[T:(int|str,*Ts)]', '[T: (int | str, *Ts)]'),
+])
+def test_pep_695_and_pep_696_whitespaces_in_constraints(app, tp_list, tptext):
+ text = f'.. py:function:: f{tp_list}()'
+ doctree = restructuredtext.parse(app, text)
+ assert doctree.astext() == f'\n\nf{tptext}()\n\n'
+
+
+@pytest.mark.parametrize(('tp_list', 'tptext'), [
+ ('[T=int]', '[T = int]'),
+ ('[T:int=int]', '[T: int = int]'),
+ ('[*V=*Ts]', '[*V = *Ts]'),
+ ('[*V=(*Ts)]', '[*V = (*Ts)]'),
+ ('[*V=*tuple[str,...]]', '[*V = *tuple[str, ...]]'),
+ ('[*V=*tuple[*Ts,...]]', '[*V = *tuple[*Ts, ...]]'),
+ ('[*V=*tuple[int,*Ts]]', '[*V = *tuple[int, *Ts]]'),
+ ('[*V=*tuple[*Ts,int]]', '[*V = *tuple[*Ts, int]]'),
+ ('[**P=[int,*Ts]]', '[**P = [int, *Ts]]'),
+ ('[**P=[int, int*3]]', '[**P = [int, int * 3]]'),
+ ('[**P=[int, *Ts*3]]', '[**P = [int, *Ts * 3]]'),
+ ('[**P=[int,A[int,ctype("char")]]]', '[**P = [int, A[int, ctype("char")]]]'),
+])
+def test_pep_695_and_pep_696_whitespaces_in_default(app, tp_list, tptext):
+ text = f'.. py:function:: f{tp_list}()'
+ doctree = restructuredtext.parse(app, text)
+ assert doctree.astext() == f'\n\nf{tptext}()\n\n'
diff --git a/tests/test_domains/test_domain_py_canonical.py b/tests/test_domains/test_domain_py_canonical.py
new file mode 100644
index 0000000..3635cd1
--- /dev/null
+++ b/tests/test_domains/test_domain_py_canonical.py
@@ -0,0 +1,77 @@
+"""Tests the Python Domain"""
+
+from __future__ import annotations
+
+import pytest
+
+from sphinx import addnodes
+from sphinx.addnodes import (
+ desc,
+ desc_addname,
+ desc_annotation,
+ desc_content,
+ desc_name,
+ desc_sig_space,
+ desc_signature,
+)
+from sphinx.testing import restructuredtext
+from sphinx.testing.util import assert_node
+
+
+@pytest.mark.sphinx('html', testroot='domain-py', freshenv=True)
+def test_domain_py_canonical(app, status, warning):
+ app.build(force_all=True)
+
+ content = (app.outdir / 'canonical.html').read_text(encoding='utf8')
+ assert ('<a class="reference internal" href="#canonical.Foo" title="canonical.Foo">'
+ '<code class="xref py py-class docutils literal notranslate">'
+ '<span class="pre">Foo</span></code></a>' in content)
+ assert warning.getvalue() == ''
+
+
+def test_canonical(app):
+ text = (".. py:class:: io.StringIO\n"
+ " :canonical: _io.StringIO")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_addname, "io."],
+ [desc_name, "StringIO"])],
+ desc_content)]))
+ assert 'io.StringIO' in domain.objects
+ assert domain.objects['io.StringIO'] == ('index', 'io.StringIO', 'class', False)
+ assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', True)
+
+
+def test_canonical_definition_overrides(app, warning):
+ text = (".. py:class:: io.StringIO\n"
+ " :canonical: _io.StringIO\n"
+ ".. py:class:: _io.StringIO\n")
+ restructuredtext.parse(app, text)
+ assert warning.getvalue() == ""
+
+ domain = app.env.get_domain('py')
+ assert domain.objects['_io.StringIO'] == ('index', 'id0', 'class', False)
+
+
+def test_canonical_definition_skip(app, warning):
+ text = (".. py:class:: _io.StringIO\n"
+ ".. py:class:: io.StringIO\n"
+ " :canonical: _io.StringIO\n")
+
+ restructuredtext.parse(app, text)
+ assert warning.getvalue() == ""
+
+ domain = app.env.get_domain('py')
+ assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', False)
+
+
+def test_canonical_duplicated(app, warning):
+ text = (".. py:class:: mypackage.StringIO\n"
+ " :canonical: _io.StringIO\n"
+ ".. py:class:: io.StringIO\n"
+ " :canonical: _io.StringIO\n")
+
+ restructuredtext.parse(app, text)
+ assert warning.getvalue() != ""
diff --git a/tests/test_domains/test_domain_py_fields.py b/tests/test_domains/test_domain_py_fields.py
new file mode 100644
index 0000000..47c40f5
--- /dev/null
+++ b/tests/test_domains/test_domain_py_fields.py
@@ -0,0 +1,326 @@
+"""Tests the Python Domain"""
+
+from __future__ import annotations
+
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.addnodes import (
+ desc,
+ desc_addname,
+ desc_annotation,
+ desc_content,
+ desc_name,
+ desc_sig_punctuation,
+ desc_sig_space,
+ desc_signature,
+ pending_xref,
+)
+from sphinx.testing import restructuredtext
+from sphinx.testing.util import assert_node
+
+
+def test_info_field_list(app):
+ text = (".. py:module:: example\n"
+ ".. py:class:: Class\n"
+ "\n"
+ " :meta blah: this meta-field must not show up in the toc-tree\n"
+ " :param str name: blah blah\n"
+ " :meta another meta field:\n"
+ " :param age: blah blah\n"
+ " :type age: int\n"
+ " :param items: blah blah\n"
+ " :type items: Tuple[str, ...]\n"
+ " :param Dict[str, str] params: blah blah\n")
+ doctree = restructuredtext.parse(app, text)
+ print(doctree)
+
+ assert_node(doctree, (addnodes.index,
+ addnodes.index,
+ nodes.target,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_addname, "example."],
+ [desc_name, "Class"])],
+ [desc_content, nodes.field_list, nodes.field])]))
+ assert_node(doctree[3][1][0][0],
+ ([nodes.field_name, "Parameters"],
+ [nodes.field_body, nodes.bullet_list, ([nodes.list_item, nodes.paragraph],
+ [nodes.list_item, nodes.paragraph],
+ [nodes.list_item, nodes.paragraph],
+ [nodes.list_item, nodes.paragraph])]))
+
+ # :param str name:
+ assert_node(doctree[3][1][0][0][1][0][0][0],
+ ([addnodes.literal_strong, "name"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ ")",
+ " -- ",
+ "blah blah"))
+ assert_node(doctree[3][1][0][0][1][0][0][0][2], pending_xref,
+ refdomain="py", reftype="class", reftarget="str",
+ **{"py:module": "example", "py:class": "Class"})
+
+ # :param age: + :type age:
+ assert_node(doctree[3][1][0][0][1][0][1][0],
+ ([addnodes.literal_strong, "age"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "int"],
+ ")",
+ " -- ",
+ "blah blah"))
+ assert_node(doctree[3][1][0][0][1][0][1][0][2], pending_xref,
+ refdomain="py", reftype="class", reftarget="int",
+ **{"py:module": "example", "py:class": "Class"})
+
+ # :param items: + :type items:
+ assert_node(doctree[3][1][0][0][1][0][2][0],
+ ([addnodes.literal_strong, "items"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "Tuple"],
+ [addnodes.literal_emphasis, "["],
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ [addnodes.literal_emphasis, ", "],
+ [addnodes.literal_emphasis, "..."],
+ [addnodes.literal_emphasis, "]"],
+ ")",
+ " -- ",
+ "blah blah"))
+ assert_node(doctree[3][1][0][0][1][0][2][0][2], pending_xref,
+ refdomain="py", reftype="class", reftarget="Tuple",
+ **{"py:module": "example", "py:class": "Class"})
+ assert_node(doctree[3][1][0][0][1][0][2][0][4], pending_xref,
+ refdomain="py", reftype="class", reftarget="str",
+ **{"py:module": "example", "py:class": "Class"})
+
+ # :param Dict[str, str] params:
+ assert_node(doctree[3][1][0][0][1][0][3][0],
+ ([addnodes.literal_strong, "params"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "Dict"],
+ [addnodes.literal_emphasis, "["],
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ [addnodes.literal_emphasis, ", "],
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ [addnodes.literal_emphasis, "]"],
+ ")",
+ " -- ",
+ "blah blah"))
+ assert_node(doctree[3][1][0][0][1][0][3][0][2], pending_xref,
+ refdomain="py", reftype="class", reftarget="Dict",
+ **{"py:module": "example", "py:class": "Class"})
+ assert_node(doctree[3][1][0][0][1][0][3][0][4], pending_xref,
+ refdomain="py", reftype="class", reftarget="str",
+ **{"py:module": "example", "py:class": "Class"})
+ assert_node(doctree[3][1][0][0][1][0][3][0][6], pending_xref,
+ refdomain="py", reftype="class", reftarget="str",
+ **{"py:module": "example", "py:class": "Class"})
+
+
+def test_info_field_list_piped_type(app):
+ text = (".. py:module:: example\n"
+ ".. py:class:: Class\n"
+ "\n"
+ " :param age: blah blah\n"
+ " :type age: int | str\n")
+ doctree = restructuredtext.parse(app, text)
+
+ assert_node(doctree,
+ (addnodes.index,
+ addnodes.index,
+ nodes.target,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_addname, "example."],
+ [desc_name, "Class"])],
+ [desc_content, nodes.field_list, nodes.field, (nodes.field_name,
+ nodes.field_body)])]))
+ assert_node(doctree[3][1][0][0][1],
+ ([nodes.paragraph, ([addnodes.literal_strong, "age"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "int"],
+ [addnodes.literal_emphasis, " | "],
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ ")",
+ " -- ",
+ "blah blah")],))
+ assert_node(doctree[3][1][0][0][1][0][2], pending_xref,
+ refdomain="py", reftype="class", reftarget="int",
+ **{"py:module": "example", "py:class": "Class"})
+ assert_node(doctree[3][1][0][0][1][0][4], pending_xref,
+ refdomain="py", reftype="class", reftarget="str",
+ **{"py:module": "example", "py:class": "Class"})
+
+
+def test_info_field_list_Literal(app):
+ text = (".. py:module:: example\n"
+ ".. py:class:: Class\n"
+ "\n"
+ " :param age: blah blah\n"
+ " :type age: Literal['foo', 'bar', 'baz']\n")
+ doctree = restructuredtext.parse(app, text)
+
+ assert_node(doctree,
+ (addnodes.index,
+ addnodes.index,
+ nodes.target,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_addname, "example."],
+ [desc_name, "Class"])],
+ [desc_content, nodes.field_list, nodes.field, (nodes.field_name,
+ nodes.field_body)])]))
+ assert_node(doctree[3][1][0][0][1],
+ ([nodes.paragraph, ([addnodes.literal_strong, "age"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "Literal"],
+ [addnodes.literal_emphasis, "["],
+ [addnodes.literal_emphasis, "'foo'"],
+ [addnodes.literal_emphasis, ", "],
+ [addnodes.literal_emphasis, "'bar'"],
+ [addnodes.literal_emphasis, ", "],
+ [addnodes.literal_emphasis, "'baz'"],
+ [addnodes.literal_emphasis, "]"],
+ ")",
+ " -- ",
+ "blah blah")],))
+ assert_node(doctree[3][1][0][0][1][0][2], pending_xref,
+ refdomain="py", reftype="class", reftarget="Literal",
+ **{"py:module": "example", "py:class": "Class"})
+
+
+def test_info_field_list_var(app):
+ text = (".. py:class:: Class\n"
+ "\n"
+ " :var int attr: blah blah\n")
+ doctree = restructuredtext.parse(app, text)
+
+ assert_node(doctree, (addnodes.index,
+ [desc, (desc_signature,
+ [desc_content, nodes.field_list, nodes.field])]))
+ assert_node(doctree[1][1][0][0], ([nodes.field_name, "Variables"],
+ [nodes.field_body, nodes.paragraph]))
+
+ # :var int attr:
+ assert_node(doctree[1][1][0][0][1][0],
+ ([addnodes.literal_strong, "attr"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "int"],
+ ")",
+ " -- ",
+ "blah blah"))
+ assert_node(doctree[1][1][0][0][1][0][2], pending_xref,
+ refdomain="py", reftype="class", reftarget="int", **{"py:class": "Class"})
+
+
+def test_info_field_list_napoleon_deliminator_of(app):
+ text = (".. py:module:: example\n"
+ ".. py:class:: Class\n"
+ "\n"
+ " :param list_str_var: example description.\n"
+ " :type list_str_var: list of str\n"
+ " :param tuple_int_var: example description.\n"
+ " :type tuple_int_var: tuple of tuple of int\n"
+ )
+ doctree = restructuredtext.parse(app, text)
+
+ # :param list of str list_str_var:
+ assert_node(doctree[3][1][0][0][1][0][0][0],
+ ([addnodes.literal_strong, "list_str_var"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "list"],
+ [addnodes.literal_emphasis, " of "],
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ ")",
+ " -- ",
+ "example description."))
+
+ # :param tuple of tuple of int tuple_int_var:
+ assert_node(doctree[3][1][0][0][1][0][1][0],
+ ([addnodes.literal_strong, "tuple_int_var"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "tuple"],
+ [addnodes.literal_emphasis, " of "],
+ [pending_xref, addnodes.literal_emphasis, "tuple"],
+ [addnodes.literal_emphasis, " of "],
+ [pending_xref, addnodes.literal_emphasis, "int"],
+ ")",
+ " -- ",
+ "example description."))
+
+
+def test_info_field_list_napoleon_deliminator_or(app):
+ text = (".. py:module:: example\n"
+ ".. py:class:: Class\n"
+ "\n"
+ " :param bool_str_var: example description.\n"
+ " :type bool_str_var: bool or str\n"
+ " :param str_float_int_var: example description.\n"
+ " :type str_float_int_var: str or float or int\n"
+ )
+ doctree = restructuredtext.parse(app, text)
+
+ # :param bool or str bool_str_var:
+ assert_node(doctree[3][1][0][0][1][0][0][0],
+ ([addnodes.literal_strong, "bool_str_var"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "bool"],
+ [addnodes.literal_emphasis, " or "],
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ ")",
+ " -- ",
+ "example description."))
+
+ # :param str or float or int str_float_int_var:
+ assert_node(doctree[3][1][0][0][1][0][1][0],
+ ([addnodes.literal_strong, "str_float_int_var"],
+ " (",
+ [pending_xref, addnodes.literal_emphasis, "str"],
+ [addnodes.literal_emphasis, " or "],
+ [pending_xref, addnodes.literal_emphasis, "float"],
+ [addnodes.literal_emphasis, " or "],
+ [pending_xref, addnodes.literal_emphasis, "int"],
+ ")",
+ " -- ",
+ "example description."))
+
+
+def test_type_field(app):
+ text = (".. py:data:: var1\n"
+ " :type: .int\n"
+ ".. py:data:: var2\n"
+ " :type: ~builtins.int\n"
+ ".. py:data:: var3\n"
+ " :type: typing.Optional[typing.Tuple[int, typing.Any]]\n")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "var1"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "int"])])],
+ [desc_content, ()])],
+ addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "var2"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "int"])])],
+ [desc_content, ()])],
+ addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "var3"],
+ [desc_annotation, ([desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [pending_xref, "Optional"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "Tuple"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "int"],
+ [desc_sig_punctuation, ","],
+ desc_sig_space,
+ [pending_xref, "Any"],
+ [desc_sig_punctuation, "]"],
+ [desc_sig_punctuation, "]"])])],
+ [desc_content, ()])]))
+ assert_node(doctree[1][0][1][2], pending_xref, reftarget='int', refspecific=True)
+ assert_node(doctree[3][0][1][2], pending_xref, reftarget='builtins.int', refspecific=False)
+ assert_node(doctree[5][0][1][2], pending_xref, reftarget='typing.Optional', refspecific=False)
+ assert_node(doctree[5][0][1][4], pending_xref, reftarget='typing.Tuple', refspecific=False)
+ assert_node(doctree[5][0][1][6], pending_xref, reftarget='int', refspecific=False)
+ assert_node(doctree[5][0][1][9], pending_xref, reftarget='typing.Any', refspecific=False)
diff --git a/tests/test_domains/test_domain_py_pyfunction.py b/tests/test_domains/test_domain_py_pyfunction.py
new file mode 100644
index 0000000..187b919
--- /dev/null
+++ b/tests/test_domains/test_domain_py_pyfunction.py
@@ -0,0 +1,396 @@
+"""Tests the Python Domain"""
+
+from __future__ import annotations
+
+import pytest
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.addnodes import (
+ desc,
+ desc_addname,
+ desc_annotation,
+ desc_content,
+ desc_name,
+ desc_optional,
+ desc_parameter,
+ desc_parameterlist,
+ desc_returns,
+ desc_sig_keyword,
+ desc_sig_name,
+ desc_sig_operator,
+ desc_sig_punctuation,
+ desc_sig_space,
+ desc_signature,
+ pending_xref,
+)
+from sphinx.testing import restructuredtext
+from sphinx.testing.util import assert_node
+
+
+def test_pyfunction(app):
+ text = (".. py:function:: func1\n"
+ ".. py:module:: example\n"
+ ".. py:function:: func2\n"
+ " :async:\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "func1"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()])],
+ addnodes.index,
+ addnodes.index,
+ nodes.target,
+ [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'async'],
+ desc_sig_space)],
+ [desc_addname, "example."],
+ [desc_name, "func2"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()])]))
+ assert_node(doctree[0], addnodes.index,
+ entries=[('pair', 'built-in function; func1()', 'func1', '', None)])
+ assert_node(doctree[2], addnodes.index,
+ entries=[('pair', 'module; example', 'module-example', '', None)])
+ assert_node(doctree[3], addnodes.index,
+ entries=[('single', 'func2() (in module example)', 'example.func2', '', None)])
+
+ assert 'func1' in domain.objects
+ assert domain.objects['func1'] == ('index', 'func1', 'function', False)
+ assert 'example.func2' in domain.objects
+ assert domain.objects['example.func2'] == ('index', 'example.func2', 'function', False)
+
+
+def test_pyfunction_signature(app):
+ text = ".. py:function:: hello(name: str) -> str"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"])],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, desc_parameter, ([desc_sig_name, "name"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"])])
+
+
+def test_pyfunction_signature_full(app):
+ text = (".. py:function:: hello(a: str, b = 1, *args: str, "
+ "c: bool = True, d: tuple = (1, 2), **kwargs: str) -> str")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"])],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [desc_sig_name, pending_xref, "str"])],
+ [desc_parameter, ([desc_sig_name, "b"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "1"])],
+ [desc_parameter, ([desc_sig_operator, "*"],
+ [desc_sig_name, "args"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [desc_sig_name, pending_xref, "str"])],
+ [desc_parameter, ([desc_sig_name, "c"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [desc_sig_name, pending_xref, "bool"],
+ desc_sig_space,
+ [desc_sig_operator, "="],
+ desc_sig_space,
+ [nodes.inline, "True"])],
+ [desc_parameter, ([desc_sig_name, "d"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [desc_sig_name, pending_xref, "tuple"],
+ desc_sig_space,
+ [desc_sig_operator, "="],
+ desc_sig_space,
+ [nodes.inline, "(1, 2)"])],
+ [desc_parameter, ([desc_sig_operator, "**"],
+ [desc_sig_name, "kwargs"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [desc_sig_name, pending_xref, "str"])])])
+ # case: separator at head
+ text = ".. py:function:: hello(*, a)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, nodes.inline, "*"],
+ [desc_parameter, desc_sig_name, "a"])])
+
+ # case: separator in the middle
+ text = ".. py:function:: hello(a, /, b, *, c)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"],
+ [desc_parameter, desc_sig_operator, "/"],
+ [desc_parameter, desc_sig_name, "b"],
+ [desc_parameter, desc_sig_operator, "*"],
+ [desc_parameter, desc_sig_name, "c"])])
+
+ # case: separator in the middle (2)
+ text = ".. py:function:: hello(a, /, *, b)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"],
+ [desc_parameter, desc_sig_operator, "/"],
+ [desc_parameter, desc_sig_operator, "*"],
+ [desc_parameter, desc_sig_name, "b"])])
+
+ # case: separator at tail
+ text = ".. py:function:: hello(a, /)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"],
+ [desc_parameter, desc_sig_operator, "/"])])
+
+
+def test_pyfunction_with_unary_operators(app):
+ text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "+1"])],
+ [desc_parameter, ([desc_sig_name, "bacon"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "-1"])],
+ [desc_parameter, ([desc_sig_name, "sausage"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "~1"])],
+ [desc_parameter, ([desc_sig_name, "spam"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "not spam"])])])
+
+
+def test_pyfunction_with_binary_operators(app):
+ text = ".. py:function:: menu(spam=2**64)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "2**64"])])])
+
+
+def test_pyfunction_with_number_literals(app):
+ text = ".. py:function:: hello(age=0x10, height=1_6_0)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "0x10"])],
+ [desc_parameter, ([desc_sig_name, "height"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "1_6_0"])])])
+
+
+def test_pyfunction_with_union_type_operator(app):
+ text = ".. py:function:: hello(age: int | None)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [desc_sig_name, ([pending_xref, "int"],
+ desc_sig_space,
+ [desc_sig_punctuation, "|"],
+ desc_sig_space,
+ [pending_xref, "None"])])])])
+
+
+def test_optional_pyfunction_signature(app):
+ text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "compile"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "ast object"])],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1],
+ ([desc_parameter, ([desc_sig_name, "source"])],
+ [desc_optional, ([desc_parameter, ([desc_sig_name, "filename"])],
+ [desc_optional, desc_parameter, ([desc_sig_name, "symbol"])])]))
+
+
+@pytest.mark.sphinx('html', confoverrides={
+ 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
+})
+def test_pyfunction_signature_with_python_maximum_signature_line_length_equal(app):
+ text = ".. py:function:: hello(name: str) -> str"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"],
+ )],
+ desc_content,
+ )],
+ ))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
+ [desc_sig_name, "name"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"],
+ )])
+ assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
+
+
+@pytest.mark.sphinx('html', confoverrides={
+ 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
+})
+def test_pyfunction_signature_with_python_maximum_signature_line_length_force_single(app):
+ text = (".. py:function:: hello(names: str) -> str\n"
+ " :single-line-parameter-list:")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"],
+ )],
+ desc_content,
+ )],
+ ))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
+ [desc_sig_name, "names"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"],
+ )])
+ assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
+
+
+@pytest.mark.sphinx('html', confoverrides={
+ 'python_maximum_signature_line_length': len("hello(name: str) -> str"),
+})
+def test_pyfunction_signature_with_python_maximum_signature_line_length_break(app):
+ text = ".. py:function:: hello(names: str) -> str"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"],
+ )],
+ desc_content,
+ )],
+ ))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
+ [desc_sig_name, "names"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"],
+ )])
+ assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True)
+
+
+@pytest.mark.sphinx('html', confoverrides={
+ 'maximum_signature_line_length': len("hello(name: str) -> str"),
+})
+def test_pyfunction_signature_with_maximum_signature_line_length_equal(app):
+ text = ".. py:function:: hello(name: str) -> str"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"],
+ )],
+ desc_content,
+ )],
+ ))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
+ [desc_sig_name, "name"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"],
+ )])
+ assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
+
+
+@pytest.mark.sphinx('html', confoverrides={
+ 'maximum_signature_line_length': len("hello(name: str) -> str"),
+})
+def test_pyfunction_signature_with_maximum_signature_line_length_force_single(app):
+ text = (".. py:function:: hello(names: str) -> str\n"
+ " :single-line-parameter-list:")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"],
+ )],
+ desc_content,
+ )],
+ ))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
+ [desc_sig_name, "names"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"],
+ )])
+ assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False)
+
+
+@pytest.mark.sphinx('html', confoverrides={
+ 'maximum_signature_line_length': len("hello(name: str) -> str"),
+})
+def test_pyfunction_signature_with_maximum_signature_line_length_break(app):
+ text = ".. py:function:: hello(names: str) -> str"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, pending_xref, "str"],
+ )],
+ desc_content,
+ )],
+ ))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+ assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, (
+ [desc_sig_name, "names"],
+ [desc_sig_punctuation, ":"],
+ desc_sig_space,
+ [nodes.inline, pending_xref, "str"],
+ )])
+ assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True)
diff --git a/tests/test_domains/test_domain_py_pyobject.py b/tests/test_domains/test_domain_py_pyobject.py
new file mode 100644
index 0000000..04f9341
--- /dev/null
+++ b/tests/test_domains/test_domain_py_pyobject.py
@@ -0,0 +1,487 @@
+"""Tests the Python Domain"""
+
+from __future__ import annotations
+
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.addnodes import (
+ desc,
+ desc_addname,
+ desc_annotation,
+ desc_content,
+ desc_name,
+ desc_parameterlist,
+ desc_sig_punctuation,
+ desc_sig_space,
+ desc_signature,
+ pending_xref,
+)
+from sphinx.testing import restructuredtext
+from sphinx.testing.util import assert_node
+
+
+def test_pyexception_signature(app):
+ text = ".. py:exception:: builtins.IOError"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ('exception', desc_sig_space)],
+ [desc_addname, "builtins."],
+ [desc_name, "IOError"])],
+ desc_content)]))
+ assert_node(doctree[1], desc, desctype="exception",
+ domain="py", objtype="exception", no_index=False)
+
+
+def test_pydata_signature(app):
+ text = (".. py:data:: version\n"
+ " :type: int\n"
+ " :value: 1\n")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "version"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "int"])],
+ [desc_annotation, (
+ desc_sig_space,
+ [desc_sig_punctuation, '='],
+ desc_sig_space,
+ "1")],
+ )],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="data",
+ domain="py", objtype="data", no_index=False)
+
+
+def test_pydata_signature_old(app):
+ text = (".. py:data:: version\n"
+ " :annotation: = 1\n")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "version"],
+ [desc_annotation, (desc_sig_space,
+ "= 1")])],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="data",
+ domain="py", objtype="data", no_index=False)
+
+
+def test_pydata_with_union_type_operator(app):
+ text = (".. py:data:: version\n"
+ " :type: int | str")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0],
+ ([desc_name, "version"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "int"],
+ desc_sig_space,
+ [desc_sig_punctuation, "|"],
+ desc_sig_space,
+ [pending_xref, "str"])]))
+
+
+def test_pyobject_prefix(app):
+ text = (".. py:class:: Foo\n"
+ "\n"
+ " .. py:method:: Foo.say\n"
+ " .. py:method:: FooBar.say")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)],
+ [desc_name, "Foo"])],
+ [desc_content, (addnodes.index,
+ desc,
+ addnodes.index,
+ desc)])]))
+ assert doctree[1][1][1].astext().strip() == 'say()' # prefix is stripped
+ assert doctree[1][1][3].astext().strip() == 'FooBar.say()' # not stripped
+
+
+def test_pydata(app):
+ text = (".. py:module:: example\n"
+ ".. py:data:: var\n"
+ " :type: int\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ addnodes.index,
+ nodes.target,
+ [desc, ([desc_signature, ([desc_addname, "example."],
+ [desc_name, "var"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "int"])])],
+ [desc_content, ()])]))
+ assert_node(doctree[3][0][2][2], pending_xref, **{"py:module": "example"})
+ assert 'example.var' in domain.objects
+ assert domain.objects['example.var'] == ('index', 'example.var', 'data', False)
+
+
+def test_pyclass_options(app):
+ text = (".. py:class:: Class1\n"
+ ".. py:class:: Class2\n"
+ " :final:\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_name, "Class1"])],
+ [desc_content, ()])],
+ addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("final",
+ desc_sig_space,
+ "class",
+ desc_sig_space)],
+ [desc_name, "Class2"])],
+ [desc_content, ()])]))
+
+ # class
+ assert_node(doctree[0], addnodes.index,
+ entries=[('single', 'Class1 (built-in class)', 'Class1', '', None)])
+ assert 'Class1' in domain.objects
+ assert domain.objects['Class1'] == ('index', 'Class1', 'class', False)
+
+ # :final:
+ assert_node(doctree[2], addnodes.index,
+ entries=[('single', 'Class2 (built-in class)', 'Class2', '', None)])
+ assert 'Class2' in domain.objects
+ assert domain.objects['Class2'] == ('index', 'Class2', 'class', False)
+
+
+def test_pymethod_options(app):
+ text = (".. py:class:: Class\n"
+ "\n"
+ " .. py:method:: meth1\n"
+ " .. py:method:: meth2\n"
+ " :classmethod:\n"
+ " .. py:method:: meth3\n"
+ " :staticmethod:\n"
+ " .. py:method:: meth4\n"
+ " :async:\n"
+ " .. py:method:: meth5\n"
+ " :abstractmethod:\n"
+ " .. py:method:: meth6\n"
+ " :final:\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_name, "Class"])],
+ [desc_content, (addnodes.index,
+ desc,
+ addnodes.index,
+ desc,
+ addnodes.index,
+ desc,
+ addnodes.index,
+ desc,
+ addnodes.index,
+ desc,
+ addnodes.index,
+ desc)])]))
+
+ # method
+ assert_node(doctree[1][1][0], addnodes.index,
+ entries=[('single', 'meth1() (Class method)', 'Class.meth1', '', None)])
+ assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth1' in domain.objects
+ assert domain.objects['Class.meth1'] == ('index', 'Class.meth1', 'method', False)
+
+ # :classmethod:
+ assert_node(doctree[1][1][2], addnodes.index,
+ entries=[('single', 'meth2() (Class class method)', 'Class.meth2', '', None)])
+ assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)],
+ [desc_name, "meth2"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth2' in domain.objects
+ assert domain.objects['Class.meth2'] == ('index', 'Class.meth2', 'method', False)
+
+ # :staticmethod:
+ assert_node(doctree[1][1][4], addnodes.index,
+ entries=[('single', 'meth3() (Class static method)', 'Class.meth3', '', None)])
+ assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)],
+ [desc_name, "meth3"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth3' in domain.objects
+ assert domain.objects['Class.meth3'] == ('index', 'Class.meth3', 'method', False)
+
+ # :async:
+ assert_node(doctree[1][1][6], addnodes.index,
+ entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)])
+ assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, ("async", desc_sig_space)],
+ [desc_name, "meth4"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth4' in domain.objects
+ assert domain.objects['Class.meth4'] == ('index', 'Class.meth4', 'method', False)
+
+ # :abstractmethod:
+ assert_node(doctree[1][1][8], addnodes.index,
+ entries=[('single', 'meth5() (Class method)', 'Class.meth5', '', None)])
+ assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space)],
+ [desc_name, "meth5"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth5' in domain.objects
+ assert domain.objects['Class.meth5'] == ('index', 'Class.meth5', 'method', False)
+
+ # :final:
+ assert_node(doctree[1][1][10], addnodes.index,
+ entries=[('single', 'meth6() (Class method)', 'Class.meth6', '', None)])
+ assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, ("final", desc_sig_space)],
+ [desc_name, "meth6"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth6' in domain.objects
+ assert domain.objects['Class.meth6'] == ('index', 'Class.meth6', 'method', False)
+
+
+def test_pyclassmethod(app):
+ text = (".. py:class:: Class\n"
+ "\n"
+ " .. py:classmethod:: meth\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_name, "Class"])],
+ [desc_content, (addnodes.index,
+ desc)])]))
+ assert_node(doctree[1][1][0], addnodes.index,
+ entries=[('single', 'meth() (Class class method)', 'Class.meth', '', None)])
+ assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)],
+ [desc_name, "meth"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth' in domain.objects
+ assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False)
+
+
+def test_pystaticmethod(app):
+ text = (".. py:class:: Class\n"
+ "\n"
+ " .. py:staticmethod:: meth\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_name, "Class"])],
+ [desc_content, (addnodes.index,
+ desc)])]))
+ assert_node(doctree[1][1][0], addnodes.index,
+ entries=[('single', 'meth() (Class static method)', 'Class.meth', '', None)])
+ assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)],
+ [desc_name, "meth"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()]))
+ assert 'Class.meth' in domain.objects
+ assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False)
+
+
+def test_pyattribute(app):
+ text = (".. py:class:: Class\n"
+ "\n"
+ " .. py:attribute:: attr\n"
+ " :type: Optional[str]\n"
+ " :value: ''\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_name, "Class"])],
+ [desc_content, (addnodes.index,
+ desc)])]))
+ assert_node(doctree[1][1][0], addnodes.index,
+ entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)])
+ assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "str"],
+ desc_sig_space,
+ [desc_sig_punctuation, "|"],
+ desc_sig_space,
+ [pending_xref, "None"])],
+ [desc_annotation, (desc_sig_space,
+ [desc_sig_punctuation, '='],
+ desc_sig_space,
+ "''")],
+ )],
+ [desc_content, ()]))
+ assert_node(doctree[1][1][1][0][1][2], pending_xref, **{"py:class": "Class"})
+ assert_node(doctree[1][1][1][0][1][6], pending_xref, **{"py:class": "Class"})
+ assert 'Class.attr' in domain.objects
+ assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False)
+
+
+def test_pyproperty(app):
+ text = (".. py:class:: Class\n"
+ "\n"
+ " .. py:property:: prop1\n"
+ " :abstractmethod:\n"
+ " :type: str\n"
+ "\n"
+ " .. py:property:: prop2\n"
+ " :classmethod:\n"
+ " :type: str\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)],
+ [desc_name, "Class"])],
+ [desc_content, (addnodes.index,
+ desc,
+ addnodes.index,
+ desc)])]))
+ assert_node(doctree[1][1][0], addnodes.index,
+ entries=[('single', 'prop1 (Class property)', 'Class.prop1', '', None)])
+ assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space,
+ "property", desc_sig_space)],
+ [desc_name, "prop1"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "str"])])],
+ [desc_content, ()]))
+ assert_node(doctree[1][1][2], addnodes.index,
+ entries=[('single', 'prop2 (Class property)', 'Class.prop2', '', None)])
+ assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("class", desc_sig_space,
+ "property", desc_sig_space)],
+ [desc_name, "prop2"],
+ [desc_annotation, ([desc_sig_punctuation, ':'],
+ desc_sig_space,
+ [pending_xref, "str"])])],
+ [desc_content, ()]))
+ assert 'Class.prop1' in domain.objects
+ assert domain.objects['Class.prop1'] == ('index', 'Class.prop1', 'property', False)
+ assert 'Class.prop2' in domain.objects
+ assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False)
+
+
+def test_pydecorator_signature(app):
+ text = ".. py:decorator:: deco"
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_addname, "@"],
+ [desc_name, "deco"])],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", no_index=False)
+
+ assert 'deco' in domain.objects
+ assert domain.objects['deco'] == ('index', 'deco', 'function', False)
+
+
+def test_pydecoratormethod_signature(app):
+ text = ".. py:decoratormethod:: deco"
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_addname, "@"],
+ [desc_name, "deco"])],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="method",
+ domain="py", objtype="method", no_index=False)
+
+ assert 'deco' in domain.objects
+ assert domain.objects['deco'] == ('index', 'deco', 'method', False)
+
+
+def test_pycurrentmodule(app):
+ text = (".. py:module:: Other\n"
+ "\n"
+ ".. py:module:: Module\n"
+ ".. py:class:: A\n"
+ "\n"
+ " .. py:method:: m1\n"
+ " .. py:method:: m2\n"
+ "\n"
+ ".. py:currentmodule:: Other\n"
+ "\n"
+ ".. py:class:: B\n"
+ "\n"
+ " .. py:method:: m3\n"
+ " .. py:method:: m4\n")
+ domain = app.env.get_domain('py')
+ doctree = restructuredtext.parse(app, text)
+ print(doctree)
+ assert_node(
+ doctree, (
+ addnodes.index,
+ addnodes.index,
+ addnodes.index,
+ nodes.target,
+ nodes.target,
+ [desc, (
+ [desc_signature, (
+ [desc_annotation, ("class", desc_sig_space)],
+ [desc_addname, "Module."],
+ [desc_name, "A"],
+ )],
+ [desc_content, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "m1"],
+ [desc_parameterlist, ()],
+ )],
+ [desc_content, ()],
+ )],
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "m2"],
+ [desc_parameterlist, ()],
+ )],
+ [desc_content, ()],
+ )],
+ )],
+ )],
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_annotation, ("class", desc_sig_space)],
+ [desc_addname, "Other."],
+ [desc_name, "B"],
+ )],
+ [desc_content, (
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "m3"],
+ [desc_parameterlist, ()],
+ )],
+ [desc_content, ()],
+ )],
+ addnodes.index,
+ [desc, (
+ [desc_signature, (
+ [desc_name, "m4"],
+ [desc_parameterlist, ()],
+ )],
+ [desc_content, ()],
+ )],
+ )],
+ )],
+ ))
+ assert 'Module' in domain.objects
+ assert domain.objects['Module'] == ('index', 'module-Module', 'module', False)
+ assert 'Other' in domain.objects
+ assert domain.objects['Other'] == ('index', 'module-Other', 'module', False)
+ assert 'Module.A' in domain.objects
+ assert domain.objects['Module.A'] == ('index', 'Module.A', 'class', False)
+ assert 'Other.B' in domain.objects
+ assert domain.objects['Other.B'] == ('index', 'Other.B', 'class', False)
+ assert 'Module.A.m1' in domain.objects
+ assert domain.objects['Module.A.m1'] == ('index', 'Module.A.m1', 'method', False)
+ assert 'Module.A.m2' in domain.objects
+ assert domain.objects['Module.A.m2'] == ('index', 'Module.A.m2', 'method', False)
+ assert 'Other.B.m3' in domain.objects
+ assert domain.objects['Other.B.m3'] == ('index', 'Other.B.m3', 'method', False)
+ assert 'Other.B.m4' in domain.objects
+ assert domain.objects['Other.B.m4'] == ('index', 'Other.B.m4', 'method', False)
diff --git a/tests/test_domain_rst.py b/tests/test_domains/test_domain_rst.py
index 4445da1..4445da1 100644
--- a/tests/test_domain_rst.py
+++ b/tests/test_domains/test_domain_rst.py
diff --git a/tests/test_domain_std.py b/tests/test_domains/test_domain_std.py
index 6d7ab53..52ecdf5 100644
--- a/tests/test_domain_std.py
+++ b/tests/test_domains/test_domain_std.py
@@ -5,7 +5,6 @@ from unittest import mock
import pytest
from docutils import nodes
from docutils.nodes import definition, definition_list, definition_list_item, term
-from html5lib import HTMLParser
from sphinx import addnodes
from sphinx.addnodes import (
@@ -20,7 +19,7 @@ from sphinx.addnodes import (
)
from sphinx.domains.std import StandardDomain
from sphinx.testing import restructuredtext
-from sphinx.testing.util import assert_node
+from sphinx.testing.util import assert_node, etree_parse
def test_process_doc_handle_figure_caption():
@@ -368,16 +367,18 @@ def test_multiple_cmdoptions(app):
@pytest.mark.sphinx(testroot='productionlist')
def test_productionlist(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
warnings = warning.getvalue().split("\n")
assert len(warnings) == 2
assert warnings[-1] == ''
assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0]
- with (app.outdir / 'index.html').open('rb') as f:
- etree = HTMLParser(namespaceHTMLElements=False).parse(f)
- ul = list(etree.iter('ul'))[1]
+ etree = etree_parse(app.outdir / 'index.html')
+ nodes = list(etree.iter('ul'))
+ assert len(nodes) >= 2
+
+ ul = nodes[1]
cases = []
for li in list(ul):
assert len(list(li)) == 1
@@ -493,3 +494,24 @@ def test_labeled_field(app):
assert domain.labels['label1'] == ('index', 'label1', 'Foo blah blah blah')
assert 'label2' in domain.labels
assert domain.labels['label2'] == ('index', 'label2', 'Bar blah blah blah')
+
+
+@pytest.mark.sphinx('html', testroot='manpage_url',
+ confoverrides={'manpages_url': 'https://example.com/{page}.{section}'})
+def test_html_manpage(app):
+ app.build(force_all=True)
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<em class="manpage">'
+ '<a class="manpage reference external" href="https://example.com/man.1">man(1)</a>'
+ '</em>') in content
+ assert ('<em class="manpage">'
+ '<a class="manpage reference external" href="https://example.com/ls.1">ls.1</a>'
+ '</em>') in content
+ assert ('<em class="manpage">'
+ '<a class="manpage reference external" href="https://example.com/sphinx.">sphinx</a>'
+ '</em>') in content
+ assert ('<em class="manpage">'
+ '<a class="manpage reference external" href="https://example.com/bsd-mailx/mailx.1">mailx(1)</a>'
+ '</em>') in content
+ assert '<em class="manpage">man(1)</em>' in content
diff --git a/tests/test_environment/__init__.py b/tests/test_environment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_environment/__init__.py
diff --git a/tests/test_environment.py b/tests/test_environment/test_environment.py
index 8a34457..8a34457 100644
--- a/tests/test_environment.py
+++ b/tests/test_environment/test_environment.py
diff --git a/tests/test_environment_indexentries.py b/tests/test_environment/test_environment_indexentries.py
index 4cfdc28..4cfdc28 100644
--- a/tests/test_environment_indexentries.py
+++ b/tests/test_environment/test_environment_indexentries.py
diff --git a/tests/test_environment_record_dependencies.py b/tests/test_environment/test_environment_record_dependencies.py
index 0a17253..0a17253 100644
--- a/tests/test_environment_record_dependencies.py
+++ b/tests/test_environment/test_environment_record_dependencies.py
diff --git a/tests/test_environment_toctree.py b/tests/test_environment/test_environment_toctree.py
index 5123715..175c6ab 100644
--- a/tests/test_environment_toctree.py
+++ b/tests/test_environment/test_environment_toctree.py
@@ -33,7 +33,7 @@ def test_process_doc(app):
assert_node(toctree[0][1][0], addnodes.toctree,
caption="Table of Contents", glob=False, hidden=False,
titlesonly=False, maxdepth=2, numbered=999,
- entries=[(None, 'foo'), (None, 'bar'), (None, 'http://sphinx-doc.org/'),
+ entries=[(None, 'foo'), (None, 'bar'), (None, 'https://sphinx-doc.org/'),
(None, 'self')],
includefiles=['foo', 'bar'])
@@ -59,8 +59,8 @@ def test_process_doc(app):
assert_node(toctree[1][1][1], addnodes.toctree,
caption=None, glob=False, hidden=True,
titlesonly=False, maxdepth=-1, numbered=0,
- entries=[('Latest reference', 'http://sphinx-doc.org/latest/'),
- ('Python', 'http://python.org/')])
+ entries=[('Latest reference', 'https://sphinx-doc.org/latest/'),
+ ('Python', 'https://python.org/')])
assert_node(toctree[2][0],
[compact_paragraph, reference, "Indices and tables"])
@@ -246,7 +246,7 @@ def test_global_toctree_for_doc(app):
([list_item, ([compact_paragraph, reference, "foo"],
bullet_list)],
[list_item, compact_paragraph, reference, "bar"],
- [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"],
+ [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"],
[list_item, compact_paragraph, reference,
"Welcome to Sphinx Tests’s documentation!"]))
assert_node(toctree[1][0][1],
@@ -259,7 +259,7 @@ def test_global_toctree_for_doc(app):
assert_node(toctree[1][0][1][1][0][0], reference, refuri="foo#foo-1", secnumber=[1, 2])
assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3])
assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2])
- assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/")
+ assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/")
assert_node(toctree[1][3][0][0], reference, refuri="")
assert_node(toctree[2],
@@ -267,8 +267,8 @@ def test_global_toctree_for_doc(app):
assert_node(toctree[3],
([list_item, compact_paragraph, reference, "Latest reference"],
[list_item, compact_paragraph, reference, "Python"]))
- assert_node(toctree[3][0][0][0], reference, refuri="http://sphinx-doc.org/latest/")
- assert_node(toctree[3][1][0][0], reference, refuri="http://python.org/")
+ assert_node(toctree[3][0][0][0], reference, refuri="https://sphinx-doc.org/latest/")
+ assert_node(toctree[3][1][0][0], reference, refuri="https://python.org/")
@pytest.mark.sphinx('xml', testroot='toctree')
@@ -285,12 +285,12 @@ def test_global_toctree_for_doc_collapse(app):
assert_node(toctree[1],
([list_item, compact_paragraph, reference, "foo"],
[list_item, compact_paragraph, reference, "bar"],
- [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"],
+ [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"],
[list_item, compact_paragraph, reference,
"Welcome to Sphinx Tests’s documentation!"]))
assert_node(toctree[1][0][0][0], reference, refuri="foo", secnumber=[1])
assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2])
- assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/")
+ assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/")
assert_node(toctree[1][3][0][0], reference, refuri="")
assert_node(toctree[2],
@@ -298,8 +298,8 @@ def test_global_toctree_for_doc_collapse(app):
assert_node(toctree[3],
([list_item, compact_paragraph, reference, "Latest reference"],
[list_item, compact_paragraph, reference, "Python"]))
- assert_node(toctree[3][0][0][0], reference, refuri="http://sphinx-doc.org/latest/")
- assert_node(toctree[3][1][0][0], reference, refuri="http://python.org/")
+ assert_node(toctree[3][0][0][0], reference, refuri="https://sphinx-doc.org/latest/")
+ assert_node(toctree[3][1][0][0], reference, refuri="https://python.org/")
@pytest.mark.sphinx('xml', testroot='toctree')
@@ -318,7 +318,7 @@ def test_global_toctree_for_doc_maxdepth(app):
([list_item, ([compact_paragraph, reference, "foo"],
bullet_list)],
[list_item, compact_paragraph, reference, "bar"],
- [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"],
+ [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"],
[list_item, compact_paragraph, reference,
"Welcome to Sphinx Tests’s documentation!"]))
assert_node(toctree[1][0][1],
@@ -336,7 +336,7 @@ def test_global_toctree_for_doc_maxdepth(app):
reference, refuri="foo#foo-1-1", secnumber=[1, 2, 1])
assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3])
assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2])
- assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/")
+ assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/")
assert_node(toctree[1][3][0][0], reference, refuri="")
assert_node(toctree[2],
@@ -344,8 +344,8 @@ def test_global_toctree_for_doc_maxdepth(app):
assert_node(toctree[3],
([list_item, compact_paragraph, reference, "Latest reference"],
[list_item, compact_paragraph, reference, "Python"]))
- assert_node(toctree[3][0][0][0], reference, refuri="http://sphinx-doc.org/latest/")
- assert_node(toctree[3][1][0][0], reference, refuri="http://python.org/")
+ assert_node(toctree[3][0][0][0], reference, refuri="https://sphinx-doc.org/latest/")
+ assert_node(toctree[3][1][0][0], reference, refuri="https://python.org/")
@pytest.mark.sphinx('xml', testroot='toctree')
@@ -363,7 +363,7 @@ def test_global_toctree_for_doc_includehidden(app):
([list_item, ([compact_paragraph, reference, "foo"],
bullet_list)],
[list_item, compact_paragraph, reference, "bar"],
- [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"],
+ [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"],
[list_item, compact_paragraph, reference,
"Welcome to Sphinx Tests’s documentation!"]))
assert_node(toctree[1][0][1],
@@ -376,7 +376,7 @@ def test_global_toctree_for_doc_includehidden(app):
assert_node(toctree[1][0][1][1][0][0], reference, refuri="foo#foo-1", secnumber=[1, 2])
assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3])
assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2])
- assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/")
+ assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/")
assert_node(toctree[2],
[bullet_list, list_item, compact_paragraph, reference, "baz"])
diff --git a/tests/test_events.py b/tests/test_events.py
index d850a91..5097dc8 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -9,11 +9,11 @@ from sphinx.events import EventManager
def test_event_priority():
result = []
events = EventManager(object()) # pass an dummy object as an app
- events.connect('builder-inited', lambda app: result.append(1), priority = 500)
- events.connect('builder-inited', lambda app: result.append(2), priority = 500)
- events.connect('builder-inited', lambda app: result.append(3), priority = 200) # earlier
- events.connect('builder-inited', lambda app: result.append(4), priority = 700) # later
- events.connect('builder-inited', lambda app: result.append(5), priority = 500)
+ events.connect('builder-inited', lambda app: result.append(1), priority=500)
+ events.connect('builder-inited', lambda app: result.append(2), priority=500)
+ events.connect('builder-inited', lambda app: result.append(3), priority=200) # earlier
+ events.connect('builder-inited', lambda app: result.append(4), priority=700) # later
+ events.connect('builder-inited', lambda app: result.append(5), priority=500)
events.emit('builder-inited')
assert result == [3, 1, 2, 5, 4]
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/ext_napoleon_pep526_data_google.py b/tests/test_extensions/ext_napoleon_pep526_data_google.py
index bb55b0f..d0692e0 100644
--- a/tests/ext_napoleon_pep526_data_google.py
+++ b/tests/test_extensions/ext_napoleon_pep526_data_google.py
@@ -5,7 +5,7 @@ module_level_var: int = 99
class PEP526GoogleClass:
- """Sample class with PEP 526 annotations and google docstring
+ """Sample class with PEP 526 annotations and google docstring.
Attributes:
attr1: Attr1 description.
diff --git a/tests/ext_napoleon_pep526_data_numpy.py b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py
index b3093a7..eff7746 100644
--- a/tests/ext_napoleon_pep526_data_numpy.py
+++ b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py
@@ -16,5 +16,6 @@ class PEP526NumpyClass:
attr2:
Attr2 description
"""
+
attr1: int
attr2: str
diff --git a/tests/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py
index 1e089a3..c3c979f 100644
--- a/tests/test_ext_apidoc.py
+++ b/tests/test_extensions/test_ext_apidoc.py
@@ -15,7 +15,7 @@ def apidoc(rootdir, tmp_path, 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', [])
+ args = ['-o', str(outdir), '-F', str(coderoot), *excludes, *kwargs.get('options', [])]
apidoc_main(args)
return namedtuple('apidoc', 'coderoot,outdir')(coderoot, outdir)
@@ -26,8 +26,7 @@ def apidoc_params(request):
kwargs = {}
for info in reversed(list(request.node.iter_markers("apidoc"))):
- for i, a in enumerate(info.args):
- pargs[i] = a
+ pargs |= dict(enumerate(info.args))
kwargs.update(info.kwargs)
args = [pargs[i] for i in sorted(pargs.keys())]
@@ -302,9 +301,9 @@ def test_extension_parsed(make_app, apidoc):
)
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.
+ 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()
@@ -332,9 +331,9 @@ def test_toc_all_references_should_exist_pep420_enabled(make_app, apidoc):
)
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.
+ 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()
@@ -379,7 +378,7 @@ def extract_toc(path):
)
def test_subpackage_in_toc(make_app, apidoc):
"""Make sure that empty subpackages with non-empty subpackages in them
- are not skipped (issue #4520)
+ are not skipped (issue #4520)
"""
outdir = apidoc.outdir
assert (outdir / 'conf.py').is_file()
@@ -643,7 +642,6 @@ def test_no_duplicates(rootdir, tmp_path):
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
diff --git a/tests/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py
index 7062763..54f81f2 100644
--- a/tests/test_ext_autodoc.py
+++ b/tests/test_extensions/test_ext_autodoc.py
@@ -4,8 +4,14 @@ 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
@@ -14,8 +20,8 @@ from docutils.statemachine import ViewList
from sphinx import addnodes
from sphinx.ext.autodoc import ALL, ModuleLevelDocumenter, Options
-from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options
-from sphinx.util.docutils import LoggingReporter
+
+from tests.test_extensions.autodoc_util import do_autodoc
try:
# Enable pyximport to test cython module
@@ -24,47 +30,35 @@ try:
except ImportError:
pyximport = None
-
-def do_autodoc(app, objtype, name, options=None):
- if options is None:
- options = {}
- 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
+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,
+ 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(),
+ env=env,
+ genopt=options,
+ result=ViewList(),
+ record_dependencies=set(),
+ state=Mock(),
)
directive.state.document.settings.tab_width = 8
@@ -74,23 +68,6 @@ def make_directive_bridge(env):
processed_signatures = []
-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
-
-
def test_parse_name(app):
def verify(objtype, name, result):
inst = app.registry.documenters[objtype](directive, name)
@@ -103,7 +80,7 @@ def test_parse_name(app):
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()
+ assert 'signature arguments' in app.warning.getvalue()
# for functions/classes
verify('function', 'test_ext_autodoc.raises',
@@ -131,6 +108,21 @@ def test_parse_name(app):
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)
@@ -226,12 +218,14 @@ def test_format_signature(app):
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
@@ -330,7 +324,7 @@ def test_get_doc(app):
inst.format_signature() # handle docstring signatures!
ds = inst.get_doc()
# for testing purposes, concat them and strip the empty line at the end
- res = sum(ds, [])[:-1]
+ res = functools.reduce(operator.iadd, ds, [])[:-1]
print(res)
return res
@@ -342,6 +336,7 @@ def test_get_doc(app):
# standard function, diverse docstring styles...
def f():
"""Docstring"""
+
def g():
"""
Docstring
@@ -837,9 +832,13 @@ def test_autodoc_special_members(app):
]
# all special methods
- options = {"members": None,
- "undoc-members": None,
- "special-members": None}
+ 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)',
@@ -1402,73 +1401,411 @@ def test_slots(app):
]
+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(app):
- options = {"members": None}
- actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options)
-
- if 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)'
+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) == [
- '',
- '.. py:class:: EnumCls' + args,
- ' :module: target.enums',
- '',
- ' this is enum class',
- '',
- '',
- ' .. py:method:: EnumCls.say_goodbye()',
- ' :module: target.enums',
- ' :classmethod:',
- '',
- ' a classmethod says good-bye to you.',
- '',
- '',
- ' .. py:method:: EnumCls.say_hello()',
- ' :module: target.enums',
- '',
- ' a method says hello to you.',
- '',
- '',
- ' .. py:attribute:: EnumCls.val1',
- ' :module: target.enums',
- ' :value: 12',
- '',
- ' doc for val1',
- '',
- '',
- ' .. py:attribute:: EnumCls.val2',
- ' :module: target.enums',
- ' :value: 23',
- '',
- ' doc for val2',
- '',
- '',
- ' .. py:attribute:: EnumCls.val3',
- ' :module: target.enums',
- ' :value: 34',
- '',
- ' doc for val3',
- '',
+ *fmt.brief('this is enum class'),
+ *fmt.method('say_goodbye', 'docstring', 'classmethod'),
+ *fmt.method('say_hello', 'docstring'),
+ *fmt.member('x', 'x', ''),
]
- # checks for an attribute of EnumClass
- actual = do_autodoc(app, 'attribute', 'target.enums.EnumCls.val1')
+ options = autodoc_enum_options | {'inherited-members': None}
+ actual = do_autodoc(app, 'class', fmt.target, options)
assert list(actual) == [
- '',
- '.. py:attribute:: EnumCls.val1',
- ' :module: target.enums',
- ' :value: 12',
- '',
- ' doc for val1',
- '',
+ *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'),
]
@@ -2103,6 +2440,55 @@ def test_singledispatchmethod_automethod(app):
]
+@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')
@@ -2276,7 +2662,7 @@ def test_pyclass_for_ClassLevelDocumenter(app):
@pytest.mark.sphinx('dummy', testroot='ext-autodoc')
def test_autodoc(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = app.env.get_doctree('index')
assert isinstance(content[3], addnodes.desc)
diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_extensions/test_ext_autodoc_autoattribute.py
index 0424af0..41fcc99 100644
--- a/tests/test_ext_autodoc_autoattribute.py
+++ b/tests/test_extensions/test_ext_autodoc_autoattribute.py
@@ -6,7 +6,7 @@ source file translated by test_build.
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py
index 92c259a..3e68d60 100644
--- a/tests/test_ext_autodoc_autoclass.py
+++ b/tests/test_extensions/test_ext_autodoc_autoclass.py
@@ -11,7 +11,7 @@ from typing import Union
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
@@ -169,6 +169,7 @@ def test_undocumented_uninitialized_attributes(app):
]
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_decorators(app):
actual = do_autodoc(app, 'class', 'target.decorator.Baz')
assert list(actual) == [
@@ -275,8 +276,7 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
'.. py:class:: Quux(iterable=(), /)',
' :module: target.classes',
'',
- ' Bases: :py:class:`~typing.List`\\ '
- '[:py:obj:`~typing.Union`\\ [:py:class:`int`, :py:class:`float`]]',
+ ' Bases: :py:class:`~typing.List`\\ [:py:class:`int` | :py:class:`float`]',
'',
' A subclass of List[Union[int, float]]',
'',
@@ -373,6 +373,7 @@ def test_class_doc_from_both(app):
]
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_alias(app):
def autodoc_process_docstring(*args):
"""A handler always raises an error.
@@ -391,6 +392,7 @@ def test_class_alias(app):
]
+@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) == [
@@ -403,6 +405,7 @@ def test_class_alias_having_doccomment(app):
]
+@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) == [
@@ -515,3 +518,49 @@ def test_autoattribute_TypeVar_module_level(app):
" 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_ext_autodoc_autodata.py b/tests/test_extensions/test_ext_autodoc_autodata.py
index 83647d9..b794666 100644
--- a/tests/test_ext_autodoc_autodata.py
+++ b/tests/test_extensions/test_ext_autodoc_autodata.py
@@ -6,7 +6,7 @@ source file translated by test_build.
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_extensions/test_ext_autodoc_autofunction.py
index b0cd7d9..5dfa42d 100644
--- a/tests/test_ext_autodoc_autofunction.py
+++ b/tests/test_extensions/test_ext_autodoc_autofunction.py
@@ -6,7 +6,7 @@ source file translated by test_build.
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
@@ -199,3 +199,14 @@ def test_async_generator(app):
' :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_ext_autodoc_automodule.py b/tests/test_extensions/test_ext_autodoc_automodule.py
index 2855020..92565ae 100644
--- a/tests/test_ext_autodoc_automodule.py
+++ b/tests/test_extensions/test_ext_autodoc_automodule.py
@@ -8,7 +8,7 @@ import sys
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
diff --git a/tests/test_ext_autodoc_autoproperty.py b/tests/test_extensions/test_ext_autodoc_autoproperty.py
index ca8b981..de33117 100644
--- a/tests/test_ext_autodoc_autoproperty.py
+++ b/tests/test_extensions/test_ext_autodoc_autoproperty.py
@@ -6,7 +6,7 @@ source file translated by test_build.
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py
index 45bc729..6c2af5a 100644
--- a/tests/test_ext_autodoc_configs.py
+++ b/tests/test_extensions/test_ext_autodoc_configs.py
@@ -8,7 +8,7 @@ import pytest
from sphinx.testing import restructuredtext
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
IS_PYPY = platform.python_implementation() == 'PyPy'
@@ -969,9 +969,9 @@ def test_autodoc_typehints_description(app):
assert ('target.typehints.incr(a, b=1)\n'
'\n'
' Parameters:\n'
- ' * **a** (*int*) --\n'
+ ' * **a** (*int*)\n'
'\n'
- ' * **b** (*int*) --\n'
+ ' * **b** (*int*)\n'
'\n'
' Return type:\n'
' int\n'
@@ -979,7 +979,7 @@ def test_autodoc_typehints_description(app):
assert ('target.typehints.tuple_args(x)\n'
'\n'
' Parameters:\n'
- ' **x** (*tuple**[**int**, **int** | **str**]*) --\n'
+ ' **x** (*tuple**[**int**, **int** | **str**]*)\n'
'\n'
' Return type:\n'
' tuple[int, int]\n'
@@ -1117,11 +1117,11 @@ def test_autodoc_typehints_description_with_documented_init(app):
' Class docstring.\n'
'\n'
' Parameters:\n'
- ' * **x** (*int*) --\n'
+ ' * **x** (*int*)\n'
'\n'
- ' * **args** (*int*) --\n'
+ ' * **args** (*int*)\n'
'\n'
- ' * **kwargs** (*int*) --\n'
+ ' * **kwargs** (*int*)\n'
'\n'
' __init__(x, *args, **kwargs)\n'
'\n'
@@ -1217,9 +1217,9 @@ def test_autodoc_typehints_both(app):
assert ('target.typehints.incr(a: int, b: int = 1) -> int\n'
'\n'
' Parameters:\n'
- ' * **a** (*int*) --\n'
+ ' * **a** (*int*)\n'
'\n'
- ' * **b** (*int*) --\n'
+ ' * **b** (*int*)\n'
'\n'
' Return type:\n'
' int\n'
@@ -1227,7 +1227,7 @@ def test_autodoc_typehints_both(app):
assert ('target.typehints.tuple_args(x: tuple[int, int | str]) -> tuple[int, int]\n'
'\n'
' Parameters:\n'
- ' **x** (*tuple**[**int**, **int** | **str**]*) --\n'
+ ' **x** (*tuple**[**int**, **int** | **str**]*)\n'
'\n'
' Return type:\n'
' tuple[int, int]\n'
@@ -1401,9 +1401,9 @@ def test_autodoc_typehints_description_and_type_aliases(app):
' docstring\n'
'\n'
' Parameters:\n'
- ' * **x** (*myint*) --\n'
+ ' * **x** (*myint*)\n'
'\n'
- ' * **y** (*myint*) --\n'
+ ' * **y** (*myint*)\n'
'\n'
' Return type:\n'
' myint\n'
@@ -1584,6 +1584,14 @@ def test_autodoc_typehints_format_fully_qualified_for_newtype_alias(app):
@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
@@ -1627,7 +1635,7 @@ def test_autodoc_default_options(app):
assert ' Iterate squares of each value.' in actual
if not IS_PYPY:
assert ' .. py:attribute:: CustomIter.__weakref__' in actual
- assert ' list of weak references to the object (if defined)' 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
@@ -1651,13 +1659,21 @@ def test_autodoc_default_options(app):
assert ' Iterate squares of each value.' in actual
if not IS_PYPY:
assert ' .. py:attribute:: CustomIter.__weakref__' in actual
- assert ' list of weak references to the object (if defined)' 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')
@@ -1698,7 +1714,7 @@ def test_autodoc_default_options_with_values(app):
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 to the object (if defined)' not in actual
+ assert list_of_weak_references not in actual
# with :exclude-members:
app.config.autodoc_default_options = {
@@ -1722,6 +1738,6 @@ def test_autodoc_default_options_with_values(app):
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 to the object (if defined)' 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_ext_autodoc_events.py b/tests/test_extensions/test_ext_autodoc_events.py
index d821f4c..c0af254 100644
--- a/tests/test_ext_autodoc_events.py
+++ b/tests/test_extensions/test_ext_autodoc_events.py
@@ -4,7 +4,7 @@ import pytest
from sphinx.ext.autodoc import between, cut_lines
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
diff --git a/tests/test_ext_autodoc_mock.py b/tests/test_extensions/test_ext_autodoc_mock.py
index 3b90693..3b90693 100644
--- a/tests/test_ext_autodoc_mock.py
+++ b/tests/test_extensions/test_ext_autodoc_mock.py
diff --git a/tests/test_ext_autodoc_preserve_defaults.py b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py
index 70b6146..c1a00ab 100644
--- a/tests/test_ext_autodoc_preserve_defaults.py
+++ b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py
@@ -2,7 +2,7 @@
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc',
diff --git a/tests/test_ext_autodoc_private_members.py b/tests/test_extensions/test_ext_autodoc_private_members.py
index bf707bf..bf14414 100644
--- a/tests/test_ext_autodoc_private_members.py
+++ b/tests/test_extensions/test_ext_autodoc_private_members.py
@@ -3,7 +3,7 @@
import pytest
-from .test_ext_autodoc import do_autodoc
+from tests.test_extensions.autodoc_util import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc')
diff --git a/tests/test_ext_autosectionlabel.py b/tests/test_extensions/test_ext_autosectionlabel.py
index f99a6d3..f854ecf 100644
--- a/tests/test_ext_autosectionlabel.py
+++ b/tests/test_extensions/test_ext_autosectionlabel.py
@@ -7,42 +7,42 @@ import pytest
@pytest.mark.sphinx('html', testroot='ext-autosectionlabel')
def test_autosectionlabel_html(app, status, warning, skipped_labels=False):
- app.builder.build_all()
+ 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.S)
+ 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.S)
+ 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.S)
+ 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.S)
+ 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.S)
+ 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.S)
+ 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.S)
+ assert re.search(html, content, re.DOTALL)
-# Re-use test definition from above, just change the test root directory
+# 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)
@@ -51,27 +51,27 @@ def test_autosectionlabel_prefix_document_html(app, status, warning):
@pytest.mark.sphinx('html', testroot='ext-autosectionlabel',
confoverrides={'autosectionlabel_maxdepth': 3})
def test_autosectionlabel_maxdepth(app, status, warning):
- app.builder.build_all()
+ 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.S)
+ 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.S)
+ 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.S)
+ 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.S)
+ assert re.search(html, content, re.DOTALL)
assert "WARNING: undefined label: 'linux'" in warning.getvalue()
diff --git a/tests/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py
index 43f3ae0..d761978 100644
--- a/tests/test_ext_autosummary.py
+++ b/tests/test_extensions/test_ext_autosummary.py
@@ -154,7 +154,7 @@ def test_get_items_summary(make_app, app_params):
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
+ autosummary_items[name] = result # NoQA: PERF403
return results
def handler(app, what, name, obj, options, lines):
@@ -167,7 +167,7 @@ def test_get_items_summary(make_app, app_params):
sphinx.ext.autosummary.Autosummary.get_items = new_get_items
try:
- app.builder.build_all()
+ app.build(force_all=True)
finally:
sphinx.ext.autosummary.Autosummary.get_items = orig_get_items
@@ -207,7 +207,7 @@ def str_content(elem):
@pytest.mark.sphinx('xml', **default_kw)
def test_escaping(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
outdir = Path(app.builder.outdir)
@@ -358,7 +358,7 @@ def test_autosummary_generate_content_for_module_imported_members_inherited_modu
@pytest.mark.sphinx('dummy', testroot='ext-autosummary')
def test_autosummary_generate(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
doctree = app.env.get_doctree('index')
assert_node(doctree, (nodes.paragraph,
@@ -544,7 +544,7 @@ def test_autosummary_filename_map(app, status, warning):
@pytest.mark.sphinx('latex', **default_kw)
def test_autosummary_latex_table_colspec(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(status.getvalue())
print(warning.getvalue())
diff --git a/tests/test_ext_coverage.py b/tests/test_extensions/test_ext_coverage.py
index af8cf53..c9e9ba9 100644
--- a/tests/test_ext_coverage.py
+++ b/tests/test_extensions/test_ext_coverage.py
@@ -7,7 +7,7 @@ import pytest
@pytest.mark.sphinx('coverage')
def test_build(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
py_undoc = (app.outdir / 'python.txt').read_text(encoding='utf8')
assert py_undoc.startswith('Undocumented Python objects\n'
@@ -45,7 +45,7 @@ def test_build(app, status, warning):
@pytest.mark.sphinx('coverage', testroot='ext-coverage')
def test_coverage_ignore_pyobjects(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
actual = (app.outdir / 'python.txt').read_text(encoding='utf8')
expected = '''\
Undocumented Python objects
@@ -78,7 +78,7 @@ Classes:
@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True})
def test_show_missing_items(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert "undocumented" in status.getvalue()
@@ -92,7 +92,7 @@ def test_show_missing_items(app, status, warning):
@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True})
def test_show_missing_items_quiet(app, status, warning):
app.quiet = True
- app.builder.build_all()
+ 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()
diff --git a/tests/test_ext_doctest.py b/tests/test_extensions/test_ext_doctest.py
index c83e582..ab0dd62 100644
--- a/tests/test_ext_doctest.py
+++ b/tests/test_extensions/test_ext_doctest.py
@@ -16,9 +16,8 @@ cleanup_called = 0
def test_build(app, status, warning):
global cleanup_called
cleanup_called = 0
- app.builder.build_all()
- if app.statuscode != 0:
- raise AssertionError('failures in doctests:' + status.getvalue())
+ 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'
@@ -80,13 +79,13 @@ def test_skipif(app, status, warning):
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.builder.build_all()`` run, i.e.
+ 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.builder.build_all()
+ app.build(force_all=True)
if app.statuscode != 0:
raise AssertionError('failures in doctests:' + status.getvalue())
# The `:skipif:` expressions are always run.
@@ -124,7 +123,7 @@ 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.builder.build_all()
+ app.build(force_all=True)
failures = [line.replace(os.sep, '/')
for line in '\n'.join(written).splitlines()
diff --git a/tests/test_ext_duration.py b/tests/test_extensions/test_ext_duration.py
index 4fa4dfc..4fa4dfc 100644
--- a/tests/test_ext_duration.py
+++ b/tests/test_extensions/test_ext_duration.py
diff --git a/tests/test_ext_extlinks.py b/tests/test_extensions/test_ext_extlinks.py
index 7634db6..7634db6 100644
--- a/tests/test_ext_extlinks.py
+++ b/tests/test_extensions/test_ext_extlinks.py
diff --git a/tests/test_ext_githubpages.py b/tests/test_extensions/test_ext_githubpages.py
index 8e41537..879b6d1 100644
--- a/tests/test_ext_githubpages.py
+++ b/tests/test_extensions/test_ext_githubpages.py
@@ -5,7 +5,7 @@ import pytest
@pytest.mark.sphinx('html', testroot='ext-githubpages')
def test_githubpages(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / '.nojekyll').exists()
assert not (app.outdir / 'CNAME').exists()
@@ -13,7 +13,7 @@ def test_githubpages(app, status, warning):
@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.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / '.nojekyll').exists()
assert not (app.outdir / 'CNAME').exists()
@@ -21,6 +21,6 @@ def test_no_cname_for_github_io_domain(app, status, warning):
@pytest.mark.sphinx('html', testroot='ext-githubpages',
confoverrides={'html_baseurl': 'https://sphinx-doc.org'})
def test_cname_for_custom_domain(app, status, warning):
- app.builder.build_all()
+ 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_ext_graphviz.py b/tests/test_extensions/test_ext_graphviz.py
index d63dc2a..866a92a 100644
--- a/tests/test_ext_graphviz.py
+++ b/tests/test_extensions/test_ext_graphviz.py
@@ -11,40 +11,40 @@ 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.builder.build_all()
+ 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.S)
+ assert re.search(html, content, re.DOTALL)
html = 'Hello <div class="graphviz"><img .*?/></div>\n graphviz world'
- assert re.search(html, content, re.S)
+ 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.S)
+ 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.S)
+ 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.S)
+ 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.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
@@ -57,12 +57,12 @@ def test_graphviz_svg_html(app, status, warning):
r'<p><span class=\"caption-text\">caption of graph</span>.*</p>\n'
r'</figcaption>\n'
r'</figure>')
- assert re.search(html, content, re.S)
+ 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.S)
+ assert re.search(html, content, re.DOTALL)
html = (r'<figure class=\"align-right\" .*\>\n'
r'<div class="graphviz"><object data=\".*\.svg\".*>\n'
@@ -73,7 +73,7 @@ def test_graphviz_svg_html(app, status, warning):
r'<p><span class=\"caption-text\">on <em>right</em></span>.*</p>\n'
r'</figcaption>\n'
r'</figure>')
- assert re.search(html, content, re.S)
+ assert re.search(html, content, re.DOTALL)
html = (r'<div align=\"center\" class=\"align-center\">'
r'<div class="graphviz"><object data=\".*\.svg\".*>\n'
@@ -81,10 +81,10 @@ def test_graphviz_svg_html(app, status, warning):
r'centered\n'
r'}</p></object></div>\n'
r'</div>')
- assert re.search(html, content, re.S)
+ assert re.search(html, content, re.DOTALL)
image_re = r'.*data="([^"]+)".*?digraph test'
- image_path_match = re.search(image_re, content, re.S)
+ image_path_match = re.search(image_re, content, re.DOTALL)
assert image_path_match
image_path = image_path_match.group(1)
@@ -103,37 +103,37 @@ def test_graphviz_svg_html(app, status, warning):
@pytest.mark.sphinx('latex', testroot='ext-graphviz')
@pytest.mark.usefixtures('if_graphviz_found')
def test_graphviz_latex(app, status, warning):
- app.builder.build_all()
+ 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.S)
+ assert re.search(macro, content, re.DOTALL)
macro = 'Hello \\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf} graphviz world'
- assert re.search(macro, content, re.S)
+ 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.S)
+ assert re.search(macro, content, re.DOTALL)
macro = (r'\{\\hfill'
r'\\sphinxincludegraphics\[\]{graphviz-.*}'
r'\\hspace\*{\\fill}}')
- assert re.search(macro, content, re.S)
+ 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.builder.build_all()
+ 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.M)
+ assert re.search(html, content, re.MULTILINE)
def test_graphviz_parse_mapfile():
@@ -150,17 +150,17 @@ def test_graphviz_parse_mapfile():
# normal graph
code = ('digraph {\n'
- ' foo [href="http://www.google.com/"];\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="http://www.google.com/" title="foo" alt=""'
+ '<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 == 'grapviza4ccdd48ce'
+ assert cmap.id == 'grapvizff087ab863'
assert len(cmap.clickable) == 1
assert cmap.generate_clickable_map() == content.replace('%3', cmap.id)
diff --git a/tests/test_ext_ifconfig.py b/tests/test_extensions/test_ext_ifconfig.py
index 0292699..3e46b1e 100644
--- a/tests/test_ext_ifconfig.py
+++ b/tests/test_extensions/test_ext_ifconfig.py
@@ -9,7 +9,7 @@ from sphinx.testing import restructuredtext
@pytest.mark.sphinx('text', testroot='ext-ifconfig')
def test_ifconfig(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'index.txt').read_text(encoding='utf8')
assert 'spam' in result
assert 'ham' not in result
diff --git a/tests/test_ext_imgconverter.py b/tests/test_extensions/test_ext_imgconverter.py
index 18be700..c1d2061 100644
--- a/tests/test_ext_imgconverter.py
+++ b/tests/test_extensions/test_ext_imgconverter.py
@@ -10,7 +10,8 @@ def _if_converter_found(app):
image_converter = getattr(app.config, 'image_converter', '')
try:
if image_converter:
- subprocess.run([image_converter, '-version'], capture_output=True) # show version
+ # 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
@@ -21,7 +22,7 @@ def _if_converter_found(app):
@pytest.mark.usefixtures('_if_converter_found')
@pytest.mark.sphinx('latex', testroot='ext-imgconverter')
def test_ext_imgconverter(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'python.tex').read_text(encoding='utf8')
diff --git a/tests/test_ext_imgmockconverter.py b/tests/test_extensions/test_ext_imgmockconverter.py
index b5d4e79..4c3c64e 100644
--- a/tests/test_ext_imgmockconverter.py
+++ b/tests/test_extensions/test_ext_imgmockconverter.py
@@ -5,7 +5,7 @@ import pytest
@pytest.mark.sphinx('latex', testroot='ext-imgmockconverter')
def test_ext_imgmockconverter(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'python.tex').read_text(encoding='utf8')
diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_extensions/test_ext_inheritance_diagram.py
index 9ace5ad..c13ccea 100644
--- a/tests/test_ext_inheritance_diagram.py
+++ b/tests/test_extensions/test_ext_inheritance_diagram.py
@@ -33,7 +33,7 @@ def test_inheritance_diagram(app, status, warning):
InheritanceDiagram.run = new_run
try:
- app.builder.build_all()
+ app.build(force_all=True)
finally:
InheritanceDiagram.run = orig_run
@@ -160,7 +160,7 @@ def test_inheritance_diagram_png_html(tmp_path, app):
normalize_intersphinx_mapping(app, app.config)
load_mappings(app)
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
base_maps = re.findall('<map .+\n.+\n</map>', content)
@@ -171,7 +171,7 @@ def test_inheritance_diagram_png_html(tmp_path, app):
'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.M)
+ 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)
@@ -207,7 +207,7 @@ def test_inheritance_diagram_svg_html(tmp_path, app):
normalize_intersphinx_mapping(app, app.config)
load_mappings(app)
- app.builder.build_all()
+ 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)
@@ -216,12 +216,12 @@ def test_inheritance_diagram_svg_html(tmp_path, app):
'<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>'
+ '<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.M)
+ 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)
@@ -249,14 +249,14 @@ def test_inheritance_diagram_svg_html(tmp_path, app):
@pytest.mark.sphinx('latex', testroot='ext-inheritance_diagram')
@pytest.mark.usefixtures('if_graphviz_found')
def test_inheritance_diagram_latex(app, status, warning):
- app.builder.build_all()
+ 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.M)
+ assert re.search(pattern, content, re.MULTILINE)
@pytest.mark.sphinx('html', testroot='ext-inheritance_diagram',
@@ -264,7 +264,7 @@ def test_inheritance_diagram_latex(app, status, warning):
@pytest.mark.usefixtures('if_graphviz_found')
def test_inheritance_diagram_latex_alias(app, status, warning):
app.config.inheritance_alias = {'test.Foo': 'alias.Foo'}
- app.builder.build_all()
+ app.build(force_all=True)
doc = app.env.get_and_resolve_doctree('index', app)
aliased_graph = doc.children[0].children[3]['graph'].class_info
@@ -282,7 +282,7 @@ def test_inheritance_diagram_latex_alias(app, status, warning):
'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.M)
+ assert re.search(pattern, content, re.MULTILINE)
def test_import_classes(rootdir):
diff --git a/tests/test_ext_intersphinx.py b/tests/test_extensions/test_ext_intersphinx.py
index 82bec9e..ef5a9b1 100644
--- a/tests/test_ext_intersphinx.py
+++ b/tests/test_extensions/test_ext_intersphinx.py
@@ -18,9 +18,10 @@ from sphinx.ext.intersphinx import (
normalize_intersphinx_mapping,
)
from sphinx.ext.intersphinx import setup as intersphinx_setup
+from sphinx.util.console import strip_colors
-from .test_util_inventory import inventory_v2, inventory_v2_not_having_version
-from .utils import http_server
+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):
@@ -52,46 +53,46 @@ def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status,
_read_from_url().readline.return_value = b'# Sphinx inventory version 2'
# same uri and inv, not redirected
- _read_from_url().url = 'http://hostname/' + INVENTORY_FILENAME
- fetch_inventory(app, 'http://hostname/', 'http://hostname/' + INVENTORY_FILENAME)
+ _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] == 'http://hostname/'
+ assert InventoryFile.load.call_args[0][1] == 'https://hostname/'
# same uri and inv, redirected
status.seek(0)
status.truncate(0)
- _read_from_url().url = 'http://hostname/new/' + INVENTORY_FILENAME
+ _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME
- fetch_inventory(app, 'http://hostname/', 'http://hostname/' + INVENTORY_FILENAME)
+ fetch_inventory(app, 'https://hostname/', 'https://hostname/' + INVENTORY_FILENAME)
assert status.getvalue() == ('intersphinx inventory has moved: '
- 'http://hostname/%s -> http://hostname/new/%s\n' %
+ 'https://hostname/%s -> https://hostname/new/%s\n' %
(INVENTORY_FILENAME, INVENTORY_FILENAME))
- assert InventoryFile.load.call_args[0][1] == 'http://hostname/new'
+ 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 = 'http://hostname/new/' + INVENTORY_FILENAME
+ _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME
- fetch_inventory(app, 'http://hostname/', 'http://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] == 'http://hostname/'
+ assert InventoryFile.load.call_args[0][1] == 'https://hostname/'
# different uri and inv, redirected
status.seek(0)
status.truncate(0)
- _read_from_url().url = 'http://hostname/other/' + INVENTORY_FILENAME
+ _read_from_url().url = 'https://hostname/other/' + INVENTORY_FILENAME
- fetch_inventory(app, 'http://hostname/', 'http://hostname/new/' + INVENTORY_FILENAME)
+ fetch_inventory(app, 'https://hostname/', 'https://hostname/new/' + INVENTORY_FILENAME)
assert status.getvalue() == ('intersphinx inventory has moved: '
- 'http://hostname/new/%s -> http://hostname/other/%s\n' %
+ 'https://hostname/new/%s -> https://hostname/other/%s\n' %
(INVENTORY_FILENAME, INVENTORY_FILENAME))
- assert InventoryFile.load.call_args[0][1] == 'http://hostname/'
+ 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)
+ 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)),
@@ -169,7 +170,7 @@ def test_missing_reference(tmp_path, app, status, warning):
def test_missing_reference_pydomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory'
- inv_file.write_bytes(inventory_v2)
+ inv_file.write_bytes(INVENTORY_V2)
set_config(app, {
'https://docs.python.org/': str(inv_file),
})
@@ -196,20 +197,10 @@ def test_missing_reference_pydomain(tmp_path, app, status, warning):
rn = missing_reference(app, app.env, node, contnode)
assert rn.astext() == 'Foo.bar'
- # 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'
-
def test_missing_reference_stddomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory'
- inv_file.write_bytes(inventory_v2)
+ inv_file.write_bytes(INVENTORY_V2)
set_config(app, {
'cmd': ('https://docs.python.org/', str(inv_file)),
})
@@ -236,11 +227,31 @@ def test_missing_reference_stddomain(tmp_path, app, status, warning):
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)
+ inv_file.write_bytes(INVENTORY_V2)
set_config(app, {
'https://docs.python.org/': str(inv_file),
})
@@ -266,7 +277,7 @@ def test_missing_reference_cppdomain(tmp_path, app, status, warning):
def test_missing_reference_jsdomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory'
- inv_file.write_bytes(inventory_v2)
+ inv_file.write_bytes(INVENTORY_V2)
set_config(app, {
'https://docs.python.org/': str(inv_file),
})
@@ -290,7 +301,7 @@ def test_missing_reference_jsdomain(tmp_path, app, status, warning):
def test_missing_reference_disabled_domain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory'
- inv_file.write_bytes(inventory_v2)
+ inv_file.write_bytes(INVENTORY_V2)
set_config(app, {
'inv': ('https://docs.python.org/', str(inv_file)),
})
@@ -352,7 +363,7 @@ def test_missing_reference_disabled_domain(tmp_path, app, status, warning):
def test_inventory_not_having_version(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory'
- inv_file.write_bytes(inventory_v2_not_having_version)
+ inv_file.write_bytes(INVENTORY_V2_NO_VERSION)
set_config(app, {
'https://docs.python.org/': str(inv_file),
})
@@ -374,14 +385,14 @@ def test_load_mappings_warnings(tmp_path, app, status, warning):
identifiers are not string
"""
inv_file = tmp_path / 'inventory'
- inv_file.write_bytes(inventory_v2)
+ 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': ('http://docs.repoze.org/workflow/', str(inv_file)),
- 'django-taggit': ('http://django-taggit.readthedocs.org/en/latest/',
+ 'repoze.workflow': ('https://docs.repoze.org/workflow/', str(inv_file)),
+ 'django-taggit': ('https://django-taggit.readthedocs.org/en/latest/',
str(inv_file)),
- 12345: ('http://www.sphinx-doc.org/en/stable/', str(inv_file)),
+ 12345: ('https://www.sphinx-doc.org/en/stable/', str(inv_file)),
})
# load the inventory and check if it's done correctly
@@ -395,7 +406,7 @@ def test_load_mappings_warnings(tmp_path, app, status, warning):
def test_load_mappings_fallback(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory'
- inv_file.write_bytes(inventory_v2)
+ inv_file.write_bytes(INVENTORY_V2)
set_config(app, {})
# connect to invalid path
@@ -429,23 +440,25 @@ def test_load_mappings_fallback(tmp_path, app, status, warning):
class TestStripBasicAuth:
"""Tests for sphinx.ext.intersphinx._strip_basic_auth()"""
+
def test_auth_stripped(self):
- """basic auth creds stripped from URL containing creds"""
+ """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 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"""
+ """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)
@@ -492,7 +505,7 @@ def test_inspect_main_noargs(capsys):
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)
+ inv_file.write_bytes(INVENTORY_V2)
inspect_main([str(inv_file)])
@@ -507,15 +520,14 @@ def test_inspect_main_url(capsys):
def do_GET(self):
self.send_response(200, "OK")
self.end_headers()
- self.wfile.write(inventory_v2)
+ self.wfile.write(INVENTORY_V2)
def log_message(*args, **kwargs):
# Silenced.
pass
- url = 'http://localhost:7777/' + INVENTORY_FILENAME
-
- with http_server(InventoryHandler):
+ with http_server(InventoryHandler) as server:
+ url = f'http://localhost:{server.server_port}/{INVENTORY_FILENAME}'
inspect_main([url])
stdout, stderr = capsys.readouterr()
@@ -526,9 +538,9 @@ def test_inspect_main_url(capsys):
@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)
+ inv_file.write_bytes(INVENTORY_V2)
app.config.intersphinx_mapping = {
- 'inv': ('http://example.org/', str(inv_file)),
+ 'inv': ('https://example.org/', str(inv_file)),
}
app.config.intersphinx_cache_limit = 0
app.config.nitpicky = True
@@ -539,22 +551,27 @@ def test_intersphinx_role(app, warning):
app.build()
content = (app.outdir / 'index.html').read_text(encoding='utf8')
- wStr = warning.getvalue()
-
- html = '<a class="reference external" href="http://example.org/{}" title="(in foo v2.0)">'
+ 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 "WARNING: external py:mod reference target not found: module3" in wStr
- assert "WARNING: external py:mod reference target not found: module10" in wStr
assert html.format('sub/foo.html#module1.func') in content
- assert "WARNING: external py:meth reference target not found: inv:Foo.bar" in wStr
-
- assert "WARNING: role for external cross-reference not found: py:nope" in wStr
# default domain
assert html.format('index.html#std_uint8_t') in content
- assert "WARNING: role for external cross-reference not found: nope" in wStr
# std roles without domain prefix
assert html.format('docname.html') in content
@@ -562,7 +579,6 @@ def test_intersphinx_role(app, warning):
# explicit inventory
assert html.format('cfunc.html#CFunc') in content
- assert "WARNING: inventory for external cross-reference not found: invNope" in wStr
# explicit title
assert html.format('index.html#foons') in content
diff --git a/tests/test_ext_math.py b/tests/test_extensions/test_ext_math.py
index d5331f8..b673f83 100644
--- a/tests/test_ext_math.py
+++ b/tests/test_extensions/test_ext_math.py
@@ -25,9 +25,9 @@ def has_binary(binary):
@pytest.mark.skipif(not has_binary('dvipng'),
reason='Requires dvipng" binary')
@pytest.mark.sphinx('html', testroot='ext-math-simple',
- confoverrides = {'extensions': ['sphinx.ext.imgmath']})
+ confoverrides={'extensions': ['sphinx.ext.imgmath']})
def test_imgmath_png(app, status, warning):
- app.builder.build_all()
+ 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)
@@ -39,7 +39,7 @@ def test_imgmath_png(app, status, warning):
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.S)
+ assert re.search(html, content, re.DOTALL)
@pytest.mark.skipif(not has_binary('dvisvgm'),
@@ -48,7 +48,7 @@ def test_imgmath_png(app, status, warning):
confoverrides={'extensions': ['sphinx.ext.imgmath'],
'imgmath_image_format': 'svg'})
def test_imgmath_svg(app, status, warning):
- app.builder.build_all()
+ 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)
@@ -60,7 +60,7 @@ def test_imgmath_svg(app, status, warning):
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.S)
+ assert re.search(html, content, re.DOTALL)
@pytest.mark.skipif(not has_binary('dvisvgm'),
@@ -70,7 +70,7 @@ def test_imgmath_svg(app, status, warning):
'imgmath_image_format': 'svg',
'imgmath_embed': True})
def test_imgmath_svg_embed(app, status, warning):
- app.builder.build_all()
+ 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)
@@ -88,7 +88,7 @@ def test_imgmath_svg_embed(app, status, warning):
confoverrides={'extensions': ['sphinx.ext.mathjax'],
'mathjax_options': {'integrity': 'sha384-0123456789'}})
def test_mathjax_options(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
shutil.rmtree(app.outdir)
@@ -100,14 +100,14 @@ def test_mathjax_options(app, status, warning):
@pytest.mark.sphinx('html', testroot='ext-math',
confoverrides={'extensions': ['sphinx.ext.mathjax']})
def test_mathjax_align(app, status, warning):
- app.builder.build_all()
+ 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.S)
+ assert re.search(html, content, re.DOTALL)
@pytest.mark.sphinx('html', testroot='ext-math',
@@ -119,7 +119,7 @@ def test_math_number_all_mathjax(app, status, warning):
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.S)
+ assert re.search(html, content, re.DOTALL)
@pytest.mark.sphinx('latex', testroot='ext-math',
@@ -131,31 +131,31 @@ def test_math_number_all_latex(app, status, warning):
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.S)
+ assert re.search(macro, content, re.DOTALL)
macro = r'Inline \\\(E=mc\^2\\\)'
- assert re.search(macro, content, re.S)
+ 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.S)
+ 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.S)
+ assert re.search(macro, content, re.DOTALL)
macro = r'Referencing equation \\eqref{equation:math:foo}.'
- assert re.search(macro, content, re.S)
+ 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.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'math.html').read_text(encoding='utf8')
html = ('<p>Referencing equation <a class="reference internal" '
@@ -168,12 +168,12 @@ def test_math_eqref_format_html(app, status, warning):
confoverrides={'extensions': ['sphinx.ext.mathjax'],
'math_eqref_format': 'Eq.{number}'})
def test_math_eqref_format_latex(app, status, warning):
- app.builder.build_all()
+ 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.S)
+ assert re.search(macro, content, re.DOTALL)
@pytest.mark.sphinx('html', testroot='ext-math',
@@ -181,7 +181,7 @@ def test_math_eqref_format_latex(app, status, warning):
'numfig': True,
'math_numfig': True})
def test_mathjax_numfig_html(app, status, warning):
- app.builder.build_all()
+ 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'
@@ -199,7 +199,7 @@ def test_mathjax_numfig_html(app, status, warning):
'numfig_secnum_depth': 0,
'math_numfig': True})
def test_imgmath_numfig_html(app, status, warning):
- app.builder.build_all()
+ 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"'
@@ -213,7 +213,7 @@ def test_imgmath_numfig_html(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='ext-math-compat')
def test_math_compat(app, status, warning):
with warnings.catch_warnings(record=True):
- app.builder.build_all()
+ app.build(force_all=True)
doctree = app.env.get_and_resolve_doctree('index', app.builder)
assert_node(doctree,
@@ -239,7 +239,7 @@ def test_math_compat(app, status, warning):
confoverrides={'extensions': ['sphinx.ext.mathjax'],
'mathjax3_config': {'extensions': ['tex2jax.js']}})
def test_mathjax3_config(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert MATHJAX_URL in content
@@ -251,7 +251,7 @@ def test_mathjax3_config(app, status, warning):
confoverrides={'extensions': ['sphinx.ext.mathjax'],
'mathjax2_config': {'extensions': ['tex2jax.js']}})
def test_mathjax2_config(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert ('<script async="async" src="%s">' % MATHJAX_URL in content)
@@ -265,7 +265,7 @@ def test_mathjax2_config(app, status, warning):
'mathjax_options': {'async': 'async'},
'mathjax3_config': {'extensions': ['tex2jax.js']}})
def test_mathjax_options_async_for_mathjax3(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert MATHJAX_URL in content
@@ -277,7 +277,7 @@ def test_mathjax_options_async_for_mathjax3(app, status, warning):
'mathjax_options': {'defer': 'defer'},
'mathjax2_config': {'extensions': ['tex2jax.js']}})
def test_mathjax_options_defer_for_mathjax2(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert ('<script defer="defer" src="%s">' % MATHJAX_URL in content)
@@ -291,7 +291,7 @@ def test_mathjax_options_defer_for_mathjax2(app, status, warning):
},
)
def test_mathjax_path(app):
- app.builder.build_all()
+ 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
@@ -305,7 +305,7 @@ def test_mathjax_path(app):
},
)
def test_mathjax_path_config(app):
- app.builder.build_all()
+ 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
@@ -314,7 +314,7 @@ def test_mathjax_path_config(app):
@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.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert MATHJAX_URL in content
@@ -326,7 +326,7 @@ def test_mathjax_is_installed_only_if_document_having_math(app, status, warning)
@pytest.mark.sphinx('html', testroot='basic',
confoverrides={'extensions': ['sphinx.ext.mathjax']})
def test_mathjax_is_not_installed_if_no_equations(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert 'MathJax.js' not in content
@@ -336,10 +336,54 @@ def test_mathjax_is_not_installed_if_no_equations(app, status, warning):
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.builder.build_all()
+ 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_ext_napoleon.py b/tests/test_extensions/test_ext_napoleon.py
index 00b7ac1..466bd49 100644
--- a/tests/test_ext_napoleon.py
+++ b/tests/test_extensions/test_ext_napoleon.py
@@ -55,7 +55,7 @@ class SampleClass:
@simple_decorator
def __decorated_func__(self):
- """doc"""
+ """Doc"""
pass
diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_extensions/test_ext_napoleon_docstring.py
index 87fad61..d7ef489 100644
--- a/tests/test_ext_napoleon_docstring.py
+++ b/tests/test_extensions/test_ext_napoleon_docstring.py
@@ -1,13 +1,16 @@
"""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,
@@ -17,9 +20,10 @@ from sphinx.ext.napoleon.docstring import (
_token_type,
_tokenize_type_spec,
)
+from sphinx.testing.util import etree_parse
-from .ext_napoleon_pep526_data_google import PEP526GoogleClass
-from .ext_napoleon_pep526_data_numpy import PEP526NumpyClass
+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'))):
@@ -36,6 +40,7 @@ class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))):
Adds a newline after the type
"""
+
# To avoid creating a dict, as a namedtuple doesn't have it:
__slots__ = ()
@@ -1156,7 +1161,7 @@ Methods:
description
-""" # noqa: W293
+""" # NoQA: W293
config = Config()
actual = str(GoogleDocstring(docstring, config=config, app=None, what='module',
options={'no-index': True}))
@@ -1186,7 +1191,7 @@ Do as you please
actual = str(GoogleDocstring(cleandoc(PEP526GoogleClass.__doc__), config, app=None, what="class",
obj=PEP526GoogleClass))
expected = """\
-Sample class with PEP 526 annotations and google docstring
+Sample class with PEP 526 annotations and google docstring.
.. attribute:: attr1
@@ -2658,3 +2663,41 @@ def test_napoleon_and_autodoc_typehints_description_documented_params(app, statu
'\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_ext_todo.py b/tests/test_extensions/test_ext_todo.py
index 7d39495..1903f9f 100644
--- a/tests/test_ext_todo.py
+++ b/tests/test_extensions/test_ext_todo.py
@@ -14,7 +14,7 @@ def test_todo(app, status, warning):
todos.append(node)
app.connect('todo-defined', on_todo_defined)
- app.builder.build_all()
+ app.build(force_all=True)
# check todolist
content = (app.outdir / 'index.html').read_text(encoding='utf8')
@@ -52,7 +52,7 @@ def test_todo_not_included(app, status, warning):
todos.append(node)
app.connect('todo-defined', on_todo_defined)
- app.builder.build_all()
+ app.build(force_all=True)
# check todolist
content = (app.outdir / 'index.html').read_text(encoding='utf8')
@@ -86,9 +86,8 @@ def test_todo_valid_link(app, status, warning):
that exists in the LaTeX output. The target was previously incorrectly
omitted (GitHub issue #1020).
"""
-
# Ensure the LaTeX output is built.
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'python.tex').read_text(encoding='utf8')
diff --git a/tests/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py
index a1a0a6d..b2c6fc0 100644
--- a/tests/test_ext_viewcode.py
+++ b/tests/test_extensions/test_ext_viewcode.py
@@ -44,7 +44,7 @@ def check_viewcode_output(app, warning):
confoverrides={"viewcode_line_numbers": True})
def test_viewcode_linenos(app, warning):
shutil.rmtree(app.outdir / '_modules', ignore_errors=True)
- app.builder.build_all()
+ app.build(force_all=True)
result = check_viewcode_output(app, warning)
assert '<span class="linenos"> 1</span>' in result
@@ -54,7 +54,7 @@ def test_viewcode_linenos(app, warning):
confoverrides={"viewcode_line_numbers": False})
def test_viewcode(app, warning):
shutil.rmtree(app.outdir / '_modules', ignore_errors=True)
- app.builder.build_all()
+ app.build(force_all=True)
result = check_viewcode_output(app, warning)
assert 'class="linenos">' not in result
@@ -63,7 +63,7 @@ def test_viewcode(app, warning):
@pytest.mark.sphinx('epub', testroot='ext-viewcode')
def test_viewcode_epub_default(app, status, warning):
shutil.rmtree(app.outdir)
- app.builder.build_all()
+ app.build(force_all=True)
assert not (app.outdir / '_modules/spam/mod1.xhtml').exists()
@@ -74,7 +74,7 @@ def test_viewcode_epub_default(app, status, warning):
@pytest.mark.sphinx('epub', testroot='ext-viewcode',
confoverrides={'viewcode_enable_epub': True})
def test_viewcode_epub_enabled(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / '_modules/spam/mod1.xhtml').exists()
@@ -84,14 +84,14 @@ def test_viewcode_epub_enabled(app, status, warning):
@pytest.mark.sphinx(testroot='ext-viewcode', tags=['test_linkcode'])
def test_linkcode(app, status, warning):
- app.builder.build(['objects'])
+ app.build(filenames=[app.srcdir / 'objects.rst'])
stuff = (app.outdir / 'objects.html').read_text(encoding='utf8')
- assert 'http://foobar/source/foolib.py' in stuff
- assert 'http://foobar/js/' in stuff
- assert 'http://foobar/c/' in stuff
- assert 'http://foobar/cpp/' in stuff
+ 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)
@@ -117,7 +117,7 @@ def test_local_source_files(app, status, warning):
return (source, tags)
app.connect('viewcode-find-source', find_source)
- app.builder.build_all()
+ app.build(force_all=True)
warnings = re.sub(r'\\+', '/', warning.getvalue())
assert re.findall(
diff --git a/tests/test_extension.py b/tests/test_extensions/test_extension.py
index d74743c..d74743c 100644
--- a/tests/test_extension.py
+++ b/tests/test_extensions/test_extension.py
diff --git a/tests/test_intl/__init__.py b/tests/test_intl/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_intl/__init__.py
diff --git a/tests/test_catalogs.py b/tests/test_intl/test_catalogs.py
index b7fd7be..b7fd7be 100644
--- a/tests/test_catalogs.py
+++ b/tests/test_intl/test_catalogs.py
diff --git a/tests/test_intl.py b/tests/test_intl/test_intl.py
index a07ebfb..6b1e9ba 100644
--- a/tests/test_intl.py
+++ b/tests/test_intl/test_intl.py
@@ -8,7 +8,6 @@ import os.path
import re
import shutil
import time
-from pathlib import Path
import pytest
from babel.messages import mofile, pofile
@@ -16,13 +15,16 @@ from babel.messages.catalog import Catalog
from docutils import nodes
from sphinx import locale
-from sphinx.testing.util import assert_node, etree_parse, strip_escseq
+from sphinx.testing.util import assert_node, etree_parse
+from sphinx.util.console import strip_colors
from sphinx.util.nodes import NodeMatcher
+_CATALOG_LOCALE = 'xx'
+
sphinx_intl = pytest.mark.sphinx(
testroot='intl',
confoverrides={
- 'language': 'xx', 'locale_dirs': ['.'],
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
'gettext_compact': False,
},
)
@@ -38,22 +40,20 @@ def write_mo(pathname, po):
return mofile.write_mo(f, po)
-@pytest.fixture(autouse=True)
-def _setup_intl(app_params):
- assert isinstance(app_params.kwargs['srcdir'], Path)
- srcdir = app_params.kwargs['srcdir']
- for dirpath, _dirs, files in os.walk(srcdir):
- dirpath = Path(dirpath)
- for f in [f for f in files if f.endswith('.po')]:
- po = str(dirpath / f)
- mo = srcdir / 'xx' / 'LC_MESSAGES' / (
- os.path.relpath(po[:-3], srcdir) + '.mo')
- if not mo.parent.exists():
- mo.parent.mkdir(parents=True, exist_ok=True)
-
- if not mo.exists() or os.stat(mo).st_mtime < os.stat(po).st_mtime:
- # compile .mo file only if needed
- write_mo(mo, read_po(po))
+def _set_mtime_ns(target, value):
+ os.utime(target, ns=(value, value))
+ return os.stat(target).st_mtime_ns
+
+
+def _get_bom_intl_path(app):
+ basedir = app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES'
+ return basedir / 'bom.po', basedir / 'bom.mo'
+
+
+def _get_update_targets(app):
+ app.env.find_files(app.config, app.builder)
+ added, changed, removed = app.env.get_outdated_files(config_changed=False)
+ return added, changed, removed
@pytest.fixture(autouse=True)
@@ -316,7 +316,7 @@ def test_text_glossary_term_inconsistencies(app, warning):
def test_gettext_section(app):
app.build()
# --- section
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'section.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po')
actual = read_po(app.outdir / 'section.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -329,7 +329,7 @@ def test_text_section(app):
app.build()
# --- section
result = (app.outdir / 'section.txt').read_text(encoding='utf8')
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'section.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.string in result
@@ -468,12 +468,12 @@ def test_text_admonitions(app):
def test_gettext_toctree(app):
app.build()
# --- toctree (index.rst)
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'index.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'index.po')
actual = read_po(app.outdir / 'index.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
# --- toctree (toctree.rst)
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po')
actual = read_po(app.outdir / 'toctree.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -485,7 +485,7 @@ def test_gettext_toctree(app):
def test_gettext_table(app):
app.build()
# --- toctree
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'table.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po')
actual = read_po(app.outdir / 'table.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -498,7 +498,7 @@ def test_text_table(app):
app.build()
# --- toctree
result = (app.outdir / 'table.txt').read_text(encoding='utf8')
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'table.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.string in result
@@ -515,7 +515,7 @@ def test_text_toctree(app):
assert 'TABLE OF CONTENTS' in result
# --- toctree (toctree.rst)
result = (app.outdir / 'toctree.txt').read_text(encoding='utf8')
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po')
for expect_msg in (m for m in expect if m.id):
assert expect_msg.string in result
@@ -526,7 +526,7 @@ def test_text_toctree(app):
def test_gettext_topic(app):
app.build()
# --- topic
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'topic.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po')
actual = read_po(app.outdir / 'topic.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -539,7 +539,7 @@ def test_text_topic(app):
app.build()
# --- topic
result = (app.outdir / 'topic.txt').read_text(encoding='utf8')
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'topic.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.string in result
@@ -550,7 +550,7 @@ def test_text_topic(app):
def test_gettext_definition_terms(app):
app.build()
# --- definition terms: regression test for #2198, #2205
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'definition_terms.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'definition_terms.po')
actual = read_po(app.outdir / 'definition_terms.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -562,7 +562,7 @@ def test_gettext_definition_terms(app):
def test_gettext_glossary_terms(app, warning):
app.build()
# --- glossary terms: regression test for #1090
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'glossary_terms.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms.po')
actual = read_po(app.outdir / 'glossary_terms.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -576,7 +576,7 @@ def test_gettext_glossary_terms(app, warning):
def test_gettext_glossary_term_inconsistencies(app):
app.build()
# --- glossary term inconsistencies: regression test for #1090
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'glossary_terms_inconsistency.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms_inconsistency.po')
actual = read_po(app.outdir / 'glossary_terms_inconsistency.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -588,7 +588,7 @@ def test_gettext_glossary_term_inconsistencies(app):
def test_gettext_literalblock(app):
app.build()
# --- gettext builder always ignores ``only`` directive
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'literalblock.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'literalblock.po')
actual = read_po(app.outdir / 'literalblock.pot')
for expect_msg in [m for m in expect if m.id]:
if len(expect_msg.id.splitlines()) == 1:
@@ -604,7 +604,7 @@ def test_gettext_literalblock(app):
def test_gettext_buildr_ignores_only_directive(app):
app.build()
# --- gettext builder always ignores ``only`` directive
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'only.po')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'only.po')
actual = read_po(app.outdir / 'only.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
@@ -612,7 +612,7 @@ def test_gettext_buildr_ignores_only_directive(app):
@sphinx_intl
def test_node_translated_attribute(app):
- app.builder.build_specific([app.srcdir / 'translation_progress.txt'])
+ app.build(filenames=[app.srcdir / 'translation_progress.txt'])
doctree = app.env.get_doctree('translation_progress')
@@ -625,7 +625,7 @@ def test_node_translated_attribute(app):
@sphinx_intl
def test_translation_progress_substitution(app):
- app.builder.build_specific([app.srcdir / 'translation_progress.txt'])
+ app.build(filenames=[app.srcdir / 'translation_progress.txt'])
doctree = app.env.get_doctree('translation_progress')
@@ -633,12 +633,12 @@ def test_translation_progress_substitution(app):
@pytest.mark.sphinx(testroot='intl', freshenv=True, confoverrides={
- 'language': 'xx', 'locale_dirs': ['.'],
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
'gettext_compact': False,
'translation_progress_classes': True,
})
def test_translation_progress_classes_true(app):
- app.builder.build_specific([app.srcdir / 'translation_progress.txt'])
+ app.build(filenames=[app.srcdir / 'translation_progress.txt'])
doctree = app.env.get_doctree('translation_progress')
@@ -681,50 +681,188 @@ def test_translation_progress_classes_true(app):
assert len(doctree[0]) == 20
+class _MockClock:
+ """Object for mocking :func:`time.time_ns` (if needed).
+
+ Use :meth:`sleep` to make this specific clock sleep for some time.
+ """
+
+ def time(self) -> int:
+ """Nanosecond since 'fake' epoch."""
+ raise NotImplementedError
+
+ def sleep(self, ds: float) -> None:
+ """Sleep *ds* seconds."""
+ raise NotImplementedError
+
+
+class _MockWindowsClock(_MockClock):
+ """Object for mocking :func:`time.time_ns` on Windows platforms.
+
+ The result is in 'nanoseconds' but with a microsecond resolution
+ so that the division by 1_000 does not cause rounding issues.
+ """
+
+ def __init__(self) -> None:
+ self.us: int = 0 # current microsecond 'tick'
+
+ def time(self) -> int:
+ ret = 1_000 * self.us
+ self.us += 1
+ return ret
+
+ def sleep(self, ds: float) -> None:
+ self.us += int(ds * 1e6)
+
+
+class _MockUnixClock(_MockClock):
+ """Object for mocking :func:`time.time_ns` on Unix platforms.
+
+ Since nothing is needed for Unix platforms, this object acts as
+ a proxy so that the API is the same as :class:`_MockWindowsClock`.
+ """
+
+ def time(self) -> int:
+ return time.time_ns()
+
+ def sleep(self, ds: float) -> None:
+ time.sleep(ds)
+
+
+@pytest.fixture()
+def mock_time_and_i18n(
+ monkeypatch: pytest.MonkeyPatch,
+) -> tuple[pytest.MonkeyPatch, _MockClock]:
+ from sphinx.util.i18n import CatalogInfo
+
+ # save the 'original' definition
+ catalog_write_mo = CatalogInfo.write_mo
+
+ def mock_write_mo(self, locale, use_fuzzy=False):
+ catalog_write_mo(self, locale, use_fuzzy)
+ # ensure that the .mo file being written has a correct fake timestamp
+ _set_mtime_ns(self.mo_path, time.time_ns())
+
+ # see: https://github.com/pytest-dev/pytest/issues/363
+ with pytest.MonkeyPatch.context() as mock:
+ if os.name == 'posix':
+ clock = _MockUnixClock()
+ else:
+ # When using pytest.mark.parametrize() to emulate test repetition,
+ # the teardown phase on Windows fails due to an error apparently in
+ # the colorama.ansitowin32 module, so we forcibly disable colors.
+ mock.setenv('NO_COLOR', '1')
+ # apply the patch only for Windows
+ clock = _MockWindowsClock()
+ mock.setattr('time.time_ns', clock.time)
+ # Use clock.sleep() to emulate time.sleep() but do not try
+ # to mock the latter since this might break other libraries.
+ mock.setattr('sphinx.util.i18n.CatalogInfo.write_mo', mock_write_mo)
+ yield mock, clock
+
+
@sphinx_intl
-# use individual shared_result directory to avoid "incompatible doctree" error
-@pytest.mark.sphinx(testroot='builder-gettext-dont-rebuild-mo')
-def test_gettext_dont_rebuild_mo(make_app, app_params):
- # --- don't rebuild by .mo mtime
- def get_update_targets(app_):
- app_.env.find_files(app_.config, app_.builder)
- added, changed, removed = app_.env.get_outdated_files(config_changed=False)
- return added, changed, removed
+# use the same testroot as 'gettext' since the latter contains less PO files
+@pytest.mark.sphinx('dummy', testroot='builder-gettext-dont-rebuild-mo', freshenv=True)
+def test_dummy_should_rebuild_mo(mock_time_and_i18n, make_app, app_params):
+ mock, clock = mock_time_and_i18n
+ assert os.name == 'posix' or clock.time() == 0
args, kwargs = app_params
+ app = make_app(*args, **kwargs)
+ po_path, mo_path = _get_bom_intl_path(app)
+
+ # creation time of the those files (order does not matter)
+ bom_rst = app.srcdir / 'bom.rst'
+ bom_rst_time = time.time_ns()
+
+ index_rst = app.srcdir / 'index.rst'
+ index_rst_time = time.time_ns()
+ po_time = time.time_ns()
+
+ # patch the 'creation time' of the source files
+ assert _set_mtime_ns(po_path, po_time) == po_time
+ assert _set_mtime_ns(bom_rst, bom_rst_time) == bom_rst_time
+ assert _set_mtime_ns(index_rst, index_rst_time) == index_rst_time
+
+ assert not mo_path.exists()
+ # when writing mo files, the counter is updated by calling
+ # patch_write_mo which is called to create .mo files (and
+ # thus the timestamp of the files are not those given by
+ # the OS but our fake ones)
+ app.build()
+ assert mo_path.exists()
+ # Do a real sleep on POSIX, or simulate a sleep on Windows
+ # to ensure that calls to time.time_ns() remain consistent.
+ clock.sleep(0.1 if os.name == 'posix' else 1)
+
+ # check that the source files were not modified
+ assert bom_rst.stat().st_mtime_ns == bom_rst_time
+ assert index_rst.stat().st_mtime_ns == index_rst_time
+ # check that the 'bom' document is discovered after the .mo
+ # file has been written on the disk (i.e., read_doc() is called
+ # after the creation of the .mo files)
+ assert app.env.all_docs['bom'] > mo_path.stat().st_mtime_ns // 1000
- # phase1: build document with non-gettext builder and generate mo file in srcdir
- app0 = make_app('dummy', *args, **kwargs)
- app0.build()
- time.sleep(0.01)
- assert (app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').exists()
# Since it is after the build, the number of documents to be updated is 0
- update_targets = get_update_targets(app0)
- assert update_targets[1] == set(), update_targets
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == set()
# When rewriting the timestamp of mo file, the number of documents to be
# updated will be changed.
- mtime = (app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').stat().st_mtime
- os.utime(app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo', (mtime + 5, mtime + 5))
- update_targets = get_update_targets(app0)
- assert update_targets[1] == {'bom'}, update_targets
+ new_mo_time = time.time_ns()
+ assert _set_mtime_ns(mo_path, new_mo_time) == new_mo_time
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == {'bom'}
+ mock.undo() # explicit call since it's not a context
- # Because doctree for gettext builder can not be shared with other builders,
- # erase doctreedir before gettext build.
- shutil.rmtree(app0.doctreedir)
+ # remove all sources for the next test
+ shutil.rmtree(app.srcdir, ignore_errors=True)
+ time.sleep(0.1 if os.name == 'posix' else 0.5) # real sleep
- # phase2: build document with gettext builder.
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext', testroot='builder-gettext-dont-rebuild-mo', freshenv=True)
+def test_gettext_dont_rebuild_mo(mock_time_and_i18n, app):
+ mock, clock = mock_time_and_i18n
+ assert os.name == 'posix' or clock.time() == 0
+
+ assert app.srcdir.exists()
+
+ # patch the 'creation time' of the source files
+ bom_rst = app.srcdir / 'bom.rst'
+ bom_rst_time = time.time_ns()
+ assert _set_mtime_ns(bom_rst, bom_rst_time) == bom_rst_time
+
+ index_rst = app.srcdir / 'index.rst'
+ index_rst_time = time.time_ns()
+ assert _set_mtime_ns(index_rst, index_rst_time) == index_rst_time
+
+ # phase 1: create fake MO file in the src directory
+ po_path, mo_path = _get_bom_intl_path(app)
+ write_mo(mo_path, read_po(po_path))
+ po_time = time.time_ns()
+ assert _set_mtime_ns(po_path, po_time) == po_time
+
+ # phase 2: build document with gettext builder.
# The mo file in the srcdir directory is retained.
- app = make_app('gettext', *args, **kwargs)
app.build()
- time.sleep(0.01)
+ # Do a real sleep on POSIX, or simulate a sleep on Windows
+ # to ensure that calls to time.time_ns() remain consistent.
+ clock.sleep(0.5 if os.name == 'posix' else 1)
# Since it is after the build, the number of documents to be updated is 0
- update_targets = get_update_targets(app)
- assert update_targets[1] == set(), update_targets
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == set()
# Even if the timestamp of the mo file is updated, the number of documents
# to be updated is 0. gettext builder does not rebuild because of mo update.
- os.utime(app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo', (mtime + 10, mtime + 10))
- update_targets = get_update_targets(app)
- assert update_targets[1] == set(), update_targets
+ new_mo_time = time.time_ns()
+ assert _set_mtime_ns(mo_path, new_mo_time) == new_mo_time
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == set()
+ mock.undo() # remove the patch
+
+ # remove all sources for the next test
+ shutil.rmtree(app.srcdir, ignore_errors=True)
+ time.sleep(0.1 if os.name == 'posix' else 0.5) # real sleep
@sphinx_intl
@@ -761,7 +899,7 @@ def test_html_undefined_refs(app):
result = (app.outdir / 'refs_inconsistency.html').read_text(encoding='utf8')
expected_expr = ('<a class="reference external" '
- 'href="http://www.example.com">reference</a>')
+ 'href="https://www.example.com">reference</a>')
assert len(re.findall(expected_expr, result)) == 2
expected_expr = ('<a class="reference internal" '
@@ -829,7 +967,7 @@ def test_html_versionchanges(app):
assert expect1 == matched_content
expect2 = (
- """<p><span class="versionmodified added">New in version 1.0: </span>"""
+ """<p><span class="versionmodified added">Added in version 1.0: </span>"""
"""THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONADDED.</p>\n""")
matched_content = get_content(result, "versionadded")
assert expect2 == matched_content
@@ -840,6 +978,12 @@ def test_html_versionchanges(app):
matched_content = get_content(result, "versionchanged")
assert expect3 == matched_content
+ expect4 = (
+ """<p><span class="versionmodified removed">Removed in version 1.0: </span>"""
+ """THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONREMOVED.</p>\n""")
+ matched_content = get_content(result, "versionremoved")
+ assert expect4 == matched_content
+
@sphinx_intl
@pytest.mark.sphinx('html')
@@ -868,16 +1012,17 @@ def test_html_template(app):
def test_html_rebuild_mo(app):
app.build()
# --- rebuild by .mo mtime
- app.builder.build_update()
- app.env.find_files(app.config, app.builder)
- _, updated, _ = app.env.get_outdated_files(config_changed=False)
- assert len(updated) == 0
+ app.build()
+ _, updated, _ = _get_update_targets(app)
+ assert updated == set()
- mtime = (app.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').stat().st_mtime
- os.utime(app.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo', (mtime + 5, mtime + 5))
- app.env.find_files(app.config, app.builder)
- _, updated, _ = app.env.get_outdated_files(config_changed=False)
- assert len(updated) == 1
+ _, bom_file = _get_bom_intl_path(app)
+ old_mtime = bom_file.stat().st_mtime
+ new_mtime = old_mtime + (dt := 5)
+ os.utime(bom_file, (new_mtime, new_mtime))
+ assert old_mtime + dt == new_mtime, (old_mtime + dt, new_mtime)
+ _, updated, _ = _get_update_targets(app)
+ assert updated == {'bom'}
@sphinx_intl
@@ -985,7 +1130,7 @@ def test_xml_keep_external_links(app):
assert_elem(
para0[0],
['EXTERNAL LINK TO', 'Python', '.'],
- ['http://python.org/index.html'])
+ ['https://python.org/index.html'])
# internal link check
assert_elem(
@@ -997,13 +1142,13 @@ def test_xml_keep_external_links(app):
assert_elem(
para0[2],
['INLINE LINK BY', 'THE SPHINX SITE', '.'],
- ['http://sphinx-doc.org'])
+ ['https://sphinx-doc.org'])
# unnamed link check
assert_elem(
para0[3],
['UNNAMED', 'LINK', '.'],
- ['http://google.com'])
+ ['https://google.com'])
# link target swapped translation
para1 = secs[1].findall('paragraph')
@@ -1015,7 +1160,7 @@ def test_xml_keep_external_links(app):
assert_elem(
para1[1],
['LINK TO', 'THE PYTHON SITE', 'AND', 'THE SPHINX SITE', '.'],
- ['http://python.org', 'http://sphinx-doc.org'])
+ ['https://python.org', 'https://sphinx-doc.org'])
# multiple references in the same line
para2 = secs[2].findall('paragraph')
@@ -1024,9 +1169,9 @@ def test_xml_keep_external_links(app):
['LINK TO', 'EXTERNAL LINKS', ',', 'Python', ',',
'THE SPHINX SITE', ',', 'UNNAMED', 'AND',
'THE PYTHON SITE', '.'],
- ['i18n-with-external-links', 'http://python.org/index.html',
- 'http://sphinx-doc.org', 'http://google.com',
- 'http://python.org'])
+ ['i18n-with-external-links', 'https://python.org/index.html',
+ 'https://sphinx-doc.org', 'https://google.com',
+ 'https://python.org'])
@sphinx_intl
@@ -1195,7 +1340,7 @@ def test_additional_targets_should_not_be_translated(app):
result = (app.outdir / 'raw.html').read_text(encoding='utf8')
# raw block should not be translated
- expected_expr = """<iframe src="http://sphinx-doc.org"></iframe></section>"""
+ expected_expr = """<iframe src="https://sphinx-doc.org"></iframe></section>"""
assert_count(expected_expr, result, 1)
# [figure.txt]
@@ -1216,7 +1361,7 @@ def test_additional_targets_should_not_be_translated(app):
'html',
srcdir='test_additional_targets_should_be_translated',
confoverrides={
- 'language': 'xx', 'locale_dirs': ['.'],
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
'gettext_compact': False,
'gettext_additional_targets': [
'index',
@@ -1274,7 +1419,7 @@ def test_additional_targets_should_be_translated(app):
result = (app.outdir / 'raw.html').read_text(encoding='utf8')
# raw block should be translated
- expected_expr = """<iframe src="HTTP://SPHINX-DOC.ORG"></iframe></section>"""
+ expected_expr = """<iframe src="HTTPS://SPHINX-DOC.ORG"></iframe></section>"""
assert_count(expected_expr, result, 1)
# [figure.txt]
@@ -1294,7 +1439,7 @@ def test_additional_targets_should_be_translated(app):
'html',
testroot='intl_substitution_definitions',
confoverrides={
- 'language': 'xx', 'locale_dirs': ['.'],
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
'gettext_compact': False,
'gettext_additional_targets': [
'index',
@@ -1306,7 +1451,7 @@ def test_additional_targets_should_be_translated(app):
},
)
def test_additional_targets_should_be_translated_substitution_definitions(app):
- app.builder.build_all()
+ app.build(force_all=True)
# [prolog_epilog_substitution.txt]
@@ -1325,7 +1470,7 @@ def test_additional_targets_should_be_translated_substitution_definitions(app):
@pytest.mark.sphinx('text')
@pytest.mark.test_params(shared_result='test_intl_basic')
def test_text_references(app, warning):
- app.builder.build_specific([app.srcdir / 'refs.txt'])
+ app.build(filenames=[app.srcdir / 'refs.txt'])
warnings = warning.getvalue().replace(os.sep, '/')
warning_expr = 'refs.txt:\\d+: ERROR: Unknown target name:'
@@ -1336,7 +1481,7 @@ def test_text_references(app, warning):
'text',
testroot='intl_substitution_definitions',
confoverrides={
- 'language': 'xx', 'locale_dirs': ['.'],
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
'gettext_compact': False,
},
)
@@ -1362,7 +1507,7 @@ SUBSTITUTED IMAGE [image: SUBST_EPILOG_2 TRANSLATED][image] HERE.
@pytest.mark.sphinx(
'dummy', testroot='images',
srcdir='test_intl_images',
- confoverrides={'language': 'xx'},
+ confoverrides={'language': _CATALOG_LOCALE},
)
def test_image_glob_intl(app):
app.build()
@@ -1406,7 +1551,7 @@ def test_image_glob_intl(app):
'dummy', testroot='images',
srcdir='test_intl_images',
confoverrides={
- 'language': 'xx',
+ 'language': _CATALOG_LOCALE,
'figure_language_filename': '{root}{ext}.{language}',
},
)
@@ -1449,7 +1594,7 @@ def test_image_glob_intl_using_figure_language_filename(app):
def getwarning(warnings):
- return strip_escseq(warnings.getvalue().replace(os.sep, '/'))
+ return strip_colors(warnings.getvalue().replace(os.sep, '/'))
@pytest.mark.sphinx('html', testroot='basic',
@@ -1491,13 +1636,13 @@ def test_gettext_disallow_fuzzy_translations(app):
@pytest.mark.sphinx('html', testroot='basic', confoverrides={'language': 'de'})
-def test_customize_system_message(make_app, app_params, sphinx_test_tempdir):
+def test_customize_system_message(make_app, app_params):
try:
# clear translators cache
locale.translators.clear()
# prepare message catalog (.po)
- locale_dir = sphinx_test_tempdir / 'basic' / 'locales' / 'de' / 'LC_MESSAGES'
+ locale_dir = app_params.kwargs['srcdir'] / 'locales' / 'de' / 'LC_MESSAGES'
locale_dir.mkdir(parents=True, exist_ok=True)
with (locale_dir / 'sphinx.po').open('wb') as f:
catalog = Catalog()
diff --git a/tests/test_locale.py b/tests/test_intl/test_locale.py
index 11dd95d..11dd95d 100644
--- a/tests/test_locale.py
+++ b/tests/test_intl/test_locale.py
diff --git a/tests/test_markup/__init__.py b/tests/test_markup/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_markup/__init__.py
diff --git a/tests/test_markup.py b/tests/test_markup/test_markup.py
index 0d877b3..c933481 100644
--- a/tests/test_markup.py
+++ b/tests/test_markup/test_markup.py
@@ -520,7 +520,7 @@ def test_XRefRole(inliner):
@pytest.mark.sphinx('dummy', testroot='prolog')
def test_rst_prolog(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
rst = app.env.get_doctree('restructuredtext')
md = app.env.get_doctree('markdown')
@@ -544,7 +544,7 @@ def test_rst_prolog(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='keep_warnings')
def test_keep_warnings_is_True(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
doctree = app.env.get_doctree('index')
assert_node(doctree[0], nodes.section)
assert len(doctree[0]) == 2
@@ -554,7 +554,7 @@ def test_keep_warnings_is_True(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='keep_warnings',
confoverrides={'keep_warnings': False})
def test_keep_warnings_is_False(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
doctree = app.env.get_doctree('index')
assert_node(doctree[0], nodes.section)
assert len(doctree[0]) == 1
@@ -562,7 +562,7 @@ def test_keep_warnings_is_False(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='refonly_bullet_list')
def test_compact_refonly_bullet_list(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
doctree = app.env.get_doctree('index')
assert_node(doctree[0], nodes.section)
assert len(doctree[0]) == 5
@@ -580,7 +580,7 @@ def test_compact_refonly_bullet_list(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='default_role')
def test_default_role1(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
# default-role: pep
doctree = app.env.get_doctree('index')
@@ -601,7 +601,7 @@ def test_default_role1(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='default_role',
confoverrides={'default_role': 'guilabel'})
def test_default_role2(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
# default-role directive is stronger than configratuion
doctree = app.env.get_doctree('index')
diff --git a/tests/test_metadata.py b/tests/test_markup/test_metadata.py
index 7f31997..7f31997 100644
--- a/tests/test_metadata.py
+++ b/tests/test_markup/test_metadata.py
diff --git a/tests/test_parser.py b/tests/test_markup/test_parser.py
index 86163c6..86163c6 100644
--- a/tests/test_parser.py
+++ b/tests/test_markup/test_parser.py
diff --git a/tests/test_smartquotes.py b/tests/test_markup/test_smartquotes.py
index 1d4e8e1..6c84386 100644
--- a/tests/test_smartquotes.py
+++ b/tests/test_markup/test_smartquotes.py
@@ -1,7 +1,8 @@
"""Test smart quotes."""
import pytest
-from html5lib import HTMLParser
+
+from sphinx.testing.util import etree_parse
@pytest.mark.sphinx(buildername='html', testroot='smartquotes', freshenv=True)
@@ -16,9 +17,7 @@ def test_basic(app, status, warning):
def test_literals(app, status, warning):
app.build()
- with (app.outdir / 'literals.html').open(encoding='utf-8') as html_file:
- etree = HTMLParser(namespaceHTMLElements=False).parse(html_file)
-
+ etree = etree_parse(app.outdir / 'literals.html')
for code_element in etree.iter('code'):
code_text = ''.join(code_element.itertext())
diff --git a/tests/test_pycode/__init__.py b/tests/test_pycode/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_pycode/__init__.py
diff --git a/tests/test_pycode.py b/tests/test_pycode/test_pycode.py
index 5739787..5739787 100644
--- a/tests/test_pycode.py
+++ b/tests/test_pycode/test_pycode.py
diff --git a/tests/test_pycode_ast.py b/tests/test_pycode/test_pycode_ast.py
index 5efd0cb..1ed43e1 100644
--- a/tests/test_pycode_ast.py
+++ b/tests/test_pycode/test_pycode_ast.py
@@ -18,7 +18,7 @@ from sphinx.pycode.ast import unparse as ast_unparse
("a and b and c", "a and b and c"), # BoolOp
("b'bytes'", "b'bytes'"), # Bytes
("object()", "object()"), # Call
- ("1234", "1234"), # Constant
+ ("1234", "1234"), # Constant, Num
("{'key1': 'value1', 'key2': 'value2'}",
"{'key1': 'value1', 'key2': 'value2'}"), # Dict
("a / b", "a / b"), # Div
@@ -34,7 +34,6 @@ from sphinx.pycode.ast import unparse as ast_unparse
("a % b", "a % b"), # Mod
("a * b", "a * b"), # Mult
("sys", "sys"), # Name, NameConstant
- ("1234", "1234"), # Num
("not a", "not a"), # Not
("a or b", "a or b"), # Or
("a**b", "a**b"), # Pow
@@ -52,6 +51,13 @@ from sphinx.pycode.ast import unparse as ast_unparse
"lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs
("0x1234", "0x1234"), # Constant
("1_000_000", "1_000_000"), # Constant
+ ("Tuple[:,:]", "Tuple[:, :]"), # Index, Subscript, 2x Slice
+ ("Tuple[1:2]", "Tuple[1:2]"), # Index, Subscript, Slice(no-step)
+ ("Tuple[1:2:3]", "Tuple[1:2:3]"), # Index, Subscript, Slice
+ ("x[:, np.newaxis, :, :]",
+ "x[:, np.newaxis, :, :]"), # Index, Subscript, numpy extended syntax
+ ("y[:, 1:3][np.array([0, 2, 4]), :]",
+ "y[:, 1:3][np.array([0, 2, 4]), :]"), # Index, 2x Subscript, numpy extended syntax
])
def test_unparse(source, expected):
module = ast.parse(source)
diff --git a/tests/test_pycode_parser.py b/tests/test_pycode/test_pycode_parser.py
index fde648d..fde648d 100644
--- a/tests/test_pycode_parser.py
+++ b/tests/test_pycode/test_pycode_parser.py
diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py
index 6a9f5c7..671aed4 100644
--- a/tests/test_quickstart.py
+++ b/tests/test_quickstart.py
@@ -211,7 +211,7 @@ def test_quickstart_and_build(tmp_path):
'html', # buildername
status=StringIO(),
warning=warnfile)
- app.builder.build_all()
+ app.build(force_all=True)
warnings = warnfile.getvalue()
assert not warnings
diff --git a/tests/test_search.py b/tests/test_search.py
index 68a7b01..63443a8 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -1,4 +1,5 @@
"""Test the search index builder."""
+from __future__ import annotations
import json
import warnings
@@ -72,7 +73,7 @@ test that non-comments are indexed: fermion
@pytest.mark.sphinx(testroot='ext-viewcode')
def test_objects_are_escaped(app):
- app.builder.build_all()
+ app.build(force_all=True)
index = load_searchindex(app.outdir / 'searchindex.js')
for item in index.get('objects').get(''):
if item[-1] == 'n::Array&lt;T, d&gt;': # n::Array<T,d> is escaped
@@ -83,7 +84,7 @@ def test_objects_are_escaped(app):
@pytest.mark.sphinx(testroot='search')
def test_meta_keys_are_handled_for_language_en(app):
- app.builder.build_all()
+ app.build(force_all=True)
searchindex = load_searchindex(app.outdir / 'searchindex.js')
assert not is_registered_term(searchindex, 'thisnoteith')
assert is_registered_term(searchindex, 'thisonetoo')
@@ -96,7 +97,7 @@ def test_meta_keys_are_handled_for_language_en(app):
@pytest.mark.sphinx(testroot='search', confoverrides={'html_search_language': 'de'}, freshenv=True)
def test_meta_keys_are_handled_for_language_de(app):
- app.builder.build_all()
+ app.build(force_all=True)
searchindex = load_searchindex(app.outdir / 'searchindex.js')
assert not is_registered_term(searchindex, 'thisnoteith')
assert is_registered_term(searchindex, 'thisonetoo')
@@ -109,14 +110,14 @@ def test_meta_keys_are_handled_for_language_de(app):
@pytest.mark.sphinx(testroot='search')
def test_stemmer_does_not_remove_short_words(app):
- app.builder.build_all()
+ app.build(force_all=True)
searchindex = (app.outdir / 'searchindex.js').read_text(encoding='utf8')
assert 'bat' in searchindex
@pytest.mark.sphinx(testroot='search')
def test_stemmer(app):
- app.builder.build_all()
+ app.build(force_all=True)
searchindex = load_searchindex(app.outdir / 'searchindex.js')
print(searchindex)
assert is_registered_term(searchindex, 'findthisstemmedkei')
@@ -125,7 +126,7 @@ def test_stemmer(app):
@pytest.mark.sphinx(testroot='search')
def test_term_in_heading_and_section(app):
- app.builder.build_all()
+ app.build(force_all=True)
searchindex = (app.outdir / 'searchindex.js').read_text(encoding='utf8')
# if search term is in the title of one doc and in the text of another
# both documents should be a hit in the search index as a title,
@@ -136,7 +137,7 @@ def test_term_in_heading_and_section(app):
@pytest.mark.sphinx(testroot='search')
def test_term_in_raw_directive(app):
- app.builder.build_all()
+ app.build(force_all=True)
searchindex = load_searchindex(app.outdir / 'searchindex.js')
assert not is_registered_term(searchindex, 'raw')
assert is_registered_term(searchindex, 'rawword')
@@ -157,8 +158,8 @@ def test_IndexBuilder():
index = IndexBuilder(env, 'en', {}, None)
index.feed('docname1_1', 'filename1_1', 'title1_1', doc)
index.feed('docname1_2', 'filename1_2', 'title1_2', doc)
- index.feed('docname2_1', 'filename2_1', 'title2_1', doc)
index.feed('docname2_2', 'filename2_2', 'title2_2', doc)
+ index.feed('docname2_1', 'filename2_1', 'title2_1', doc)
assert index._titles == {'docname1_1': 'title1_1', 'docname1_2': 'title1_2',
'docname2_1': 'title2_1', 'docname2_2': 'title2_2'}
assert index._filenames == {'docname1_1': 'filename1_1', 'docname1_2': 'filename1_2',
@@ -279,7 +280,7 @@ def test_IndexBuilder_lookup():
srcdir='search_zh',
)
def test_search_index_gen_zh(app):
- app.builder.build_all()
+ app.build(force_all=True)
index = load_searchindex(app.outdir / 'searchindex.js')
assert 'chinesetest ' not in index['terms']
assert 'chinesetest' in index['terms']
@@ -304,3 +305,44 @@ def test_parallel(app):
app.build()
index = load_searchindex(app.outdir / 'searchindex.js')
assert index['docnames'] == ['index', 'nosearch', 'tocitem']
+
+
+@pytest.mark.sphinx(testroot='search')
+def test_search_index_is_deterministic(app):
+ app.build(force_all=True)
+ index = load_searchindex(app.outdir / 'searchindex.js')
+ # Pretty print the index. Only shown by pytest on failure.
+ print(f'searchindex.js contents:\n\n{json.dumps(index, indent=2)}')
+ assert_is_sorted(index, '')
+
+
+def is_title_tuple_type(item: list[int | str]):
+ """
+ In the search index, titles inside .alltitles are stored as a tuple of
+ (document_idx, title_anchor). Tuples are represented as lists in JSON,
+ but their contents must not be sorted. We cannot sort them anyway, as
+ document_idx is an int and title_anchor is a str.
+ """
+ return len(item) == 2 and isinstance(item[0], int) and isinstance(item[1], str)
+
+
+def assert_is_sorted(item, path: str):
+ lists_not_to_sort = {
+ # Each element of .titles is related to the element of .docnames in the same position.
+ # The ordering is deterministic because .docnames is sorted.
+ '.titles',
+ # Each element of .filenames is related to the element of .docnames in the same position.
+ # The ordering is deterministic because .docnames is sorted.
+ '.filenames',
+ }
+
+ err_path = path or '<root>'
+ if isinstance(item, dict):
+ assert list(item.keys()) == sorted(item.keys()), f'{err_path} is not sorted'
+ for key, value in item.items():
+ assert_is_sorted(value, f'{path}.{key}')
+ elif isinstance(item, list):
+ if not is_title_tuple_type(item) and path not in lists_not_to_sort:
+ assert item == sorted(item), f'{err_path} is not sorted'
+ for i, child in enumerate(item):
+ assert_is_sorted(child, f'{path}[{i}]')
diff --git a/tests/test_theming.py b/tests/test_theming.py
deleted file mode 100644
index b4c8511..0000000
--- a/tests/test_theming.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""Test the Theme class."""
-
-import os
-
-import alabaster
-import pytest
-
-import sphinx.builders.html
-from sphinx.theming import ThemeError
-
-
-@pytest.mark.sphinx(
- testroot='theming',
- confoverrides={'html_theme': 'ziptheme',
- 'html_theme_options.testopt': 'foo'})
-def test_theme_api(app, status, warning):
- cfg = app.config
-
- themes = ['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku',
- 'traditional', 'epub', 'nature', 'pyramid', 'bizstyle', 'classic', 'nonav',
- 'test-theme', 'ziptheme', 'staticfiles', 'parent', 'child']
- try:
- alabaster_version = alabaster.__version_info__
- except AttributeError:
- alabaster_version = alabaster.version.__version_info__
- if alabaster_version >= (0, 7, 11):
- themes.append('alabaster')
-
- # test Theme class API
- assert set(app.registry.html_themes.keys()) == set(themes)
- assert app.registry.html_themes['test-theme'] == str(app.srcdir / 'test_theme' / 'test-theme')
- assert app.registry.html_themes['ziptheme'] == str(app.srcdir / 'ziptheme.zip')
- assert app.registry.html_themes['staticfiles'] == str(app.srcdir / 'test_theme' / 'staticfiles')
-
- # test Theme instance API
- theme = app.builder.theme
- assert theme.name == 'ziptheme'
- themedir = theme.themedir
- assert theme.base.name == 'basic'
- assert len(theme.get_theme_dirs()) == 2
-
- # direct setting
- assert theme.get_config('theme', 'stylesheet') == 'custom.css'
- # inherited setting
- assert theme.get_config('options', 'nosidebar') == 'false'
- # nonexisting setting
- assert theme.get_config('theme', 'foobar', 'def') == 'def'
- with pytest.raises(ThemeError):
- theme.get_config('theme', 'foobar')
-
- # options API
-
- options = theme.get_options({'nonexisting': 'foo'})
- assert 'nonexisting' not in options
-
- options = theme.get_options(cfg.html_theme_options)
- assert options['testopt'] == 'foo'
- assert options['nosidebar'] == 'false'
-
- # cleanup temp directories
- theme.cleanup()
- assert not os.path.exists(themedir)
-
-
-@pytest.mark.sphinx(testroot='double-inheriting-theme')
-def test_double_inheriting_theme(app, status, warning):
- assert app.builder.theme.name == 'base_theme2'
- app.build() # => not raises TemplateNotFound
-
-
-@pytest.mark.sphinx(testroot='theming',
- confoverrides={'html_theme': 'child'})
-def test_nested_zipped_theme(app, status, warning):
- assert app.builder.theme.name == 'child'
- app.build() # => not raises TemplateNotFound
-
-
-@pytest.mark.sphinx(testroot='theming',
- confoverrides={'html_theme': 'staticfiles'})
-def test_staticfiles(app, status, warning):
- app.build()
- assert (app.outdir / '_static' / 'staticimg.png').exists()
- assert (app.outdir / '_static' / 'statictmpl.html').exists()
- assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == (
- '<!-- testing static templates -->\n'
- '<html><project>Python</project></html>'
- )
-
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert '<meta name="testopt" content="optdefault" />' in result
-
-
-@pytest.mark.sphinx(testroot='theming',
- confoverrides={'html_theme': 'test-theme'})
-def test_dark_style(app, monkeypatch):
- monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '')
-
- style = app.builder.dark_highlighter.formatter_args.get('style')
- assert style.__name__ == 'MonokaiStyle'
-
- app.build()
- assert (app.outdir / '_static' / 'pygments_dark.css').exists()
-
- css_file, properties = app.registry.css_files[0]
- assert css_file == 'pygments_dark.css'
- assert "media" in properties
- assert properties["media"] == '(prefers-color-scheme: dark)'
-
- assert sorted(f.filename for f in app.builder._css_files) == [
- '_static/classic.css',
- '_static/pygments.css',
- '_static/pygments_dark.css',
- ]
-
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert '<link rel="stylesheet" type="text/css" href="_static/pygments.css" />' in result
- assert ('<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" '
- 'rel="stylesheet" type="text/css" '
- 'href="_static/pygments_dark.css" />') in result
-
-
-@pytest.mark.sphinx(testroot='theming')
-def test_theme_sidebars(app, status, warning):
- app.build()
-
- # test-theme specifies globaltoc and searchbox as default sidebars
- result = (app.outdir / 'index.html').read_text(encoding='utf8')
- assert '<h3><a href="#">Table of Contents</a></h3>' in result
- assert '<h3>Related Topics</h3>' not in result
- assert '<h3>This Page</h3>' not in result
- assert '<h3 id="searchlabel">Quick search</h3>' in result
diff --git a/tests/test_theming/__init__.py b/tests/test_theming/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_theming/__init__.py
diff --git a/tests/test_theming/test_html_theme.py b/tests/test_theming/test_html_theme.py
new file mode 100644
index 0000000..e9a183f
--- /dev/null
+++ b/tests/test_theming/test_html_theme.py
@@ -0,0 +1,35 @@
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='theming')
+def test_theme_options(app, status, warning):
+ app.build()
+
+ result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8')
+ assert 'NAVIGATION_WITH_KEYS: false' in result
+ assert 'ENABLE_SEARCH_SHORTCUTS: true' in result
+
+
+@pytest.mark.sphinx(
+ 'html',
+ testroot='theming',
+ confoverrides={
+ 'html_theme_options.navigation_with_keys': True,
+ 'html_theme_options.enable_search_shortcuts': False,
+ },
+)
+def test_theme_options_with_override(app, status, warning):
+ app.build()
+
+ result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8')
+ assert 'NAVIGATION_WITH_KEYS: true' in result
+ assert 'ENABLE_SEARCH_SHORTCUTS: false' in result
+
+
+@pytest.mark.sphinx('html', testroot='build-html-theme-having-multiple-stylesheets')
+def test_theme_having_multiple_stylesheets(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf-8')
+
+ assert '<link rel="stylesheet" type="text/css" href="_static/mytheme.css" />' in content
+ assert '<link rel="stylesheet" type="text/css" href="_static/extra.css" />' in content
diff --git a/tests/test_templating.py b/tests/test_theming/test_templating.py
index a41af93..bc02e97 100644
--- a/tests/test_templating.py
+++ b/tests/test_theming/test_templating.py
@@ -10,7 +10,7 @@ def test_layout_overloading(make_app, app_params):
args, kwargs = app_params
app = make_app(*args, **kwargs)
setup_documenters(app)
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '<!-- layout overloading -->' in result
@@ -21,21 +21,28 @@ def test_autosummary_class_template_overloading(make_app, app_params):
args, kwargs = app_params
app = make_app(*args, **kwargs)
setup_documenters(app)
- app.builder.build_update()
+ app.build()
- result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(encoding='utf8')
+ result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(
+ encoding='utf8'
+ )
assert 'autosummary/class.rst method block overloading' in result
assert 'foobar' not in result
-@pytest.mark.sphinx('html', testroot='templating',
- confoverrides={'autosummary_context': {'sentence': 'foobar'}})
+@pytest.mark.sphinx(
+ 'html',
+ testroot='templating',
+ confoverrides={'autosummary_context': {'sentence': 'foobar'}},
+)
def test_autosummary_context(make_app, app_params):
args, kwargs = app_params
app = make_app(*args, **kwargs)
setup_documenters(app)
- app.builder.build_update()
+ app.build()
- result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(encoding='utf8')
+ result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(
+ encoding='utf8'
+ )
assert 'autosummary/class.rst method block overloading' in result
assert 'foobar' in result
diff --git a/tests/test_theming/test_theming.py b/tests/test_theming/test_theming.py
new file mode 100644
index 0000000..867f8a0
--- /dev/null
+++ b/tests/test_theming/test_theming.py
@@ -0,0 +1,227 @@
+"""Test the Theme class."""
+
+import os
+import shutil
+from pathlib import Path
+from xml.etree.ElementTree import ParseError
+
+import pytest
+from defusedxml.ElementTree import parse as xml_parse
+
+import sphinx.builders.html
+from sphinx.errors import ThemeError
+from sphinx.theming import (
+ _ConfigFile,
+ _convert_theme_conf,
+ _convert_theme_toml,
+ _load_theme,
+ _load_theme_conf,
+ _load_theme_toml,
+)
+
+HERE = Path(__file__).resolve().parent
+
+
+@pytest.mark.sphinx(
+ testroot='theming',
+ confoverrides={'html_theme': 'ziptheme', 'html_theme_options.testopt': 'foo'},
+)
+def test_theme_api(app, status, warning):
+ themes = [
+ 'basic',
+ 'default',
+ 'scrolls',
+ 'agogo',
+ 'sphinxdoc',
+ 'haiku',
+ 'traditional',
+ 'epub',
+ 'nature',
+ 'pyramid',
+ 'bizstyle',
+ 'classic',
+ 'nonav',
+ 'test-theme',
+ 'ziptheme',
+ 'staticfiles',
+ 'parent',
+ 'child',
+ 'alabaster',
+ ]
+
+ # test Theme class API
+ assert set(app.registry.html_themes.keys()) == set(themes)
+ assert app.registry.html_themes['test-theme'] == str(
+ app.srcdir / 'test_theme' / 'test-theme'
+ )
+ assert app.registry.html_themes['ziptheme'] == str(app.srcdir / 'ziptheme.zip')
+ assert app.registry.html_themes['staticfiles'] == str(
+ app.srcdir / 'test_theme' / 'staticfiles'
+ )
+
+ # test Theme instance API
+ theme = app.builder.theme
+ assert theme.name == 'ziptheme'
+ assert len(theme.get_theme_dirs()) == 2
+
+ # direct setting
+ assert theme.get_config('theme', 'stylesheet') == 'custom.css'
+ # inherited setting
+ assert theme.get_config('options', 'nosidebar') == 'false'
+ # nonexisting setting
+ assert theme.get_config('theme', 'foobar', 'def') == 'def'
+ with pytest.raises(ThemeError):
+ theme.get_config('theme', 'foobar')
+
+ # options API
+
+ options = theme.get_options({'nonexisting': 'foo'})
+ assert 'nonexisting' not in options
+
+ options = theme.get_options(app.config.html_theme_options)
+ assert options['testopt'] == 'foo'
+ assert options['nosidebar'] == 'false'
+
+ # cleanup temp directories
+ theme._cleanup()
+ assert not any(map(os.path.exists, theme._tmp_dirs))
+
+
+def test_nonexistent_theme_settings(tmp_path):
+ # Check that error occurs with a non-existent theme.toml or theme.conf
+ # (https://github.com/sphinx-doc/sphinx/issues/11668)
+ with pytest.raises(ThemeError):
+ _load_theme('', str(tmp_path))
+
+
+@pytest.mark.sphinx(testroot='double-inheriting-theme')
+def test_double_inheriting_theme(app, status, warning):
+ assert app.builder.theme.name == 'base_theme2'
+ app.build() # => not raises TemplateNotFound
+
+
+@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'child'})
+def test_nested_zipped_theme(app, status, warning):
+ assert app.builder.theme.name == 'child'
+ app.build() # => not raises TemplateNotFound
+
+
+@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'staticfiles'})
+def test_staticfiles(app, status, warning):
+ app.build()
+ assert (app.outdir / '_static' / 'staticimg.png').exists()
+ assert (app.outdir / '_static' / 'statictmpl.html').exists()
+ assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == (
+ '<!-- testing static templates -->\n<html><project>Python</project></html>'
+ )
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<meta name="testopt" content="optdefault" />' in result
+
+
+@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'test-theme'})
+def test_dark_style(app, monkeypatch):
+ monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '')
+
+ style = app.builder.dark_highlighter.formatter_args.get('style')
+ assert style.__name__ == 'MonokaiStyle'
+
+ app.build()
+ assert (app.outdir / '_static' / 'pygments_dark.css').exists()
+
+ css_file, properties = app.registry.css_files[0]
+ assert css_file == 'pygments_dark.css'
+ assert 'media' in properties
+ assert properties['media'] == '(prefers-color-scheme: dark)'
+
+ assert sorted(f.filename for f in app.builder._css_files) == [
+ '_static/classic.css',
+ '_static/pygments.css',
+ '_static/pygments_dark.css',
+ ]
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="stylesheet" type="text/css" href="_static/pygments.css" />' in result
+ assert (
+ '<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" '
+ 'rel="stylesheet" type="text/css" '
+ 'href="_static/pygments_dark.css" />'
+ ) in result
+
+
+@pytest.mark.sphinx(testroot='theming')
+def test_theme_sidebars(app, status, warning):
+ app.build()
+
+ # test-theme specifies globaltoc and searchbox as default sidebars
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<h3><a href="#">Table of Contents</a></h3>' in result
+ assert '<h3>Related Topics</h3>' not in result
+ assert '<h3>This Page</h3>' not in result
+ assert '<h3 id="searchlabel">Quick search</h3>' in result
+
+
+@pytest.mark.parametrize(
+ 'theme_name',
+ [
+ 'alabaster',
+ 'agogo',
+ 'basic',
+ 'bizstyle',
+ 'classic',
+ 'default',
+ 'epub',
+ 'haiku',
+ 'nature',
+ 'nonav',
+ 'pyramid',
+ 'scrolls',
+ 'sphinxdoc',
+ 'traditional',
+ ],
+)
+def test_theme_builds(make_app, rootdir, sphinx_test_tempdir, theme_name):
+ """Test all the themes included with Sphinx build a simple project and produce valid XML."""
+ testroot_path = rootdir / 'test-basic'
+ srcdir = sphinx_test_tempdir / f'test-theme-{theme_name}'
+ shutil.copytree(testroot_path, srcdir)
+
+ app = make_app(srcdir=srcdir, confoverrides={'html_theme': theme_name})
+ app.build()
+ assert not app.warning.getvalue().strip()
+ assert app.outdir.joinpath('index.html').exists()
+
+ # check that the generated HTML files are well-formed (as strict XML)
+ for html_file in app.outdir.rglob('*.html'):
+ try:
+ xml_parse(html_file)
+ except ParseError as exc:
+ pytest.fail(f'Failed to parse {html_file.relative_to(app.outdir)}: {exc}')
+
+
+def test_config_file_toml():
+ config_path = HERE / 'theme.toml'
+ cfg = _load_theme_toml(str(config_path))
+ config = _convert_theme_toml(cfg)
+
+ assert config == _ConfigFile(
+ stylesheets=('spam.css', 'ham.css'),
+ sidebar_templates=None,
+ pygments_style_default='spam',
+ pygments_style_dark=None,
+ options={'lobster': 'thermidor'},
+ )
+
+
+def test_config_file_conf():
+ config_path = HERE / 'theme.conf'
+ cfg = _load_theme_conf(str(config_path))
+ config = _convert_theme_conf(cfg)
+
+ assert config == _ConfigFile(
+ stylesheets=('spam.css', 'ham.css'),
+ sidebar_templates=None,
+ pygments_style_default='spam',
+ pygments_style_dark=None,
+ options={'lobster': 'thermidor'},
+ )
diff --git a/tests/test_theming/theme.conf b/tests/test_theming/theme.conf
new file mode 100644
index 0000000..b53fcb7
--- /dev/null
+++ b/tests/test_theming/theme.conf
@@ -0,0 +1,7 @@
+[theme]
+inherit = none
+stylesheet = spam.css, ham.css
+pygments_style = spam
+
+[options]
+lobster = thermidor
diff --git a/tests/test_theming/theme.toml b/tests/test_theming/theme.toml
new file mode 100644
index 0000000..96a5668
--- /dev/null
+++ b/tests/test_theming/theme.toml
@@ -0,0 +1,10 @@
+[theme]
+inherit = "none"
+stylesheets = [
+ "spam.css",
+ "ham.css",
+]
+pygments_style = { default = "spam" }
+
+[options]
+lobster = "thermidor"
diff --git a/tests/test_toctree.py b/tests/test_toctree.py
index 39d0916..e59085d 100644
--- a/tests/test_toctree.py
+++ b/tests/test_toctree.py
@@ -6,7 +6,7 @@ import pytest
@pytest.mark.sphinx(testroot='toctree-glob')
def test_relations(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert app.builder.relations['index'] == [None, None, 'foo']
assert app.builder.relations['foo'] == ['index', 'index', 'bar/index']
assert app.builder.relations['bar/index'] == ['index', 'foo', 'bar/bar_1']
@@ -23,7 +23,7 @@ def test_relations(app, status, warning):
@pytest.mark.sphinx('singlehtml', testroot='toctree-empty')
def test_singlehtml_toctree(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
try:
app.builder._get_local_toctree('index')
except AttributeError:
@@ -36,4 +36,4 @@ def test_numbered_toctree(app, status, warning):
index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
index = re.sub(':numbered:.*', ':numbered: 1', index)
(app.srcdir / 'index.rst').write_text(index, encoding='utf8')
- app.builder.build_all()
+ app.build(force_all=True)
diff --git a/tests/test_transforms/__init__.py b/tests/test_transforms/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_transforms/__init__.py
diff --git a/tests/test_transforms_move_module_targets.py b/tests/test_transforms/test_transforms_move_module_targets.py
index e0e9f1d..e0e9f1d 100644
--- a/tests/test_transforms_move_module_targets.py
+++ b/tests/test_transforms/test_transforms_move_module_targets.py
diff --git a/tests/test_transforms_post_transforms.py b/tests/test_transforms/test_transforms_post_transforms.py
index b9b6126..c4e699b 100644
--- a/tests/test_transforms_post_transforms.py
+++ b/tests/test_transforms/test_transforms_post_transforms.py
@@ -79,6 +79,7 @@ def test_keyboard_hyphen_spaces(app):
class TestSigElementFallbackTransform:
"""Integration test for :class:`sphinx.transforms.post_transforms.SigElementFallbackTransform`."""
+
# safe copy of the "built-in" desc_sig_* nodes (during the test, instances of such nodes
# will be created sequentially, so we fix a possible order at the beginning using a tuple)
_builtin_sig_elements: tuple[type[addnodes.desc_sig_element], ...] = tuple(SIG_ELEMENTS)
@@ -264,5 +265,5 @@ class TestSigElementFallbackTransform:
# extract messages
messages = caplog.record_tuples
stdout = [message for _, lvl, message in messages if lvl == logging.INFO]
- stderr = [message for _, lvl, message in messages if lvl == logging.WARN]
+ stderr = [message for _, lvl, message in messages if lvl == logging.WARNING]
return document, stdout, stderr
diff --git a/tests/test_transforms_post_transforms_code.py b/tests/test_transforms/test_transforms_post_transforms_code.py
index 4423d5b..4423d5b 100644
--- a/tests/test_transforms_post_transforms_code.py
+++ b/tests/test_transforms/test_transforms_post_transforms_code.py
diff --git a/tests/test_transforms_reorder_nodes.py b/tests/test_transforms/test_transforms_reorder_nodes.py
index 7ffdae6..7ffdae6 100644
--- a/tests/test_transforms_reorder_nodes.py
+++ b/tests/test_transforms/test_transforms_reorder_nodes.py
diff --git a/tests/test_util/__init__.py b/tests/test_util/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_util/__init__.py
diff --git a/tests/test_util/intersphinx_data.py b/tests/test_util/intersphinx_data.py
new file mode 100644
index 0000000..042ee76
--- /dev/null
+++ b/tests/test_util/intersphinx_data.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import zlib
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Final
+
+INVENTORY_V1: Final[bytes] = b'''\
+# Sphinx inventory version 1
+# Project: foo
+# Version: 1.0
+module mod foo.html
+module.cls class foo.html
+'''
+
+INVENTORY_V2: Final[bytes] = b'''\
+# Sphinx inventory version 2
+# Project: foo
+# Version: 2.0
+# The remainder of this file is compressed with zlib.
+''' + zlib.compress(b'''\
+module1 py:module 0 foo.html#module-module1 Long Module desc
+module2 py:module 0 foo.html#module-$ -
+module1.func py:function 1 sub/foo.html#$ -
+module1.Foo.bar py:method 1 index.html#foo.Bar.baz -
+CFunc c:function 2 cfunc.html#CFunc -
+std cpp:type 1 index.html#std -
+std::uint8_t cpp:type 1 index.html#std_uint8_t -
+foo::Bar cpp:class 1 index.html#cpp_foo_bar -
+foo::Bar::baz cpp:function 1 index.html#cpp_foo_bar_baz -
+foons cpp:type 1 index.html#foons -
+foons::bartype cpp:type 1 index.html#foons_bartype -
+a term std:term -1 glossary.html#term-a-term -
+ls.-l std:cmdoption 1 index.html#cmdoption-ls-l -
+docname std:doc -1 docname.html -
+foo js:module 1 index.html#foo -
+foo.bar js:class 1 index.html#foo.bar -
+foo.bar.baz js:method 1 index.html#foo.bar.baz -
+foo.bar.qux js:data 1 index.html#foo.bar.qux -
+a term including:colon std:term -1 glossary.html#term-a-term-including-colon -
+The-Julia-Domain std:label -1 write_inventory/#$ The Julia Domain
+''')
+
+INVENTORY_V2_NO_VERSION: Final[bytes] = b'''\
+# Sphinx inventory version 2
+# Project: foo
+# Version:
+# The remainder of this file is compressed with zlib.
+''' + zlib.compress(b'''\
+module1 py:module 0 foo.html#module-module1 Long Module desc
+''')
diff --git a/tests/test_util.py b/tests/test_util/test_util.py
index 4389894..4389894 100644
--- a/tests/test_util.py
+++ b/tests/test_util/test_util.py
diff --git a/tests/test_util/test_util_console.py b/tests/test_util/test_util_console.py
new file mode 100644
index 0000000..b617a33
--- /dev/null
+++ b/tests/test_util/test_util_console.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+
+import itertools
+import operator
+from typing import TYPE_CHECKING
+
+import pytest
+
+from sphinx.util.console import blue, reset, strip_colors, strip_escape_sequences
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Sequence
+ from typing import Final, TypeVar
+
+ _T = TypeVar('_T')
+
+CURSOR_UP: Final[str] = '\x1b[2A' # ignored ANSI code
+ERASE_LINE: Final[str] = '\x1b[2K' # supported ANSI code
+TEXT: Final[str] = '\x07 Hello world!'
+
+
+@pytest.mark.parametrize(
+ ('strip_function', 'ansi_base_blocks', 'text_base_blocks'),
+ [
+ (
+ strip_colors,
+ # double ERASE_LINE so that the tested strings may have 2 of them
+ [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP],
+ # :func:`strip_colors` removes color codes but keeps ERASE_LINE and CURSOR_UP
+ [TEXT, TEXT, TEXT, ERASE_LINE, ERASE_LINE, CURSOR_UP],
+ ),
+ (
+ strip_escape_sequences,
+ # double ERASE_LINE so that the tested strings may have 2 of them
+ [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP],
+ # :func:`strip_escape_sequences` strips ANSI codes known by Sphinx
+ [TEXT, TEXT, TEXT, '', '', CURSOR_UP],
+ ),
+ ],
+ ids=[strip_colors.__name__, strip_escape_sequences.__name__],
+)
+def test_strip_ansi(
+ strip_function: Callable[[str], str],
+ ansi_base_blocks: Sequence[str],
+ text_base_blocks: Sequence[str],
+) -> None:
+ assert callable(strip_function)
+ assert len(text_base_blocks) == len(ansi_base_blocks)
+ N = len(ansi_base_blocks)
+
+ def next_ansi_blocks(choices: Sequence[str], n: int) -> Sequence[str]:
+ # Get a list of *n* words from a cyclic sequence of *choices*.
+ #
+ # For instance ``next_ansi_blocks(['a', 'b'], 3) == ['a', 'b', 'a']``.
+ stream = itertools.cycle(choices)
+ return list(map(operator.itemgetter(0), zip(stream, range(n))))
+
+ # generate all permutations of length N
+ for sigma in itertools.permutations(range(N), N):
+ # apply the permutation on the blocks with ANSI codes
+ ansi_blocks = list(map(ansi_base_blocks.__getitem__, sigma))
+ # apply the permutation on the blocks with stripped codes
+ text_blocks = list(map(text_base_blocks.__getitem__, sigma))
+
+ for glue, n in itertools.product(['.', '\n', '\r\n'], range(4 * N)):
+ ansi_strings = next_ansi_blocks(ansi_blocks, n)
+ text_strings = next_ansi_blocks(text_blocks, n)
+ assert len(ansi_strings) == len(text_strings) == n
+
+ ansi_string = glue.join(ansi_strings)
+ text_string = glue.join(text_strings)
+ assert strip_function(ansi_string) == text_string
+
+
+def test_strip_ansi_short_forms():
+ # In Sphinx, we always "normalize" the color codes so that they
+ # match "\x1b\[(\d\d;){0,2}(\d\d)m" but it might happen that
+ # some messages use '\x1b[0m' instead of ``reset(s)``, so we
+ # test whether this alternative form is supported or not.
+
+ for strip_function in [strip_colors, strip_escape_sequences]:
+ # \x1b[m and \x1b[0m are equivalent to \x1b[00m
+ assert strip_function('\x1b[m') == ''
+ assert strip_function('\x1b[0m') == ''
+
+ # \x1b[1m is equivalent to \x1b[01m
+ assert strip_function('\x1b[1mbold\x1b[0m') == 'bold'
+
+ # \x1b[K is equivalent to \x1b[0K
+ assert strip_escape_sequences('\x1b[K') == ''
diff --git a/tests/test_util_display.py b/tests/test_util/test_util_display.py
index 9ecdd6a..a18fa1e 100644
--- a/tests/test_util_display.py
+++ b/tests/test_util/test_util_display.py
@@ -2,8 +2,8 @@
import pytest
-from sphinx.testing.util import strip_escseq
from sphinx.util import logging
+from sphinx.util.console import strip_colors
from sphinx.util.display import (
SkipProgressMessage,
display_chunk,
@@ -28,13 +28,14 @@ def test_status_iterator_length_0(app, status, warning):
status.seek(0)
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... '))
- output = strip_escseq(status.getvalue())
+ output = strip_colors(status.getvalue())
assert 'testing ... hello sphinx world \n' in output
assert yields == ['hello', 'sphinx', 'world']
@pytest.mark.sphinx('dummy')
-def test_status_iterator_verbosity_0(app, status, warning):
+def test_status_iterator_verbosity_0(app, status, warning, monkeypatch):
+ monkeypatch.setenv("FORCE_COLOR", "1")
logging.setup(app, status, warning)
# test for status_iterator (verbosity=0)
@@ -42,7 +43,7 @@ def test_status_iterator_verbosity_0(app, status, warning):
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ',
length=3, verbosity=0))
- output = strip_escseq(status.getvalue())
+ output = strip_colors(status.getvalue())
assert 'testing ... [ 33%] hello\r' in output
assert 'testing ... [ 67%] sphinx\r' in output
assert 'testing ... [100%] world\r\n' in output
@@ -50,7 +51,8 @@ def test_status_iterator_verbosity_0(app, status, warning):
@pytest.mark.sphinx('dummy')
-def test_status_iterator_verbosity_1(app, status, warning):
+def test_status_iterator_verbosity_1(app, status, warning, monkeypatch):
+ monkeypatch.setenv("FORCE_COLOR", "1")
logging.setup(app, status, warning)
# test for status_iterator (verbosity=1)
@@ -58,7 +60,7 @@ def test_status_iterator_verbosity_1(app, status, warning):
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ',
length=3, verbosity=1))
- output = strip_escseq(status.getvalue())
+ output = strip_colors(status.getvalue())
assert 'testing ... [ 33%] hello\n' in output
assert 'testing ... [ 67%] sphinx\n' in output
assert 'testing ... [100%] world\n\n' in output
@@ -73,14 +75,14 @@ def test_progress_message(app, status, warning):
with progress_message('testing'):
logger.info('blah ', nonl=True)
- output = strip_escseq(status.getvalue())
+ output = strip_colors(status.getvalue())
assert 'testing... blah done\n' in output
# skipping case
with progress_message('testing'):
raise SkipProgressMessage('Reason: %s', 'error') # NoQA: EM101
- output = strip_escseq(status.getvalue())
+ output = strip_colors(status.getvalue())
assert 'testing... skipped\nReason: error\n' in output
# error case
@@ -90,7 +92,7 @@ def test_progress_message(app, status, warning):
except Exception:
pass
- output = strip_escseq(status.getvalue())
+ output = strip_colors(status.getvalue())
assert 'testing... failed\n' in output
# decorator
@@ -99,5 +101,5 @@ def test_progress_message(app, status, warning):
logger.info('in func ', nonl=True)
func()
- output = strip_escseq(status.getvalue())
+ output = strip_colors(status.getvalue())
assert 'testing... in func done\n' in output
diff --git a/tests/test_util_docstrings.py b/tests/test_util/test_util_docstrings.py
index 813e84e..813e84e 100644
--- a/tests/test_util_docstrings.py
+++ b/tests/test_util/test_util_docstrings.py
diff --git a/tests/test_util_docutils.py b/tests/test_util/test_util_docutils.py
index 69999eb..69999eb 100644
--- a/tests/test_util_docutils.py
+++ b/tests/test_util/test_util_docutils.py
diff --git a/tests/test_util_fileutil.py b/tests/test_util/test_util_fileutil.py
index 9c23821..9c23821 100644
--- a/tests/test_util_fileutil.py
+++ b/tests/test_util/test_util_fileutil.py
diff --git a/tests/test_util_i18n.py b/tests/test_util/test_util_i18n.py
index 9a1ecc5..f6baa04 100644
--- a/tests/test_util_i18n.py
+++ b/tests/test_util/test_util_i18n.py
@@ -160,6 +160,8 @@ def test_CatalogRepository(tmp_path):
(tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES').mkdir(parents=True, exist_ok=True)
(tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#', encoding='utf8')
(tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test7.po').write_text('#', encoding='utf8')
+ (tmp_path / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir2').mkdir(parents=True, exist_ok=True)
+ (tmp_path / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir2' / 'test8.po').write_text('#', encoding='utf8')
# for language xx
repo = i18n.CatalogRepository(tmp_path, ['loc1', 'loc2'], 'xx', 'utf-8')
diff --git a/tests/test_util_images.py b/tests/test_util/test_util_images.py
index 15853c7..15853c7 100644
--- a/tests/test_util_images.py
+++ b/tests/test_util/test_util_images.py
diff --git a/tests/test_util_inspect.py b/tests/test_util/test_util_inspect.py
index 73f9656..32840b8 100644
--- a/tests/test_util_inspect.py
+++ b/tests/test_util/test_util_inspect.py
@@ -62,6 +62,7 @@ async def coroutinefunc():
async def asyncgenerator():
yield
+
partial_func = functools.partial(func)
partial_coroutinefunc = functools.partial(coroutinefunc)
@@ -222,168 +223,140 @@ def test_signature_partialmethod():
def test_signature_annotations():
- from .typing_test_data import (
- Node,
- f0,
- f1,
- f2,
- f3,
- f4,
- f5,
- f6,
- f7,
- f8,
- f9,
- f10,
- f11,
- f12,
- f13,
- f14,
- f15,
- f16,
- f17,
- f18,
- f19,
- f20,
- f21,
- f22,
- f23,
- f24,
- f25,
- )
+ import tests.test_util.typing_test_data as mod
# Class annotations
- sig = inspect.signature(f0)
+ sig = inspect.signature(mod.f0)
assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None'
# Generic types with concrete parameters
- sig = inspect.signature(f1)
+ sig = inspect.signature(mod.f1)
assert stringify_signature(sig) == '(x: list[int]) -> typing.List[int]'
# TypeVars and generic types with TypeVars
- sig = inspect.signature(f2)
- assert stringify_signature(sig) == ('(x: typing.List[tests.typing_test_data.T],'
- ' y: typing.List[tests.typing_test_data.T_co],'
- ' z: tests.typing_test_data.T'
- ') -> typing.List[tests.typing_test_data.T_contra]')
+ sig = inspect.signature(mod.f2)
+ assert stringify_signature(sig) == ('(x: typing.List[tests.test_util.typing_test_data.T],'
+ ' y: typing.List[tests.test_util.typing_test_data.T_co],'
+ ' z: tests.test_util.typing_test_data.T'
+ ') -> typing.List[tests.test_util.typing_test_data.T_contra]')
# Union types
- sig = inspect.signature(f3)
+ sig = inspect.signature(mod.f3)
assert stringify_signature(sig) == '(x: str | numbers.Integral) -> None'
# Quoted annotations
- sig = inspect.signature(f4)
+ sig = inspect.signature(mod.f4)
assert stringify_signature(sig) == '(x: str, y: str) -> None'
# Keyword-only arguments
- sig = inspect.signature(f5)
+ sig = inspect.signature(mod.f5)
assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None'
# Keyword-only arguments with varargs
- sig = inspect.signature(f6)
+ sig = inspect.signature(mod.f6)
assert stringify_signature(sig) == '(x: int, *args, y: str, z: str) -> None'
# Space around '=' for defaults
- sig = inspect.signature(f7)
+ sig = inspect.signature(mod.f7)
if sys.version_info[:2] <= (3, 10):
assert stringify_signature(sig) == '(x: int | None = None, y: dict = {}) -> None'
else:
assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None'
# Callable types
- sig = inspect.signature(f8)
+ sig = inspect.signature(mod.f8)
assert stringify_signature(sig) == '(x: typing.Callable[[int, str], int]) -> None'
- sig = inspect.signature(f9)
+ sig = inspect.signature(mod.f9)
assert stringify_signature(sig) == '(x: typing.Callable) -> None'
# Tuple types
- sig = inspect.signature(f10)
+ sig = inspect.signature(mod.f10)
assert stringify_signature(sig) == '(x: typing.Tuple[int, str], y: typing.Tuple[int, ...]) -> None'
# Instance annotations
- sig = inspect.signature(f11)
+ sig = inspect.signature(mod.f11)
assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None'
# tuple with more than two items
- sig = inspect.signature(f12)
+ sig = inspect.signature(mod.f12)
assert stringify_signature(sig) == '() -> typing.Tuple[int, str, int]'
# optional
- sig = inspect.signature(f13)
+ sig = inspect.signature(mod.f13)
assert stringify_signature(sig) == '() -> str | None'
# optional union
- sig = inspect.signature(f20)
+ sig = inspect.signature(mod.f20)
assert stringify_signature(sig) in ('() -> int | str | None',
'() -> str | int | None')
# Any
- sig = inspect.signature(f14)
+ sig = inspect.signature(mod.f14)
assert stringify_signature(sig) == '() -> typing.Any'
# ForwardRef
- sig = inspect.signature(f15)
+ sig = inspect.signature(mod.f15)
assert stringify_signature(sig) == '(x: Unknown, y: int) -> typing.Any'
# keyword only arguments (1)
- sig = inspect.signature(f16)
+ sig = inspect.signature(mod.f16)
assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)'
# keyword only arguments (2)
- sig = inspect.signature(f17)
+ sig = inspect.signature(mod.f17)
assert stringify_signature(sig) == '(*, arg3, arg4)'
- sig = inspect.signature(f18)
+ sig = inspect.signature(mod.f18)
assert stringify_signature(sig) == ('(self, arg1: int | typing.Tuple = 10) -> '
'typing.List[typing.Dict]')
# annotations for variadic and keyword parameters
- sig = inspect.signature(f19)
+ sig = inspect.signature(mod.f19)
assert stringify_signature(sig) == '(*args: int, **kwargs: str)'
# default value is inspect.Signature.empty
- sig = inspect.signature(f21)
+ sig = inspect.signature(mod.f21)
assert stringify_signature(sig) == "(arg1='whatever', arg2)"
# type hints by string
- sig = inspect.signature(Node.children)
- assert stringify_signature(sig) == '(self) -> typing.List[tests.typing_test_data.Node]'
+ sig = inspect.signature(mod.Node.children)
+ assert stringify_signature(sig) == '(self) -> typing.List[tests.test_util.typing_test_data.Node]'
- sig = inspect.signature(Node.__init__)
- assert stringify_signature(sig) == '(self, parent: tests.typing_test_data.Node | None) -> None'
+ sig = inspect.signature(mod.Node.__init__)
+ assert stringify_signature(sig) == '(self, parent: tests.test_util.typing_test_data.Node | None) -> None'
# show_annotation is False
- sig = inspect.signature(f7)
+ sig = inspect.signature(mod.f7)
assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})'
# show_return_annotation is False
- sig = inspect.signature(f7)
+ sig = inspect.signature(mod.f7)
if sys.version_info[:2] <= (3, 10):
assert stringify_signature(sig, show_return_annotation=False) == '(x: int | None = None, y: dict = {})'
else:
assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})'
# unqualified_typehints is True
- sig = inspect.signature(f7)
+ sig = inspect.signature(mod.f7)
if sys.version_info[:2] <= (3, 10):
assert stringify_signature(sig, unqualified_typehints=True) == '(x: int | None = None, y: dict = {}) -> None'
else:
assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None'
# case: separator at head
- sig = inspect.signature(f22)
+ sig = inspect.signature(mod.f22)
assert stringify_signature(sig) == '(*, a, b)'
# case: separator in the middle
- sig = inspect.signature(f23)
+ sig = inspect.signature(mod.f23)
assert stringify_signature(sig) == '(a, b, /, c, d)'
- sig = inspect.signature(f24)
+ sig = inspect.signature(mod.f24)
assert stringify_signature(sig) == '(a, /, *, b)'
# case: separator at tail
- sig = inspect.signature(f25)
+ sig = inspect.signature(mod.f25)
assert stringify_signature(sig) == '(a, b, /)'
@@ -667,6 +640,17 @@ def test_object_description_enum():
assert inspect.object_description(MyEnum.FOO) == "MyEnum.FOO"
+def test_object_description_enum_custom_repr():
+ class MyEnum(enum.Enum):
+ FOO = 1
+ BAR = 2
+
+ def __repr__(self):
+ return self.name
+
+ assert inspect.object_description(MyEnum.FOO) == "FOO"
+
+
def test_getslots():
class Foo:
pass
@@ -842,7 +826,7 @@ def test_getdoc_inherited_decorated_method():
"""
class Bar(Foo):
- @functools.lru_cache # noqa: B019
+ @functools.lru_cache # NoQA: B019
def meth(self):
# inherited and decorated method
pass
diff --git a/tests/test_util_inventory.py b/tests/test_util/test_util_inventory.py
index 2c20763..81d31b0 100644
--- a/tests/test_util_inventory.py
+++ b/tests/test_util/test_util_inventory.py
@@ -1,59 +1,21 @@
"""Test inventory util functions."""
import os
import posixpath
-import zlib
from io import BytesIO
+import sphinx.locale
from sphinx.testing.util import SphinxTestApp
from sphinx.util.inventory import InventoryFile
-inventory_v1 = b'''\
-# Sphinx inventory version 1
-# Project: foo
-# Version: 1.0
-module mod foo.html
-module.cls class foo.html
-'''
-
-inventory_v2 = b'''\
-# Sphinx inventory version 2
-# Project: foo
-# Version: 2.0
-# The remainder of this file is compressed with zlib.
-''' + zlib.compress(b'''\
-module1 py:module 0 foo.html#module-module1 Long Module desc
-module2 py:module 0 foo.html#module-$ -
-module1.func py:function 1 sub/foo.html#$ -
-module1.Foo.bar py:method 1 index.html#foo.Bar.baz -
-CFunc c:function 2 cfunc.html#CFunc -
-std cpp:type 1 index.html#std -
-std::uint8_t cpp:type 1 index.html#std_uint8_t -
-foo::Bar cpp:class 1 index.html#cpp_foo_bar -
-foo::Bar::baz cpp:function 1 index.html#cpp_foo_bar_baz -
-foons cpp:type 1 index.html#foons -
-foons::bartype cpp:type 1 index.html#foons_bartype -
-a term std:term -1 glossary.html#term-a-term -
-ls.-l std:cmdoption 1 index.html#cmdoption-ls-l -
-docname std:doc -1 docname.html -
-foo js:module 1 index.html#foo -
-foo.bar js:class 1 index.html#foo.bar -
-foo.bar.baz js:method 1 index.html#foo.bar.baz -
-foo.bar.qux js:data 1 index.html#foo.bar.qux -
-a term including:colon std:term -1 glossary.html#term-a-term-including-colon -
-''')
-
-inventory_v2_not_having_version = b'''\
-# Sphinx inventory version 2
-# Project: foo
-# Version:
-# The remainder of this file is compressed with zlib.
-''' + zlib.compress(b'''\
-module1 py:module 0 foo.html#module-module1 Long Module desc
-''')
+from tests.test_util.intersphinx_data import (
+ INVENTORY_V1,
+ INVENTORY_V2,
+ INVENTORY_V2_NO_VERSION,
+)
def test_read_inventory_v1():
- f = BytesIO(inventory_v1)
+ f = BytesIO(INVENTORY_V1)
invdata = InventoryFile.load(f, '/util', posixpath.join)
assert invdata['py:module']['module'] == \
('foo', '1.0', '/util/foo.html#module-module', '-')
@@ -62,7 +24,7 @@ def test_read_inventory_v1():
def test_read_inventory_v2():
- f = BytesIO(inventory_v2)
+ f = BytesIO(INVENTORY_V2)
invdata = InventoryFile.load(f, '/util', posixpath.join)
assert len(invdata['py:module']) == 2
@@ -80,7 +42,7 @@ def test_read_inventory_v2():
def test_read_inventory_v2_not_having_version():
- f = BytesIO(inventory_v2_not_having_version)
+ f = BytesIO(INVENTORY_V2_NO_VERSION)
invdata = InventoryFile.load(f, '/util', posixpath.join)
assert invdata['py:module']['module1'] == \
('foo', '', '/util/foo.html#module-module1', 'Long Module desc')
@@ -99,8 +61,8 @@ def _write_appconfig(dir, language, prefix=None):
def _build_inventory(srcdir):
app = SphinxTestApp(srcdir=srcdir)
app.build()
- app.cleanup()
- return (app.outdir / 'objects.inv')
+ sphinx.locale.translators.clear()
+ return app.outdir / 'objects.inv'
def test_inventory_localization(tmp_path):
diff --git a/tests/test_util_logging.py b/tests/test_util/test_util_logging.py
index 4d506a8..4ee548a 100644
--- a/tests/test_util_logging.py
+++ b/tests/test_util/test_util_logging.py
@@ -8,9 +8,8 @@ import pytest
from docutils import nodes
from sphinx.errors import SphinxWarning
-from sphinx.testing.util import strip_escseq
from sphinx.util import logging, osutil
-from sphinx.util.console import colorize
+from sphinx.util.console import colorize, strip_colors
from sphinx.util.logging import is_suppressed_warning, prefixed_warnings
from sphinx.util.parallel import ParallelTasks
@@ -110,7 +109,7 @@ def test_once_warning_log(app, status, warning):
logger.warning('message: %d', 1, once=True)
logger.warning('message: %d', 2, once=True)
- assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_escseq(warning.getvalue())
+ assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_colors(warning.getvalue())
def test_is_suppressed_warning():
@@ -278,7 +277,7 @@ def test_pending_warnings(app, status, warning):
assert 'WARNING: message3' not in warning.getvalue()
# actually logged as ordered
- assert 'WARNING: message2\nWARNING: message3' in strip_escseq(warning.getvalue())
+ assert 'WARNING: message2\nWARNING: message3' in strip_colors(warning.getvalue())
def test_colored_logs(app, status, warning):
@@ -396,3 +395,20 @@ def test_get_node_location_abspath():
location = logging.get_node_location(n)
assert location == absolute_filename + ':'
+
+
+@pytest.mark.sphinx(confoverrides={'show_warning_types': True})
+def test_show_warning_types(app, status, warning):
+ logging.setup(app, status, warning)
+ logger = logging.getLogger(__name__)
+ logger.warning('message2')
+ logger.warning('message3', type='test')
+ logger.warning('message4', type='test', subtype='logging')
+
+ warnings = strip_colors(warning.getvalue()).splitlines()
+
+ assert warnings == [
+ 'WARNING: message2',
+ 'WARNING: message3 [test]',
+ 'WARNING: message4 [test.logging]',
+ ]
diff --git a/tests/test_util_matching.py b/tests/test_util/test_util_matching.py
index 7d865ba..7d865ba 100644
--- a/tests/test_util_matching.py
+++ b/tests/test_util/test_util_matching.py
diff --git a/tests/test_util_nodes.py b/tests/test_util/test_util_nodes.py
index 92e4dc1..ddd5974 100644
--- a/tests/test_util_nodes.py
+++ b/tests/test_util/test_util_nodes.py
@@ -237,8 +237,8 @@ def test_split_explicit_target(title, expected):
def test_apply_source_workaround_literal_block_no_source():
"""Regression test for #11091.
- Test that apply_source_workaround doesn't raise.
- """
+ Test that apply_source_workaround doesn't raise.
+ """
literal_block = nodes.literal_block('', '')
list_item = nodes.list_item('', literal_block)
bullet_list = nodes.bullet_list('', list_item)
diff --git a/tests/test_util_rst.py b/tests/test_util/test_util_rst.py
index d50c90c..d50c90c 100644
--- a/tests/test_util_rst.py
+++ b/tests/test_util/test_util_rst.py
diff --git a/tests/test_util_template.py b/tests/test_util/test_util_template.py
index 4601179..4601179 100644
--- a/tests/test_util_template.py
+++ b/tests/test_util/test_util_template.py
diff --git a/tests/test_util_typing.py b/tests/test_util/test_util_typing.py
index d79852e..9c28029 100644
--- a/tests/test_util_typing.py
+++ b/tests/test_util/test_util_typing.py
@@ -1,15 +1,38 @@
"""Tests util.typing functions."""
import sys
+from contextvars import Context, ContextVar, Token
from enum import Enum
from numbers import Integral
from struct import Struct
-from types import TracebackType
+from types import (
+ AsyncGeneratorType,
+ BuiltinFunctionType,
+ BuiltinMethodType,
+ CellType,
+ ClassMethodDescriptorType,
+ CodeType,
+ CoroutineType,
+ FrameType,
+ FunctionType,
+ GeneratorType,
+ GetSetDescriptorType,
+ LambdaType,
+ MappingProxyType,
+ MemberDescriptorType,
+ MethodDescriptorType,
+ MethodType,
+ MethodWrapperType,
+ ModuleType,
+ TracebackType,
+ WrapperDescriptorType,
+)
from typing import (
Any,
Callable,
Dict,
Generator,
+ Iterator,
List,
NewType,
Optional,
@@ -21,7 +44,7 @@ from typing import (
import pytest
from sphinx.ext.autodoc import mock
-from sphinx.util.typing import INVALID_BUILTIN_CLASSES, restify, stringify_annotation
+from sphinx.util.typing import _INVALID_BUILTIN_CLASSES, restify, stringify_annotation
class MyClass1:
@@ -76,11 +99,55 @@ def test_restify():
def test_is_invalid_builtin_class():
# if these tests start failing, it means that the __module__
- # of one of these classes has changed, and INVALID_BUILTIN_CLASSES
+ # of one of these classes has changed, and _INVALID_BUILTIN_CLASSES
# in sphinx.util.typing needs to be updated.
- assert INVALID_BUILTIN_CLASSES.keys() == {Struct, TracebackType}
+ assert _INVALID_BUILTIN_CLASSES.keys() == {
+ Context,
+ ContextVar,
+ Token,
+ Struct,
+ AsyncGeneratorType,
+ BuiltinFunctionType,
+ BuiltinMethodType,
+ CellType,
+ ClassMethodDescriptorType,
+ CodeType,
+ CoroutineType,
+ FrameType,
+ FunctionType,
+ GeneratorType,
+ GetSetDescriptorType,
+ LambdaType,
+ MappingProxyType,
+ MemberDescriptorType,
+ MethodDescriptorType,
+ MethodType,
+ MethodWrapperType,
+ ModuleType,
+ TracebackType,
+ WrapperDescriptorType,
+ }
assert Struct.__module__ == '_struct'
+ assert AsyncGeneratorType.__module__ == 'builtins'
+ assert BuiltinFunctionType.__module__ == 'builtins'
+ assert BuiltinMethodType.__module__ == 'builtins'
+ assert CellType.__module__ == 'builtins'
+ assert ClassMethodDescriptorType.__module__ == 'builtins'
+ assert CodeType.__module__ == 'builtins'
+ assert CoroutineType.__module__ == 'builtins'
+ assert FrameType.__module__ == 'builtins'
+ assert FunctionType.__module__ == 'builtins'
+ assert GeneratorType.__module__ == 'builtins'
+ assert GetSetDescriptorType.__module__ == 'builtins'
+ assert LambdaType.__module__ == 'builtins'
+ assert MappingProxyType.__module__ == 'builtins'
+ assert MemberDescriptorType.__module__ == 'builtins'
+ assert MethodDescriptorType.__module__ == 'builtins'
+ assert MethodType.__module__ == 'builtins'
+ assert MethodWrapperType.__module__ == 'builtins'
+ assert ModuleType.__module__ == 'builtins'
assert TracebackType.__module__ == 'builtins'
+ assert WrapperDescriptorType.__module__ == 'builtins'
def test_restify_type_hints_containers():
@@ -103,12 +170,14 @@ def test_restify_type_hints_containers():
assert restify(List[Dict[str, Tuple]]) == (":py:class:`~typing.List`\\ "
"[:py:class:`~typing.Dict`\\ "
"[:py:class:`str`, :py:class:`~typing.Tuple`]]")
- assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util_typing.MyList`\\ "
+ assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util.test_util_typing.MyList`\\ "
"[:py:class:`~typing.Tuple`\\ "
"[:py:class:`int`, :py:class:`int`]]")
assert restify(Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ "
"[:py:obj:`None`, :py:obj:`None`, "
":py:obj:`None`]")
+ assert restify(Iterator[None]) == (":py:class:`~typing.Iterator`\\ "
+ "[:py:obj:`None`]")
def test_restify_type_hints_Callable():
@@ -121,24 +190,44 @@ def test_restify_type_hints_Callable():
def test_restify_type_hints_Union():
- assert restify(Optional[int]) == ":py:obj:`~typing.Optional`\\ [:py:class:`int`]"
- assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]"
- assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ "
- "[:py:class:`int`, :py:class:`str`]")
- assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ "
- "[:py:class:`int`, :py:class:`numbers.Integral`]")
- assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ "
- "[:py:class:`int`,"
- " :py:class:`~numbers.Integral`]")
+ assert restify(Union[int]) == ":py:class:`int`"
+ assert restify(Union[int, str]) == ":py:class:`int` | :py:class:`str`"
+ assert restify(Optional[int]) == ":py:class:`int` | :py:obj:`None`"
+
+ assert restify(Union[str, None]) == ":py:class:`str` | :py:obj:`None`"
+ assert restify(Union[None, str]) == ":py:obj:`None` | :py:class:`str`"
+ assert restify(Optional[str]) == ":py:class:`str` | :py:obj:`None`"
+
+ assert restify(Union[int, str, None]) == (
+ ":py:class:`int` | :py:class:`str` | :py:obj:`None`"
+ )
+ assert restify(Optional[Union[int, str]]) in {
+ ":py:class:`str` | :py:class:`int` | :py:obj:`None`",
+ ":py:class:`int` | :py:class:`str` | :py:obj:`None`",
+ }
+
+ assert restify(Union[int, Integral]) == (
+ ":py:class:`int` | :py:class:`numbers.Integral`"
+ )
+ assert restify(Union[int, Integral], "smart") == (
+ ":py:class:`int` | :py:class:`~numbers.Integral`"
+ )
assert (restify(Union[MyClass1, MyClass2]) ==
- (":py:obj:`~typing.Union`\\ "
- "[:py:class:`tests.test_util_typing.MyClass1`, "
- ":py:class:`tests.test_util_typing.<MyClass2>`]"))
+ (":py:class:`tests.test_util.test_util_typing.MyClass1`"
+ " | :py:class:`tests.test_util.test_util_typing.<MyClass2>`"))
assert (restify(Union[MyClass1, MyClass2], "smart") ==
- (":py:obj:`~typing.Union`\\ "
- "[:py:class:`~tests.test_util_typing.MyClass1`,"
- " :py:class:`~tests.test_util_typing.<MyClass2>`]"))
+ (":py:class:`~tests.test_util.test_util_typing.MyClass1`"
+ " | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`"))
+
+ assert (restify(Optional[Union[MyClass1, MyClass2]]) ==
+ (":py:class:`tests.test_util.test_util_typing.MyClass1`"
+ " | :py:class:`tests.test_util.test_util_typing.<MyClass2>`"
+ " | :py:obj:`None`"))
+ assert (restify(Optional[Union[MyClass1, MyClass2]], "smart") ==
+ (":py:class:`~tests.test_util.test_util_typing.MyClass1`"
+ " | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`"
+ " | :py:obj:`None`"))
def test_restify_type_hints_typevars():
@@ -146,35 +235,35 @@ def test_restify_type_hints_typevars():
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
- assert restify(T) == ":py:obj:`tests.test_util_typing.T`"
- assert restify(T, "smart") == ":py:obj:`~tests.test_util_typing.T`"
+ assert restify(T) == ":py:obj:`tests.test_util.test_util_typing.T`"
+ assert restify(T, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T`"
- assert restify(T_co) == ":py:obj:`tests.test_util_typing.T_co`"
- assert restify(T_co, "smart") == ":py:obj:`~tests.test_util_typing.T_co`"
+ assert restify(T_co) == ":py:obj:`tests.test_util.test_util_typing.T_co`"
+ assert restify(T_co, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T_co`"
- assert restify(T_contra) == ":py:obj:`tests.test_util_typing.T_contra`"
- assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util_typing.T_contra`"
+ assert restify(T_contra) == ":py:obj:`tests.test_util.test_util_typing.T_contra`"
+ assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T_contra`"
- assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]"
- assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]"
+ assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util.test_util_typing.T`]"
+ assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util.test_util_typing.T`]"
- assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util_typing.T`]"
- assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util_typing.T`]"
+ assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util.test_util_typing.T`]"
+ assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util.test_util_typing.T`]"
if sys.version_info[:2] >= (3, 10):
- assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`"
- assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`"
+ assert restify(MyInt) == ":py:class:`tests.test_util.test_util_typing.MyInt`"
+ assert restify(MyInt, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyInt`"
else:
assert restify(MyInt) == ":py:class:`MyInt`"
assert restify(MyInt, "smart") == ":py:class:`MyInt`"
def test_restify_type_hints_custom_class():
- assert restify(MyClass1) == ":py:class:`tests.test_util_typing.MyClass1`"
- assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util_typing.MyClass1`"
+ assert restify(MyClass1) == ":py:class:`tests.test_util.test_util_typing.MyClass1`"
+ assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyClass1`"
- assert restify(MyClass2) == ":py:class:`tests.test_util_typing.<MyClass2>`"
- assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util_typing.<MyClass2>`"
+ assert restify(MyClass2) == ":py:class:`tests.test_util.test_util_typing.<MyClass2>`"
+ assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util.test_util_typing.<MyClass2>`"
def test_restify_type_hints_alias():
@@ -199,8 +288,8 @@ def test_restify_type_Literal():
from typing import Literal # type: ignore[attr-defined]
assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']"
- assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util_typing.MyEnum.a`]'
- assert restify(Literal[MyEnum.a], 'smart') == ':py:obj:`~typing.Literal`\\ [:py:attr:`~tests.test_util_typing.MyEnum.a`]'
+ assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util.test_util_typing.MyEnum.a`]'
+ assert restify(Literal[MyEnum.a], 'smart') == ':py:obj:`~typing.Literal`\\ [:py:attr:`~tests.test_util.test_util_typing.MyEnum.a`]'
def test_restify_pep_585():
@@ -223,7 +312,7 @@ def test_restify_pep_585():
"[:py:class:`str`, :py:class:`~typing.Tuple`\\ "
"[:py:class:`str`, ...]]]")
assert restify(tuple[MyList[list[int]], int]) == (":py:class:`tuple`\\ ["
- ":py:class:`tests.test_util_typing.MyList`\\ "
+ ":py:class:`tests.test_util.test_util_typing.MyList`\\ "
"[:py:class:`list`\\ [:py:class:`int`]], "
":py:class:`int`]")
@@ -231,14 +320,15 @@ def test_restify_pep_585():
@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.')
def test_restify_type_union_operator():
assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore[attr-defined]
+ assert restify(None | int) == ":py:obj:`None` | :py:class:`int`" # type: ignore[attr-defined]
assert restify(int | str) == ":py:class:`int` | :py:class:`str`" # type: ignore[attr-defined]
assert restify(int | str | None) == (":py:class:`int` | :py:class:`str` | " # type: ignore[attr-defined]
":py:obj:`None`")
def test_restify_broken_type_hints():
- assert restify(BrokenType) == ':py:class:`tests.test_util_typing.BrokenType`'
- assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util_typing.BrokenType`'
+ assert restify(BrokenType) == ':py:class:`tests.test_util.test_util_typing.BrokenType`'
+ assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util.test_util_typing.BrokenType`'
def test_restify_mock():
@@ -315,14 +405,18 @@ def test_stringify_type_hints_containers():
assert stringify_annotation(List[Dict[str, Tuple]], "fully-qualified") == "typing.List[typing.Dict[str, typing.Tuple]]"
assert stringify_annotation(List[Dict[str, Tuple]], "smart") == "~typing.List[~typing.Dict[str, ~typing.Tuple]]"
- assert stringify_annotation(MyList[Tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[Tuple[int, int]]"
- assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[typing.Tuple[int, int]]"
- assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[~typing.Tuple[int, int]]"
+ assert stringify_annotation(MyList[Tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyList[Tuple[int, int]]"
+ assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[typing.Tuple[int, int]]"
+ assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[~typing.Tuple[int, int]]"
assert stringify_annotation(Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]"
assert stringify_annotation(Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]"
assert stringify_annotation(Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]"
+ assert stringify_annotation(Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]"
+ assert stringify_annotation(Iterator[None], "fully-qualified") == "typing.Iterator[None]"
+ assert stringify_annotation(Iterator[None], "smart") == "~typing.Iterator[None]"
+
def test_stringify_type_hints_pep_585():
assert stringify_annotation(list[int], 'fully-qualified-except-typing') == "list[int]"
@@ -346,9 +440,9 @@ def test_stringify_type_hints_pep_585():
assert stringify_annotation(list[dict[str, tuple]], 'fully-qualified-except-typing') == "list[dict[str, tuple]]"
assert stringify_annotation(list[dict[str, tuple]], "smart") == "list[dict[str, tuple]]"
- assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[tuple[int, int]]"
- assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[tuple[int, int]]"
- assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[tuple[int, int]]"
+ assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyList[tuple[int, int]]"
+ assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[tuple[int, int]]"
+ assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[tuple[int, int]]"
assert stringify_annotation(type[int], 'fully-qualified-except-typing') == "type[int]"
assert stringify_annotation(type[int], "smart") == "type[int]"
@@ -413,9 +507,12 @@ def test_stringify_type_hints_Union():
assert stringify_annotation(Optional[int], "fully-qualified") == "int | None"
assert stringify_annotation(Optional[int], "smart") == "int | None"
- assert stringify_annotation(Union[str, None], 'fully-qualified-except-typing') == "str | None"
- assert stringify_annotation(Union[str, None], "fully-qualified") == "str | None"
- assert stringify_annotation(Union[str, None], "smart") == "str | None"
+ assert stringify_annotation(Union[int, None], 'fully-qualified-except-typing') == "int | None"
+ assert stringify_annotation(Union[None, int], 'fully-qualified-except-typing') == "None | int"
+ assert stringify_annotation(Union[int, None], "fully-qualified") == "int | None"
+ assert stringify_annotation(Union[None, int], "fully-qualified") == "None | int"
+ assert stringify_annotation(Union[int, None], "smart") == "int | None"
+ assert stringify_annotation(Union[None, int], "smart") == "None | int"
assert stringify_annotation(Union[int, str], 'fully-qualified-except-typing') == "int | str"
assert stringify_annotation(Union[int, str], "fully-qualified") == "int | str"
@@ -426,11 +523,11 @@ def test_stringify_type_hints_Union():
assert stringify_annotation(Union[int, Integral], "smart") == "int | ~numbers.Integral"
assert (stringify_annotation(Union[MyClass1, MyClass2], 'fully-qualified-except-typing') ==
- "tests.test_util_typing.MyClass1 | tests.test_util_typing.<MyClass2>")
+ "tests.test_util.test_util_typing.MyClass1 | tests.test_util.test_util_typing.<MyClass2>")
assert (stringify_annotation(Union[MyClass1, MyClass2], "fully-qualified") ==
- "tests.test_util_typing.MyClass1 | tests.test_util_typing.<MyClass2>")
+ "tests.test_util.test_util_typing.MyClass1 | tests.test_util.test_util_typing.<MyClass2>")
assert (stringify_annotation(Union[MyClass1, MyClass2], "smart") ==
- "~tests.test_util_typing.MyClass1 | ~tests.test_util_typing.<MyClass2>")
+ "~tests.test_util.test_util_typing.MyClass1 | ~tests.test_util.test_util_typing.<MyClass2>")
def test_stringify_type_hints_typevars():
@@ -438,35 +535,35 @@ def test_stringify_type_hints_typevars():
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
- assert stringify_annotation(T, 'fully-qualified-except-typing') == "tests.test_util_typing.T"
- assert stringify_annotation(T, "smart") == "~tests.test_util_typing.T"
+ assert stringify_annotation(T, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T"
+ assert stringify_annotation(T, "smart") == "~tests.test_util.test_util_typing.T"
- assert stringify_annotation(T_co, 'fully-qualified-except-typing') == "tests.test_util_typing.T_co"
- assert stringify_annotation(T_co, "smart") == "~tests.test_util_typing.T_co"
+ assert stringify_annotation(T_co, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T_co"
+ assert stringify_annotation(T_co, "smart") == "~tests.test_util.test_util_typing.T_co"
- assert stringify_annotation(T_contra, 'fully-qualified-except-typing') == "tests.test_util_typing.T_contra"
- assert stringify_annotation(T_contra, "smart") == "~tests.test_util_typing.T_contra"
+ assert stringify_annotation(T_contra, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T_contra"
+ assert stringify_annotation(T_contra, "smart") == "~tests.test_util.test_util_typing.T_contra"
- assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util_typing.T]"
- assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util_typing.T]"
+ assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util.test_util_typing.T]"
+ assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util.test_util_typing.T]"
- assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util_typing.T]"
- assert stringify_annotation(list[T], "smart") == "list[~tests.test_util_typing.T]"
+ assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util.test_util_typing.T]"
+ assert stringify_annotation(list[T], "smart") == "list[~tests.test_util.test_util_typing.T]"
if sys.version_info[:2] >= (3, 10):
- assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util_typing.MyInt"
- assert stringify_annotation(MyInt, "smart") == "~tests.test_util_typing.MyInt"
+ assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyInt"
+ assert stringify_annotation(MyInt, "smart") == "~tests.test_util.test_util_typing.MyInt"
else:
assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "MyInt"
assert stringify_annotation(MyInt, "smart") == "MyInt"
def test_stringify_type_hints_custom_class():
- assert stringify_annotation(MyClass1, 'fully-qualified-except-typing') == "tests.test_util_typing.MyClass1"
- assert stringify_annotation(MyClass1, "smart") == "~tests.test_util_typing.MyClass1"
+ assert stringify_annotation(MyClass1, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyClass1"
+ assert stringify_annotation(MyClass1, "smart") == "~tests.test_util.test_util_typing.MyClass1"
- assert stringify_annotation(MyClass2, 'fully-qualified-except-typing') == "tests.test_util_typing.<MyClass2>"
- assert stringify_annotation(MyClass2, "smart") == "~tests.test_util_typing.<MyClass2>"
+ assert stringify_annotation(MyClass2, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.<MyClass2>"
+ assert stringify_annotation(MyClass2, "smart") == "~tests.test_util.test_util_typing.<MyClass2>"
def test_stringify_type_hints_alias():
@@ -486,8 +583,8 @@ def test_stringify_type_Literal():
assert stringify_annotation(Literal[1, "2", "\r"], "fully-qualified") == "typing.Literal[1, '2', '\\r']"
assert stringify_annotation(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']"
- assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified-except-typing') == 'Literal[tests.test_util_typing.MyEnum.a]'
- assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified') == 'typing.Literal[tests.test_util_typing.MyEnum.a]'
+ assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified-except-typing') == 'Literal[tests.test_util.test_util_typing.MyEnum.a]'
+ assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified') == 'typing.Literal[tests.test_util.test_util_typing.MyEnum.a]'
assert stringify_annotation(Literal[MyEnum.a], 'smart') == '~typing.Literal[MyEnum.a]'
@@ -510,8 +607,8 @@ def test_stringify_type_union_operator():
def test_stringify_broken_type_hints():
- assert stringify_annotation(BrokenType, 'fully-qualified-except-typing') == 'tests.test_util_typing.BrokenType'
- assert stringify_annotation(BrokenType, "smart") == '~tests.test_util_typing.BrokenType'
+ assert stringify_annotation(BrokenType, 'fully-qualified-except-typing') == 'tests.test_util.test_util_typing.BrokenType'
+ assert stringify_annotation(BrokenType, "smart") == '~tests.test_util.test_util_typing.BrokenType'
def test_stringify_mock():
diff --git a/tests/typing_test_data.py b/tests/test_util/typing_test_data.py
index 8a7ebc4..e29b600 100644
--- a/tests/typing_test_data.py
+++ b/tests/test_util/typing_test_data.py
@@ -39,7 +39,7 @@ def f6(x: int, *args, y: str, z: str) -> None:
pass
-def f7(x: int = None, y: dict = {}) -> None: # NoQA: B006
+def f7(x: int = None, y: dict = {}) -> None: # NoQA: B006,RUF013
pass
@@ -77,7 +77,7 @@ def f14() -> Any:
pass
-def f15(x: "Unknown", y: "int") -> Any: # noqa: F821 # type: ignore[attr-defined]
+def f15(x: "Unknown", y: "int") -> Any: # NoQA: F821 # type: ignore[attr-defined]
pass
diff --git a/tests/test_writers/__init__.py b/tests/test_writers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_writers/__init__.py
diff --git a/tests/test_api_translator.py b/tests/test_writers/test_api_translator.py
index 9f2bd44..9f2bd44 100644
--- a/tests/test_api_translator.py
+++ b/tests/test_writers/test_api_translator.py
diff --git a/tests/test_docutilsconf.py b/tests/test_writers/test_docutilsconf.py
index def6cb6..6422c30 100644
--- a/tests/test_docutilsconf.py
+++ b/tests/test_writers/test_docutilsconf.py
@@ -18,8 +18,8 @@ def test_html_with_default_docutilsconf(app, status, warning):
@pytest.mark.sphinx('dummy', testroot='docutilsconf', freshenv=True,
- docutilsconf=('[restructuredtext parser]\n'
- 'trim_footnote_reference_space: true\n'))
+ docutils_conf=('[restructuredtext parser]\n'
+ 'trim_footnote_reference_space: true\n'))
def test_html_with_docutilsconf(app, status, warning):
with patch_docutils(app.confdir):
app.build()
diff --git a/tests/test_writer_latex.py b/tests/test_writers/test_writer_latex.py
index a0ab3ee..a0ab3ee 100644
--- a/tests/test_writer_latex.py
+++ b/tests/test_writers/test_writer_latex.py
diff --git a/tests/utils.py b/tests/utils.py
index 32636b7..5636a13 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,55 +1,125 @@
-import contextlib
-import http.server
-import pathlib
-import threading
+from __future__ import annotations
+
+__all__ = ('http_server',)
+
+import socket
+from contextlib import contextmanager
+from http.server import ThreadingHTTPServer
+from pathlib import Path
from ssl import PROTOCOL_TLS_SERVER, SSLContext
+from threading import Thread
+from typing import TYPE_CHECKING
+from urllib.parse import urlparse
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from http.server import HTTPServer
+ from socketserver import BaseRequestHandler
+ from typing import Final
-import filelock
+ from sphinx.application import Sphinx
# Generated with:
# $ openssl req -new -x509 -days 3650 -nodes -out cert.pem \
# -keyout cert.pem -addext "subjectAltName = DNS:localhost"
-TESTS_ROOT = pathlib.Path(__file__).parent
-CERT_FILE = str(TESTS_ROOT / "certs" / "cert.pem")
+TESTS_ROOT: Final[Path] = Path(__file__).parent
+CERT_FILE: Final[str] = str(TESTS_ROOT / 'certs' / 'cert.pem')
-# File lock for tests
-LOCK_PATH = str(TESTS_ROOT / 'test-server.lock')
+class HttpServerThread(Thread):
+ def __init__(self, handler: type[BaseRequestHandler], *, port: int = 0) -> None:
+ """
+ Constructs a threaded HTTP server. The default port number of ``0``
+ delegates selection of a port number to bind to to Python.
-class HttpServerThread(threading.Thread):
- def __init__(self, handler, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.server = http.server.ThreadingHTTPServer(("localhost", 7777), handler)
+ Ref: https://docs.python.org/3.11/library/socketserver.html#asynchronous-mixins
+ """
+ super().__init__(daemon=True)
+ self.server = ThreadingHTTPServer(('localhost', port), handler)
- def run(self):
+ def run(self) -> None:
self.server.serve_forever(poll_interval=0.001)
- def terminate(self):
+ def terminate(self) -> None:
self.server.shutdown()
self.server.server_close()
self.join()
class HttpsServerThread(HttpServerThread):
- def __init__(self, handler, *args, **kwargs):
- super().__init__(handler, *args, **kwargs)
+ def __init__(self, handler: type[BaseRequestHandler], *, port: int = 0) -> None:
+ super().__init__(handler, port=port)
sslcontext = SSLContext(PROTOCOL_TLS_SERVER)
sslcontext.load_cert_chain(CERT_FILE)
self.server.socket = sslcontext.wrap_socket(self.server.socket, server_side=True)
-def create_server(thread_class):
- def server(handler):
- lock = filelock.FileLock(LOCK_PATH)
- with lock:
- server_thread = thread_class(handler, daemon=True)
- server_thread.start()
- try:
- yield server_thread
- finally:
- server_thread.terminate()
- return contextlib.contextmanager(server)
+@contextmanager
+def http_server(
+ handler: type[BaseRequestHandler],
+ *,
+ tls_enabled: bool = False,
+ port: int = 0,
+) -> Iterator[HTTPServer]:
+ server_cls = HttpsServerThread if tls_enabled else HttpServerThread
+ server_thread = server_cls(handler, port=port)
+ server_thread.start()
+ server_port = server_thread.server.server_port
+ assert port == 0 or server_port == port
+ try:
+ socket.create_connection(('localhost', server_port), timeout=0.5).close()
+ yield server_thread.server # Connection has been confirmed possible; proceed.
+ finally:
+ server_thread.terminate()
+
+
+@contextmanager
+def rewrite_hyperlinks(app: Sphinx, server: HTTPServer) -> Iterator[None]:
+ """
+ Rewrite hyperlinks that refer to network location 'localhost:7777',
+ allowing that location to vary dynamically with the arbitrary test HTTP
+ server port assigned during unit testing.
+
+ :param app: The Sphinx application where link replacement is to occur.
+ :param server: Destination server to redirect the hyperlinks to.
+ """
+ match_netloc, replacement_netloc = (
+ 'localhost:7777',
+ f'localhost:{server.server_port}',
+ )
+
+ def rewrite_hyperlink(_app: Sphinx, uri: str) -> str | None:
+ parsed_uri = urlparse(uri)
+ if parsed_uri.netloc != match_netloc:
+ return uri
+ return parsed_uri._replace(netloc=replacement_netloc).geturl()
+
+ listener_id = app.connect('linkcheck-process-uri', rewrite_hyperlink)
+ yield
+ app.disconnect(listener_id)
+
+
+@contextmanager
+def serve_application(
+ app: Sphinx,
+ handler: type[BaseRequestHandler],
+ *,
+ tls_enabled: bool = False,
+ port: int = 0,
+) -> Iterator[str]:
+ """
+ Prepare a temporary server to handle HTTP requests related to the links
+ found in a Sphinx application project.
+ :param app: The Sphinx application.
+ :param handler: Determines how each request will be handled.
+ :param tls_enabled: Whether TLS (SSL) should be enabled for the server.
+ :param port: Optional server port (default: auto).
-http_server = create_server(HttpServerThread)
-https_server = create_server(HttpsServerThread)
+ :return: The address of the temporary HTTP server.
+ """
+ with (
+ http_server(handler, tls_enabled=tls_enabled, port=port) as server,
+ rewrite_hyperlinks(app, server),
+ ):
+ yield f'localhost:{server.server_port}'