Passed
Push — master ( 83a312...2d3bb0 )
by Max
44s
created

structured_data.match.ProcessorList.__init__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 as_pattern_processor(target):
68
    def processor(value):
69
        if target is value:
70
            yield target.match
71
            yield target.matcher
72
        else:
73
            yield value
74
            yield value
75
    return processor
76
77
78
def enum_processor(target):
79
    def processor(value):
80
        yield from reversed(desugar(type(target), value))
81
    return processor
82
83
84
def tuple_processor(target):
85
    def processor(value):
86
        if isinstance(value, target.__class__) and len(target) == len(value):
87
            yield from reversed(value)
88
        else:
89
            raise MatchFailure
90
    return processor
91
92
93
class ProcessorList:
94
95
    def __init__(self, processors=()):
96
        self.processors = list(processors)
97
98
    def get_processor(self, item):
99
        for typ, meta_processor in self.processors:
100
            if isinstance(item, typ):
101
                return meta_processor(item)
102
        return None
103
104
105
PROCESSORS = ProcessorList((
106
    (AsPattern, as_pattern_processor),
107
    (EnumConstructor, enum_processor),
108
    (tuple, tuple_processor),
109
))
110
111
112
def not_in(container, name):
113
    if name in container:
114
        raise ValueError
115
116
117
def names(target):
118
    """Return every name bound by a target."""
119
    name_list = []
120
    names_seen = set()
121
    to_process = [target]
122
    while to_process:
123
        item = to_process.pop()
124
        if isinstance(item, Pattern):
125
            not_in(names_seen, item.name)
126
            names_seen.add(item.name)
127
            name_list.append(item.name)
128
        else:
129
            processor = PROCESSORS.get_processor(item)
130
            if processor:
131
                to_process.extend(processor(item))
132
    return name_list
133
134
135
def _as_name(key):
136
    if isinstance(key, Pattern):
137
        return key.name
138
    return key
139
140
141
class MatchDict(collections.abc.MutableMapping):
142
143
    def __init__(self):
144
        self.data = {}
145
146
    def __getitem__(self, key):
147
        key = _as_name(key)
148
        if isinstance(key, str):
149
            return self.data[key]
150
        if isinstance(key, tuple):
151
            return tuple(self[sub_key] for sub_key in key)
152
        if isinstance(key, dict):
153
            return {name: self[value] for (name, value) in key.items()}
154
        raise KeyError(key)
155
156
    def __setitem__(self, key, value):
157
        key = _as_name(key)
158
        if not isinstance(key, str):
159
            raise TypeError
160
        self.data[key] = value
161
162
    def __delitem__(self, key):
163
        del self.data[_as_name(key)]
164
165
    def __iter__(self):
166
        yield from self.data
167
168
    def __len__(self):
169
        return len(self.data)
170
171
172
def _match(target, value):
173
    match_dict = MatchDict()
174
    to_process = [(target, value)]
175
    while to_process:
176
        target, value = to_process.pop()
177
        if target is DISCARD:
178
            continue
179
        if isinstance(target, Pattern):
180
            not_in(match_dict, target.name)
181
            match_dict[target.name] = value
182
            continue
183
        processor = PROCESSORS.get_processor(target)
184
        if processor:
185
            to_process.extend(zip(processor(target), processor(value)))
186
        elif target != value:
187
            raise MatchFailure
188
    return match_dict
189
190
191
class ValueMatcher:
192
    """Given a value, attempt to match against a target."""
193
194
    def __init__(self, value):
195
        self.value = value
196
        self.matches = None
197
198
    def match(self, target):
199
        """Match against target, generating a set of bindings."""
200
        try:
201
            self.matches = _match(target, self.value)
202
        except MatchFailure:
203
            self.matches = None
204
        return self.matches is not None
205
206
207
pat = AttributeConstructor(Pattern)
208
209
210
__all__ = ['Pattern', 'ValueMatcher', 'desugar', 'names', 'pat']
211