Passed
Push — master ( deac8e...af9889 )
by Max
01:03
created

structured_data._descriptor   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 47
eloc 152
dl 0
loc 203
rs 8.64
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A Function.__call__() 0 12 3
A Property.deleter() 0 7 1
A Property.__new__() 0 3 1
A Property.__setattr__() 0 4 3
A Property.delete_when() 0 5 1
A Property.__init__() 0 11 2
A Function.__init__() 0 4 1
A Descriptor.__new__() 0 6 2
A Property.__get__() 0 10 5
A Function.when() 0 6 1
A Function.__get__() 0 4 2
A Property.get_when() 0 5 1
A Property.__set__() 0 9 4
A Property.__delattr__() 0 4 3
A Property.__delete__() 0 9 4
A Function._bound_and_values() 0 18 4
A Property.getter() 0 7 1
A Property.set_when() 0 5 1
A Property.setter() 0 7 1

2 Functions

Rating   Name   Duplication   Size   Complexity  
A _dispatch() 0 12 5
A _decorate() 0 3 1

How to fix   Complexity   

Complexity

Complex classes like structured_data._descriptor 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 functools
2
import inspect
3
4
from . import _destructure
5
from . import _doc_wrapper
6
from . import _matchable
7
from . import _pep_570_when
8
from ._patterns import mapping_match
9
10
11
def _decorate(matchers, structure, func):
12
    matchers.append((structure, func))
13
    return func
14
15
16
class Descriptor:
17
    """Base class for decorator classes."""
18
19
    __wrapped__ = None
20
21
    def __new__(cls, func, *args, **kwargs):
22
        new = super().__new__(cls, *args, **kwargs)
23
        new.__doc__ = None
24
        if func is None:
25
            return new
26
        return functools.wraps(func)(new)
27
28
29
@_doc_wrapper.DocWrapper.wrap_class
30
class Property(Descriptor):
31
    """Decorator with value-based dispatch. Acts as a property."""
32
33
    fset = None
34
    fdel = None
35
36
    protected = False
37
38
    def __new__(cls, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
39
        del fset, fdel, doc
40
        return super().__new__(cls, func, *args, **kwargs)
41
42
    def __init__(self, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
43
        del func
44
        super().__init__(*args, **kwargs)
45
        self.fset = fset
46
        self.fdel = fdel
47
        if doc is not None:
48
            self.__doc__ = doc
49
        self.get_matchers = []
50
        self.set_matchers = []
51
        self.delete_matchers = []
52
        self.protected = True
53
54
    def __setattr__(self, name, value):
55
        if self.protected and name != "__doc__":
56
            raise AttributeError
57
        super().__setattr__(name, value)
58
59
    def __delattr__(self, name):
60
        if self.protected and name != "__doc__":
61
            raise AttributeError
62
        super().__delattr__(name)
63
64
    def getter(self, getter):
65
        """Return a copy of self with the getter replaced."""
66
        new = Property(getter, self.fset, self.fdel, self.__doc__)
67
        new.get_matchers.extend(self.get_matchers)
68
        new.set_matchers.extend(self.set_matchers)
69
        new.delete_matchers.extend(self.delete_matchers)
70
        return new
71
72
    def setter(self, setter):
73
        """Return a copy of self with the setter replaced."""
74
        new = Property(self.__wrapped__, setter, self.fdel, self.__doc__)
75
        new.get_matchers.extend(self.get_matchers)
76
        new.set_matchers.extend(self.set_matchers)
77
        new.delete_matchers.extend(self.delete_matchers)
78
        return new
79
80
    def deleter(self, deleter):
81
        """Return a copy of self with the deleter replaced."""
82
        new = Property(self.__wrapped__, self.fset, deleter, self.__doc__)
83
        new.get_matchers.extend(self.get_matchers)
84
        new.set_matchers.extend(self.set_matchers)
85
        new.delete_matchers.extend(self.delete_matchers)
86
        return new
87
88
    def __get__(self, instance, owner):
89
        if instance is None:
90
            return self
91
        matchable = _matchable.Matchable(instance)
92
        for (structure, func) in self.get_matchers:
93
            if matchable(structure):
94
                return func(**matchable.matches)
95
        if self.__wrapped__ is None:
96
            raise ValueError(self)
97
        return self.__wrapped__(instance)
98
99
    def __set__(self, instance, value):
100
        matchable = _matchable.Matchable((instance, value))
101
        for (structure, func) in self.set_matchers:
102
            if matchable(structure):
103
                func(**matchable.matches)
104
                return
105
        if self.fset is None:
106
            raise ValueError((instance, value))
107
        self.fset(instance, value)
108
109
    def __delete__(self, instance):
110
        matchable = _matchable.Matchable(instance)
111
        for (structure, func) in self.delete_matchers:
112
            if matchable(structure):
113
                func(**matchable.matches)
114
                return
115
        if self.fdel is None:
116
            raise ValueError(instance)
117
        self.fdel(instance)
118
119
    def get_when(self, instance):
120
        """Add a binding to the getter."""
121
        structure = instance
122
        _destructure.names(structure)  # Raise ValueError if there are duplicates
123
        return functools.partial(_decorate, self.get_matchers, structure)
124
125
    def set_when(self, instance, value):
126
        """Add a binding to the setter."""
127
        structure = (instance, value)
128
        _destructure.names(structure)  # Raise ValueError if there are duplicates
129
        return functools.partial(_decorate, self.set_matchers, structure)
130
131
    def delete_when(self, instance):
132
        """Add a binding to the deleter."""
133
        structure = instance
134
        _destructure.names(structure)  # Raise ValueError if there are duplicates
135
        return functools.partial(_decorate, self.delete_matchers, structure)
136
137
138
def _dispatch(func, matches, bound_args, bound_kwargs):
139
    for key, value in matches.items():
140
        if key in bound_kwargs:
141
            raise TypeError
142
        bound_kwargs[key] = value
143
    function_sig = inspect.signature(func)
144
    function_args = function_sig.bind(**bound_kwargs)
145
    for parameter in function_sig.parameters.values():
146
        if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
147
            function_args.arguments[parameter.name] = bound_args
148
    function_args.apply_defaults()
149
    return func(*function_args.args, **function_args.kwargs)
150
151
152
class Function(Descriptor):
153
    """Decorator with value-based dispatch. Acts as a function."""
154
155
    def __init__(self, func, *args, **kwargs):
156
        del func
157
        super().__init__(*args, **kwargs)
158
        self.matchers = []
159
160
    def __get__(self, instance, owner):
161
        if instance is None:
162
            return self
163
        return functools.partial(self, instance)
164
165
    def _bound_and_values(self, args, kwargs):
166
        # Then we figure out what signature we're giving the outside world.
167
        signature = inspect.signature(self)
168
        # The signature lets us regularize the call and apply any defaults
169
        bound_arguments = signature.bind(*args, **kwargs)
170
        bound_arguments.apply_defaults()
171
172
        # Extract the *args and **kwargs, if any.
173
        # These are never used in the matching, just passed to the underlying function
174
        bound_args = ()
175
        bound_kwargs = {}
176
        values = bound_arguments.arguments.copy()
177
        for parameter in signature.parameters.values():
178
            if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
179
                bound_args = values.pop(parameter.name)
180
            if parameter.kind is inspect.Parameter.VAR_KEYWORD:
181
                bound_kwargs = values.pop(parameter.name)
182
        return bound_args, bound_kwargs, values
183
184
    def __call__(*args, **kwargs):
185
        # Okay, so, this is a convoluted mess.
186
        # First, we extract self from the beginning of the argument list
187
        self, *args = args
188
189
        bound_args, bound_kwargs, values = self._bound_and_values(args, kwargs)
190
191
        matchable = _matchable.Matchable(values)
192
        for structure, func in self.matchers:
193
            if matchable(structure):
194
                return _dispatch(func, matchable.matches, bound_args, bound_kwargs)
195
        raise ValueError(values)
196
197
    @_pep_570_when.pep_570_when
198
    def when(self, kwargs):
199
        """Add a binding for this function."""
200
        structure = mapping_match.DictPattern(kwargs, exhaustive=True)
201
        _destructure.names(structure)  # Raise ValueError if there are duplicates
202
        return functools.partial(_decorate, self.matchers, structure)
203