Total Complexity | 55 |
Total Lines | 215 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like structured_data._patterns often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | import keyword |
||
2 | |||
3 | from ._match_failure import MatchFailure |
||
4 | from ._not_in import not_in |
||
5 | |||
6 | DISCARD = object() |
||
7 | |||
8 | |||
9 | class CompoundMatch: |
||
10 | |||
11 | __slots__ = () |
||
12 | |||
13 | def destructure(self, value): |
||
14 | raise NotImplementedError |
||
15 | |||
16 | |||
17 | class Pattern(tuple): |
||
18 | """A matcher that binds a value to a name.""" |
||
19 | |||
20 | __slots__ = () |
||
21 | |||
22 | def __new__(cls, name: str): |
||
23 | if name == "_": |
||
24 | return DISCARD |
||
25 | if not name.isidentifier(): |
||
26 | raise ValueError |
||
27 | if keyword.iskeyword(name): |
||
28 | raise ValueError |
||
29 | return super().__new__(cls, (name,)) |
||
30 | |||
31 | @property |
||
32 | def name(self): |
||
33 | """Return the name of the matcher.""" |
||
34 | return tuple.__getitem__(self, 0) |
||
35 | |||
36 | def __getitem__(self, other): |
||
37 | return AsPattern(self, other) |
||
38 | |||
39 | |||
40 | class AsPattern(CompoundMatch, tuple): |
||
41 | """A matcher that contains further bindings.""" |
||
42 | |||
43 | __slots__ = () |
||
44 | |||
45 | def __new__(cls, matcher: Pattern, match): |
||
46 | if match is DISCARD: |
||
47 | return matcher |
||
48 | return super().__new__(cls, (matcher, match)) |
||
49 | |||
50 | @property |
||
51 | def matcher(self): |
||
52 | """Return the left-hand-side of the as-match.""" |
||
53 | return self[0] |
||
54 | |||
55 | @property |
||
56 | def match(self): |
||
57 | """Return the right-hand-side of the as-match.""" |
||
58 | return self[1] |
||
59 | |||
60 | def destructure(self, value): |
||
61 | if isinstance(value, AsPattern): |
||
62 | if value is self: |
||
63 | return (self.match, self.matcher) |
||
64 | return (value.match, value) |
||
65 | return (value, value) |
||
66 | |||
67 | |||
68 | def value_cant_be_smaller(target_match_dict, value_match_dict): |
||
69 | if len(value_match_dict) < len(target_match_dict): |
||
70 | raise MatchFailure |
||
71 | |||
72 | |||
73 | def exhaustive_length_must_match(target, value_match_dict): |
||
74 | if target.exhaustive and len(value_match_dict) != len(target.match_dict): |
||
75 | raise MatchFailure |
||
76 | |||
77 | |||
78 | class AttrPattern(CompoundMatch, tuple): |
||
79 | """A matcher that destructures an object using attribute access.""" |
||
80 | |||
81 | __slots__ = () |
||
82 | |||
83 | def __new__(*args, **kwargs): |
||
84 | cls, *args = args |
||
85 | if args: |
||
86 | raise ValueError(args) |
||
87 | return super(AttrPattern, cls).__new__(cls, (tuple(kwargs.items()),)) |
||
88 | |||
89 | @property |
||
90 | def match_dict(self): |
||
91 | return self[0] |
||
92 | |||
93 | def destructure(self, value): |
||
94 | if isinstance(value, AttrPattern): |
||
95 | value_cant_be_smaller(self.match_dict, value.match_dict) |
||
96 | if value.match_dict: |
||
97 | first_match, *remainder = value.match_dict |
||
98 | return (AttrPattern(**dict(remainder)), first_match[1]) |
||
1 ignored issue
–
show
|
|||
99 | elif self.match_dict: |
||
100 | first_match = self.match_dict[0] |
||
101 | try: |
||
102 | return (value, getattr(value, first_match[0])) |
||
103 | except AttributeError: |
||
104 | raise MatchFailure |
||
105 | return () |
||
106 | |||
107 | |||
108 | class DictPattern(CompoundMatch, tuple): |
||
109 | """A matcher that destructures a dictionary by key.""" |
||
110 | |||
111 | __slots__ = () |
||
112 | |||
113 | def __new__(cls, match_dict, *, exhaustive=False): |
||
114 | return super(DictPattern, cls).__new__( |
||
115 | cls, (tuple(match_dict.items()), exhaustive) |
||
116 | ) |
||
117 | |||
118 | @property |
||
119 | def match_dict(self): |
||
120 | return self[0] |
||
121 | |||
122 | @property |
||
123 | def exhaustive(self): |
||
124 | return self[1] |
||
125 | |||
126 | def destructure(self, value): |
||
127 | if isinstance(value, DictPattern): |
||
128 | value_cant_be_smaller(self.match_dict, value.match_dict) |
||
129 | exhaustive_length_must_match(self, value.match_dict) |
||
130 | if value.match_dict: |
||
131 | first_match, *remainder = value.match_dict |
||
132 | return (DictPattern(dict(remainder)), first_match[1]) |
||
1 ignored issue
–
show
|
|||
133 | elif self.match_dict: |
||
134 | exhaustive_length_must_match(self, value) |
||
135 | first_match = self.match_dict[0] |
||
136 | try: |
||
137 | return (value, value[first_match[0]]) |
||
138 | except KeyError: |
||
139 | raise MatchFailure |
||
140 | exhaustive_length_must_match(self, value) |
||
141 | return () |
||
142 | |||
143 | |||
144 | class Bind(CompoundMatch, tuple): |
||
145 | """A wrapper that adds additional bindings to a successful match.""" |
||
146 | |||
147 | __slots__ = () |
||
148 | |||
149 | def __new__(*args, **kwargs): |
||
150 | cls, structure = args |
||
151 | not_in(kwargs, "_") |
||
152 | return super(Bind, cls).__new__(cls, (structure, tuple(kwargs.items()))) |
||
153 | |||
154 | @property |
||
155 | def structure(self): |
||
156 | return self[0] |
||
157 | |||
158 | @property |
||
159 | def bindings(self): |
||
160 | return self[1] |
||
161 | |||
162 | def destructure(self, value): |
||
163 | if value is self: |
||
164 | return [Pattern(name) for (name, _) in reversed(self.bindings)] + [ |
||
165 | self.structure |
||
166 | ] |
||
167 | return [binding_value for (_, binding_value) in reversed(self.bindings)] + [ |
||
168 | value |
||
169 | ] |
||
170 | |||
171 | |||
172 | class Guard(CompoundMatch, tuple): |
||
173 | |||
174 | __slots__ = () |
||
175 | |||
176 | def __new__(cls, guard, structure=DISCARD): |
||
177 | if structure is not DISCARD: |
||
178 | return AsGuard(guard, structure) |
||
179 | return super().__new__(cls, (guard,)) |
||
180 | |||
181 | def __getitem__(self, key): |
||
182 | return Guard(self.guard, key) |
||
183 | |||
184 | @property |
||
185 | def guard(self): |
||
186 | return tuple.__getitem__(self, 0) |
||
187 | |||
188 | def destructure(self, value): |
||
189 | if value is self or self.guard(value): |
||
190 | return () |
||
191 | raise MatchFailure |
||
192 | |||
193 | |||
194 | class AsGuard(CompoundMatch, tuple): |
||
195 | |||
196 | __slots__ = () |
||
197 | |||
198 | def __new__(cls, guard, structure): |
||
199 | return super().__new__(cls, (guard, structure)) |
||
200 | |||
201 | @property |
||
202 | def guard(self): |
||
203 | return self[0] |
||
204 | |||
205 | @property |
||
206 | def structure(self): |
||
207 | return self[1] |
||
208 | |||
209 | def destructure(self, value): |
||
210 | if value is self: |
||
211 | return (self.structure,) |
||
212 | if self.guard(value): |
||
213 | return (value,) |
||
214 | raise MatchFailure |
||
215 |