2024-03-25 05:03:44 -05:00
|
|
|
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
|
|
|
|
|
2024-08-11 08:58:56 -05:00
|
|
|
assert len(nodes) >= min_count, (
|
|
|
|
f'expecting at least {min_count} node(s) '
|
|
|
|
f'to satisfy {xpath!r} in file {filename}'
|
|
|
|
)
|
2024-03-25 05:03:44 -05:00
|
|
|
|
|
|
|
if check == '':
|
|
|
|
return
|
|
|
|
|
|
|
|
if callable(check):
|
|
|
|
check(nodes)
|
|
|
|
return
|
|
|
|
|
|
|
|
rex = re.compile(check)
|
|
|
|
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)
|
2024-08-11 08:58:56 -05:00
|
|
|
msg = (
|
|
|
|
f'{check!r} not found in any node matching {xpath!r} in file {filename}:\n{ctx}'
|
|
|
|
)
|
2024-03-25 05:03:44 -05:00
|
|
|
raise AssertionError(msg)
|