Passed
Push — master ( d2554f...13a5fa )
by Max
55s
created

structured_data.match   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 151
dl 0
loc 221
rs 7.44
c 0
b 0
f 0
wmc 52

16 Methods

Rating   Name   Duplication   Size   Complexity  
A Pattern.__matmul__() 0 2 1
A AsPattern.__new__() 0 4 2
A AttributeConstructor.__getattribute__() 0 3 1
A AsPattern.match() 0 4 1
A AsPattern.matcher() 0 4 1
A Pattern.__new__() 0 8 4
A Pattern.name() 0 4 1
A AttributeConstructor.__init__() 0 3 1
A MatchDict.__iter__() 0 2 1
A MatchDict.__setitem__() 0 6 3
A ValueMatcher.__init__() 0 3 1
A MatchDict.__len__() 0 2 1
A MatchDict.__getitem__() 0 8 5
A ValueMatcher.match() 0 7 2
A MatchDict.__init__() 0 2 1
A MatchDict.__delitem__() 0 4 2

8 Functions

Rating   Name   Duplication   Size   Complexity  
A desugar() 0 5 2
A isinstance_predicate() 0 4 1
A tuple_processor() 0 7 3
A as_pattern_processor() 0 9 2
A enum_processor() 0 4 1
A names() 0 17 5
B _match() 0 18 7
A get_processor() 0 5 3

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
import collections
2
import keyword
3
import weakref
4
5
from ._enum_constructor import EnumConstructor
6
from ._unpack import unpack
7
8
ATTRIBUTE_CONSTRUCTORS = weakref.WeakKeyDictionary()
9
ATTRIBUTE_CACHE = weakref.WeakKeyDictionary()
10
11
12
class AttributeConstructor:
13
14
    __slots__ = ('__weakref__',)
15
16
    def __init__(self, constructor):
17
        ATTRIBUTE_CONSTRUCTORS[self] = constructor
18
        ATTRIBUTE_CACHE[self] = {}
19
20
    def __getattribute__(self, name):
21
        return ATTRIBUTE_CACHE[self].setdefault(
22
            name, ATTRIBUTE_CONSTRUCTORS[self](name))
23
24
25
class MatchFailure(BaseException):
26
    """An exception that signals a failure in ADT matching."""
27
28
29
def desugar(constructor: type, instance: tuple) -> tuple:
30
    """Return the inside of an ADT instance, given its constructor."""
31
    if instance.__class__ is not constructor:
32
        raise MatchFailure
33
    return unpack(instance)
34
35
36
DISCARD = object()
37
38
39
class Pattern(tuple):
40
    """A matcher that binds a value to a name."""
41
42
    __slots__ = ()
43
44
    def __new__(cls, name: str):
45
        if name == '_':
46
            return DISCARD
47
        if not name.isidentifier():
48
            raise ValueError
49
        if keyword.iskeyword(name):
50
            raise ValueError
51
        return super().__new__(cls, (name,))
52
53
    @property
54
    def name(self):
55
        """Return the name of the matcher."""
56
        return self[0]
57
58
    def __matmul__(self, other):
59
        return AsPattern(self, other)
60
61
62
class AsPattern(tuple):
63
    """A matcher that contains further bindings."""
64
65
    __slots__ = ()
66
67
    def __new__(cls, matcher: Pattern, match):
68
        if match is DISCARD:
69
            return matcher
70
        return super().__new__(cls, (matcher, match))
71
72
    @property
73
    def matcher(self):
74
        """Return the left-hand-side of the as-match."""
75
        return self[0]
76
77
    @property
78
    def match(self):
79
        """Return the right-hand-side of the as-match."""
80
        return self[1]
81
82
83
def isinstance_predicate(typ):
84
    def predicate(target):
85
        return isinstance(target, typ)
86
    return predicate
87
88
89
def as_pattern_processor(target):
90
    def processor(value):
91
        if target is value:
92
            yield target.match
93
            yield target.matcher
94
        else:
95
            yield value
96
            yield value
97
    return processor
98
99
100
def enum_processor(target):
101
    def processor(value):
102
        yield from reversed(desugar(type(target), value))
103
    return processor
104
105
106
def tuple_processor(target):
107
    def processor(value):
108
        if isinstance(value, target.__class__) and len(target) == len(value):
109
            yield from reversed(value)
110
        else:
111
            raise MatchFailure
112
    return processor
113
114
115
PROCESSORS = (
116
    (isinstance_predicate(AsPattern), as_pattern_processor),
117
    (isinstance_predicate(EnumConstructor), enum_processor),
118
    (isinstance_predicate(tuple), tuple_processor),
119
)
120
121
122
def get_processor(processor_pairs, item):
123
    for predicate, meta in processor_pairs:
124
        if predicate(item):
125
            return meta(item)
126
    return None
127
128
129
def names(target):
130
    """Return every name bound by a target."""
131
    name_list = []
132
    names_seen = set()
133
    to_process = [target]
134
    while to_process:
135
        item = to_process.pop()
136
        if isinstance(item, Pattern):
137
            if item.name in names_seen:
138
                raise ValueError
139
            names_seen.add(item.name)
140
            name_list.append(item.name)
141
        else:
142
            processor = get_processor(PROCESSORS, item)
143
            if processor:
144
                to_process.extend(processor(item))
145
    return name_list
146
147
148
class MatchDict(collections.abc.MutableMapping):
149
150
    def __init__(self):
151
        self.data = {}
152
153
    def __getitem__(self, key):
154
        if isinstance(key, Pattern):
155
            key = key.name
156
        if isinstance(key, str):
157
            return self.data[key]
158
        if isinstance(key, tuple):
159
            return tuple(self[sub_key] for sub_key in key)
160
        raise KeyError(key)
161
162
    def __setitem__(self, key, value):
163
        if isinstance(key, Pattern):
164
            key = key.name
165
        if not isinstance(key, str):
166
            raise TypeError
167
        self.data[key] = value
168
169
    def __delitem__(self, key):
170
        if isinstance(key, Pattern):
171
            key = key.name
172
        del self.data[key]
173
174
    def __iter__(self):
175
        yield from self.data
176
177
    def __len__(self):
178
        return len(self.data)
179
180
181
def _match(target, value):
182
    match_dict = MatchDict()
183
    to_process = [(target, value)]
184
    while to_process:
185
        target, value = to_process.pop()
186
        if target is DISCARD:
187
            continue
188
        if isinstance(target, Pattern):
189
            if target.name in match_dict:
190
                raise ValueError
191
            match_dict[target.name] = value
192
            continue
193
        processor = get_processor(PROCESSORS, target)
194
        if processor:
195
            to_process.extend(zip(processor(target), processor(value)))
196
        elif target != value:
197
            raise MatchFailure
198
    return match_dict
199
200
201
class ValueMatcher:
202
    """Given a value, attempt to match against a target."""
203
204
    def __init__(self, value):
205
        self.value = value
206
        self.matches = None
207
208
    def match(self, target):
209
        """Match against target, generating a set of bindings."""
210
        try:
211
            self.matches = _match(target, self.value)
212
        except MatchFailure:
213
            self.matches = None
214
        return self.matches is not None
215
216
217
pat = AttributeConstructor(Pattern)
218
219
220
__all__ = ['MatchFailure', 'Pattern', 'ValueMatcher', 'desugar', 'names', 'pat']
221