Source code for joythief.data_structures

import typing as tp
from collections.abc import Hashable, Iterable, Mapping

from .core import Matcher, MaybeMatcher
from .objects import Nothing

T = tp.TypeVar("T")


[docs] class DictContaining(Matcher[Mapping[Hashable, tp.Any]], dict[Hashable, tp.Any]): """Match the specified keys in a mapping, ignoring any extra keys. :param content: mapping or iterable of key-value pairs to include in the comparison :param \\**kwargs: additional key-value pairs to include in the comparison :raises ValueError: if no keys are specified (use :py:class:`~joythief.objects.InstanceOf` with :py:class:`dict` instead). .. versionadded:: 0.7.0 .. versionchanged:: 0.8.0 added :py:meth:`optionally`. .. code-block:: python assert ( actual == DictContaining([("foo", 123), ("bar", 456)], baz=InstanceOf(int)) ) **Note**: this subclasses :py:class:`dict` so that ``pytest`` will show the common and differing items, e.g.: .. code-block:: python > assert mapping == dict(foo=123, bar=456) E AssertionError: assert DictContaining(**{'foo': 123, 'bar': 0, 'baz': 789}) == {'foo': 123, 'bar': 456} E E Common items: E {'foo': 123} E Differing items: E {'bar': 0} != {'bar': 456} E Left contains 1 more item: E {'baz': 789} # ... """ @tp.overload def __init__(self, /, **kwargs: tp.Any) -> None: ... @tp.overload def __init__( self, content: Mapping[Hashable, tp.Any], /, **kwargs: tp.Any ) -> None: ... @tp.overload def __init__( self, content: Iterable[tuple[Hashable, tp.Any]], /, **kwargs: tp.Any ) -> None: ... def __init__(self, content: tp.Any = None, /, **kwargs: tp.Any) -> None: if not content and not kwargs: raise ValueError("an empty DictContaining matches any mapping") args: tuple[tp.Any, ...] = () if content is None else (content,) super().__init__(*args, **kwargs) def compare(self, other: tp.Any) -> bool: if not isinstance(other, Mapping): return self.not_implemented is_equal: bool = True for key, value in self.items(): if key in other: if self[key] != other[key]: is_equal = False elif isinstance(value, _OptionalKey): _ = value == Nothing() else: is_equal = False return is_equal def represent(self) -> str: return f"DictContaining(**{dict.__repr__(self)})"
[docs] @staticmethod def optionally(value: MaybeMatcher[T]) -> MaybeMatcher[T]: """Matcher factory for keys that may not be present. .. code-block:: python assert ( actual == DictContaining(foo=DictContaining.optionally(123)) ) **Note**: this allows keys to be missing entirely, but matches the value strictly. For the equivalent of :py:class:`typing.Optional`, use :py:class:`~joythief.objects.Nullable`. These can be combined if required, e.g. to allow the key ``"foo"`` to be either: missing; present with the value ``123``; or present with the value ``None``, use: .. code-block:: python assert ( actual == DictContaining(foo=DictContaining.optionally(Nullable(123))) ) """ return _OptionalKey(value)
class _OptionalKey(Matcher[T]): _value: MaybeMatcher[T] def __init__(self, value: MaybeMatcher[T], /): super().__init__() self._value = value def compare(self, other: tp.Any) -> bool: return tp.cast(bool, self._value == other) def represent(self) -> str: return f"DictContaining.optionally({self._value!r})"