Passed
Push — master ( c3ccf9...0a4dd5 )
by Max
50s
created

structured_data.match   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 154
dl 0
loc 226
rs 6.4799
c 0
b 0
f 0
wmc 54

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.__init__() 0 2 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
B MatchDict.__getitem__() 0 10 7
A ValueMatcher.match() 0 7 2
A MatchDict.__delitem__() 0 4 2

9 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 get_processor() 0 5 3
A names() 0 16 4
A not_in() 0 3 2
B _match() 0 17 6

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 not_in(container, name):
130
    if name in container:
131
        raise ValueError
132
133
134
def names(target):
135
    """Return every name bound by a target."""
136
    name_list = []
137
    names_seen = set()
138
    to_process = [target]
139
    while to_process:
140
        item = to_process.pop()
141
        if isinstance(item, Pattern):
142
            not_in(names_seen, item.name)
143
            names_seen.add(item.name)
144
            name_list.append(item.name)
145
        else:
146
            processor = get_processor(PROCESSORS, item)
147
            if processor:
148
                to_process.extend(processor(item))
149
    return name_list
150
151
152
class MatchDict(collections.abc.MutableMapping):
153
154
    def __init__(self):
155
        self.data = {}
156
157
    def __getitem__(self, key):
158
        if isinstance(key, Pattern):
159
            key = key.name
160
        if isinstance(key, str):
161
            return self.data[key]
162
        if isinstance(key, tuple):
163
            return tuple(self[sub_key] for sub_key in key)
164
        if isinstance(key, dict):
165
            return {name: self[value] for (name, value) in key.items()}
166
        raise KeyError(key)
167
168
    def __setitem__(self, key, value):
169
        if isinstance(key, Pattern):
170
            key = key.name
171
        if not isinstance(key, str):
172
            raise TypeError
173
        self.data[key] = value
174
175
    def __delitem__(self, key):
176
        if isinstance(key, Pattern):
177
            key = key.name
178
        del self.data[key]
179
180
    def __iter__(self):
181
        yield from self.data
182
183
    def __len__(self):
184
        return len(self.data)
185
186
187
def _match(target, value):
188
    match_dict = MatchDict()
189
    to_process = [(target, value)]
190
    while to_process:
191
        target, value = to_process.pop()
192
        if target is DISCARD:
193
            continue
194
        if isinstance(target, Pattern):
195
            not_in(match_dict, target.name)
196
            match_dict[target.name] = value
197
            continue
198
        processor = get_processor(PROCESSORS, target)
199
        if processor:
200
            to_process.extend(zip(processor(target), processor(value)))
201
        elif target != value:
202
            raise MatchFailure
203
    return match_dict
204
205
206
class ValueMatcher:
207
    """Given a value, attempt to match against a target."""
208
209
    def __init__(self, value):
210
        self.value = value
211
        self.matches = None
212
213
    def match(self, target):
214
        """Match against target, generating a set of bindings."""
215
        try:
216
            self.matches = _match(target, self.value)
217
        except MatchFailure:
218
            self.matches = None
219
        return self.matches is not None
220
221
222
pat = AttributeConstructor(Pattern)
223
224
225
__all__ = ['MatchFailure', 'Pattern', 'ValueMatcher', 'desugar', 'names', 'pat']
226