Passed
Push — master ( 2d3bb0...8ab39c )
by Max
47s
created

structured_data.match.desugar()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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