Passed
Push — master ( d2554f...13a5fa )
by Max
55s
created

structured_data.match.names()   A

Complexity

Conditions 5

Size

Total Lines 17
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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