1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
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
# https://github.com/astral-sh/ty/issues/117
# callable(...) does not currently narrow in ty.
rex = re.compile(check) # ty: ignore[no-matching-overload]
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)
|