Passed
Push — master ( 03d6e0...3c2934 )
by Max
01:14
created

structured_data.match.Descriptor._decorate()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 4
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
"""Utilities for destructuring values using matchables and match targets.
2
3
Given a value to destructure, called ``value``:
4
5
- Construct a matchable: ``matchable = Matchable(value)``
6
- The matchable is initially falsy, but it will become truthy if it is passed a
7
  **match target** that matches ``value``:
8
  ``assert matchable(some_pattern_that_matches)`` (Matchable returns itself
9
  from the call, so you can put the calls in an if-elif block, and only make a
10
  given call at most once.)
11
- When the matchable is truthy, it can be indexed to access bindings created by
12
  the target.
13
"""
14
15
from __future__ import annotations
16
17
import collections
18
import functools
19
import inspect
20
import typing
21
22
from ._attribute_constructor import AttributeConstructor
23
from ._destructure import DESTRUCTURERS
24
from ._match_failure import MatchFailure
25
from ._not_in import not_in
26
from ._patterns.basic_patterns import DISCARD
27
from ._patterns.basic_patterns import Pattern
28
from ._patterns.bind import Bind
29
from ._patterns.mapping_match import AttrPattern
30
from ._patterns.mapping_match import DictPattern
31
from ._pep_570_when import pep_570_when
32
from ._stack_iter import Action
33
from ._stack_iter import Extend
34
from ._stack_iter import Yield
35
from ._stack_iter import stack_iter
36
37
38
def names(target) -> typing.List[str]:
39
    """Return every name bound by a target."""
40
    return DESTRUCTURERS.names(target)
41
42
43
def _as_name(key):
44
    if isinstance(key, Pattern):
45
        return key.name
46
    return key
47
48
49
def _multi_index(dct, key):
50
    if isinstance(key, tuple):
51
        return tuple(dct[sub_key] for sub_key in key)
52
    if isinstance(key, dict):
53
        return {name: dct[value] for (name, value) in key.items()}
54
    raise KeyError(key)
55
56
57
class MatchDict(collections.abc.MutableMapping):
58
    """A MutableMapping that allows for retrieval into structures.
59
60
    The actual keys in the mapping must be string values. Most of the mapping
61
    methods will only operate on or yield string keys. The exception is
62
    subscription: the "key" in subscription can be a structure made of tuples
63
    and dicts. For example, ``md["a", "b"] == (md["a"], md["b"])``, and
64
    ``md[{1: "a"}] == {1: md["a"]}``. The typical use of this will be to
65
    extract many match values at once, as in ``a, b, c == md["a", "b", "c"]``.
66
67
    The behavior of most of the pre-defined MutableMapping methods is currently
68
    neither tested nor guaranteed.
69
    """
70
71
    def __init__(self) -> None:
72
        self.data: typing.Dict[str, typing.Any] = {}
73
74
    def __getitem__(self, key):
75
        key = _as_name(key)
76
        if isinstance(key, str):
77
            return self.data[key]
78
        return _multi_index(self, key)
79
80
    def __setitem__(self, key, value):
81
        key = _as_name(key)
82
        if not isinstance(key, str):
83
            raise TypeError
84
        self.data[key] = value
85
86
    def __delitem__(self, key):
87
        del self.data[_as_name(key)]
88
89
    def __iter__(self):
90
        yield from self.data
91
92
    def __len__(self):
93
        return len(self.data)
94
95
96
def _stack_iteration(item) -> typing.Optional[Action]:
97
    target, value = item
98
    if target is DISCARD:
99
        return None
100
    if isinstance(target, Pattern):
101
        return Yield(item)
102
    destructurer = DESTRUCTURERS.get_destructurer(target)
103
    if destructurer:
104
        return Extend(zip(destructurer(target), destructurer(value)))
105
    if target != value:
106
        raise MatchFailure
107
    return None
108
109
110
def _match(target, value) -> MatchDict:
111
    local_target = target
112
    local_value = value
113
    match_dict = MatchDict()
114
    for local_target, local_value in stack_iter(
115
        (local_target, local_value), _stack_iteration
116
    ):
117
        not_in(container=match_dict, item=local_target.name)
118
        match_dict[local_target.name] = local_value
119
    return match_dict
120
121
122
class Matchable:
123
    """Given a value, attempt to match against a target.
124
125
    The truthiness of ``Matchable`` values varies on whether they have bindings
126
    associated with them. They are truthy exactly when they have bindings.
127
128
    ``Matchable`` values provide two basic forms of syntactic sugar.
129
    ``m_able(target)`` is equivalent to ``m_able.match(target)``, and
130
    ``m_able[k]`` will return ``m_able.matches[k]`` if the ``Matchable`` is
131
    truthy, and raise a ``ValueError`` otherwise.
132
    """
133
134
    value: typing.Any
135
    matches: typing.Optional[MatchDict]
136
137
    def __init__(self, value: typing.Any):
138
        self.value = value
139
        self.matches = None
140
141
    def match(self, target) -> Matchable:
142
        """Match against target, generating a set of bindings."""
143
        try:
144
            self.matches = _match(target, self.value)
145
        except MatchFailure:
146
            self.matches = None
147
        return self
148
149
    def __call__(self, target) -> Matchable:
150
        return self.match(target)
151
152
    def __getitem__(self, key):
153
        if self.matches is None:
154
            raise ValueError
155
        return self.matches[key]
156
157
    def __bool__(self):
158
        return self.matches is not None
159
160
161
# In lower-case for aesthetics.
162
pat = AttributeConstructor(Pattern)  # pylint: disable=invalid-name
163
164
165
def _decorate(matchers, structure, function):
166
    matchers.append((structure, function))
167
    return function
168
169
170
class Descriptor:
171
    __wrapped__ = None
172
173
    def __new__(cls, func, *args, **kwargs):
174
        new = super().__new__(cls, *args, **kwargs)
175
        if func is None:
176
            return new
177
        return functools.wraps(func)(new)
178
179
180
class Property(Descriptor):
181
182
    fset = None
183
    fdel = None
184
185
    protected = False
186
187
    def __new__(cls, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
188
        return super().__new__(cls, func, *args, **kwargs)
189
190
    def __init__(self, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
191
        super().__init__(*args, **kwargs)
192
        self.fset = fset
193
        self.fdel = fdel
194
        if self.__doc__ is None:
195
            self.__doc__ = doc
196
        self.get_matchers = []
197
        self.set_matchers = []
198
        self.delete_matchers = []
199
        self.protected = True
200
201
    def __setattr__(self, name, value):
202
        if self.protected and name != "__doc__":
203
            raise AttributeError
204
        super().__setattr__(name, value)
205
206
    def __delattr__(self, name):
207
        if self.protected and name != "__doc__":
208
            raise AttributeError
209
        super().__delattr__(name)
210
211
    def getter(self, getter):
212
        return Property(getter, self.fset, self.fdel, self.__doc__)
213
214
    def setter(self, setter):
215
        return Property(self.__wrapped__, setter, self.fdel, self.__doc__)
216
217
    def deleter(self, deleter):
218
        return Property(self.__wrapped__, self.fset, deleter, self.__doc__)
219
220
    def __get__(self, instance, owner):
221
        if instance is None:
222
            return self
223
        matchable = Matchable(instance)
224
        for (structure, function) in self.get_matchers:
225
            if matchable(structure):
226
                return function(**matchable.matches)
227
        if self.__wrapped__ is None:
228
            raise ValueError(self)
229
        return self.__wrapped__(instance)
230
231
    def __set__(self, instance, value):
232
        matchable = Matchable((instance, value))
233
        for (structure, function) in self.set_matchers:
234
            if matchable(structure):
235
                function(**matchable.matches)
236
                return
237
        if self.fset is None:
238
            raise ValueError((instance, value))
239
        self.fset(instance, value)
240
241
    def __delete__(self, instance):
242
        matchable = Matchable(instance)
243
        for (structure, function) in self.delete_matchers:
244
            if matchable(structure):
245
                function(**matchable.matches)
246
                return
247
        if self.fdel is None:
248
            raise ValueError(instance)
249
        self.fdel(instance)
250
251
    def get_when(self, instance):
252
        structure = instance
253
        names(structure)  # Raise ValueError if there are duplicates
254
        return functools.partial(_decorate, self.get_matchers, structure)
255
256
    def set_when(self, instance, value):
257
        structure = (instance, value)
258
        names(structure)  # Raise ValueError if there are duplicates
259
        return functools.partial(_decorate, self.set_matchers, structure)
260
261
    def delete_when(self, instance):
262
        structure = instance
263
        names(structure)  # Raise ValueError if there are duplicates
264
        return functools.partial(_decorate, self.delete_matchers, structure)
265
266
267
class Function(Descriptor):
268
269
    def __init__(self, func, *args, **kwargs):
270
        super().__init__(*args, **kwargs)
271
        self.matchers = []
272
273
    def __get__(self, instance, owner):
274
        if instance is None:
275
            return self
276
        return functools.partial(self, instance)
277
278
    def __call__(*args, **kwargs):
279
        # Okay, so, this is a convoluted mess.
280
        # First, we extract self from the beginning of the argument list
281
        self, *args = args
282
        # Then we figure out what signature we're giving the outside world.
283
        signature = inspect.signature(self)
284
        # The signature lets us regularize the call and apply any defaults
285
        bound_arguments = signature.bind(*args, **kwargs)
286
        bound_arguments.apply_defaults()
287
288
        # Extract the *args and **kwargs, if any.
289
        # These are never used in the matching, just passed to the underlying function
290
        bound_args = ()
291
        bound_kwargs = {}
292
        values = bound_arguments.arguments.copy()
293
        for parameter in signature.parameters.values():
294
            if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
295
                bound_args = values.pop(parameter.name)
296
            if parameter.kind is inspect.Parameter.VAR_KEYWORD:
297
                bound_kwargs = values.pop(parameter.name)
298
299
        matchable = Matchable(values)
300
        for structure, function in self.matchers:
301
            if matchable(structure):
302
                for k, v in matchable.matches.items():
303
                    if k in bound_kwargs:
304
                        raise TypeError
305
                    bound_kwargs[k] = v
306
                function_sig = inspect.signature(function)
307
                function_args = function_sig.bind(**bound_kwargs)
308
                for parameter in function_sig.parameters.values():
309
                    if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
310
                        function_args.arguments[parameter.name] = bound_args
311
                function_args.apply_defaults()
312
                return function(*function_args.args, **function_args.kwargs)
313
        raise ValueError(values)
314
315
    @pep_570_when
316
    def when(self, kwargs):
317
        structure = DictPattern(kwargs, exhaustive=True)
318
        names(structure)  # Raise ValueError if there are duplicates
319
        return functools.partial(_decorate, self.matchers, structure)
320
321
322
# This wraps a function that, for reasons, can't be called directly by the code
323
# The function body should probably just be a docstring.
324
def function(_func=None, *, positional_until=0):
325
    def wrap(func):
326
        signature = inspect.signature(func)
327
        new_parameters = []
328
        for index, parameter in enumerate(signature.parameters.values()):
329
            if parameter.kind is inspect.Parameter.POSITIONAL_ONLY:
330
                raise ValueError("Signature already contains positional-only arguments")
331
            if index < positional_until:
332
                if parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD:
333
                    raise ValueError("Cannot overwrite non-POSITIONAL_OR_KEYWORD kind")
334
                parameter = parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY)
335
            new_parameters.append(parameter)
336
        new_signature = signature.replace(parameters=new_parameters)
337
        if new_signature != signature:
338
            func.__signature__ = new_signature
339
        return Function(func)
340
341
    if _func is None:
342
        return wrap
343
344
    return wrap(_func)
345
346
347
def decorate_in_order(*args):
348
    def decorator(function):
349
        for arg in args:
350
            function = arg(function)
351
        return function
352
353
    return decorator
354
355
356
__all__ = [
357
    "AttrPattern",
358
    "Bind",
359
    "DictPattern",
360
    "MatchDict",
361
    "Matchable",
362
    "Pattern",
363
    "Property",
364
    "decorate_in_order",
365
    "function",
366
    "names",
367
    "pat",
368
]
369