Passed
Push — master ( f6a8a8...2149f7 )
by Max
45s
created

structured_data.match   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 161
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 116
dl 0
loc 161
rs 9.1199
c 0
b 0
f 0
wmc 41

8 Functions

Rating   Name   Duplication   Size   Complexity  
A names() 0 16 4
A _match() 0 6 2
A tuple_processor() 0 7 3
A _as_name() 0 4 2
A as_pattern_processor() 0 9 2
A not_in() 0 3 2
A enum_processor() 0 6 2
A _match_iteration() 0 12 5

10 Methods

Rating   Name   Duplication   Size   Complexity  
A MatchDict.__iter__() 0 2 1
A ProcessorList.__init__() 0 2 1
A MatchDict.__setitem__() 0 5 2
A ValueMatcher.__init__() 0 3 1
A MatchDict.__len__() 0 2 1
B MatchDict.__getitem__() 0 9 6
A ValueMatcher.match() 0 7 2
A MatchDict.__init__() 0 2 1
A MatchDict.__delitem__() 0 2 1
A ProcessorList.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
3
from ._attribute_constructor import AttributeConstructor
4
from ._enum_constructor import EnumConstructor
5
from ._match_failure import MatchFailure
6
from ._patterns import DISCARD
7
from ._patterns import AsPattern
8
from ._patterns import Pattern
9
from ._unpack import unpack
10
11
12
def as_pattern_processor(target):
13
    def processor(value):
14
        if target is value:
15
            yield target.match
16
            yield target.matcher
17
        else:
18
            yield value
19
            yield value
20
    return processor
21
22
23
def enum_processor(target):
24
    def processor(value):
25
        if value.__class__ is not target.__class__:
26
            raise MatchFailure
27
        yield from reversed(unpack(value))
28
    return processor
29
30
31
def tuple_processor(target):
32
    def processor(value):
33
        if isinstance(value, target.__class__) and len(target) == len(value):
34
            yield from reversed(value)
35
        else:
36
            raise MatchFailure
37
    return processor
38
39
40
class ProcessorList:
41
42
    def __init__(self, processors=()):
43
        self.processors = list(processors)
44
45
    def get_processor(self, item):
46
        for typ, meta_processor in self.processors:
47
            if isinstance(item, typ):
48
                return meta_processor(item)
49
        return None
50
51
52
PROCESSORS = ProcessorList((
53
    (AsPattern, as_pattern_processor),
54
    (EnumConstructor, enum_processor),
55
    (tuple, tuple_processor),
56
))
57
58
59
def not_in(container, name):
60
    if name in container:
61
        raise ValueError
62
63
64
def names(target):
65
    """Return every name bound by a target."""
66
    name_list = []
67
    names_seen = set()
68
    to_process = [target]
69
    while to_process:
70
        item = to_process.pop()
71
        if isinstance(item, Pattern):
72
            not_in(names_seen, item.name)
73
            names_seen.add(item.name)
74
            name_list.append(item.name)
75
        else:
76
            processor = PROCESSORS.get_processor(item)
77
            if processor:
78
                to_process.extend(processor(item))
79
    return name_list
80
81
82
def _as_name(key):
83
    if isinstance(key, Pattern):
84
        return key.name
85
    return key
86
87
88
class MatchDict(collections.abc.MutableMapping):
89
90
    def __init__(self):
91
        self.data = {}
92
93
    def __getitem__(self, key):
94
        key = _as_name(key)
95
        if isinstance(key, str):
96
            return self.data[key]
97
        if isinstance(key, tuple):
98
            return tuple(self[sub_key] for sub_key in key)
99
        if isinstance(key, dict):
100
            return {name: self[value] for (name, value) in key.items()}
101
        raise KeyError(key)
102
103
    def __setitem__(self, key, value):
104
        key = _as_name(key)
105
        if not isinstance(key, str):
106
            raise TypeError
107
        self.data[key] = value
108
109
    def __delitem__(self, key):
110
        del self.data[_as_name(key)]
111
112
    def __iter__(self):
113
        yield from self.data
114
115
    def __len__(self):
116
        return len(self.data)
117
118
119
def _match_iteration(match_dict, target, value):
120
    if target is DISCARD:
121
        return
122
    if isinstance(target, Pattern):
123
        not_in(match_dict, target.name)
124
        match_dict[target.name] = value
125
        return
126
    processor = PROCESSORS.get_processor(target)
127
    if processor:
128
        yield from zip(processor(target), processor(value))
129
    elif target != value:
130
        raise MatchFailure
131
132
133
def _match(target, value):
134
    match_dict = MatchDict()
135
    to_process = [(target, value)]
136
    while to_process:
137
        to_process.extend(_match_iteration(match_dict, *to_process.pop()))
138
    return match_dict
139
140
141
class ValueMatcher:
142
    """Given a value, attempt to match against a target."""
143
144
    def __init__(self, value):
145
        self.value = value
146
        self.matches = None
147
148
    def match(self, target):
149
        """Match against target, generating a set of bindings."""
150
        try:
151
            self.matches = _match(target, self.value)
152
        except MatchFailure:
153
            self.matches = None
154
        return self.matches is not None
155
156
157
pat = AttributeConstructor(Pattern)
158
159
160
__all__ = ['Pattern', 'ValueMatcher', 'names', 'pat']
161