Completed
Pull Request — master (#49)
by Max
03:39
created

structured_data._match.patterns.mapping_match   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 153
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 63
dl 0
loc 153
rs 10
c 0
b 0
f 0
wmc 20
1
"""Matches that extract values via attribute access or dict indexing."""
2
3
import typing
4
5
from ..match_failure import MatchFailure
6
from .compound_match import CompoundMatch
7
8
9
def value_cant_be_smaller(
10
    target_match_dict: typing.Sized, value_match_dict: typing.Sized
11
) -> None:
12
    """If the target is too small, fail."""
13
    if len(value_match_dict) < len(target_match_dict):
14
        raise MatchFailure
15
16
17
class AttrPattern(CompoundMatch, tuple):
18
    """A matcher that destructures an object using attribute access.
19
20
    The ``AttrPattern`` constructor takes keyword arguments. Each name-value
21
    pair is the name of an attribute, and a matcher to apply to that attribute.
22
23
    Attributes are checked in the order they were passed.
24
    """
25
26
    __slots__ = ()
27
28
    def __new__(cls, /, **kwargs) -> "AttrPattern":  # noqa: E225
29
        return super().__new__(
30
            cls, (tuple(kwargs.items()),)  # type: ignore
31
        )
32
33
    @property
34
    def match_dict(self):
35
        """Return the dict of matches to check."""
36
        return self[0]
37
38
    def destructure(
39
        self, value
40
    ) -> typing.Union[typing.Tuple[()], typing.Tuple[typing.Any, typing.Any]]:
41
        """Return a tuple of sub-values to check.
42
43
        If self is empty, return no values from self or the target.
44
45
        Special-case matching against another AttrPattern as follows:
46
        Confirm that the target isn't smaller than self, then
47
        Extract the first match from the target's match_dict, and
48
        Return the smaller value, and the first match's value.
49
        (This works as desired when value is self, but all other cases
50
        where ``isinstance(value, AttrPattern)`` are unspecified.)
51
52
        By default, it takes the first match from the match_dict, and
53
        returns the original value, and the result of calling ``getattr`` with
54
        the target and the match's key.
55
        """
56
        if not self.match_dict:
57
            return ()
58
        if isinstance(value, AttrPattern):
59
            value_cant_be_smaller(self.match_dict, value.match_dict)
60
            first_match, *remainder = value.match_dict
61
            return (AttrPattern(**dict(remainder)), first_match[1])
62
        first_match = self.match_dict[0]
63
        try:
64
            return (value, getattr(value, first_match[0]))
65
        except AttributeError:
66
            raise MatchFailure
67
68
69
def dict_pattern_length(dp_or_d: typing.Sized):
70
    """Return the length of the argument for the purposes of ``DictPattern``.
71
72
    Normally, this is just the length of the argument, but if the argument is a
73
    DictPattern, it is the argument's match_dict's length.
74
    """
75
    if isinstance(dp_or_d, DictPattern):
76
        return len(dp_or_d.match_dict)
77
    return len(dp_or_d)
78
79
80
class DictPattern(CompoundMatch, tuple):
81
    """A matcher that destructures a dictionary by key.
82
83
    The ``DictPattern`` constructor takes a required argument, a dictionary
84
    where the keys are keys to check, and the values are matchers to apply.
85
    It also takes an optional keyword argument, "exhaustive", which defaults to
86
    False.
87
    If "exhaustive" is True, then the match requires that the matched
88
    dictionary has no keys not in the ``DictPattern``. Otherwise, "extra" keys
89
    are ignored.
90
91
    Keys are checked in iteration order.
92
    """
93
94
    __slots__ = ()
95
96
    def __new__(cls, match_dict, *, exhaustive=False) -> "DictPattern":
97
        return super().__new__(
98
            cls, (tuple(match_dict.items()), exhaustive)  # type: ignore
99
        )
100
101
    @property
102
    def match_dict(self):
103
        """Return the dict of matches to check."""
104
        return self[0]
105
106
    @property
107
    def exhaustive(self):
108
        """Return whether the target must of the exact keys as self."""
109
        return self[1]
110
111
    def exhaustive_length_must_match(self, value: typing.Sized):
112
        """If the match is exhaustive and the lengths differ, fail."""
113
        if self.exhaustive and dict_pattern_length(value) != dict_pattern_length(self):
114
            raise MatchFailure
115
116
    def destructure(
117
        self, value
118
    ) -> typing.Union[typing.Tuple[()], typing.Tuple[typing.Any, typing.Any]]:
119
        """Return a tuple of sub-values to check.
120
121
        If self is exhaustive and the lengths don't match, fail.
122
123
        If self is empty, return no values from self or the target.
124
125
        Special-case matching against another DictPattern as follows:
126
        Confirm that the target isn't smaller than self, then
127
        Extract the first match from the target's match_dict, and
128
        Return the smaller value, and the first match's value.
129
        Note that the returned DictPattern is never exhaustive; the
130
        exhaustiveness check is accomplished by asserting that the lengths
131
        start out the same, and that every key in self is present in value.
132
        (This works as desired when value is self, but all other cases
133
        where ``isinstance(value, DictPattern)`` are unspecified.)
134
135
        By default, it takes the first match from the match_dict, and
136
        returns the original value, and the result of indexing the target with
137
        the match's key.
138
        """
139
        self.exhaustive_length_must_match(value)
140
        if not self.match_dict:
141
            return ()
142
        if isinstance(value, DictPattern):
143
            value_cant_be_smaller(self.match_dict, value.match_dict)
144
            first_match, *remainder = value.match_dict
145
            return (DictPattern(dict(remainder)), first_match[1])
146
        first_match = self.match_dict[0]
147
        try:
148
            return (value, value[first_match[0]])
149
        except KeyError:
150
            raise MatchFailure
151