Passed
Push — master ( c793e7...3ccb78 )
by Max
47s
created

structured_data.match   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 144
dl 0
loc 210
rs 7.44
c 0
b 0
f 0
wmc 52

9 Functions

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

14 Methods

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