diff options
Diffstat (limited to 'sphinx/testing/util.py')
-rw-r--r-- | sphinx/testing/util.py | 171 |
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) |