Passed
Push — master ( bd0479...03d6e0 )
by Max
01:17
created

structured_data.match   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 252
dl 0
loc 367
rs 2
c 0
b 0
f 0
wmc 82

7 Functions

Rating   Name   Duplication   Size   Complexity  
A names() 0 3 1
A _match() 0 10 2
A _multi_index() 0 6 3
A _stack_iteration() 0 12 5
A _as_name() 0 4 2
B function() 0 21 7
A decorate_in_order() 0 7 2

30 Methods

Rating   Name   Duplication   Size   Complexity  
A Property.__setattr__() 0 4 3
A MatchDict.__iter__() 0 2 1
A Property.get_when() 0 4 1
A MatchDict.__setitem__() 0 5 2
A Property.getter() 0 2 1
A Property.__new__() 0 2 1
A Property.__get__() 0 10 5
A Matchable.match() 0 7 2
A Property.deleter() 0 2 1
A Property.__set__() 0 9 4
A Property.setter() 0 2 1
A Matchable.__getitem__() 0 4 2
A Descriptor._decorate() 0 3 1
A Property.__init__() 0 10 2
A Descriptor.__new__() 0 5 2
A Function.__get__() 0 4 2
A Property.__delattr__() 0 4 3
A Matchable.__bool__() 0 2 1
A MatchDict.__len__() 0 2 1
A Property.delete_when() 0 4 1
A MatchDict.__getitem__() 0 5 2
A Matchable.__call__() 0 2 1
A Property.__delete__() 0 9 4
A Property.set_when() 0 4 1
A MatchDict.__init__() 0 2 1
A Function.when() 0 5 1
A Function.__init__() 0 3 1
A Matchable.__init__() 0 3 1
C Function.__call__() 0 36 10
A MatchDict.__delitem__() 0 2 1

How to fix   Complexity   

Complexity

Complex classes like structured_data.match 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
"""Utilities for destructuring values using matchables and match targets.
2
3
Given a value to destructure, called ``value``:
4
5
- Construct a matchable: ``matchable = Matchable(value)``
6
- The matchable is initially falsy, but it will become truthy if it is passed a
7
  **match target** that matches ``value``:
8
  ``assert matchable(some_pattern_that_matches)`` (Matchable returns itself
9
  from the call, so you can put the calls in an if-elif block, and only make a
10
  given call at most once.)
11
- When the matchable is truthy, it can be indexed to access bindings created by
12
  the target.
13
"""
14
15
from __future__ import annotations
16
17
import collections
18
import functools
19
import inspect
20
import typing
21
22
from ._attribute_constructor import AttributeConstructor
23
from ._destructure import DESTRUCTURERS
24
from ._match_failure import MatchFailure
25
from ._not_in import not_in
26
from ._patterns.basic_patterns import DISCARD
27
from ._patterns.basic_patterns import Pattern
28
from ._patterns.bind import Bind
29
from ._patterns.mapping_match import AttrPattern
30
from ._patterns.mapping_match import DictPattern
31
from ._pep_570_when import pep_570_when
32
from ._stack_iter import Action
33
from ._stack_iter import Extend
34
from ._stack_iter import Yield
35
from ._stack_iter import stack_iter
36
37
38
def names(target) -> typing.List[str]:
39
    """Return every name bound by a target."""
40
    return DESTRUCTURERS.names(target)
41
42
43
def _as_name(key):
44
    if isinstance(key, Pattern):
45
        return key.name
46
    return key
47
48
49
def _multi_index(dct, key):
50
    if isinstance(key, tuple):
51
        return tuple(dct[sub_key] for sub_key in key)
52
    if isinstance(key, dict):
53
        return {name: dct[value] for (name, value) in key.items()}
54
    raise KeyError(key)
55
56
57
class MatchDict(collections.abc.MutableMapping):
58
    """A MutableMapping that allows for retrieval into structures.
59
60
    The actual keys in the mapping must be string values. Most of the mapping
61
    methods will only operate on or yield string keys. The exception is
62
    subscription: the "key" in subscription can be a structure made of tuples
63
    and dicts. For example, ``md["a", "b"] == (md["a"], md["b"])``, and
64
    ``md[{1: "a"}] == {1: md["a"]}``. The typical use of this will be to
65
    extract many match values at once, as in ``a, b, c == md["a", "b", "c"]``.
66
67
    The behavior of most of the pre-defined MutableMapping methods is currently
68
    neither tested nor guaranteed.
69
    """
70
71
    def __init__(self) -> None:
72
        self.data: typing.Dict[str, typing.Any] = {}
73
74
    def __getitem__(self, key):
75
        key = _as_name(key)
76
        if isinstance(key, str):
77
            return self.data[key]
78
        return _multi_index(self, key)
79
80
    def __setitem__(self, key, value):
81
        key = _as_name(key)
82
        if not isinstance(key, str):
83
            raise TypeError
84
        self.data[key] = value
85
86
    def __delitem__(self, key):
87
        del self.data[_as_name(key)]
88
89
    def __iter__(self):
90
        yield from self.data
91
92
    def __len__(self):
93
        return len(self.data)
94
95
96
def _stack_iteration(item) -> typing.Optional[Action]:
97
    target, value = item
98
    if target is DISCARD:
99
        return None
100
    if isinstance(target, Pattern):
101
        return Yield(item)
102
    destructurer = DESTRUCTURERS.get_destructurer(target)
103
    if destructurer:
104
        return Extend(zip(destructurer(target), destructurer(value)))
105
    if target != value:
106
        raise MatchFailure
107
    return None
108
109
110
def _match(target, value) -> MatchDict:
111
    local_target = target
112
    local_value = value
113
    match_dict = MatchDict()
114
    for local_target, local_value in stack_iter(
115
        (local_target, local_value), _stack_iteration
116
    ):
117
        not_in(container=match_dict, item=local_target.name)
118
        match_dict[local_target.name] = local_value
119
    return match_dict
120
121
122
class Matchable:
123
    """Given a value, attempt to match against a target.
124
125
    The truthiness of ``Matchable`` values varies on whether they have bindings
126
    associated with them. They are truthy exactly when they have bindings.
127
128
    ``Matchable`` values provide two basic forms of syntactic sugar.
129
    ``m_able(target)`` is equivalent to ``m_able.match(target)``, and
130
    ``m_able[k]`` will return ``m_able.matches[k]`` if the ``Matchable`` is
131
    truthy, and raise a ``ValueError`` otherwise.
132
    """
133
134
    value: typing.Any
135
    matches: typing.Optional[MatchDict]
136
137
    def __init__(self, value: typing.Any):
138
        self.value = value
139
        self.matches = None
140
141
    def match(self, target) -> Matchable:
142
        """Match against target, generating a set of bindings."""
143
        try:
144
            self.matches = _match(target, self.value)
145
        except MatchFailure:
146
            self.matches = None
147
        return self
148
149
    def __call__(self, target) -> Matchable:
150
        return self.match(target)
151
152
    def __getitem__(self, key):
153
        if self.matches is None:
154
            raise ValueError
155
        return self.matches[key]
156
157
    def __bool__(self):
158
        return self.matches is not None
159
160
161
# In lower-case for aesthetics.
162
pat = AttributeConstructor(Pattern)  # pylint: disable=invalid-name
163
164
165
class Descriptor:
166
    __wrapped__ = None
167
168
    def __new__(cls, func, *args, **kwargs):
169
        new = super().__new__(cls, *args, **kwargs)
170
        if func is None:
171
            return new
172
        return functools.wraps(func)(new)
173
174
    def _decorate(self, matchers, structure, function):
175
        matchers.append((structure, function))
176
        return function
177
178
179
class Property(Descriptor):
180
181
    fset = None
182
    fdel = None
183
184
    protected = False
185
186
    def __new__(cls, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
187
        return super().__new__(cls, func, *args, **kwargs)
188
189
    def __init__(self, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
190
        super().__init__(*args, **kwargs)
191
        self.fset = fset
192
        self.fdel = fdel
193
        if self.__doc__ is None:
194
            self.__doc__ = doc
195
        self.get_matchers = []
196
        self.set_matchers = []
197
        self.delete_matchers = []
198
        self.protected = True
199
200
    def __setattr__(self, name, value):
201
        if self.protected and name != "__doc__":
202
            raise AttributeError
203
        super().__setattr__(name, value)
204
205
    def __delattr__(self, name):
206
        if self.protected and name != "__doc__":
207
            raise AttributeError
208
        super().__delattr__(name)
209
210
    def getter(self, getter):
211
        return Property(getter, self.fset, self.fdel, self.__doc__)
212
213
    def setter(self, setter):
214
        return Property(self.__wrapped__, setter, self.fdel, self.__doc__)
215
216
    def deleter(self, deleter):
217
        return Property(self.__wrapped__, self.fset, deleter, self.__doc__)
218
219
    def __get__(self, instance, owner):
220
        if instance is None:
221
            return self
222
        matchable = Matchable(instance)
223
        for (structure, function) in self.get_matchers:
224
            if matchable(structure):
225
                return function(**matchable.matches)
226
        if self.__wrapped__ is None:
227
            raise ValueError(self)
228
        return self.__wrapped__(instance)
229
230
    def __set__(self, instance, value):
231
        matchable = Matchable((instance, value))
232
        for (structure, function) in self.set_matchers:
233
            if matchable(structure):
234
                function(**matchable.matches)
235
                return
236
        if self.fset is None:
237
            raise ValueError((instance, value))
238
        self.fset(instance, value)
239
240
    def __delete__(self, instance):
241
        matchable = Matchable(instance)
242
        for (structure, function) in self.delete_matchers:
243
            if matchable(structure):
244
                function(**matchable.matches)
245
                return
246
        if self.fdel is None:
247
            raise ValueError(instance)
248
        self.fdel(instance)
249
250
    def get_when(self, instance):
251
        structure = instance
252
        names(structure)  # Raise ValueError if there are duplicates
253
        return functools.partial(self._decorate, self.get_matchers, structure)
254
255
    def set_when(self, instance, value):
256
        structure = (instance, value)
257
        names(structure)  # Raise ValueError if there are duplicates
258
        return functools.partial(self._decorate, self.set_matchers, structure)
259
260
    def delete_when(self, instance):
261
        structure = instance
262
        names(structure)  # Raise ValueError if there are duplicates
263
        return functools.partial(self._decorate, self.delete_matchers, structure)
264
265
266
class Function(Descriptor):
267
268
    def __init__(self, func, *args, **kwargs):
269
        super().__init__(*args, **kwargs)
270
        self.matchers = []
271
272
    def __get__(self, instance, owner):
273
        if instance is None:
274
            return self
275
        return functools.partial(self, instance)
276
277
    def __call__(*args, **kwargs):
278
        # Okay, so, this is a convoluted mess.
279
        # First, we extract self from the beginning of the argument list
280
        self, *args = args
281
        # Then we figure out what signature we're giving the outside world.
282
        signature = inspect.signature(self)
283
        # The signature lets us regularize the call and apply any defaults
284
        bound_arguments = signature.bind(*args, **kwargs)
285
        bound_arguments.apply_defaults()
286
287
        # Extract the *args and **kwargs, if any.
288
        # These are never used in the matching, just passed to the underlying function
289
        bound_args = ()
290
        bound_kwargs = {}
291
        values = bound_arguments.arguments.copy()
292
        for parameter in signature.parameters.values():
293
            if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
294
                bound_args = values.pop(parameter.name)
295
            if parameter.kind is inspect.Parameter.VAR_KEYWORD:
296
                bound_kwargs = values.pop(parameter.name)
297
298
        matchable = Matchable(values)
299
        for structure, function in self.matchers:
300
            if matchable(structure):
301
                for k, v in matchable.matches.items():
302
                    if k in bound_kwargs:
303
                        raise TypeError
304
                    bound_kwargs[k] = v
305
                function_sig = inspect.signature(function)
306
                function_args = function_sig.bind(**bound_kwargs)
307
                for parameter in function_sig.parameters.values():
308
                    if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
309
                        function_args.arguments[parameter.name] = bound_args
310
                function_args.apply_defaults()
311
                return function(*function_args.args, **function_args.kwargs)
312
        raise ValueError(values)
313
314
    @pep_570_when
315
    def when(self, kwargs):
316
        structure = DictPattern(kwargs, exhaustive=True)
317
        names(structure)  # Raise ValueError if there are duplicates
318
        return functools.partial(self._decorate, self.matchers, structure)
319
320
321
# This wraps a function that, for reasons, can't be called directly by the code
322
# The function body should probably just be a docstring.
323
def function(_func=None, *, positional_until=0):
324
    def wrap(func):
325
        signature = inspect.signature(func)
326
        new_parameters = []
327
        for index, parameter in enumerate(signature.parameters.values()):
328
            if parameter.kind is inspect.Parameter.POSITIONAL_ONLY:
329
                raise ValueError("Signature already contains positional-only arguments")
330
            if index < positional_until:
331
                if parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD:
332
                    raise ValueError("Cannot overwrite non-POSITIONAL_OR_KEYWORD kind")
333
                parameter = parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY)
334
            new_parameters.append(parameter)
335
        new_signature = signature.replace(parameters=new_parameters)
336
        if new_signature != signature:
337
            func.__signature__ = new_signature
338
        return Function(func)
339
340
    if _func is None:
341
        return wrap
342
343
    return wrap(_func)
344
345
346
def decorate_in_order(*args):
347
    def decorator(function):
348
        for arg in args:
349
            function = arg(function)
350
        return function
351
352
    return decorator
353
354
355
__all__ = [
356
    "AttrPattern",
357
    "Bind",
358
    "DictPattern",
359
    "MatchDict",
360
    "Matchable",
361
    "Pattern",
362
    "Property",
363
    "decorate_in_order",
364
    "function",
365
    "names",
366
    "pat",
367
]
368