Source code for behave_kit.assertions.diff

"""Assertions that fail with a legible diff instead of a bare `AssertionError`."""

from __future__ import annotations

from collections.abc import Callable, Mapping, Sequence
from typing import Any, Protocol

from behave_kit.assertions._matchers import CompareOptions, DiffResult, deep_compare


class _TableLike(Protocol):
    """Structural shape of a `behave.model.Table` (and compatible objects)."""

    headings: Sequence[str]
    rows: Sequence[Sequence[str]]


def _safe_equal(actual: object, expected: object) -> bool:
    """Compare two values, tolerating array-like objects that do not return a scalar bool."""
    try:
        result = actual == expected
    except Exception:
        return False
    if isinstance(result, bool):
        return result
    try:
        return bool(result.all())
    except (AttributeError, ValueError, TypeError):
        pass
    try:
        return bool(result)
    except (ValueError, TypeError):
        pass
    return False


def _format_diff_result(result: DiffResult, msg: str) -> str:
    lines = [msg] if msg else []
    lines.append(f"Found {len(result.diffs)} difference(s):")
    for diff in result.diffs:
        lines.append(f"  at {diff.path}: {diff.message}")
    return "\n".join(lines)


[docs] def assert_json_equals( actual: object, expected: object, *, msg: str = "", options: CompareOptions | None = None, ) -> None: """Assert that ``actual`` deep-equals ``expected``, showing which keys differ.""" result = deep_compare(actual, expected, options) if not result.equal: raise AssertionError(_format_diff_result(result, msg))
[docs] def assert_dict_contains( d: Mapping[Any, Any], subset: Mapping[Any, Any], *, msg: str = "", ) -> None: """Assert that ``d`` contains every key/value pair present in ``subset``.""" if not isinstance(d, Mapping) or not isinstance(subset, Mapping): types = f"{type(d).__name__} and {type(subset).__name__}" raise AssertionError(f"assert_dict_contains requires two mappings, got {types}") missing_keys = set(subset) - set(d) mismatched = { key: (subset[key], d[key]) for key in subset if key in d and not _safe_equal(d[key], subset[key]) } if not missing_keys and not mismatched: return details = [ f" missing key '{key}' (expected {subset[key]!r})" for key in sorted(missing_keys, key=str) ] for key in sorted(mismatched, key=str): expected_value, actual_value = mismatched[key] details.append(f" key '{key}': expected {expected_value!r}, got {actual_value!r}") lines = [msg] if msg else [] lines.extend(details) raise AssertionError("\n".join(lines))
[docs] def assert_list_ordered( lst: Sequence[Any], *, key: Callable[[Any], Any] | None = None, msg: str = "", ) -> None: """Assert that ``lst`` is sorted, optionally by a ``key`` function.""" key_fn = key or (lambda item: item) values = [key_fn(item) for item in lst] try: ordered = values == sorted(values) except TypeError as exc: raise AssertionError(msg or f"List elements are not comparable: {values!r}") from exc if not ordered: raise AssertionError(msg or f"List is not ordered: {values!r}")
def _table_to_rows(table: _TableLike) -> list[dict[str, str]]: headings = list(table.headings) rows: list[dict[str, str]] = [] for row in table.rows: try: rows.append(dict(zip(headings, list(row), strict=True))) except (ValueError, TypeError) as exc: raise AssertionError(f"Table row {row!r} does not match headings {headings!r}") from exc return rows
[docs] def assert_table_equals(actual: _TableLike, expected: _TableLike, *, msg: str = "") -> None: """Assert that two Behave Data Tables have the same headings and rows.""" try: result = deep_compare(_table_to_rows(actual), _table_to_rows(expected)) except (ValueError, TypeError) as exc: raise AssertionError(f"Tables have incompatible shape: {exc}") from exc if not result.equal: raise AssertionError(_format_diff_result(result, msg))