Passed
Push — master ( bcf14c...4a1f1b )
by Max
57s
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

8 Methods

Rating   Name   Duplication   Size   Complexity  
A AttrPattern.destructure() 0 29 4
A AttrPattern.__new__() 0 4 1
A DictPattern.__new__() 0 3 1
A DictPattern.exhaustive() 0 4 1
A DictPattern.destructure() 0 35 4
A AttrPattern.match_dict() 0 4 1
A DictPattern.match_dict() 0 4 1
A DictPattern.exhaustive_length_must_match() 0 4 3

2 Functions

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