Passed
Push — master ( d3c637...032f4f )
by Max
01:19
created

structured_data.match.function()   A

Complexity

Conditions 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nop 3
dl 0
loc 13
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, func):
166
    matchers.append((structure, func))
167
    return func
168
169
170
class Descriptor:
171
    """Base class for decorator classes."""
172
    __wrapped__ = None
173
174
    def __new__(cls, func, *args, **kwargs):
175
        new = super().__new__(cls, *args, **kwargs)
176
        new.__doc__ = None
177
        if func is None:
178
            return new
179
        return functools.wraps(func)(new)
180
181
182
class _DocWrapper:
183
184
    def __init__(self, doc=None):
185
        self.doc = doc
186
187
    @classmethod
188
    def wrap_class(cls, klass):
189
        """Wrapp a classes docstring to conceal it from instances."""
190
        klass.__doc__ = cls(klass.__doc__)
191
        return klass
192
193
    def __get__(self, instance, owner):
194
        if instance is None:
195
            return self.doc
196
        return vars(instance).get("__doc__")
197
198
    def __set__(self, instance, value):
199
        vars(instance)["__doc__"] = value
200
201
    def __delete__(self, instance):
202
        vars(instance).pop("__doc__", None)
203
204
205
@_DocWrapper.wrap_class
206
class Property(Descriptor):
207
    """Decorator with value-based dispatch. Acts as a property."""
208
209
    fset = None
210
    fdel = None
211
212
    protected = False
213
214
    def __new__(cls, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
215
        del fset, fdel, doc
216
        return super().__new__(cls, func, *args, **kwargs)
217
218
    def __init__(self, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
219
        del func
220
        super().__init__(*args, **kwargs)
221
        self.fset = fset
222
        self.fdel = fdel
223
        if doc is not None:
224
            self.__doc__ = doc
225
        self.get_matchers = []
226
        self.set_matchers = []
227
        self.delete_matchers = []
228
        self.protected = True
229
230
    def __setattr__(self, name, value):
231
        if self.protected and name != "__doc__":
232
            raise AttributeError
233
        super().__setattr__(name, value)
234
235
    def __delattr__(self, name):
236
        if self.protected and name != "__doc__":
237
            raise AttributeError
238
        super().__delattr__(name)
239
240
    def getter(self, getter):
241
        """Return a copy of self with the getter replaced."""
242
        new = Property(getter, self.fset, self.fdel, self.__doc__)
243
        new.get_matchers.extend(self.get_matchers)
244
        new.set_matchers.extend(self.set_matchers)
245
        new.delete_matchers.extend(self.delete_matchers)
246
        return new
247
248
    def setter(self, setter):
249
        """Return a copy of self with the setter replaced."""
250
        new = Property(self.__wrapped__, setter, self.fdel, self.__doc__)
251
        new.get_matchers.extend(self.get_matchers)
252
        new.set_matchers.extend(self.set_matchers)
253
        new.delete_matchers.extend(self.delete_matchers)
254
        return new
255
256
    def deleter(self, deleter):
257
        """Return a copy of self with the deleter replaced."""
258
        new = Property(self.__wrapped__, self.fset, deleter, self.__doc__)
259
        new.get_matchers.extend(self.get_matchers)
260
        new.set_matchers.extend(self.set_matchers)
261
        new.delete_matchers.extend(self.delete_matchers)
262
        return new
263
264
    def __get__(self, instance, owner):
265
        if instance is None:
266
            return self
267
        matchable = Matchable(instance)
268
        for (structure, func) in self.get_matchers:
269
            if matchable(structure):
270
                return func(**matchable.matches)
271
        if self.__wrapped__ is None:
272
            raise ValueError(self)
273
        return self.__wrapped__(instance)
274
275
    def __set__(self, instance, value):
276
        matchable = Matchable((instance, value))
277
        for (structure, func) in self.set_matchers:
278
            if matchable(structure):
279
                func(**matchable.matches)
280
                return
281
        if self.fset is None:
282
            raise ValueError((instance, value))
283
        self.fset(instance, value)
284
285
    def __delete__(self, instance):
286
        matchable = Matchable(instance)
287
        for (structure, func) in self.delete_matchers:
288
            if matchable(structure):
289
                func(**matchable.matches)
290
                return
291
        if self.fdel is None:
292
            raise ValueError(instance)
293
        self.fdel(instance)
294
295
    def get_when(self, instance):
296
        """Add a binding to the getter."""
297
        structure = instance
298
        names(structure)  # Raise ValueError if there are duplicates
299
        return functools.partial(_decorate, self.get_matchers, structure)
300
301
    def set_when(self, instance, value):
302
        """Add a binding to the setter."""
303
        structure = (instance, value)
304
        names(structure)  # Raise ValueError if there are duplicates
305
        return functools.partial(_decorate, self.set_matchers, structure)
306
307
    def delete_when(self, instance):
308
        """Add a binding to the deleter."""
309
        structure = instance
310
        names(structure)  # Raise ValueError if there are duplicates
311
        return functools.partial(_decorate, self.delete_matchers, structure)
312
313
314
class Function(Descriptor):
315
    """Decorator with value-based dispatch. Acts as a function."""
316
317
    def __init__(self, func, *args, **kwargs):
318
        del func
319
        super().__init__(*args, **kwargs)
320
        self.matchers = []
321
322
    def __get__(self, instance, owner):
323
        if instance is None:
324
            return self
325
        return functools.partial(self, instance)
326
327
    def __call__(*args, **kwargs):
328
        # Okay, so, this is a convoluted mess.
329
        # First, we extract self from the beginning of the argument list
330
        self, *args = args
331
        # Then we figure out what signature we're giving the outside world.
332
        signature = inspect.signature(self)
333
        # The signature lets us regularize the call and apply any defaults
334
        bound_arguments = signature.bind(*args, **kwargs)
335
        bound_arguments.apply_defaults()
336
337
        # Extract the *args and **kwargs, if any.
338
        # These are never used in the matching, just passed to the underlying function
339
        bound_args = ()
340
        bound_kwargs = {}
341
        values = bound_arguments.arguments.copy()
342
        for parameter in signature.parameters.values():
343
            if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
344
                bound_args = values.pop(parameter.name)
345
            if parameter.kind is inspect.Parameter.VAR_KEYWORD:
346
                bound_kwargs = values.pop(parameter.name)
347
348
        matchable = Matchable(values)
349
        for structure, func in self.matchers:
350
            if matchable(structure):
351
                for key, value in matchable.matches.items():
352
                    if key in bound_kwargs:
353
                        raise TypeError
354
                    bound_kwargs[key] = value
355
                function_sig = inspect.signature(func)
356
                function_args = function_sig.bind(**bound_kwargs)
357
                for parameter in function_sig.parameters.values():
358
                    if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
359
                        function_args.arguments[parameter.name] = bound_args
360
                function_args.apply_defaults()
361
                return func(*function_args.args, **function_args.kwargs)
362
        raise ValueError(values)
363
364
    @pep_570_when
365
    def when(self, kwargs):
366
        """Add a binding for this function."""
367
        structure = DictPattern(kwargs, exhaustive=True)
368
        names(structure)  # Raise ValueError if there are duplicates
369
        return functools.partial(_decorate, self.matchers, structure)
370
371
372
def _make_args_positional(func, positional_until):
373
    signature = inspect.signature(func)
374
    new_parameters = []
375
    for index, parameter in enumerate(signature.parameters.values()):
376
        if parameter.kind is inspect.Parameter.POSITIONAL_ONLY:
377
            raise ValueError("Signature already contains positional-only arguments")
378
        if index < positional_until:
379
            if parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD:
380
                raise ValueError("Cannot overwrite non-POSITIONAL_OR_KEYWORD kind")
381
            parameter = parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY)
382
        new_parameters.append(parameter)
383
    new_signature = signature.replace(parameters=new_parameters)
384
    if new_signature != signature:
385
        func.__signature__ = new_signature
386
387
388
# This wraps a function that, for reasons, can't be called directly by the code
389
# The function body should probably just be a docstring.
390
def function(_func=None, *, positional_until=0):
391
    """Convert a function to dispatch by value.
392
393
    The original function is not called when the dispatch function is invoked.
394
    """
395
    def wrap(func):
396
        _make_args_positional(func, positional_until)
397
        return Function(func)
398
399
    if _func is None:
400
        return wrap
401
402
    return wrap(_func)
403
404
405
def decorate_in_order(*args):
406
    """Apply decorators in the order they're passed to the function."""
407
    def decorator(func):
408
        for arg in args:
409
            func = arg(func)
410
        return func
411
412
    return decorator
413
414
415
__all__ = [
416
    "AttrPattern",
417
    "Bind",
418
    "DictPattern",
419
    "MatchDict",
420
    "Matchable",
421
    "Pattern",
422
    "Property",
423
    "decorate_in_order",
424
    "function",
425
    "names",
426
    "pat",
427
]
428