A Python module to test dicts in a declarative way
from .core import Contains, DoesntContains, Matcher
from .operators import ANY, ANY_NOT_NONE, Expect, ExpectNot

__version__ = "0.1.0"
__all__ = ["ANY", "ANY_NOT_NONE", "Matcher", "Contains", "DoesntContains", "Expect", "ExpectNot"]

pydiction/__init__.py
nadobando/pydiction
import itertools
from typing import (
    Any,
    Dict,
    Generic,
    Iterable,
    List,
    Sequence,
    TypeVar,
    Union,
    cast,
    overload,
)
from unittest.mock import ANY

from pydiction.operators import Expectation
from pydiction.utils import sentinel

T = TypeVar("T")

NOT_SET: object = sentinel("NOT_SET")


def is_same_type(actual, expected, type_):
    return isinstance(expected, type_) and isinstance(actual, type_)


class Matcher:
    def match(
        self, actual: Any, expected: Any, path: List[str], *, strict_keys=True, check_order=True
    ) -> list[tuple[Sequence, str, Any, ANY]]:
        # if expected is ANY or (expected is ANY_NOT_NONE and actual == ANY_NOT_NONE):
        #     return []

        if isinstance(expected, Contains):
            errors = expected.match(actual, path, self, strict_keys=strict_keys, check_order=check_order)
        elif isinstance(expected, DoesntContains):
            errors = expected.match(actual, path, self)
        elif is_same_type(actual, expected, dict):
            errors = self._compare_dicts(actual, expected, path, strict_keys)
        elif is_same_type(actual, expected, list):
            errors = self._compare_lists(actual, expected, path, strict_keys=strict_keys, check_order=check_order)
        else:
            errors = self._compare_values(actual, expected, path)

        return errors

    @staticmethod
    def _compare_values(actual: Any, expected: Any, path: Sequence[str]) -> list[tuple[Sequence, str, Any, Any]]:
        errors: list[tuple[Sequence, str, Any, Any]] = []
        if callable(expected):
            if not expected(actual):
                if hasattr(expected, "__self__") and isinstance(expected.__self__, Expectation):
                    errors.append((path, expected.__self__.error_msg or "", actual, expected.__self__.expected))
                else:
                    errors.append((path, expected.__name__ or "", actual, "UNKNOWN"))
        elif actual != expected:
            errors.append((path, "does not match", actual, expected))

        return errors

    def _compare_dicts(
        self, actual: Dict[str, Any], expected: Dict[str, Any], path: List[str], strict_keys=None
    ) -> list[tuple[Sequence, str, Any, ANY]]:
        errors: list[tuple[Sequence, str, Any, ANY]] = []
        if isinstance(expected, (Contains, DoesntContains)):
            errors.extend(expected.match(actual, path, self))
        else:
            errors.extend(
                [(path + [key], "not expected", actual.get(key), NOT_SET) for key in actual.keys() - expected.keys()]
            )
            for key, expected_value in expected.items():
                actual_value = actual.get(key)
                if key not in actual:
                    errors.append((path + [key], "not found", NOT_SET, expected_value))
                else:
                    errors.extend(
                        self.match(
                            actual_value, expected_value, path + [key], strict_keys=strict_keys, check_order=False
                        )
                    )

        return errors

    def _compare_lists(
        self, actual: List[Any], expected: List[Any], path: List[str], *, strict_keys=True, check_order=True
    ) -> list[tuple[Sequence, str, Any, ANY]]:
        errors: list[tuple[Sequence, str, Any, ANY]] = []

        if len(actual) != len(expected):
            errors.append((path, "Lists have different lengths", len(actual), len(expected)))
        if check_order:
            for i, (actual_item, expected_item) in enumerate(zip(actual, expected)):
                errors.extend(
                    self.match(
                        actual_item, expected_item, path + [str(i)], strict_keys=strict_keys, check_order=check_order
                    )
                )
        else:
            actual_permutations = itertools.permutations(actual)
            for perm in actual_permutations:
                if list(perm) == expected:
                    return errors

            if actual != expected:
                errors.append((path, "different elements (ignoring order)", actual, expected))
        return errors

    @overload
    def assert_declarative_object(
        self, actual: list, expected: Union[list, "BaseOperator"], strict_keys=True, check_order=True
    ) -> None:  # pragma: no cover
        ...

    @overload
    def assert_declarative_object(
        self,
        actual: Dict[str, Any],
        expected: Union[Dict[str, Any], "BaseOperator"],
        strict_keys=True,
        check_order=True,
    ) -> None:  # pragma: no cover
        ...

    def assert_declarative_object(
        self,
        actual: Union[Dict[str, Any], list],
        expected: Union[Dict[str, Any], list, "BaseOperator"],
        strict_keys=True,
        check_order=True,
    ) -> None:
        errors = self.match(actual, expected, [], strict_keys=strict_keys, check_order=check_order)
        self._raise_errors(errors)

    def get_declarative_diff(
        self,
        actual: Dict[str, Any],
        expected: Union[Dict[str, Any], "BaseOperator"],
        strict_keys=True,
        check_order=True,
    ) -> list:
        return self.match(actual, expected, [], strict_keys=strict_keys, check_order=check_order)

    @staticmethod
    def _raise_errors(errors) -> None:
        if errors:
            raise AssertionError("\n" + "\n".join([str((".".join(i[0]), *i[1:])) for i in errors]))


class BaseOperator:
    pass


class Contains(Generic[T], Iterable, BaseOperator):
    def __init__(self, iterable: Union[Dict[str, T], List[T]], *, recursive=False, check_pairs=True):
        self.iterable = iterable
        self.recursive = recursive
        self.check_pairs = check_pairs

    def __iter__(self):  # pragma: no cover
        return iter(self.iterable)

    def match(self, actual, path, matcher: Matcher, *, strict_keys=True, **_) -> list[tuple[Sequence, str, Any, ANY]]:
        if is_same_type(actual, self.iterable, dict):
            errors = self._match_dict(actual, matcher, path, strict_keys)
        elif is_same_type(actual, self.iterable, list):
            errors = self._match_list(actual, matcher, path, strict_keys)
        else:
            errors = [(path, "Contains can only be used with dictionaries or lists", actual, self.iterable)]

        return errors

    def _match_dict(self, actual, matcher, path, strict_keys):
        errors = []
        for key, expected_value in self.iterable.items():
            actual_value = actual.get(key)
            key_ = path + [key]
            if self.recursive and isinstance(actual_value, (list, dict)):
                errors += Contains(expected_value, recursive=self.recursive).match(actual_value, key_, matcher)
            else:
                if isinstance(expected_value, (Contains, DoesntContains)):
                    errors += expected_value.match(actual_value, key_, matcher)
                else:
                    if key not in actual:
                        errors.append((key_, "not found", actual_value, expected_value))
                    elif self.check_pairs:
                        errors += matcher.match(
                            actual_value, expected_value, key_, strict_keys=strict_keys, check_order=False
                        )
        return errors

    def _match_list(self, actual, matcher, path, strict_keys):
        errors = []
        if len(actual) < len(self.iterable):
            errors.append((path, "List is too short", actual, self.iterable))
        else:
            for i, expected_item in enumerate(self.iterable):
                actual_copy = actual.copy()
                tmp_path = path + [str(i)]
                found = False
                for j, actual_item in enumerate(actual_copy):
                    if actual_item in self.iterable:
                        found = True
                        actual.pop(j)
                        break
                    elif isinstance(actual_item, (dict, list)):
                        if self.recursive and not isinstance(self.iterable, (Contains, DoesntContains)):
                            expected_item = Contains(expected_item, recursive=self.recursive)

                        if isinstance(expected_item, (Contains, DoesntContains)):
                            inner_errors = expected_item.match(actual_item, tmp_path, matcher)
                        else:
                            inner_errors = matcher.match(
                                actual_item, expected_item, tmp_path, strict_keys=strict_keys, check_order=False
                            )
                        if not inner_errors:
                            found = True
                            actual.pop(j)
                            break

                if found:
                    break
                else:
                    errors.append((tmp_path, "not_found", actual_item, expected_item))

        return errors

    def __repr__(self):  # pragma: no cover
        return f"<Contains: {repr(self.iterable)}>"

    def __eq__(self, other):
        match = self.match(other, [], Matcher())
        return len(match) == 0


class DoesntContains(Generic[T], Iterable, BaseOperator):
    def __init__(
        self,
        iterable: Union[Dict[str, T], List[T]],
    ):
        self.iterable = iterable

    def __iter__(self):  # pragma: no cover
        return iter(self.iterable)

    def match(self, actual, path, _: Matcher, **kwargs) -> list[tuple[Sequence, str, Any, ANY]]:
        errors: list[tuple[Sequence, str, Any, ANY]] = []
        if isinstance(actual, dict):
            for key, expected_value in cast(dict, self.iterable).items():
                actual_value = actual.get(key)
                # todo:
                #   if expected_value is not ANY:
                #       continue
                #   if not (expected_value is ANY_NOT_NONE and actual_value is not None):
                #       continue

                if actual_value == expected_value:
                    errors.append((path, f"should not contain {key}", actual_value, expected_value))

        elif isinstance(actual, list):
            for i, expected_item in enumerate(self.iterable):
                actual_item = actual[i]
                if isinstance(actual_item, dict):
                    if expected_item in actual:
                        errors.append(
                            (path + [str(i)], f"list should not contain {expected_item}", actual_item, expected_item)
                        )

                elif actual_item in self.iterable:
                    errors.append((path + [str(i)], f"list should not contain {expected_item}", actual, expected_item))

        return errors

    def __repr__(self):  # pragma: no cover
        return repr(self.iterable)

pydiction/core.py
nadobando/pydiction
from typing import Optional, TypeVar
from unittest.mock import ANY


class _ANY_NOT_NONE(ANY.__class__):
    "A helper object that compares equal to everything."

    def __eq__(self, other):
        if other is None:
            return False
        return True

    def __ne__(self, other):
        if other is None:
            return True
        return False

    def __repr__(self):
        return "<ANY_NOT_NONE>"

    def __hash__(self):
        return hash(repr(self))


T = TypeVar("T")


class Expectation:
    """
    class to inverse the left operand with the right operand so the expectation are applied on the actual value
    """

    __error_mapping__: dict[str, str] = {}

    def __init__(self, expected: T, error_msg: Optional[str] = None):
        self.error_msg = error_msg
        self.expected: T = expected

    def __eq__(self, other: T) -> bool:
        return other.__eq__(self.expected)

    def __ne__(self, other: T) -> bool:
        return other.__ne__(self.expected)

    def __ge__(self, other: T) -> bool:
        return other.__ge__(self.expected)  # type: ignore[operator]

    def __gt__(self, other: T) -> bool:
        return other.__gt__(self.expected)  # type: ignore[operator]

    def __le__(self, other: T) -> bool:
        return other.__le__(self.expected)  # type: ignore[operator]

    def __lt__(self, other: T) -> bool:
        return other.__lt__(self.expected)  # type: ignore[operator]

    def __contains__(self, other: T) -> bool:
        return other.__contains__(self.expected)  # type: ignore[operator]

    def __getattribute__(self, item):
        error_mapping = object.__getattribute__(self, "__error_mapping__")
        if item in error_mapping:
            item_ = error_mapping[item]
            setattr(self, "error_msg", item_)
        #
        return object.__getattribute__(self, item)


class Expect(Expectation):
    __error_mapping__: dict[str, str] = {
        "__eq__": "not equals",
        "__ne__": "equals",
        "__ge__": "not greater or equal",
        "__gt__": "not greater than (expected)",
        "__le__": "greater (expected)",
        "__lt__": "greater or equal (expected)",
        "__contains__": "should contain",
    }


class ExpectNot(Expectation):
    __error_mapping__: dict[str, str] = {
        "__eq__": "equals (not expected)",
        "__ne__": "not equals (expected)",
        "__ge__": "lower or equal (not expected)",
        "__gt__": "lower (not expected)",
        "__le__": "greater (not expected)",
        "__lt__": "greater or equal (not expected)",
        "__contains__": "contains (not expected)",
    }

    def __eq__(self, other: T) -> bool:
        return not super().__eq__(other)

    def __ne__(self, other: T) -> bool:
        return not super().__ne__(other)

    def __ge__(self, other: T) -> bool:
        return not super().__ge__(other)

    def __gt__(self, other: T) -> bool:
        return not super().__gt__(other)

    def __le__(self, other: T) -> bool:
        return not super().__le__(other)

    def __lt__(self, other: T) -> bool:
        return not super().__lt__(other)

    def __contains__(self, other: T) -> bool:
        return not super().__contains__(other)


ANY_NOT_NONE = _ANY_NOT_NONE()

pydiction/operators.py
nadobando/pydiction
def sentinel(name: str):
    return type(name, (object,), {"__repr__": lambda x: f"<{name}>"})()

pydiction/utils.py
nadobando/pydiction
import pytest

from pydiction import Matcher


@pytest.fixture
def matcher():
    return Matcher()

tests/conftest.py
nadobando/pydiction
import pytest

from pydiction.operators import ANY_NOT_NONE, Expect, ExpectNot

TEST_KEY = "test"


@pytest.mark.parametrize(
    "actual,expected",
    (
        (1, Expect(1).__eq__),
        (1, Expect(0).__ne__),
        (1, Expect(0).__gt__),
        (1, Expect(0).__ge__),
        (1, Expect(1).__ge__),
        (0, Expect(1).__lt__),
        (0, Expect(0).__le__),
        (0, Expect(1).__le__),
        ("abc", Expect("b").__contains__),
        (0, lambda x: x < 1),
    ),
)
def test_expect_operators(matcher, actual, expected):
    matcher.assert_declarative_object({TEST_KEY: actual}, {TEST_KEY: expected})


@pytest.mark.parametrize(
    "actual,expected",
    (
        (0, ExpectNot(1).__eq__),
        (0, ExpectNot(0).__ne__),
        (0, ExpectNot(0).__gt__),
        (0, ExpectNot(1).__gt__),
        (0, ExpectNot(1).__ge__),
        (1, ExpectNot(0).__lt__),
        (1, ExpectNot(1).__lt__),
        (1, ExpectNot(0).__le__),
        ("abc", ExpectNot("z").__contains__),
    ),
)
def test_does_not_expect(matcher, actual, expected):
    matcher.assert_declarative_object({TEST_KEY: actual}, {TEST_KEY: expected})


@pytest.mark.parametrize(
    "actual, expected",
    (
        (1, Expect(0).__eq__),
        (1, Expect(1).__ne__),
        (0, Expect(1).__gt__),
        (0, Expect(0).__gt__),
        (0, Expect(1).__ge__),
        (1, Expect(1).__lt__),
        (1, Expect(0).__lt__),
        (1, Expect(0).__le__),
        ("abc", Expect("d").__contains__),
        (0, lambda x: x < 0),
    ),
)
def test_expect_negative(matcher, actual, expected):
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object({TEST_KEY: actual}, {TEST_KEY: expected})


@pytest.mark.parametrize(
    "actual, expected",
    (
        (1, ExpectNot(1).__eq__),
        (1, ExpectNot(0).__ne__),
        (1, ExpectNot(0).__gt__),
        (1, ExpectNot(0).__ge__),
        (1, ExpectNot(2).__lt__),
        (1, ExpectNot(1).__le__),
        ("abc", ExpectNot("b").__contains__),
    ),
)
def test_does_not_expect_negative(matcher, actual, expected):
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object({TEST_KEY: actual}, {TEST_KEY: expected})


@pytest.mark.parametrize(
    "actual, expected",
    (
        (1, ANY_NOT_NONE),
        ("abc", ANY_NOT_NONE),
    ),
)
def test_any_not_none_eq_positive(matcher, actual, expected):
    matcher.assert_declarative_object({TEST_KEY: actual}, {TEST_KEY: expected})


def test_any_not_none_eq_negative(matcher):
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object({TEST_KEY: None}, {TEST_KEY: ANY_NOT_NONE})


@pytest.mark.parametrize(
    "actual, expected",
    (
        (1, ANY_NOT_NONE),
        ("abc", ANY_NOT_NONE),
    ),
)
def test_any_not_none_ne_positive(matcher, actual, expected):
    matcher.assert_declarative_object({TEST_KEY: actual}, {TEST_KEY: expected})


def test_any_not_none_ne_negative(matcher):
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object({TEST_KEY: None}, {TEST_KEY: ANY_NOT_NONE})


def test_any_not_none():
    assert ANY_NOT_NONE is not None
    assert 1 == ANY_NOT_NONE
    assert ANY_NOT_NONE == 1

tests/test_operators.py
nadobando/pydiction
import datetime

import pytest

from pydiction import ANY, ANY_NOT_NONE, Contains, DoesntContains
from pydiction.operators import Expect


def test_matcher_with_equal_dicts(matcher):
    actual = {"a": 1, "b": 2}
    expected = {"a": 1, "b": 2}
    matcher.assert_declarative_object(actual, expected)


@pytest.mark.parametrize("expected", ({"a": 1, "b": 3}, {"a": 1}, {"a": 1, "c": 2}))
def test_matcher_with_unequal_dicts(matcher, expected):
    actual = {"a": 1, "b": 2}
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object(actual, expected)


def test_matcher_with_contains(matcher):
    actual = {"a": 1, "b": 2}
    expected = Contains({"a": 1})
    matcher.assert_declarative_object(actual, expected)


def test_matcher_with_doesnt_contains(matcher):
    actual = {"a": 1, "b": 2}
    expected = DoesntContains({"c": 3})
    matcher.assert_declarative_object(actual, expected)


def test_nested_contains_dict(matcher):
    actual = {"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}
    expected = Contains({"a": Contains({"x": 1}), "b": Contains({"x": 3})})
    matcher.assert_declarative_object(actual, expected)


def test_nested_doesnt_contains_dict(matcher):
    actual = {"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}
    expected = {"a": DoesntContains({"z": 5}), "b": Contains({"y": 4})}

    matcher.assert_declarative_object(actual, expected)


def test_nested_contains_list(matcher):
    actual = {"items": [{"name": "item1"}, {"name": "item2"}]}
    expected = Contains({"items": [Contains({"name": "item1"}), Contains({"name": "item2"})]})
    matcher.assert_declarative_object(actual, expected)


def test_nested_doesnt_contains_list(matcher):
    actual = {"items": [{"name": "item1"}, {"name": "item2"}]}
    expected = {"items": DoesntContains([{"name": "item3"}])}
    matcher.assert_declarative_object(actual, expected)


@pytest.mark.parametrize(
    "actual, expected",
    [
        (
            {"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}},
            Contains({"a": Contains({"x": 1}), "b": Contains({"x": 3})}),
        ),
        (
            {"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}},
            Contains({"a": DoesntContains({"z": 5}), "b": Contains({"y": 4})}),
        ),
        (
            {"items": [{"name": "item1"}, {"name": "item2"}]},
            Contains({"items": Contains([{"name": "item1"}, {"name": "item2"}])}),
        ),
        (
            {"items": [{"name": "item1"}, {"name": "item2"}]},
            {"items": DoesntContains([{"name": "item3"}])},
        ),
    ],
    ids=["Nested Contains", "Nested Doesnt Contains", "Nested Contains List", "Nested Doesnt Contains List"],
)
def test_nested_cases(matcher, actual, expected):
    matcher.assert_declarative_object(actual, expected)


@pytest.mark.parametrize(
    "actual, expected",
    [
        (
            {"visited": [{"_key": None, "_rev": None}, {"_id": None, "_key": None, "_rev": None}]},
            Contains({"visited": [Contains({"_key": pytest.approx(1)}), {"_id": None}]}),
        ),
        (
            {"edges": {"visited": [{"_from": None, "_to": None}, {"_from": None, "_to": None}]}},
            Contains({"edges": Contains({"visited": [{"_from": None}, {"_id": None}]})}),
        ),
        ([1], Contains([1, 2])),
        ("1", Contains("ads")),  # type: ignore[arg-type]
        ([1], [1, 2]),
    ],
)
def test_negative_cases(
    matcher,
    actual,
    expected,
):
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object(actual, expected, strict_keys=True)


@pytest.mark.parametrize("actual,expected", [([1], [1, 2])])
def test_negative_case_check_order_false(
    matcher,
    actual,
    expected,
):
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object(actual, expected, strict_keys=True, check_order=False)


def test_complex_comparison(matcher):
    actual = {
        "name": "John",
        "email": "john@example.com",
        "age": 25,
        "friends": [
            {
                "name": "Alice",
                "email": "alice@example.com",
                "age": 21,
            }
        ],
        "comments": [{"text": "Great post!"}, {"text": "not existing post!"}],
        "likes": [
            {
                "title": "First Post",
                "content": "This is my first post!",
            },
            {"text": "Great post!"},
        ],
    }

    expected = {
        "name": ANY,
        "email": ANY,
        "age": ANY_NOT_NONE,
        "friends": [
            {
                "age": ANY_NOT_NONE,
                "email": ANY_NOT_NONE,
                "name": "Alice",
            }
        ],
        "comments": Expect({"text": "not existing post!"}).__contains__,
        "likes": Contains(
            [
                {
                    "content": ANY,
                    "title": "First Post",
                },
            ]
        ),
    }

    matcher.assert_declarative_object(actual, expected)


@pytest.mark.parametrize(
    "expected", (Expect("a").__contains__, Contains({"a": 1}), Contains({"a": 2}, check_pairs=False))
)
def test_simple_contains(matcher, expected):
    actual = {"a": 1, "b": 2}
    matcher.assert_declarative_object(actual, expected)


def test_recursive_contains(matcher):
    actual = {"a": {"b": 2, "c": {}}, "b": 3}
    expected = Contains(
        {
            "a": {
                "b": 2,
            },
            "b": 3,
        },
        recursive=True,
    )
    matcher.assert_declarative_object(actual, expected)


# def test_inner_recursive_contains(matcher):
#     actual = {"a": {"b": 2, "c": {}}, "b": 3}
#     expected = Contains(
#         Contains({
#             "a": {
#                 "b": 2,
#             },
#             "b": 3,
#         }),
#         # recursive=True,
#     )
#     matcher.assert_declarative_object(actual, expected)


def test_recursive_contains__fails(matcher):
    actual = {"a": {"b": 2, "c": {}}, "b": 3}
    expected = Contains(
        {
            "a": {
                "b": 3,
            },
            "b": 3,
        },
        recursive=True,
    )
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object(actual, expected)


def test_contains_list_with_dicts(matcher):
    actual = {
        "test": [
            {
                "_id": "comments/1091324",
                "_key": "1091324",
                "_rev": "_gmsWn4e--B",
                "text": "Great post!",
            },
            {
                "_id": "posts/1091323",
                "_key": "1091323",
                "_rev": "_gmsWn4e--A",
                "comments": None,
                "content": "This is my first post!",
                "title": "First Post",
            },
        ]
    }
    expected = {
        "test": Contains(
            [
                {"_id": ANY_NOT_NONE, "_key": ANY_NOT_NONE, "_rev": ANY_NOT_NONE, "text": "Great post!"},
                {
                    "_id": ANY_NOT_NONE,
                    "_key": ANY_NOT_NONE,
                    "_rev": ANY_NOT_NONE,
                    "comments": None,
                    "content": "This is my first post!",
                    "title": "First Post",
                },
            ],
        ),
    }

    matcher.assert_declarative_object(actual, expected)


def test_contains_nested_lists(matcher):
    actual = {
        "test": [
            [
                {
                    "_id": "comments/1091324",
                    "_key": "1091324",
                    "_rev": "_gmsWn4e--B",
                    "text": "Great post!",
                }
            ],
            [
                {
                    "_id": "posts/1091323",
                    "_key": "1091323",
                    "_rev": "_gmsWn4e--A",
                    "comments": None,
                    "content": "This is my first post!",
                    "title": "First Post",
                }
            ],
        ]
    }
    expected = {
        "test": Contains(
            [
                [{"_id": ANY_NOT_NONE, "_key": ANY_NOT_NONE, "_rev": ANY_NOT_NONE, "text": "Great post!"}],
                [
                    {
                        "_id": ANY_NOT_NONE,
                        "_key": ANY_NOT_NONE,
                        "_rev": ANY_NOT_NONE,
                        "comments": None,
                        "content": "This is my first post!",
                        "title": "First Post",
                    }
                ],
            ],
        ),
    }

    matcher.assert_declarative_object(actual, expected)


@pytest.mark.parametrize(
    "actual, expected",
    [
        (
            [[1]],
            DoesntContains([[1]]),
        ),
        ({"test": [{"test": 1}]}, DoesntContains({"test": [{"test": 1}]})),
        ({"test": [{"test": 1}]}, {"test": DoesntContains([{"test": 1}])}),
        ({"test": {"test": [1]}}, DoesntContains({"test": {"test": [1]}})),
    ],
)
def test_not_contains_negative_cases(matcher, actual, expected):
    with pytest.raises(AssertionError):
        matcher.assert_declarative_object(actual, expected, strict_keys=True)


def test_not_contains_nested_lists(matcher):
    actual = {
        "test": [
            [
                {
                    "_id": "comments/1091324",
                    "_key": "1091324",
                    "_rev": "_gmsWn4e--B",
                    "text": "Great post!",
                }
            ],
            [
                {
                    "_id": "posts/1091323",
                    "_key": "1091323",
                    "_rev": "_gmsWn4e--A",
                    "comments": None,
                    "content": "This is my first post!",
                    "title": "First Post",
                }
            ],
        ]
    }
    expected = {
        "test": Contains(
            [
                [{"_id": ANY_NOT_NONE, "_key": ANY_NOT_NONE, "_rev": ANY_NOT_NONE, "text": "Great post!"}],
                [
                    {
                        "_id": ANY_NOT_NONE,
                        "_key": ANY_NOT_NONE,
                        "_rev": ANY_NOT_NONE,
                        "comments": None,
                        "content": "This is my first post!",
                        "title": "First Post",
                    }
                ],
            ],
        ),
    }

    matcher.assert_declarative_object(actual, expected)


def test_1(matcher):
    datetime.datetime.now()
    e = {
        # "_id": ANY_NOT_NONE,
        # "_key": ANY_NOT_NONE,
        # "_rev": ANY_NOT_NONE,
        # "name": "John",
        # "age": 25,
        # "comments": [{"_id": ANY_NOT_NONE, "_key": ANY_NOT_NONE, "_rev": ANY_NOT_NONE, "text": "Great post!"}],
        # "edges": {
        #     "comments": [
        #         {
        #             "_from": ANY_NOT_NONE,
        #             "_id": ANY_NOT_NONE,
        #             "_key": ANY_NOT_NONE,
        #             "_rev": ANY_NOT_NONE,
        #             "_to": ANY_NOT_NONE,
        #             "commented_at": now,
        #         },
        #     ],
        #     "friends": [
        #         {
        #             "_from": ANY_NOT_NONE,
        #             "_id": ANY_NOT_NONE,
        #             "_key": ANY_NOT_NONE,
        #             "_rev": ANY_NOT_NONE,
        #             "_to": ANY_NOT_NONE,
        #             "since": now,
        #         }
        #     ],
        #     "likes": [
        #         {
        #             "_from": ANY_NOT_NONE,
        #             "_id": ANY_NOT_NONE,
        #             "_key": ANY_NOT_NONE,
        #             "_rev": ANY_NOT_NONE,
        #             "_to": ANY_NOT_NONE,
        #             "liked_at": now,
        #         },
        #         {
        #             "_from": ANY_NOT_NONE,
        #             "_id": ANY_NOT_NONE,
        #             "_key": ANY_NOT_NONE,
        #             "_rev": ANY_NOT_NONE,
        #             "_to": ANY_NOT_NONE,
        #             "liked_at": now,
        #         },
        #     ],
        #     "posts": [
        #         {
        #             "_from": ANY_NOT_NONE,
        #             "_id": ANY_NOT_NONE,
        #             "_key": ANY_NOT_NONE,
        #             "_rev": ANY_NOT_NONE,
        #             "_to": ANY_NOT_NONE,
        #             "created_at": now,
        #         }
        #     ],
        # },
        # "email": "john@example.com",
        # "friends": [
        #     {
        #         "_id": ANY_NOT_NONE,
        #         "_key": ANY_NOT_NONE,
        #         "_rev": ANY_NOT_NONE,
        #         "age": 21,
        #         "comments": None,
        #         "edges": None,
        #         "email": "alice@example.com",
        #         "friends": None,
        #         "likes": None,
        #         "name": "Alice",
        #         "posts": None,
        #     }
        # ],
        "likes": Contains(
            [
                {"_id": ANY_NOT_NONE, "_key": ANY_NOT_NONE, "_rev": ANY_NOT_NONE, "text": "Great post!"},
                {
                    "_id": ANY_NOT_NONE,
                    "_key": ANY_NOT_NONE,
                    "_rev": ANY_NOT_NONE,
                    "content": "This is my first post!",
                    "title": "First Post",
                },
            ]
        ),
        # "posts": [
        #     {
        #         "_id": ANY_NOT_NONE,
        #         "_key": ANY_NOT_NONE,
        #         "_rev": ANY_NOT_NONE,
        #         "comments": None,
        #         "content": "This is my first post!",
        #         "title": "First Post",
        #     }
        # ],
    }
    a = {
        # "_id": "users/1191704",
        # "_key": "1191704",
        # "_rev": "_gn91JF2---",
        # "name": "John",
        # "email": "john@example.com",
        # "age": 25,
        # "friends": [
        #     {
        #         "_id": "users/1191705",
        #         "_key": "1191705",
        #         "_rev": "_gn91JF2--_",
        #         "name": "Alice",
        #         "email": "alice@example.com",
        #         "age": 21,
        #         "friends": None,
        #         "posts": None,
        #         "comments": None,
        #         "likes": None,
        #         "edges": None,
        #     }
        # ],
        # "posts": [
        #     {
        #         "_id": "posts/1191706",
        #         "_key": "1191706",
        #         "_rev": "_gn91JF6---",
        #         "title": "First Post",
        #         "content": "This is my first post!",
        #         "comments": [
        #             {"_id": "comments/1191707", "_key": "1191707", "_rev": "_gn91JF6--_", "text": "Great post!"}
        #         ],
        #     }
        # ],
        # "comments": [{"_id": "comments/1191707", "_key": "1191707", "_rev": "_gn91JF6--_", "text": "Great post!"}],
        "likes": [
            {
                "_id": "posts/1191706",
                "_key": "1191706",
                "_rev": "_gn91JF6---",
                "title": "First Post",
                "content": "This is my first post!",
                "comments": [
                    {"_id": "comments/1191707", "_key": "1191707", "_rev": "_gn91JF6--_", "text": "Great post!"}
                ],
            },
            {"_id": "comments/1191707", "_key": "1191707", "_rev": "_gn91JF6--_", "text": "Great post!"},
        ],
        # "edges": {
        #     "friends": [
        #         {
        #             "_id": "friendships/1191708",
        #             "_key": "1191708",
        #             "_rev": "_gn91JG----",
        #             "_from": "users/1191704",
        #             "_to": "users/1191705",
        #             "since": now,
        #         }
        #     ],
        #     "posts": [
        #         {
        #             "_id": "authorships/1191709",
        #             "_key": "1191709",
        #             "_rev": "_gn91JGC---",
        #             "_from": "users/1191704",
        #             "_to": "posts/1191706",
        #             "created_at": now,
        #         }
        #     ],
        #     "comments": [
        #         {
        #             "_id": "commentaries/1191711",
        #             "_key": "1191711",
        #             "_rev": "_gn91JGK---",
        #             "_from": "users/1191704",
        #             "_to": "comments/1191707",
        #             "commented_at": now,
        #         }
        #     ],
        #     "likes": [
        #         {
        #             "_id": "likes/1191713",
        #             "_key": "1191713",
        #             "_rev": "_gn91JGS---",
        #             "_from": "users/1191704",
        #             "_to": "posts/1191706",
        #             "liked_at": now,
        #         },
        #         {
        #             "_id": "likes/1191712",
        #             "_key": "1191712",
        #             "_rev": "_gn91JGO---",
        #             "_from": "users/1191704",
        #             "_to": "comments/1191707",
        #             "liked_at": now,
        #         },
        #     ],
        # },
    }
    matcher.assert_declarative_object(a, e)

tests/test_pydiction.py
nadobando/pydiction