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