summaryrefslogtreecommitdiffstats
path: root/sphinx/testing/util.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/testing/util.py')
-rw-r--r--sphinx/testing/util.py171
1 files changed, 171 insertions, 0 deletions
diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py
new file mode 100644
index 0000000..e76d401
--- /dev/null
+++ b/sphinx/testing/util.py
@@ -0,0 +1,171 @@
+"""Sphinx test suite utilities"""
+from __future__ import annotations
+
+import contextlib
+import os
+import re
+import sys
+import warnings
+from typing import IO, TYPE_CHECKING, Any
+from xml.etree import ElementTree
+
+from docutils import nodes
+from docutils.parsers.rst import directives, roles
+
+from sphinx import application, locale
+from sphinx.pycode import ModuleAnalyzer
+
+if TYPE_CHECKING:
+ from io import StringIO
+ from pathlib import Path
+
+ from docutils.nodes import Node
+
+__all__ = 'SphinxTestApp', 'SphinxTestAppWrapperForSkipBuilding'
+
+
+def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> None:
+ if cls:
+ if isinstance(cls, list):
+ assert_node(node, cls[0], xpath=xpath, **kwargs)
+ if cls[1:]:
+ if isinstance(cls[1], tuple):
+ assert_node(node, cls[1], xpath=xpath, **kwargs)
+ else:
+ assert isinstance(node, nodes.Element), \
+ 'The node%s does not have any children' % xpath
+ assert len(node) == 1, \
+ 'The node%s has %d child nodes, not one' % (xpath, len(node))
+ assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs)
+ elif isinstance(cls, tuple):
+ assert isinstance(node, (list, nodes.Element)), \
+ 'The node%s does not have any items' % xpath
+ assert len(node) == len(cls), \
+ 'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls))
+ for i, nodecls in enumerate(cls):
+ path = xpath + "[%d]" % i
+ assert_node(node[i], nodecls, xpath=path, **kwargs)
+ elif isinstance(cls, str):
+ assert node == cls, f'The node {xpath!r} is not {cls!r}: {node!r}'
+ else:
+ assert isinstance(node, cls), \
+ f'The node{xpath} is not subclass of {cls!r}: {node!r}'
+
+ if kwargs:
+ assert isinstance(node, nodes.Element), \
+ 'The node%s does not have any attributes' % xpath
+
+ for key, value in kwargs.items():
+ if key not in node:
+ if (key := key.replace('_', '-')) not in node:
+ msg = f'The node{xpath} does not have {key!r} attribute: {node!r}'
+ raise AssertionError(msg)
+ assert node[key] == value, \
+ 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
+
+
+class SphinxTestApp(application.Sphinx):
+ """
+ A subclass of :class:`Sphinx` that runs on the test root, with some
+ better default values for the initialization parameters.
+ """
+ _status: StringIO
+ _warning: StringIO
+
+ def __init__(
+ self,
+ buildername: str = 'html',
+ srcdir: Path | None = None,
+ builddir: Path | None = None,
+ freshenv: bool = False,
+ confoverrides: dict | None = None,
+ status: IO | None = None,
+ warning: IO | None = None,
+ tags: list[str] | None = None,
+ docutilsconf: str | None = None,
+ parallel: int = 0,
+ ) -> None:
+ assert srcdir is not None
+
+ self.docutils_conf_path = srcdir / 'docutils.conf'
+ if docutilsconf is not None:
+ self.docutils_conf_path.write_text(docutilsconf, encoding='utf8')
+
+ if builddir is None:
+ builddir = srcdir / '_build'
+
+ confdir = srcdir
+ outdir = builddir.joinpath(buildername)
+ outdir.mkdir(parents=True, exist_ok=True)
+ doctreedir = builddir.joinpath('doctrees')
+ doctreedir.mkdir(parents=True, exist_ok=True)
+ if confoverrides is None:
+ confoverrides = {}
+ warningiserror = False
+
+ self._saved_path = sys.path[:]
+ self._saved_directives = directives._directives.copy() # type: ignore[attr-defined]
+ self._saved_roles = roles._roles.copy() # type: ignore[attr-defined]
+
+ self._saved_nodeclasses = {v for v in dir(nodes.GenericNodeVisitor)
+ if v.startswith('visit_')}
+
+ try:
+ super().__init__(srcdir, confdir, outdir, doctreedir,
+ buildername, confoverrides, status, warning,
+ freshenv, warningiserror, tags, parallel=parallel)
+ except Exception:
+ self.cleanup()
+ raise
+
+ def cleanup(self, doctrees: bool = False) -> None:
+ ModuleAnalyzer.cache.clear()
+ locale.translators.clear()
+ sys.path[:] = self._saved_path
+ sys.modules.pop('autodoc_fodder', None)
+ directives._directives = self._saved_directives # type: ignore[attr-defined]
+ roles._roles = self._saved_roles # type: ignore[attr-defined]
+ for method in dir(nodes.GenericNodeVisitor):
+ if method.startswith('visit_') and \
+ method not in self._saved_nodeclasses:
+ delattr(nodes.GenericNodeVisitor, 'visit_' + method[6:])
+ delattr(nodes.GenericNodeVisitor, 'depart_' + method[6:])
+ with contextlib.suppress(FileNotFoundError):
+ os.remove(self.docutils_conf_path)
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__} buildername={self.builder.name!r}>'
+
+ def build(self, force_all: bool = False, filenames: list[str] | None = None) -> None:
+ self.env._pickled_doctree_cache.clear()
+ super().build(force_all, filenames)
+
+
+class SphinxTestAppWrapperForSkipBuilding:
+ """
+ This class is a wrapper for SphinxTestApp to speed up the test by skipping
+ `app.build` process if it is already built and there is even one output
+ file.
+ """
+
+ def __init__(self, app_: SphinxTestApp) -> None:
+ self.app = app_
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self.app, name)
+
+ def build(self, *args: Any, **kwargs: Any) -> None:
+ if not os.listdir(self.app.outdir):
+ # if listdir is empty, do build.
+ self.app.build(*args, **kwargs)
+ # otherwise, we can use built cache
+
+
+def strip_escseq(text: str) -> str:
+ return re.sub('\x1b.*?m', '', text)