Passed
Push — master ( 2e0d62...d2554f )
by Max
48s
created

structured_data.match.names()   B

Complexity

Conditions 6

Size

Total Lines 19
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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