Source code for structured_data._match.patterns.mapping_match

"""Matches that extract values via attribute access or dict indexing."""

import typing

from ..match_failure import MatchFailure
from .compound_match import CompoundMatch


def value_cant_be_smaller(
    target_match_dict: typing.Sized, value_match_dict: typing.Sized
) -> None:
    """If the target is too small, fail."""
    if len(value_match_dict) < len(target_match_dict):
        raise MatchFailure


[docs]class AttrPattern(CompoundMatch, tuple): """A matcher that destructures an object using attribute access. The ``AttrPattern`` constructor takes keyword arguments. Each name-value pair is the name of an attribute, and a matcher to apply to that attribute. Attributes are checked in the order they were passed. """ __slots__ = () def __new__(cls, /, **kwargs) -> "AttrPattern": # noqa: E225 return super().__new__( cls, (tuple(kwargs.items()),) # type: ignore ) @property def match_dict(self): """Return the dict of matches to check.""" return self[0]
[docs] def destructure( self, value ) -> typing.Union[typing.Tuple[()], typing.Tuple[typing.Any, typing.Any]]: """Return a tuple of sub-values to check. If self is empty, return no values from self or the target. Special-case matching against another AttrPattern as follows: Confirm that the target isn't smaller than self, then Extract the first match from the target's match_dict, and Return the smaller value, and the first match's value. (This works as desired when value is self, but all other cases where ``isinstance(value, AttrPattern)`` are unspecified.) By default, it takes the first match from the match_dict, and returns the original value, and the result of calling ``getattr`` with the target and the match's key. """ if not self.match_dict: return () if isinstance(value, AttrPattern): value_cant_be_smaller(self.match_dict, value.match_dict) first_match, *remainder = value.match_dict return (AttrPattern(**dict(remainder)), first_match[1]) first_match = self.match_dict[0] try: return (value, getattr(value, first_match[0])) except AttributeError: raise MatchFailure
def dict_pattern_length(dp_or_d: typing.Sized): """Return the length of the argument for the purposes of ``DictPattern``. Normally, this is just the length of the argument, but if the argument is a DictPattern, it is the argument's match_dict's length. """ if isinstance(dp_or_d, DictPattern): return len(dp_or_d.match_dict) return len(dp_or_d)
[docs]class DictPattern(CompoundMatch, tuple): """A matcher that destructures a dictionary by key. The ``DictPattern`` constructor takes a required argument, a dictionary where the keys are keys to check, and the values are matchers to apply. It also takes an optional keyword argument, "exhaustive", which defaults to False. If "exhaustive" is True, then the match requires that the matched dictionary has no keys not in the ``DictPattern``. Otherwise, "extra" keys are ignored. Keys are checked in iteration order. """ __slots__ = () def __new__(cls, match_dict, *, exhaustive=False) -> "DictPattern": return super().__new__( cls, (tuple(match_dict.items()), exhaustive) # type: ignore ) @property def match_dict(self): """Return the dict of matches to check.""" return self[0] @property def exhaustive(self): """Return whether the target must of the exact keys as self.""" return self[1]
[docs] def exhaustive_length_must_match(self, value: typing.Sized): """If the match is exhaustive and the lengths differ, fail.""" if self.exhaustive and dict_pattern_length(value) != dict_pattern_length(self): raise MatchFailure
[docs] def destructure( self, value ) -> typing.Union[typing.Tuple[()], typing.Tuple[typing.Any, typing.Any]]: """Return a tuple of sub-values to check. If self is exhaustive and the lengths don't match, fail. If self is empty, return no values from self or the target. Special-case matching against another DictPattern as follows: Confirm that the target isn't smaller than self, then Extract the first match from the target's match_dict, and Return the smaller value, and the first match's value. Note that the returned DictPattern is never exhaustive; the exhaustiveness check is accomplished by asserting that the lengths start out the same, and that every key in self is present in value. (This works as desired when value is self, but all other cases where ``isinstance(value, DictPattern)`` are unspecified.) By default, it takes the first match from the match_dict, and returns the original value, and the result of indexing the target with the match's key. """ self.exhaustive_length_must_match(value) if not self.match_dict: return () if isinstance(value, DictPattern): value_cant_be_smaller(self.match_dict, value.match_dict) first_match, *remainder = value.match_dict return (DictPattern(dict(remainder)), first_match[1]) first_match = self.match_dict[0] try: return (value, value[first_match[0]]) except KeyError: raise MatchFailure