summaryrefslogtreecommitdiffstats
path: root/tests/test_transforms
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_transforms')
-rw-r--r--tests/test_transforms/__init__.py0
-rw-r--r--tests/test_transforms/test_transforms_move_module_targets.py77
-rw-r--r--tests/test_transforms/test_transforms_post_transforms.py269
-rw-r--r--tests/test_transforms/test_transforms_post_transforms_code.py44
-rw-r--r--tests/test_transforms/test_transforms_reorder_nodes.py96
5 files changed, 486 insertions, 0 deletions
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/test_transforms_move_module_targets.py b/tests/test_transforms/test_transforms_move_module_targets.py
new file mode 100644
index 0000000..e0e9f1d
--- /dev/null
+++ b/tests/test_transforms/test_transforms_move_module_targets.py
@@ -0,0 +1,77 @@
+import pytest
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.testing.util import SphinxTestApp
+from sphinx.transforms import MoveModuleTargets
+
+CONTENT_PY = """\
+move-module-targets
+===================
+
+.. py:module:: fish_licence.halibut
+"""
+CONTENT_JS = """\
+move-module-targets
+===================
+
+.. js:module:: fish_licence.halibut
+"""
+
+
+@pytest.mark.parametrize('content', [
+ CONTENT_PY, # Python
+ CONTENT_JS, # JavaScript
+])
+@pytest.mark.usefixtures("rollback_sysmodules")
+def test_move_module_targets(tmp_path, content):
+ # Test for the MoveModuleTargets transform
+ tmp_path.joinpath("conf.py").touch()
+ tmp_path.joinpath("index.rst").write_text(content, encoding="utf-8")
+
+ app = SphinxTestApp('dummy', srcdir=tmp_path)
+ app.build(force_all=True)
+ document = app.env.get_doctree('index')
+ section = document[0]
+
+ # target ID has been lifted into the section node
+ assert section["ids"] == ['module-fish_licence.halibut', 'move-module-targets']
+ # nodes.target has been removed from 'section'
+ assert isinstance(section[0], nodes.title)
+ assert isinstance(section[1], addnodes.index)
+ assert len(section) == 2
+
+
+@pytest.mark.usefixtures("rollback_sysmodules")
+def test_move_module_targets_no_section(tmp_path):
+ # Test for the MoveModuleTargets transform
+ tmp_path.joinpath("conf.py").touch()
+ tmp_path.joinpath("index.rst").write_text(".. py:module:: fish_licence.halibut\n", encoding="utf-8")
+
+ app = SphinxTestApp('dummy', srcdir=tmp_path)
+ app.build(force_all=True)
+ document = app.env.get_doctree('index')
+
+ assert document["ids"] == []
+
+
+@pytest.mark.usefixtures("rollback_sysmodules")
+def test_move_module_targets_disabled(tmp_path):
+ # Test for the MoveModuleTargets transform
+ tmp_path.joinpath("conf.py").touch()
+ tmp_path.joinpath("index.rst").write_text(CONTENT_PY, encoding="utf-8")
+
+ app = SphinxTestApp('dummy', srcdir=tmp_path)
+ app.registry.transforms.remove(MoveModuleTargets) # disable the transform
+ app.build(force_all=True)
+ document = app.env.get_doctree('index')
+ section = document[0]
+
+ # target ID is not lifted into the section node
+ assert section["ids"] == ['move-module-targets']
+ assert section[2]["ids"] == ['module-fish_licence.halibut']
+ # nodes.target remains in 'section'
+ assert isinstance(section[0], nodes.title)
+ assert isinstance(section[1], addnodes.index)
+ assert isinstance(section[2], nodes.target)
+ assert len(section) == 3
diff --git a/tests/test_transforms/test_transforms_post_transforms.py b/tests/test_transforms/test_transforms_post_transforms.py
new file mode 100644
index 0000000..c4e699b
--- /dev/null
+++ b/tests/test_transforms/test_transforms_post_transforms.py
@@ -0,0 +1,269 @@
+"""Tests the post_transforms"""
+
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING
+
+import pytest
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.addnodes import SIG_ELEMENTS
+from sphinx.testing.util import assert_node
+from sphinx.transforms.post_transforms import SigElementFallbackTransform
+from sphinx.util.docutils import new_document
+
+if TYPE_CHECKING:
+ from typing import Any, NoReturn
+
+ from _pytest.fixtures import SubRequest
+
+ from sphinx.testing.util import SphinxTestApp
+
+
+@pytest.mark.sphinx('html', testroot='transforms-post_transforms-missing-reference')
+def test_nitpicky_warning(app, warning):
+ app.build()
+ assert ('index.rst:4: WARNING: py:class reference target '
+ 'not found: io.StringIO' in warning.getvalue())
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<p><code class="xref py py-class docutils literal notranslate"><span class="pre">'
+ 'io.StringIO</span></code></p>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='transforms-post_transforms-missing-reference',
+ freshenv=True)
+def test_missing_reference(app, warning):
+ def missing_reference(app_, env_, node_, contnode_):
+ assert app_ is app
+ assert env_ is app.env
+ assert node_['reftarget'] == 'io.StringIO'
+ assert contnode_.astext() == 'io.StringIO'
+
+ return nodes.inline('', 'missing-reference.StringIO')
+
+ warning.truncate(0)
+ app.connect('missing-reference', missing_reference)
+ app.build()
+ assert warning.getvalue() == ''
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<p><span>missing-reference.StringIO</span></p>' in content
+
+
+@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names',
+ freshenv=True)
+def test_missing_reference_conditional_pending_xref(app, warning):
+ def missing_reference(_app, _env, _node, contnode):
+ return contnode
+
+ warning.truncate(0)
+ app.connect('missing-reference', missing_reference)
+ app.build()
+ assert warning.getvalue() == ''
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<span class="n"><span class="pre">Age</span></span>' in content
+
+
+@pytest.mark.sphinx('html', testroot='transforms-post_transforms-keyboard',
+ freshenv=True)
+def test_keyboard_hyphen_spaces(app):
+ """Regression test for issue 10495, we want no crash."""
+ app.build()
+ assert "spanish" in (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert "inquisition" in (app.outdir / 'index.html').read_text(encoding='utf8')
+
+
+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)
+
+ @pytest.fixture(autouse=True)
+ def builtin_sig_elements(self) -> tuple[type[addnodes.desc_sig_element], ...]:
+ """Fixture returning an ordered view on the original value of :data:`!sphinx.addnodes.SIG_ELEMENTS`."""
+ return self._builtin_sig_elements
+
+ @pytest.fixture()
+ def document(
+ self, app: SphinxTestApp, builtin_sig_elements: tuple[type[addnodes.desc_sig_element], ...],
+ ) -> nodes.document:
+ """Fixture returning a new document with built-in ``desc_sig_*`` nodes and a final ``desc_inline`` node."""
+ doc = new_document('')
+ doc.settings.env = app.env
+ # Nodes that should be supported by a default custom translator class.
+ # It is important that builtin_sig_elements has a fixed order so that
+ # the nodes can be deterministically checked.
+ doc += [node_type('', '') for node_type in builtin_sig_elements]
+ doc += addnodes.desc_inline('py')
+ return doc
+
+ @pytest.fixture()
+ def with_desc_sig_elements(self, value: Any) -> bool:
+ """Dynamic fixture acting as the identity on booleans."""
+ assert isinstance(value, bool)
+ return value
+
+ @pytest.fixture()
+ def add_visitor_method_for(self, value: Any) -> list[str]:
+ """Dynamic fixture acting as the identity on a list of strings."""
+ assert isinstance(value, list)
+ assert all(isinstance(item, str) for item in value)
+ return value
+
+ @pytest.fixture(autouse=True)
+ def translator_class(self, request: SubRequest) -> type[nodes.NodeVisitor]:
+ """Minimal interface fixture similar to SphinxTranslator but orthogonal thereof."""
+ logger = logging.getLogger(__name__)
+
+ class BaseCustomTranslatorClass(nodes.NodeVisitor):
+ """Base class for a custom translator class, orthogonal to ``SphinxTranslator``."""
+
+ def __init__(self, document, *_a):
+ super().__init__(document)
+ # ignore other arguments
+
+ def dispatch_visit(self, node):
+ for node_class in node.__class__.__mro__:
+ if method := getattr(self, f'visit_{node_class.__name__}', None):
+ method(node)
+ break
+ else:
+ logger.info('generic visit: %r', node.__class__.__name__)
+ super().dispatch_visit(node)
+
+ def unknown_visit(self, node):
+ logger.warning('unknown visit: %r', node.__class__.__name__)
+ raise nodes.SkipDeparture # ignore unknown departure
+
+ def visit_document(self, node):
+ raise nodes.SkipDeparture # ignore departure
+
+ def mark_node(self, node: nodes.Node) -> NoReturn:
+ logger.info('mark: %r', node.__class__.__name__)
+ raise nodes.SkipDeparture # ignore departure
+
+ with_desc_sig_elements = request.getfixturevalue('with_desc_sig_elements')
+ if with_desc_sig_elements:
+ desc_sig_elements_list = request.getfixturevalue('builtin_sig_elements')
+ else:
+ desc_sig_elements_list = []
+ add_visitor_method_for = request.getfixturevalue('add_visitor_method_for')
+ visitor_methods = {f'visit_{tp.__name__}' for tp in desc_sig_elements_list}
+ visitor_methods.update(f'visit_{name}' for name in add_visitor_method_for)
+ class_dict = dict.fromkeys(visitor_methods, BaseCustomTranslatorClass.mark_node)
+ return type('CustomTranslatorClass', (BaseCustomTranslatorClass,), class_dict) # type: ignore[return-value]
+
+ @pytest.mark.parametrize(
+ 'add_visitor_method_for',
+ [[], ['desc_inline']],
+ ids=[
+ 'no_explicit_visitor',
+ 'explicit_desc_inline_visitor',
+ ],
+ )
+ @pytest.mark.parametrize(
+ 'with_desc_sig_elements',
+ [True, False],
+ ids=[
+ 'with_default_visitors_for_desc_sig_elements',
+ 'without_default_visitors_for_desc_sig_elements',
+ ],
+ )
+ @pytest.mark.sphinx('dummy')
+ def test_support_desc_inline(
+ self, document: nodes.document, with_desc_sig_elements: bool,
+ add_visitor_method_for: list[str], request: SubRequest,
+ ) -> None:
+ document, _, _ = self._exec(request)
+ # count the number of desc_inline nodes with the extra _sig_node_type field
+ desc_inline_typename = addnodes.desc_inline.__name__
+ visit_desc_inline = desc_inline_typename in add_visitor_method_for
+ if visit_desc_inline:
+ assert_node(document[-1], addnodes.desc_inline)
+ else:
+ assert_node(document[-1], nodes.inline, _sig_node_type=desc_inline_typename)
+
+ @pytest.mark.parametrize(
+ 'add_visitor_method_for',
+ [
+ [], # no support
+ ['desc_sig_space'], # enable desc_sig_space visitor
+ ['desc_sig_element'], # enable generic visitor
+ ['desc_sig_space', 'desc_sig_element'], # enable desc_sig_space and generic visitors
+ ],
+ ids=[
+ 'no_explicit_visitor',
+ 'explicit_desc_sig_space_visitor',
+ 'explicit_desc_sig_element_visitor',
+ 'explicit_desc_sig_space_and_desc_sig_element_visitors',
+ ],
+ )
+ @pytest.mark.parametrize(
+ 'with_desc_sig_elements',
+ [True, False],
+ ids=[
+ 'with_default_visitors_for_desc_sig_elements',
+ 'without_default_visitors_for_desc_sig_elements',
+ ],
+ )
+ @pytest.mark.sphinx('dummy')
+ def test_custom_implementation(
+ self,
+ document: nodes.document,
+ with_desc_sig_elements: bool,
+ add_visitor_method_for: list[str],
+ request: SubRequest,
+ ) -> None:
+ document, stdout, stderr = self._exec(request)
+ assert len(self._builtin_sig_elements) == len(document.children[:-1]) == len(stdout[:-1])
+
+ visit_desc_sig_element = addnodes.desc_sig_element.__name__ in add_visitor_method_for
+ ignore_sig_element_fallback_transform = visit_desc_sig_element or with_desc_sig_elements
+
+ if ignore_sig_element_fallback_transform:
+ # desc_sig_element is implemented or desc_sig_* nodes are properly handled (and left untouched)
+ for node_type, node, mess in zip(self._builtin_sig_elements, document.children[:-1], stdout[:-1]):
+ assert_node(node, node_type)
+ assert not node.hasattr('_sig_node_type')
+ assert mess == f'mark: {node_type.__name__!r}'
+ else:
+ # desc_sig_* nodes are converted into inline nodes
+ for node_type, node, mess in zip(self._builtin_sig_elements, document.children[:-1], stdout[:-1]):
+ assert_node(node, nodes.inline, _sig_node_type=node_type.__name__)
+ assert mess == f'generic visit: {nodes.inline.__name__!r}'
+
+ # desc_inline node is never handled and always transformed
+ assert addnodes.desc_inline.__name__ not in add_visitor_method_for
+ assert_node(document[-1], nodes.inline, _sig_node_type=addnodes.desc_inline.__name__)
+ assert stdout[-1] == f'generic visit: {nodes.inline.__name__!r}'
+
+ # nodes.inline are never handled
+ assert len(stderr) == 1 if ignore_sig_element_fallback_transform else len(document.children)
+ assert set(stderr) == {f'unknown visit: {nodes.inline.__name__!r}'}
+
+ @staticmethod
+ def _exec(request: SubRequest) -> tuple[nodes.document, list[str], list[str]]:
+ caplog = request.getfixturevalue('caplog')
+ caplog.set_level(logging.INFO, logger=__name__)
+
+ app = request.getfixturevalue('app')
+ translator_class = request.getfixturevalue('translator_class')
+ app.set_translator('dummy', translator_class)
+ # run the post-transform directly [building phase]
+ # document contains SIG_ELEMENTS nodes followed by a desc_inline node
+ document = request.getfixturevalue('document')
+ SigElementFallbackTransform(document).run()
+ # run the translator [writing phase]
+ translator = translator_class(document, app.builder)
+ document.walkabout(translator)
+ # 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.WARNING]
+ return document, stdout, stderr
diff --git a/tests/test_transforms/test_transforms_post_transforms_code.py b/tests/test_transforms/test_transforms_post_transforms_code.py
new file mode 100644
index 0000000..4423d5b
--- /dev/null
+++ b/tests/test_transforms/test_transforms_post_transforms_code.py
@@ -0,0 +1,44 @@
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='trim_doctest_flags')
+def test_trim_doctest_flags_html(app, status, warning):
+ app.build()
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert 'FOO' not in result
+ assert 'BAR' in result
+ assert 'BAZ' not in result
+ assert 'QUX' not in result
+ assert 'QUUX' not in result
+ assert 'CORGE' not in result
+ assert 'GRAULT' in result
+
+
+@pytest.mark.sphinx('html', testroot='trim_doctest_flags',
+ confoverrides={'trim_doctest_flags': False})
+def test_trim_doctest_flags_disabled(app, status, warning):
+ app.build()
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert 'FOO' in result
+ assert 'BAR' in result
+ assert 'BAZ' in result
+ assert 'QUX' in result
+ assert 'QUUX' not in result
+ assert 'CORGE' not in result
+ assert 'GRAULT' in result
+
+
+@pytest.mark.sphinx('latex', testroot='trim_doctest_flags')
+def test_trim_doctest_flags_latex(app, status, warning):
+ app.build()
+
+ result = (app.outdir / 'python.tex').read_text(encoding='utf8')
+ assert 'FOO' not in result
+ assert 'BAR' in result
+ assert 'BAZ' not in result
+ assert 'QUX' not in result
+ assert 'QUUX' not in result
+ assert 'CORGE' not in result
+ assert 'GRAULT' in result
diff --git a/tests/test_transforms/test_transforms_reorder_nodes.py b/tests/test_transforms/test_transforms_reorder_nodes.py
new file mode 100644
index 0000000..7ffdae6
--- /dev/null
+++ b/tests/test_transforms/test_transforms_reorder_nodes.py
@@ -0,0 +1,96 @@
+"""Tests the transformations"""
+
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.testing import restructuredtext
+from sphinx.testing.util import assert_node
+
+
+def test_transforms_reorder_consecutive_target_and_index_nodes_preserve_order(app):
+ text = ('.. index:: abc\n'
+ '.. index:: def\n'
+ '.. index:: ghi\n'
+ '.. index:: jkl\n'
+ '\n'
+ 'text\n')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ addnodes.index,
+ addnodes.index,
+ addnodes.index,
+ nodes.target,
+ nodes.target,
+ nodes.target,
+ nodes.target,
+ nodes.paragraph))
+ assert_node(doctree[0], addnodes.index, entries=[('single', 'abc', 'index-0', '', None)])
+ assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-1', '', None)])
+ assert_node(doctree[2], addnodes.index, entries=[('single', 'ghi', 'index-2', '', None)])
+ assert_node(doctree[3], addnodes.index, entries=[('single', 'jkl', 'index-3', '', None)])
+ assert_node(doctree[4], nodes.target, refid='index-0')
+ assert_node(doctree[5], nodes.target, refid='index-1')
+ assert_node(doctree[6], nodes.target, refid='index-2')
+ assert_node(doctree[7], nodes.target, refid='index-3')
+ # assert_node(doctree[8], nodes.paragraph)
+
+
+def test_transforms_reorder_consecutive_target_and_index_nodes_no_merge_across_other_nodes(app):
+ text = ('.. index:: abc\n'
+ '.. index:: def\n'
+ '\n'
+ 'text\n'
+ '\n'
+ '.. index:: ghi\n'
+ '.. index:: jkl\n'
+ '\n'
+ 'text\n')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ addnodes.index,
+ nodes.target,
+ nodes.target,
+ nodes.paragraph,
+ addnodes.index,
+ addnodes.index,
+ nodes.target,
+ nodes.target,
+ nodes.paragraph))
+ assert_node(doctree[0], addnodes.index, entries=[('single', 'abc', 'index-0', '', None)])
+ assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-1', '', None)])
+ assert_node(doctree[2], nodes.target, refid='index-0')
+ assert_node(doctree[3], nodes.target, refid='index-1')
+ # assert_node(doctree[4], nodes.paragraph)
+ assert_node(doctree[5], addnodes.index, entries=[('single', 'ghi', 'index-2', '', None)])
+ assert_node(doctree[6], addnodes.index, entries=[('single', 'jkl', 'index-3', '', None)])
+ assert_node(doctree[7], nodes.target, refid='index-2')
+ assert_node(doctree[8], nodes.target, refid='index-3')
+ # assert_node(doctree[9], nodes.paragraph)
+
+
+def test_transforms_reorder_consecutive_target_and_index_nodes_merge_with_labels(app):
+ text = ('.. _abc:\n'
+ '.. index:: def\n'
+ '.. _ghi:\n'
+ '.. index:: jkl\n'
+ '.. _mno:\n'
+ '\n'
+ 'Heading\n'
+ '=======\n')
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (nodes.title,
+ addnodes.index,
+ addnodes.index,
+ nodes.target,
+ nodes.target,
+ nodes.target,
+ nodes.target,
+ nodes.target))
+ # assert_node(doctree[8], nodes.title)
+ assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-0', '', None)])
+ assert_node(doctree[2], addnodes.index, entries=[('single', 'jkl', 'index-1', '', None)])
+ assert_node(doctree[3], nodes.target, refid='abc')
+ assert_node(doctree[4], nodes.target, refid='index-0')
+ assert_node(doctree[5], nodes.target, refid='ghi')
+ assert_node(doctree[6], nodes.target, refid='index-1')
+ assert_node(doctree[7], nodes.target, refid='mno')