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