diff --git a/pyproject.toml b/pyproject.toml index d5843e635..aba7a1d41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,6 @@ lint = [ ] test = [ "pytest>=6.0", - "html5lib", "defusedxml>=0.7.1", # for secure XML/HTML parsing "cython>=3.0", "setuptools>=67.0", # for Cython compilation diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 55f68a395..f75c96858 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -7,12 +7,11 @@ __all__ = ('SphinxTestApp', 'SphinxTestAppWrapperForSkipBuilding') import contextlib import os import sys -import warnings from io import StringIO from types import MappingProxyType from typing import TYPE_CHECKING -from xml.etree import ElementTree +from defusedxml.ElementTree import parse as xml_parse from docutils import nodes from docutils.parsers.rst import directives, roles @@ -26,6 +25,7 @@ if TYPE_CHECKING: from collections.abc import Mapping from pathlib import Path from typing import Any, Final + from xml.etree.ElementTree import ElementTree from docutils.nodes import Node @@ -70,10 +70,10 @@ def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> f'The node{xpath}[{key}] is not {value!r}: {node[key]!r}' -def etree_parse(path: str) -> Any: - with warnings.catch_warnings(record=False): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return ElementTree.parse(path) # NoQA: S314 # using known data in tests +# keep this to restrict the API usage and to have a correct return type +def etree_parse(path: str | os.PathLike[str]) -> ElementTree: + """Parse a file into a (safe) XML element tree.""" + return xml_parse(path) class SphinxTestApp(sphinx.application.Sphinx): diff --git a/tests/test_builders/conftest.py b/tests/test_builders/conftest.py index 54a388c21..1a4c71535 100644 --- a/tests/test_builders/conftest.py +++ b/tests/test_builders/conftest.py @@ -3,26 +3,26 @@ from __future__ import annotations from typing import TYPE_CHECKING import pytest -from html5lib import HTMLParser + +from sphinx.testing.util import etree_parse if TYPE_CHECKING: from collections.abc import Callable, Generator from pathlib import Path - from xml.etree.ElementTree import Element + from xml.etree.ElementTree import ElementTree -etree_cache: dict[Path, Element] = {} +_etree_cache: dict[Path, ElementTree] = {} -def _parse(fname: Path) -> Element: - if fname in etree_cache: - return etree_cache[fname] - with fname.open('rb') as fp: - etree = HTMLParser(namespaceHTMLElements=False).parse(fp) - etree_cache[fname] = etree - return etree +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() -> Generator[Callable[[Path], Element], None, None]: +def cached_etree_parse() -> Generator[Callable[[Path], ElementTree], None, None]: yield _parse - etree_cache.clear() + _etree_cache.clear() diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 9a3b0c8bf..1fa3ba4cd 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -12,43 +12,8 @@ from sphinx.errors import ConfigError from sphinx.util.console import strip_colors from sphinx.util.inventory import InventoryFile -FIGURE_CAPTION = ".//figure/figcaption/p" - - -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 ' - f'{path!r} in file {fname}') - return - else: - assert nodes != [], ('did not find any node matching xpath ' - f'{path!r} in file {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 - - msg = (f'{check!r} not found in any node matching ' - f'{path!r} in file {fname}: {[node.text for node in nodes]!r}') - raise AssertionError(msg) +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): diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py index 9b50a106f..0350efd67 100644 --- a/tests/test_builders/test_build_html_5_output.py +++ b/tests/test_builders/test_build_html_5_output.py @@ -4,7 +4,7 @@ import re import pytest -from tests.test_builders.test_build_html import check_xpath +from tests.test_builders.xpath_util import check_xpath def tail_check(check): @@ -128,7 +128,7 @@ def tail_check(check): # ``seealso`` directive ('markup.html', ".//div/p[@class='admonition-title']", 'See also'), # a ``hlist`` directive - ('markup.html', ".//table[@class='hlist']/tbody/tr/td/ul/li/p", '^This$'), + ('markup.html', ".//table[@class='hlist']/tr/td/ul/li/p", '^This$'), # a ``centered`` directive ('markup.html', ".//p[@class='centered']/strong", 'LICENSE'), # a glossary diff --git a/tests/test_builders/test_build_html_numfig.py b/tests/test_builders/test_build_html_numfig.py index 8170cd2ce..62e68cb6d 100644 --- a/tests/test_builders/test_build_html_numfig.py +++ b/tests/test_builders/test_build_html_numfig.py @@ -5,7 +5,8 @@ import re import pytest -from tests.test_builders.test_build_html import FIGURE_CAPTION, check_xpath +from tests.test_builders.xpath_data import FIGURE_CAPTION +from tests.test_builders.xpath_util import check_xpath @pytest.mark.sphinx('html', testroot='numfig') diff --git a/tests/test_builders/test_build_html_tocdepth.py b/tests/test_builders/test_build_html_tocdepth.py index 69a932ffb..4d999956b 100644 --- a/tests/test_builders/test_build_html_tocdepth.py +++ b/tests/test_builders/test_build_html_tocdepth.py @@ -2,7 +2,7 @@ import pytest -from tests.test_builders.test_build_html import check_xpath +from tests.test_builders.xpath_util import check_xpath @pytest.mark.parametrize(("fname", "path", "check", "be_found"), [ diff --git a/tests/test_builders/xpath_data.py b/tests/test_builders/xpath_data.py new file mode 100644 index 000000000..30f8e07ab --- /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 000000000..7525c1956 --- /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_domains/test_domain_std.py b/tests/test_domains/test_domain_std.py index 42a1823aa..52ecdf53f 100644 --- a/tests/test_domains/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(): @@ -375,9 +374,11 @@ def test_productionlist(app, status, warning): 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 diff --git a/tests/test_extensions/autodoc_util.py b/tests/test_extensions/autodoc_util.py new file mode 100644 index 000000000..7c4da0797 --- /dev/null +++ b/tests/test_extensions/autodoc_util.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import Mock + +# NEVER import those objects from sphinx.ext.autodoc directly +from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options +from sphinx.util.docutils import LoggingReporter + +if TYPE_CHECKING: + from typing import Any + + from docutils.statemachine import StringList + + from sphinx.application import Sphinx + + +def do_autodoc( + app: Sphinx, + objtype: str, + name: str, + options: dict[str, Any] | None = None, +) -> StringList: + options = {} if options is None else options.copy() + app.env.temp_data.setdefault('docname', 'index') # set dummy docname + doccls = app.registry.documenters[objtype] + docoptions = process_documenter_options(doccls, app.config, options) + state = Mock() + state.document.settings.tab_width = 8 + bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state) + documenter = doccls(bridge, name) + documenter.generate() + return bridge.result diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 723e91dfb..6e708fc7e 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -20,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 @@ -34,20 +34,6 @@ if TYPE_CHECKING: from typing import Any -def do_autodoc(app, objtype, name, options=None): - 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 - - def make_directive_bridge(env): options = Options( inherited_members=False, @@ -82,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) @@ -139,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) diff --git a/tests/test_extensions/test_ext_autodoc_autoattribute.py b/tests/test_extensions/test_ext_autodoc_autoattribute.py index b2384b63c..41fcc9901 100644 --- a/tests/test_extensions/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 tests.test_extensions.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_extensions/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py index 5c4a305f7..a7c97d32e 100644 --- a/tests/test_extensions/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 tests.test_extensions.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_extensions/test_ext_autodoc_autodata.py b/tests/test_extensions/test_ext_autodoc_autodata.py index 9233d4f1b..b794666e9 100644 --- a/tests/test_extensions/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 tests.test_extensions.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_extensions/test_ext_autodoc_autofunction.py b/tests/test_extensions/test_ext_autodoc_autofunction.py index 7aa3d0b87..5dfa42d5a 100644 --- a/tests/test_extensions/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 tests.test_extensions.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_extensions/test_ext_autodoc_automodule.py b/tests/test_extensions/test_ext_autodoc_automodule.py index a4469cf26..92565aef0 100644 --- a/tests/test_extensions/test_ext_autodoc_automodule.py +++ b/tests/test_extensions/test_ext_autodoc_automodule.py @@ -8,7 +8,7 @@ import sys import pytest -from tests.test_extensions.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_extensions/test_ext_autodoc_autoproperty.py b/tests/test_extensions/test_ext_autodoc_autoproperty.py index 0b7575677..de3311770 100644 --- a/tests/test_extensions/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 tests.test_extensions.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_extensions/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py index a1e3c83f7..6c2af5a06 100644 --- a/tests/test_extensions/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 tests.test_extensions.test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc IS_PYPY = platform.python_implementation() == 'PyPy' diff --git a/tests/test_extensions/test_ext_autodoc_events.py b/tests/test_extensions/test_ext_autodoc_events.py index 455eb37ff..c0af254d5 100644 --- a/tests/test_extensions/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 tests.test_extensions.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_extensions/test_ext_autodoc_preserve_defaults.py b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py index 003cb1845..c1a00ab22 100644 --- a/tests/test_extensions/test_ext_autodoc_preserve_defaults.py +++ b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py @@ -2,7 +2,7 @@ import pytest -from tests.test_extensions.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_extensions/test_ext_autodoc_private_members.py b/tests/test_extensions/test_ext_autodoc_private_members.py index 228932d3b..bf144141f 100644 --- a/tests/test_extensions/test_ext_autodoc_private_members.py +++ b/tests/test_extensions/test_ext_autodoc_private_members.py @@ -3,7 +3,7 @@ import pytest -from tests.test_extensions.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_extensions/test_ext_intersphinx.py b/tests/test_extensions/test_ext_intersphinx.py index ae64260fc..7c4eb1ba8 100644 --- a/tests/test_extensions/test_ext_intersphinx.py +++ b/tests/test_extensions/test_ext_intersphinx.py @@ -20,7 +20,7 @@ from sphinx.ext.intersphinx import ( from sphinx.ext.intersphinx import setup as intersphinx_setup from sphinx.util.console import strip_colors -from tests.test_util.test_util_inventory import inventory_v2, inventory_v2_not_having_version +from tests.test_util.intersphinx_data import INVENTORY_V2, INVENTORY_V2_NO_VERSION from tests.utils import http_server @@ -92,7 +92,7 @@ def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, 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)), @@ -170,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), }) @@ -200,7 +200,7 @@ def test_missing_reference_pydomain(tmp_path, app, status, warning): 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)), }) @@ -251,7 +251,7 @@ def test_missing_reference_stddomain(tmp_path, app, status, warning): @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), }) @@ -277,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), }) @@ -301,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)), }) @@ -363,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), }) @@ -385,7 +385,7 @@ 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)), @@ -406,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 @@ -505,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)]) @@ -520,7 +520,7 @@ 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. @@ -539,7 +539,7 @@ 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': ('https://example.org/', str(inv_file)), } diff --git a/tests/test_extensions/test_ext_napoleon_docstring.py b/tests/test_extensions/test_ext_napoleon_docstring.py index 6784dc06a..d7ef489b9 100644 --- a/tests/test_extensions/test_ext_napoleon_docstring.py +++ b/tests/test_extensions/test_ext_napoleon_docstring.py @@ -9,7 +9,6 @@ from textwrap import dedent from unittest import mock import pytest -from html5lib import HTMLParser from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.ext.napoleon import Config @@ -21,6 +20,7 @@ from sphinx.ext.napoleon.docstring import ( _token_type, _tokenize_type_spec, ) +from sphinx.testing.util import etree_parse from tests.test_extensions.ext_napoleon_pep526_data_google import PEP526GoogleClass from tests.test_extensions.ext_napoleon_pep526_data_numpy import PEP526NumpyClass @@ -2684,8 +2684,7 @@ int py:class 1 int.html - app.build(force_all=True) - buffer = (app.outdir / 'index.html').read_bytes() - etree = HTMLParser(namespaceHTMLElements=False).parse(buffer) + etree = etree_parse(app.outdir / 'index.html') for name, typename in product(('keyword', 'kwarg', 'kwparam'), ('paramtype', 'kwtype')): param = f'{name}_{typename}' diff --git a/tests/test_markup/test_smartquotes.py b/tests/test_markup/test_smartquotes.py index 1d4e8e127..6c84386dc 100644 --- a/tests/test_markup/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_util/intersphinx_data.py b/tests/test_util/intersphinx_data.py new file mode 100644 index 000000000..042ee76d7 --- /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/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 021e4274d..32840b82e 100644 --- a/tests/test_util/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -223,168 +223,140 @@ def test_signature_partialmethod(): def test_signature_annotations(): - from tests.test_util.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) + 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) + 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__) + 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, /)' diff --git a/tests/test_util/test_util_inventory.py b/tests/test_util/test_util_inventory.py index 2d9b7462f..81d31b0ef 100644 --- a/tests/test_util/test_util_inventory.py +++ b/tests/test_util/test_util_inventory.py @@ -1,61 +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 - -The-Julia-Domain std:label -1 write_inventory/#$ The Julia Domain -''') - -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', '-') @@ -64,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 @@ -82,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') @@ -102,7 +62,7 @@ def _build_inventory(srcdir): app = SphinxTestApp(srcdir=srcdir) app.build() sphinx.locale.translators.clear() - return (app.outdir / 'objects.inv') + return app.outdir / 'objects.inv' def test_inventory_localization(tmp_path):