refactored matcher and wrote readme.txt
This commit is contained in:
@@ -133,6 +133,38 @@ class DoNotCheck:
|
||||
desc: str = None
|
||||
|
||||
|
||||
def _get_type(x):
|
||||
if hasattr(x, "tag"):
|
||||
return x.tag
|
||||
if isinstance(x, TestObject):
|
||||
return x.cls.__name__ if isinstance(x.cls, type) else str(x.cls)
|
||||
return type(x).__name__
|
||||
|
||||
|
||||
def _get_attr(x, attr):
|
||||
if hasattr(x, "attrs"):
|
||||
return x.attrs.get(attr, MISSING_ATTR)
|
||||
|
||||
if not hasattr(x, attr):
|
||||
return MISSING_ATTR
|
||||
|
||||
return getattr(x, attr, MISSING_ATTR)
|
||||
|
||||
|
||||
def _get_attributes(x):
|
||||
"""Get the attributes dict from an element."""
|
||||
if hasattr(x, "attrs"):
|
||||
return x.attrs
|
||||
return {}
|
||||
|
||||
|
||||
def _get_children(x):
|
||||
"""Get the children list from an element."""
|
||||
if hasattr(x, "children"):
|
||||
return x.children
|
||||
return []
|
||||
|
||||
|
||||
class ErrorOutput:
|
||||
def __init__(self, path, element, expected):
|
||||
self.path = path
|
||||
@@ -203,11 +235,11 @@ class ErrorOutput:
|
||||
self._add_to_output(")")
|
||||
|
||||
elif isinstance(self.expected, TestObject):
|
||||
cls = _mytype(self.element)
|
||||
attrs = {attr_name: _mygetattr(self.element, attr_name) for attr_name in self.expected.attrs}
|
||||
cls = _get_type(self.element)
|
||||
attrs = {attr_name: _get_attr(self.element, attr_name) for attr_name in self.expected.attrs}
|
||||
self._add_to_output(f"({cls} {_str_attrs(attrs)})")
|
||||
# Try to show where the differences are
|
||||
error_str = self._detect_error_2(self.element, self.expected)
|
||||
error_str = self._detect_error(self.element, self.expected)
|
||||
if error_str:
|
||||
self._add_to_output(error_str)
|
||||
|
||||
@@ -250,50 +282,35 @@ class ErrorOutput:
|
||||
return quoted_str(element)
|
||||
|
||||
def _detect_error(self, element, expected):
|
||||
if hasattr(expected, "tag") and hasattr(element, "tag"):
|
||||
tag_str = len(element.tag) * (" " if element.tag == expected.tag else "^")
|
||||
elt_attrs = {attr_name: element.attrs.get(attr_name, MISSING_ATTR) for attr_name in expected.attrs}
|
||||
attrs_in_error = [attr_name for attr_name, attr_value in elt_attrs.items() if
|
||||
not self._matches(attr_value, expected.attrs[attr_name])]
|
||||
if attrs_in_error:
|
||||
elt_attrs_error = " ".join(len(f'"{name}"="{value}"') * ("^" if name in attrs_in_error else " ")
|
||||
for name, value in elt_attrs.items())
|
||||
error_str = f" {tag_str} {elt_attrs_error}"
|
||||
return error_str
|
||||
else:
|
||||
if not self._matches(element, expected):
|
||||
return len(str(element)) * "^"
|
||||
|
||||
return None
|
||||
|
||||
def _detect_error_2(self, element, expected):
|
||||
"""
|
||||
Too lazy to refactor original _detect_error
|
||||
:param element:
|
||||
:param expected:
|
||||
:return:
|
||||
Detect errors between element and expected, returning a visual marker string.
|
||||
Unified version that handles both FT elements and TestObjects.
|
||||
"""
|
||||
# For elements with structure (FT or TestObject)
|
||||
if hasattr(expected, "tag") or isinstance(expected, TestObject):
|
||||
element_cls = _mytype(element)
|
||||
expected_cls = _mytype(expected)
|
||||
str_tag_error = (" " if self._matches(element_cls, expected_cls) else "^") * len(element_cls)
|
||||
|
||||
element_attrs = {attr_name: _mygetattr(element, attr_name) for attr_name in expected.attrs}
|
||||
expected_attrs = {attr_name: _mygetattr(expected, attr_name) for attr_name in expected.attrs}
|
||||
element_type = _get_type(element)
|
||||
expected_type = _get_type(expected)
|
||||
type_error = (" " if element_type == expected_type else "^") * len(element_type)
|
||||
|
||||
element_attrs = {attr_name: _get_attr(element, attr_name) for attr_name in _get_attributes(expected)}
|
||||
expected_attrs = {attr_name: _get_attr(expected, attr_name) for attr_name in _get_attributes(expected)}
|
||||
attrs_in_error = {attr_name for attr_name, attr_value in element_attrs.items() if
|
||||
not self._matches(attr_value, expected_attrs[attr_name])}
|
||||
str_attrs_error = " ".join(len(f'"{name}"="{value}"') * ("^" if name in attrs_in_error else " ")
|
||||
for name, value in element_attrs.items())
|
||||
if str_attrs_error.strip() or str_tag_error.strip():
|
||||
return f" {str_tag_error} {str_attrs_error}"
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
attrs_error = " ".join(
|
||||
len(f'"{name}"="{value}"') * ("^" if name in attrs_in_error else " ")
|
||||
for name, value in element_attrs.items()
|
||||
)
|
||||
|
||||
if type_error.strip() or attrs_error.strip():
|
||||
return f" {type_error} {attrs_error}"
|
||||
return None
|
||||
|
||||
# For simple values
|
||||
else:
|
||||
if not self._matches(element, expected):
|
||||
return len(str(element)) * "^"
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _matches(element, expected):
|
||||
@@ -343,39 +360,200 @@ class ErrorComparisonOutput:
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def matches(actual, expected, path=""):
|
||||
def print_path(p):
|
||||
return f"Path : '{p}'\n" if p else ""
|
||||
class Matcher:
|
||||
"""
|
||||
Matcher class for comparing actual and expected elements.
|
||||
Provides flexible comparison with support for predicates, nested structures,
|
||||
and detailed error reporting.
|
||||
"""
|
||||
|
||||
def _type(x):
|
||||
return type(x)
|
||||
def __init__(self):
|
||||
self.path = ""
|
||||
|
||||
def _debug(elt):
|
||||
return str(elt) if elt else "None"
|
||||
def matches(self, actual, expected):
|
||||
"""
|
||||
Compare actual and expected elements.
|
||||
|
||||
Args:
|
||||
actual: The actual element to compare
|
||||
expected: The expected element or pattern
|
||||
|
||||
Returns:
|
||||
True if elements match, raises AssertionError otherwise
|
||||
"""
|
||||
if actual is not None and expected is None:
|
||||
self._assert_error("Actual is not None while expected is None", _actual=actual)
|
||||
|
||||
if isinstance(expected, DoNotCheck):
|
||||
return True
|
||||
|
||||
if actual is None and expected is not None:
|
||||
self._assert_error("Actual is None while expected is ", _expected=expected)
|
||||
|
||||
# set the path
|
||||
current_path = self._get_current_path(actual)
|
||||
original_path = self.path
|
||||
self.path = self.path + "." + current_path if self.path else current_path
|
||||
|
||||
try:
|
||||
self._match_elements(actual, expected)
|
||||
finally:
|
||||
# restore the original path for sibling comparisons
|
||||
self.path = original_path
|
||||
|
||||
return True
|
||||
|
||||
def _debug_compare(a, b):
|
||||
actual_out = ErrorOutput(path, a, b)
|
||||
expected_out = ErrorOutput(path, b, b)
|
||||
def _match_elements(self, actual, expected):
|
||||
"""Internal method that performs the actual comparison logic."""
|
||||
if isinstance(expected, TestObject) or hasattr(expected, "tag"):
|
||||
self._match_element(actual, expected)
|
||||
return
|
||||
|
||||
if isinstance(expected, Predicate):
|
||||
assert expected.validate(actual), \
|
||||
self._error_msg(f"The condition '{expected}' is not satisfied.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
return
|
||||
|
||||
assert _get_type(actual) == _get_type(expected), \
|
||||
self._error_msg("The types are different.", _actual=actual, _expected=expected)
|
||||
|
||||
if isinstance(expected, (list, tuple)):
|
||||
self._match_list(actual, expected)
|
||||
elif isinstance(expected, dict):
|
||||
self._match_dict(actual, expected)
|
||||
elif isinstance(expected, NotStr):
|
||||
self._match_notstr(actual, expected)
|
||||
else:
|
||||
assert actual == expected, self._error_msg("The values are different",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
def _match_element(self, actual, expected):
|
||||
"""Match a TestObject or FT element."""
|
||||
# Validate the type/tag
|
||||
assert _get_type(actual) == _get_type(expected), \
|
||||
self._error_msg("The types are different.", _actual=_get_type(actual), _expected=_get_type(expected))
|
||||
|
||||
# Special conditions (ChildrenPredicate)
|
||||
expected_children = _get_children(expected)
|
||||
for predicate in [c for c in expected_children if isinstance(c, ChildrenPredicate)]:
|
||||
assert predicate.validate(actual), \
|
||||
self._error_msg(f"The condition '{predicate}' is not satisfied.",
|
||||
_actual=actual,
|
||||
_expected=predicate.to_debug(expected))
|
||||
|
||||
# Compare the attributes
|
||||
expected_attrs = _get_attributes(expected)
|
||||
for expected_attr, expected_value in expected_attrs.items():
|
||||
actual_value = _get_attr(actual, expected_attr)
|
||||
|
||||
# Check if attribute exists
|
||||
if actual_value == MISSING_ATTR:
|
||||
self._assert_error(f"'{expected_attr}' is not found in Actual.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
# Handle Predicate values
|
||||
if isinstance(expected_value, Predicate):
|
||||
assert expected_value.validate(actual_value), \
|
||||
self._error_msg(f"The condition '{expected_value}' is not satisfied.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
# Handle TestObject recursive matching
|
||||
elif isinstance(expected, TestObject):
|
||||
try:
|
||||
self.matches(actual_value, expected_value)
|
||||
except AssertionError as e:
|
||||
match = re.search(r"Error : (.+?)\n", str(e))
|
||||
if match:
|
||||
self._assert_error(f"{match.group(1)} for '{expected_attr}'.",
|
||||
_actual=actual_value,
|
||||
_expected=expected_value)
|
||||
else:
|
||||
self._assert_error(f"The values are different for '{expected_attr}'.",
|
||||
_actual=actual_value,
|
||||
_expected=expected_value)
|
||||
|
||||
# Handle regular value comparison
|
||||
else:
|
||||
assert actual_value == expected_value, \
|
||||
self._error_msg(f"The values are different for '{expected_attr}'.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
# Compare the children (only if present)
|
||||
if expected_children:
|
||||
# Filter out Predicate children
|
||||
expected_children = [c for c in expected_children if not isinstance(c, Predicate)]
|
||||
actual_children = _get_children(actual)
|
||||
|
||||
if len(actual_children) < len(expected_children):
|
||||
self._assert_error("Actual is lesser than expected.", _actual=actual, _expected=expected)
|
||||
|
||||
for actual_child, expected_child in zip(actual_children, expected_children):
|
||||
assert self.matches(actual_child, expected_child)
|
||||
|
||||
def _match_list(self, actual, expected):
|
||||
"""Match list or tuple."""
|
||||
if len(actual) < len(expected):
|
||||
self._assert_error("Actual is smaller than expected: ", _actual=actual, _expected=expected)
|
||||
if len(actual) > len(expected):
|
||||
self._assert_error("Actual is bigger than expected: ", _actual=actual, _expected=expected)
|
||||
|
||||
for actual_child, expected_child in zip(actual, expected):
|
||||
assert self.matches(actual_child, expected_child)
|
||||
|
||||
def _match_dict(self, actual, expected):
|
||||
"""Match dictionary."""
|
||||
if len(actual) < len(expected):
|
||||
self._assert_error("Actual is smaller than expected: ", _actual=actual, _expected=expected)
|
||||
if len(actual) > len(expected):
|
||||
self._assert_error("Actual is bigger than expected: ", _actual=actual, _expected=expected)
|
||||
for k, v in expected.items():
|
||||
assert self.matches(actual[k], v)
|
||||
|
||||
def _match_notstr(self, actual, expected):
|
||||
"""Match NotStr type."""
|
||||
to_compare = actual.s.lstrip('\n').lstrip()
|
||||
assert to_compare.startswith(expected.s), self._error_msg("Notstr values are different: ",
|
||||
_actual=to_compare,
|
||||
_expected=expected.s)
|
||||
|
||||
def _print_path(self):
|
||||
"""Format the current path for error messages."""
|
||||
return f"Path : '{self.path}'\n" if self.path else ""
|
||||
|
||||
def _debug_compare(self, a, b):
|
||||
"""Generate a comparison debug output."""
|
||||
actual_out = ErrorOutput(self.path, a, b)
|
||||
expected_out = ErrorOutput(self.path, b, b)
|
||||
|
||||
comparison_out = ErrorComparisonOutput(actual_out, expected_out)
|
||||
return comparison_out.render()
|
||||
|
||||
def _error_msg(msg, _actual=None, _expected=None):
|
||||
def _error_msg(self, msg, _actual=None, _expected=None):
|
||||
"""Generate an error message with debug information."""
|
||||
if _actual is None and _expected is None:
|
||||
debug_info = ""
|
||||
elif _actual is None:
|
||||
debug_info = _debug(_expected)
|
||||
debug_info = self._debug(_expected)
|
||||
elif _expected is None:
|
||||
debug_info = _debug(_actual)
|
||||
debug_info = self._debug(_actual)
|
||||
else:
|
||||
debug_info = _debug_compare(_actual, _expected)
|
||||
debug_info = self._debug_compare(_actual, _expected)
|
||||
|
||||
return f"{print_path(path)}Error : {msg}\n{debug_info}"
|
||||
return f"{self._print_path()}Error : {msg}\n{debug_info}"
|
||||
|
||||
def _assert_error(msg, _actual=None, _expected=None):
|
||||
assert False, _error_msg(msg, _actual=_actual, _expected=_expected)
|
||||
def _assert_error(self, msg, _actual=None, _expected=None):
|
||||
"""Raise an assertion error with formatted message."""
|
||||
assert False, self._error_msg(msg, _actual=_actual, _expected=_expected)
|
||||
|
||||
@staticmethod
|
||||
def _get_current_path(elt):
|
||||
"""Get the path representation of an element."""
|
||||
if hasattr(elt, "tag"):
|
||||
res = f"{elt.tag}"
|
||||
if "id" in elt.attrs:
|
||||
@@ -388,192 +566,118 @@ def matches(actual, expected, path=""):
|
||||
else:
|
||||
return elt.__class__.__name__
|
||||
|
||||
if actual is not None and expected is None:
|
||||
_assert_error("Actual is not None while expected is None", _actual=actual)
|
||||
@staticmethod
|
||||
def _str_attrs(attrs: dict):
|
||||
"""Format attributes as a string."""
|
||||
return " ".join(f'"{attr_name}"="{attr_value}"' for attr_name, attr_value in attrs.items())
|
||||
|
||||
if isinstance(expected, DoNotCheck):
|
||||
return True
|
||||
|
||||
if actual is None and expected is not None:
|
||||
_assert_error("Actual is None while expected is ", _expected=expected)
|
||||
|
||||
# set the path
|
||||
path += "." + _get_current_path(actual) if path else _get_current_path(actual)
|
||||
|
||||
if isinstance(expected, TestObject):
|
||||
assert _mytype(actual) == _mytype(expected), _error_msg("The types are different: ",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
for attr, value in expected.attrs.items():
|
||||
assert hasattr(actual, attr), _error_msg(f"'{attr}' is not found in Actual.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
try:
|
||||
matches(getattr(actual, attr), value)
|
||||
except AssertionError as e:
|
||||
match = re.search(r"Error : (.+?)\n", str(e))
|
||||
if match:
|
||||
assert False, _error_msg(f"{match.group(1)} for '{attr}':",
|
||||
_actual=getattr(actual, attr),
|
||||
_expected=value)
|
||||
assert False, _error_msg(f"The values are different for '{attr}': ",
|
||||
_actual=getattr(actual, attr),
|
||||
_expected=value)
|
||||
return True
|
||||
|
||||
if isinstance(expected, Predicate):
|
||||
assert expected.validate(actual), \
|
||||
_error_msg(f"The condition '{expected}' is not satisfied.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
assert _type(actual) == _type(expected) or (hasattr(actual, "tag") and hasattr(expected, "tag")), \
|
||||
_error_msg("The types are different: ", _actual=actual, _expected=expected)
|
||||
|
||||
if isinstance(expected, (list, tuple)):
|
||||
if len(actual) < len(expected):
|
||||
_assert_error("Actual is smaller than expected: ", _actual=actual, _expected=expected)
|
||||
if len(actual) > len(expected):
|
||||
_assert_error("Actual is bigger than expected: ", _actual=actual, _expected=expected)
|
||||
|
||||
for actual_child, expected_child in zip(actual, expected):
|
||||
assert matches(actual_child, expected_child, path=path)
|
||||
|
||||
elif isinstance(expected, dict):
|
||||
if len(actual) < len(expected):
|
||||
_assert_error("Actual is smaller than expected: ", _actual=actual, _expected=expected)
|
||||
if len(actual) > len(expected):
|
||||
_assert_error("Actual is bigger than expected: ", _actual=actual, _expected=expected)
|
||||
for k, v in expected.items():
|
||||
assert matches(actual[k], v, path=f"{path}[{k}={v}]")
|
||||
|
||||
elif isinstance(expected, NotStr):
|
||||
to_compare = actual.s.lstrip('\n').lstrip()
|
||||
assert to_compare.startswith(expected.s), _error_msg("Notstr values are different: ",
|
||||
_actual=to_compare,
|
||||
_expected=expected.s)
|
||||
|
||||
elif hasattr(expected, "tag"):
|
||||
# validate the tags names
|
||||
assert actual.tag == expected.tag, _error_msg("The elements are different.",
|
||||
_actual=actual.tag,
|
||||
_expected=expected.tag)
|
||||
|
||||
# special conditions
|
||||
for predicate in [c for c in expected.children if isinstance(c, ChildrenPredicate)]:
|
||||
assert predicate.validate(actual), \
|
||||
_error_msg(f"The condition '{predicate}' is not satisfied.",
|
||||
_actual=actual,
|
||||
_expected=predicate.to_debug(expected))
|
||||
|
||||
# compare the attributes
|
||||
for expected_attr, expected_value in expected.attrs.items():
|
||||
assert expected_attr in actual.attrs, _error_msg(f"'{expected_attr}' is not found in Actual.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
if isinstance(expected_value, Predicate):
|
||||
assert expected_value.validate(actual.attrs[expected_attr]), \
|
||||
_error_msg(f"The condition '{expected_value}' is not satisfied.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
else:
|
||||
assert actual.attrs[expected_attr] == expected.attrs[expected_attr], \
|
||||
_error_msg(f"The values are different for '{expected_attr}': ",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
# compare the children
|
||||
expected_children = [c for c in expected.children if not isinstance(c, Predicate)]
|
||||
if len(actual.children) < len(expected_children):
|
||||
_assert_error("Actual is lesser than expected: ", _actual=actual, _expected=expected)
|
||||
|
||||
for actual_child, expected_child in zip(actual.children, expected_children):
|
||||
assert matches(actual_child, expected_child, path=path)
|
||||
|
||||
|
||||
else:
|
||||
assert actual == expected, _error_msg("The values are different",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
return True
|
||||
@staticmethod
|
||||
def _debug(elt):
|
||||
"""Format an element for debug output."""
|
||||
return str(elt) if elt else "None"
|
||||
|
||||
|
||||
def matches(actual, expected, path=""):
|
||||
"""
|
||||
Compare actual and expected elements.
|
||||
|
||||
This is a convenience wrapper around the Matcher class.
|
||||
|
||||
Args:
|
||||
actual: The actual element to compare
|
||||
expected: The expected element or pattern
|
||||
path: Optional initial path for error reporting
|
||||
|
||||
Returns:
|
||||
True if elements match, raises AssertionError otherwise
|
||||
"""
|
||||
matcher = Matcher()
|
||||
matcher.path = path
|
||||
return matcher.matches(actual, expected)
|
||||
|
||||
|
||||
def find(ft, expected):
|
||||
res = []
|
||||
|
||||
def _type(x):
|
||||
return type(x)
|
||||
|
||||
def _same(_ft, _expected):
|
||||
if _ft == _expected:
|
||||
"""
|
||||
Find all occurrences of an expected element within a FastHTML tree.
|
||||
|
||||
Args:
|
||||
ft: A FastHTML element or list of elements to search in
|
||||
expected: The element pattern to find
|
||||
|
||||
Returns:
|
||||
List of matching elements
|
||||
|
||||
Raises:
|
||||
AssertionError: If no matching elements are found
|
||||
"""
|
||||
|
||||
def _elements_match(actual, expected):
|
||||
"""Check if two elements are the same based on tag, attributes, and children."""
|
||||
# Quick equality check
|
||||
if actual == expected:
|
||||
return True
|
||||
|
||||
if _ft.tag != _expected.tag:
|
||||
|
||||
# Check if both are FT elements
|
||||
if not (hasattr(actual, "tag") and hasattr(expected, "tag")):
|
||||
return False
|
||||
|
||||
for attr in _expected.attrs:
|
||||
if attr not in _ft.attrs or _ft.attrs[attr] != _expected.attrs[attr]:
|
||||
|
||||
# Compare tags
|
||||
if _get_type(actual) != _get_type(expected):
|
||||
return False
|
||||
|
||||
# Compare attributes
|
||||
expected_attrs = _get_attributes(expected)
|
||||
actual_attrs = _get_attributes(actual)
|
||||
|
||||
for attr_name, attr_value in expected_attrs.items():
|
||||
if attr_name not in actual_attrs or actual_attrs[attr_name] != attr_value:
|
||||
return False
|
||||
|
||||
for expected_child in _expected.children:
|
||||
for ft_child in _ft.children:
|
||||
if _same(ft_child, expected_child):
|
||||
break
|
||||
else:
|
||||
|
||||
# Compare children recursively
|
||||
expected_children = _get_children(expected)
|
||||
actual_children = _get_children(actual)
|
||||
|
||||
for expected_child in expected_children:
|
||||
# Check if this expected child exists somewhere in actual children
|
||||
if not any(_elements_match(actual_child, expected_child) for actual_child in actual_children):
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def _find(current, current_expected):
|
||||
|
||||
if _type(current) != _type(current_expected):
|
||||
|
||||
def _search_tree(current, pattern):
|
||||
"""Recursively search for pattern in the tree rooted at current."""
|
||||
# Type mismatch - can't be the same
|
||||
if type(current) != type(pattern):
|
||||
return []
|
||||
|
||||
|
||||
# For non-FT elements, simple equality check
|
||||
if not hasattr(current, "tag"):
|
||||
return [current] if current == current_expected else []
|
||||
|
||||
_found = []
|
||||
if _same(current, current_expected):
|
||||
_found.append(current)
|
||||
|
||||
# look at the children
|
||||
for child in current.children:
|
||||
_found.extend(_find(child, current_expected))
|
||||
|
||||
return _found
|
||||
|
||||
ft_as_list = [ft] if not isinstance(ft, (list, tuple, set)) else ft
|
||||
|
||||
for current_ft in ft_as_list:
|
||||
found = _find(current_ft, expected)
|
||||
res.extend(found)
|
||||
|
||||
if len(res) == 0:
|
||||
return [current] if current == pattern else []
|
||||
|
||||
# Check if current element matches
|
||||
matches = []
|
||||
if _elements_match(current, pattern):
|
||||
matches.append(current)
|
||||
|
||||
# Recursively search in children
|
||||
for child in _get_children(current):
|
||||
matches.extend(_search_tree(child, pattern))
|
||||
|
||||
return matches
|
||||
|
||||
# Normalize input to list
|
||||
elements_to_search = ft if isinstance(ft, (list, tuple, set)) else [ft]
|
||||
|
||||
# Search in all provided elements
|
||||
all_matches = []
|
||||
for element in elements_to_search:
|
||||
all_matches.extend(_search_tree(element, expected))
|
||||
|
||||
# Raise error if nothing found
|
||||
if not all_matches:
|
||||
raise AssertionError(f"No element found for '{expected}'")
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _mytype(x):
|
||||
if hasattr(x, "tag"):
|
||||
return x.tag
|
||||
if isinstance(x, TestObject):
|
||||
return x.cls.__name__ if isinstance(x.cls, type) else str(x.cls)
|
||||
return type(x).__name__
|
||||
|
||||
|
||||
def _mygetattr(x, attr):
|
||||
if hasattr(x, "attrs"):
|
||||
return x.attrs.get(attr, MISSING_ATTR)
|
||||
|
||||
if not hasattr(x, attr):
|
||||
return MISSING_ATTR
|
||||
|
||||
return getattr(x, attr, MISSING_ATTR)
|
||||
return all_matches
|
||||
|
||||
|
||||
def _str_attrs(attrs: dict):
|
||||
|
||||
Reference in New Issue
Block a user