172 lines
6.2 KiB
Python
172 lines
6.2 KiB
Python
|
"""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)
|