Passed
Push — master ( 13a5fa...37eb93 )
by Max
53s
created

structured_data.match   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 224
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 152
dl 0
loc 224
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

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
B _match() 0 17 6
A not_in() 0 3 2

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
        raise KeyError(key)
165
166
    def __setitem__(self, key, value):
167
        if isinstance(key, Pattern):
168
            key = key.name
169
        if not isinstance(key, str):
170
            raise TypeError
171
        self.data[key] = value
172
173
    def __delitem__(self, key):
174
        if isinstance(key, Pattern):
175
            key = key.name
176
        del self.data[key]
177
178
    def __iter__(self):
179
        yield from self.data
180
181
    def __len__(self):
182
        return len(self.data)
183
184
185
def _match(target, value):
186
    match_dict = MatchDict()
187
    to_process = [(target, value)]
188
    while to_process:
189
        target, value = to_process.pop()
190
        if target is DISCARD:
191
            continue
192
        if isinstance(target, Pattern):
193
            not_in(match_dict, target.name)
194
            match_dict[target.name] = value
195
            continue
196
        processor = get_processor(PROCESSORS, target)
197
        if processor:
198
            to_process.extend(zip(processor(target), processor(value)))
199
        elif target != value:
200
            raise MatchFailure
201
    return match_dict
202
203
204
class ValueMatcher:
205
    """Given a value, attempt to match against a target."""
206
207
    def __init__(self, value):
208
        self.value = value
209
        self.matches = None
210
211
    def match(self, target):
212
        """Match against target, generating a set of bindings."""
213
        try:
214
            self.matches = _match(target, self.value)
215
        except MatchFailure:
216
            self.matches = None
217
        return self.matches is not None
218
219
220
pat = AttributeConstructor(Pattern)
221
222
223
__all__ = ['MatchFailure', 'Pattern', 'ValueMatcher', 'desugar', 'names', 'pat']
224