|
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
|
|
|
|