[tests] move utilities and static data into dedicated modules and remove `html5lib` (#12173)

Since #12168, HTML files are now XML compliant, hence ``html5lib`` is no more needed as a testing dependencies.
This commit is contained in:
Bénédikt Tran 2024-03-25 11:03:44 +01:00 committed by GitHub
parent 9e239729d4
commit 885818bb7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 289 additions and 237 deletions

View File

@ -91,7 +91,6 @@ lint = [
] ]
test = [ test = [
"pytest>=6.0", "pytest>=6.0",
"html5lib",
"defusedxml>=0.7.1", # for secure XML/HTML parsing "defusedxml>=0.7.1", # for secure XML/HTML parsing
"cython>=3.0", "cython>=3.0",
"setuptools>=67.0", # for Cython compilation "setuptools>=67.0", # for Cython compilation

View File

@ -7,12 +7,11 @@ __all__ = ('SphinxTestApp', 'SphinxTestAppWrapperForSkipBuilding')
import contextlib import contextlib
import os import os
import sys import sys
import warnings
from io import StringIO from io import StringIO
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from xml.etree import ElementTree
from defusedxml.ElementTree import parse as xml_parse
from docutils import nodes from docutils import nodes
from docutils.parsers.rst import directives, roles from docutils.parsers.rst import directives, roles
@ -26,6 +25,7 @@ if TYPE_CHECKING:
from collections.abc import Mapping from collections.abc import Mapping
from pathlib import Path from pathlib import Path
from typing import Any, Final from typing import Any, Final
from xml.etree.ElementTree import ElementTree
from docutils.nodes import Node 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}' f'The node{xpath}[{key}] is not {value!r}: {node[key]!r}'
def etree_parse(path: str) -> Any: # keep this to restrict the API usage and to have a correct return type
with warnings.catch_warnings(record=False): def etree_parse(path: str | os.PathLike[str]) -> ElementTree:
warnings.filterwarnings("ignore", category=DeprecationWarning) """Parse a file into a (safe) XML element tree."""
return ElementTree.parse(path) # NoQA: S314 # using known data in tests return xml_parse(path)
class SphinxTestApp(sphinx.application.Sphinx): class SphinxTestApp(sphinx.application.Sphinx):

View File

@ -3,26 +3,26 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
from html5lib import HTMLParser
from sphinx.testing.util import etree_parse
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Generator from collections.abc import Callable, Generator
from pathlib import Path 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: def _parse(path: Path) -> ElementTree:
if fname in etree_cache: if path in _etree_cache:
return etree_cache[fname] return _etree_cache[path]
with fname.open('rb') as fp:
etree = HTMLParser(namespaceHTMLElements=False).parse(fp) _etree_cache[path] = tree = etree_parse(path)
etree_cache[fname] = etree return tree
return etree
@pytest.fixture(scope='package') @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 yield _parse
etree_cache.clear() _etree_cache.clear()

View File

@ -12,43 +12,8 @@ from sphinx.errors import ConfigError
from sphinx.util.console import strip_colors from sphinx.util.console import strip_colors
from sphinx.util.inventory import InventoryFile from sphinx.util.inventory import InventoryFile
FIGURE_CAPTION = ".//figure/figcaption/p" from tests.test_builders.xpath_data import FIGURE_CAPTION
from tests.test_builders.xpath_util import check_xpath
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)
def test_html4_error(make_app, tmp_path): def test_html4_error(make_app, tmp_path):

View File

@ -4,7 +4,7 @@ import re
import pytest 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): def tail_check(check):
@ -128,7 +128,7 @@ def tail_check(check):
# ``seealso`` directive # ``seealso`` directive
('markup.html', ".//div/p[@class='admonition-title']", 'See also'), ('markup.html', ".//div/p[@class='admonition-title']", 'See also'),
# a ``hlist`` directive # 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 # a ``centered`` directive
('markup.html', ".//p[@class='centered']/strong", 'LICENSE'), ('markup.html', ".//p[@class='centered']/strong", 'LICENSE'),
# a glossary # a glossary

View File

@ -5,7 +5,8 @@ import re
import pytest 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') @pytest.mark.sphinx('html', testroot='numfig')

View File

@ -2,7 +2,7 @@
import pytest 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"), [ @pytest.mark.parametrize(("fname", "path", "check", "be_found"), [

View File

@ -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"

View File

@ -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)

View File

@ -5,7 +5,6 @@ from unittest import mock
import pytest import pytest
from docutils import nodes from docutils import nodes
from docutils.nodes import definition, definition_list, definition_list_item, term from docutils.nodes import definition, definition_list, definition_list_item, term
from html5lib import HTMLParser
from sphinx import addnodes from sphinx import addnodes
from sphinx.addnodes import ( from sphinx.addnodes import (
@ -20,7 +19,7 @@ from sphinx.addnodes import (
) )
from sphinx.domains.std import StandardDomain from sphinx.domains.std import StandardDomain
from sphinx.testing import restructuredtext 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(): def test_process_doc_handle_figure_caption():
@ -375,9 +374,11 @@ def test_productionlist(app, status, warning):
assert warnings[-1] == '' assert warnings[-1] == ''
assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0] 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 = etree_parse(app.outdir / 'index.html')
etree = HTMLParser(namespaceHTMLElements=False).parse(f) nodes = list(etree.iter('ul'))
ul = list(etree.iter('ul'))[1] assert len(nodes) >= 2
ul = nodes[1]
cases = [] cases = []
for li in list(ul): for li in list(ul):
assert len(list(li)) == 1 assert len(list(li)) == 1

View File

@ -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

View File

@ -20,8 +20,8 @@ from docutils.statemachine import ViewList
from sphinx import addnodes from sphinx import addnodes
from sphinx.ext.autodoc import ALL, ModuleLevelDocumenter, Options 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: try:
# Enable pyximport to test cython module # Enable pyximport to test cython module
@ -34,20 +34,6 @@ if TYPE_CHECKING:
from typing import Any 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): def make_directive_bridge(env):
options = Options( options = Options(
inherited_members=False, inherited_members=False,
@ -82,23 +68,6 @@ def make_directive_bridge(env):
processed_signatures = [] 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 test_parse_name(app):
def verify(objtype, name, result): def verify(objtype, name, result):
inst = app.registry.documenters[objtype](directive, name) inst = app.registry.documenters[objtype](directive, name)
@ -139,6 +108,21 @@ def test_parse_name(app):
def test_format_signature(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-process-signature', process_signature)
app.connect('autodoc-skip-member', skip_member) app.connect('autodoc-skip-member', skip_member)

View File

@ -6,7 +6,7 @@ source file translated by test_build.
import pytest 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -11,7 +11,7 @@ from typing import Union
import pytest 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -6,7 +6,7 @@ source file translated by test_build.
import pytest 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -6,7 +6,7 @@ source file translated by test_build.
import pytest 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -8,7 +8,7 @@ import sys
import pytest 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -6,7 +6,7 @@ source file translated by test_build.
import pytest 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -8,7 +8,7 @@ import pytest
from sphinx.testing import restructuredtext 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' IS_PYPY = platform.python_implementation() == 'PyPy'

View File

@ -4,7 +4,7 @@ import pytest
from sphinx.ext.autodoc import between, cut_lines 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -2,7 +2,7 @@
import pytest 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', @pytest.mark.sphinx('html', testroot='ext-autodoc',

View File

@ -3,7 +3,7 @@
import pytest 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')

View File

@ -20,7 +20,7 @@ from sphinx.ext.intersphinx import (
from sphinx.ext.intersphinx import setup as intersphinx_setup from sphinx.ext.intersphinx import setup as intersphinx_setup
from sphinx.util.console import strip_colors 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 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): def test_missing_reference(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, { set_config(app, {
'https://docs.python.org/': str(inv_file), 'https://docs.python.org/': str(inv_file),
'py3k': ('https://docs.python.org/py3k/', 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): def test_missing_reference_pydomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, { set_config(app, {
'https://docs.python.org/': str(inv_file), '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): def test_missing_reference_stddomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, { set_config(app, {
'cmd': ('https://docs.python.org/', str(inv_file)), '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') @pytest.mark.sphinx('html', testroot='ext-intersphinx-cppdomain')
def test_missing_reference_cppdomain(tmp_path, app, status, warning): def test_missing_reference_cppdomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, { set_config(app, {
'https://docs.python.org/': str(inv_file), '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): def test_missing_reference_jsdomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, { set_config(app, {
'https://docs.python.org/': str(inv_file), '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): def test_missing_reference_disabled_domain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, { set_config(app, {
'inv': ('https://docs.python.org/', str(inv_file)), '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): def test_inventory_not_having_version(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' 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, { set_config(app, {
'https://docs.python.org/': str(inv_file), '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 identifiers are not string
""" """
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, { set_config(app, {
'https://docs.python.org/': str(inv_file), 'https://docs.python.org/': str(inv_file),
'py3k': ('https://docs.python.org/py3k/', 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): def test_load_mappings_fallback(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
set_config(app, {}) set_config(app, {})
# connect to invalid path # connect to invalid path
@ -505,7 +505,7 @@ def test_inspect_main_noargs(capsys):
def test_inspect_main_file(capsys, tmp_path): def test_inspect_main_file(capsys, tmp_path):
"""inspect_main interface, with file argument""" """inspect_main interface, with file argument"""
inv_file = tmp_path / 'inventory' inv_file = tmp_path / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
inspect_main([str(inv_file)]) inspect_main([str(inv_file)])
@ -520,7 +520,7 @@ def test_inspect_main_url(capsys):
def do_GET(self): def do_GET(self):
self.send_response(200, "OK") self.send_response(200, "OK")
self.end_headers() self.end_headers()
self.wfile.write(inventory_v2) self.wfile.write(INVENTORY_V2)
def log_message(*args, **kwargs): def log_message(*args, **kwargs):
# Silenced. # Silenced.
@ -539,7 +539,7 @@ def test_inspect_main_url(capsys):
@pytest.mark.sphinx('html', testroot='ext-intersphinx-role') @pytest.mark.sphinx('html', testroot='ext-intersphinx-role')
def test_intersphinx_role(app, warning): def test_intersphinx_role(app, warning):
inv_file = app.srcdir / 'inventory' inv_file = app.srcdir / 'inventory'
inv_file.write_bytes(inventory_v2) inv_file.write_bytes(INVENTORY_V2)
app.config.intersphinx_mapping = { app.config.intersphinx_mapping = {
'inv': ('https://example.org/', str(inv_file)), 'inv': ('https://example.org/', str(inv_file)),
} }

View File

@ -9,7 +9,6 @@ from textwrap import dedent
from unittest import mock from unittest import mock
import pytest import pytest
from html5lib import HTMLParser
from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping
from sphinx.ext.napoleon import Config from sphinx.ext.napoleon import Config
@ -21,6 +20,7 @@ from sphinx.ext.napoleon.docstring import (
_token_type, _token_type,
_tokenize_type_spec, _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_google import PEP526GoogleClass
from tests.test_extensions.ext_napoleon_pep526_data_numpy import PEP526NumpyClass 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) app.build(force_all=True)
buffer = (app.outdir / 'index.html').read_bytes() etree = etree_parse(app.outdir / 'index.html')
etree = HTMLParser(namespaceHTMLElements=False).parse(buffer)
for name, typename in product(('keyword', 'kwarg', 'kwparam'), ('paramtype', 'kwtype')): for name, typename in product(('keyword', 'kwarg', 'kwparam'), ('paramtype', 'kwtype')):
param = f'{name}_{typename}' param = f'{name}_{typename}'

View File

@ -1,7 +1,8 @@
"""Test smart quotes.""" """Test smart quotes."""
import pytest import pytest
from html5lib import HTMLParser
from sphinx.testing.util import etree_parse
@pytest.mark.sphinx(buildername='html', testroot='smartquotes', freshenv=True) @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): def test_literals(app, status, warning):
app.build() app.build()
with (app.outdir / 'literals.html').open(encoding='utf-8') as html_file: etree = etree_parse(app.outdir / 'literals.html')
etree = HTMLParser(namespaceHTMLElements=False).parse(html_file)
for code_element in etree.iter('code'): for code_element in etree.iter('code'):
code_text = ''.join(code_element.itertext()) code_text = ''.join(code_element.itertext())

View File

@ -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
''')

View File

@ -223,168 +223,140 @@ def test_signature_partialmethod():
def test_signature_annotations(): def test_signature_annotations():
from tests.test_util.typing_test_data import ( import tests.test_util.typing_test_data as mod
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,
)
# Class annotations # Class annotations
sig = inspect.signature(f0) sig = inspect.signature(mod.f0)
assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None' assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None'
# Generic types with concrete parameters # Generic types with concrete parameters
sig = inspect.signature(f1) sig = inspect.signature(mod.f1)
assert stringify_signature(sig) == '(x: list[int]) -> typing.List[int]' assert stringify_signature(sig) == '(x: list[int]) -> typing.List[int]'
# TypeVars and generic types with TypeVars # 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],' 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],' ' y: typing.List[tests.test_util.typing_test_data.T_co],'
' z: tests.test_util.typing_test_data.T' ' z: tests.test_util.typing_test_data.T'
') -> typing.List[tests.test_util.typing_test_data.T_contra]') ') -> typing.List[tests.test_util.typing_test_data.T_contra]')
# Union types # Union types
sig = inspect.signature(f3) sig = inspect.signature(mod.f3)
assert stringify_signature(sig) == '(x: str | numbers.Integral) -> None' assert stringify_signature(sig) == '(x: str | numbers.Integral) -> None'
# Quoted annotations # Quoted annotations
sig = inspect.signature(f4) sig = inspect.signature(mod.f4)
assert stringify_signature(sig) == '(x: str, y: str) -> None' assert stringify_signature(sig) == '(x: str, y: str) -> None'
# Keyword-only arguments # Keyword-only arguments
sig = inspect.signature(f5) sig = inspect.signature(mod.f5)
assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None' assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None'
# Keyword-only arguments with varargs # 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' assert stringify_signature(sig) == '(x: int, *args, y: str, z: str) -> None'
# Space around '=' for defaults # Space around '=' for defaults
sig = inspect.signature(f7) sig = inspect.signature(mod.f7)
if sys.version_info[:2] <= (3, 10): if sys.version_info[:2] <= (3, 10):
assert stringify_signature(sig) == '(x: int | None = None, y: dict = {}) -> None' assert stringify_signature(sig) == '(x: int | None = None, y: dict = {}) -> None'
else: else:
assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None' assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None'
# Callable types # Callable types
sig = inspect.signature(f8) sig = inspect.signature(mod.f8)
assert stringify_signature(sig) == '(x: typing.Callable[[int, str], int]) -> None' 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' assert stringify_signature(sig) == '(x: typing.Callable) -> None'
# Tuple types # 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' assert stringify_signature(sig) == '(x: typing.Tuple[int, str], y: typing.Tuple[int, ...]) -> None'
# Instance annotations # Instance annotations
sig = inspect.signature(f11) sig = inspect.signature(mod.f11)
assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None'
# tuple with more than two items # tuple with more than two items
sig = inspect.signature(f12) sig = inspect.signature(mod.f12)
assert stringify_signature(sig) == '() -> typing.Tuple[int, str, int]' assert stringify_signature(sig) == '() -> typing.Tuple[int, str, int]'
# optional # optional
sig = inspect.signature(f13) sig = inspect.signature(mod.f13)
assert stringify_signature(sig) == '() -> str | None' assert stringify_signature(sig) == '() -> str | None'
# optional union # optional union
sig = inspect.signature(f20) sig = inspect.signature(mod.f20)
assert stringify_signature(sig) in ('() -> int | str | None', assert stringify_signature(sig) in ('() -> int | str | None',
'() -> str | int | None') '() -> str | int | None')
# Any # Any
sig = inspect.signature(f14) sig = inspect.signature(mod.f14)
assert stringify_signature(sig) == '() -> typing.Any' assert stringify_signature(sig) == '() -> typing.Any'
# ForwardRef # ForwardRef
sig = inspect.signature(f15) sig = inspect.signature(mod.f15)
assert stringify_signature(sig) == '(x: Unknown, y: int) -> typing.Any' assert stringify_signature(sig) == '(x: Unknown, y: int) -> typing.Any'
# keyword only arguments (1) # keyword only arguments (1)
sig = inspect.signature(f16) sig = inspect.signature(mod.f16)
assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)' assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)'
# keyword only arguments (2) # keyword only arguments (2)
sig = inspect.signature(f17) sig = inspect.signature(mod.f17)
assert stringify_signature(sig) == '(*, arg3, arg4)' 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) -> ' assert stringify_signature(sig) == ('(self, arg1: int | typing.Tuple = 10) -> '
'typing.List[typing.Dict]') 'typing.List[typing.Dict]')
# annotations for variadic and keyword parameters # annotations for variadic and keyword parameters
sig = inspect.signature(f19) sig = inspect.signature(mod.f19)
assert stringify_signature(sig) == '(*args: int, **kwargs: str)' assert stringify_signature(sig) == '(*args: int, **kwargs: str)'
# default value is inspect.Signature.empty # default value is inspect.Signature.empty
sig = inspect.signature(f21) sig = inspect.signature(mod.f21)
assert stringify_signature(sig) == "(arg1='whatever', arg2)" assert stringify_signature(sig) == "(arg1='whatever', arg2)"
# type hints by string # 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]' 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' assert stringify_signature(sig) == '(self, parent: tests.test_util.typing_test_data.Node | None) -> None'
# show_annotation is False # show_annotation is False
sig = inspect.signature(f7) sig = inspect.signature(mod.f7)
assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})' assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})'
# show_return_annotation is False # show_return_annotation is False
sig = inspect.signature(f7) sig = inspect.signature(mod.f7)
if sys.version_info[:2] <= (3, 10): if sys.version_info[:2] <= (3, 10):
assert stringify_signature(sig, show_return_annotation=False) == '(x: int | None = None, y: dict = {})' assert stringify_signature(sig, show_return_annotation=False) == '(x: int | None = None, y: dict = {})'
else: else:
assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})'
# unqualified_typehints is True # unqualified_typehints is True
sig = inspect.signature(f7) sig = inspect.signature(mod.f7)
if sys.version_info[:2] <= (3, 10): if sys.version_info[:2] <= (3, 10):
assert stringify_signature(sig, unqualified_typehints=True) == '(x: int | None = None, y: dict = {}) -> None' assert stringify_signature(sig, unqualified_typehints=True) == '(x: int | None = None, y: dict = {}) -> None'
else: else:
assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None' assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None'
# case: separator at head # case: separator at head
sig = inspect.signature(f22) sig = inspect.signature(mod.f22)
assert stringify_signature(sig) == '(*, a, b)' assert stringify_signature(sig) == '(*, a, b)'
# case: separator in the middle # case: separator in the middle
sig = inspect.signature(f23) sig = inspect.signature(mod.f23)
assert stringify_signature(sig) == '(a, b, /, c, d)' assert stringify_signature(sig) == '(a, b, /, c, d)'
sig = inspect.signature(f24) sig = inspect.signature(mod.f24)
assert stringify_signature(sig) == '(a, /, *, b)' assert stringify_signature(sig) == '(a, /, *, b)'
# case: separator at tail # case: separator at tail
sig = inspect.signature(f25) sig = inspect.signature(mod.f25)
assert stringify_signature(sig) == '(a, b, /)' assert stringify_signature(sig) == '(a, b, /)'

View File

@ -1,61 +1,21 @@
"""Test inventory util functions.""" """Test inventory util functions."""
import os import os
import posixpath import posixpath
import zlib
from io import BytesIO from io import BytesIO
import sphinx.locale import sphinx.locale
from sphinx.testing.util import SphinxTestApp from sphinx.testing.util import SphinxTestApp
from sphinx.util.inventory import InventoryFile from sphinx.util.inventory import InventoryFile
inventory_v1 = b'''\ from tests.test_util.intersphinx_data import (
# Sphinx inventory version 1 INVENTORY_V1,
# Project: foo INVENTORY_V2,
# Version: 1.0 INVENTORY_V2_NO_VERSION,
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
''')
def test_read_inventory_v1(): def test_read_inventory_v1():
f = BytesIO(inventory_v1) f = BytesIO(INVENTORY_V1)
invdata = InventoryFile.load(f, '/util', posixpath.join) invdata = InventoryFile.load(f, '/util', posixpath.join)
assert invdata['py:module']['module'] == \ assert invdata['py:module']['module'] == \
('foo', '1.0', '/util/foo.html#module-module', '-') ('foo', '1.0', '/util/foo.html#module-module', '-')
@ -64,7 +24,7 @@ def test_read_inventory_v1():
def test_read_inventory_v2(): def test_read_inventory_v2():
f = BytesIO(inventory_v2) f = BytesIO(INVENTORY_V2)
invdata = InventoryFile.load(f, '/util', posixpath.join) invdata = InventoryFile.load(f, '/util', posixpath.join)
assert len(invdata['py:module']) == 2 assert len(invdata['py:module']) == 2
@ -82,7 +42,7 @@ def test_read_inventory_v2():
def test_read_inventory_v2_not_having_version(): 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) invdata = InventoryFile.load(f, '/util', posixpath.join)
assert invdata['py:module']['module1'] == \ assert invdata['py:module']['module1'] == \
('foo', '', '/util/foo.html#module-module1', 'Long Module desc') ('foo', '', '/util/foo.html#module-module1', 'Long Module desc')
@ -102,7 +62,7 @@ def _build_inventory(srcdir):
app = SphinxTestApp(srcdir=srcdir) app = SphinxTestApp(srcdir=srcdir)
app.build() app.build()
sphinx.locale.translators.clear() sphinx.locale.translators.clear()
return (app.outdir / 'objects.inv') return app.outdir / 'objects.inv'
def test_inventory_localization(tmp_path): def test_inventory_localization(tmp_path):