Passed
Push — master ( 7b43af...2895d4 )
by Max
01:15
created

structured_data.match.Property.getter()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nop 2
dl 0
loc 7
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
def _dispatch(func, matches, bound_args, bound_kwargs, values):
315
    for key, value in matches.items():
316
        if key in bound_kwargs:
317
            raise TypeError
318
        bound_kwargs[key] = value
319
    function_sig = inspect.signature(func)
320
    function_args = function_sig.bind(**bound_kwargs)
321
    for parameter in function_sig.parameters.values():
322
        if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
323
            function_args.arguments[parameter.name] = bound_args
324
    function_args.apply_defaults()
325
    return func(*function_args.args, **function_args.kwargs)
326
327
328
class Function(Descriptor):
329
    """Decorator with value-based dispatch. Acts as a function."""
330
331
    def __init__(self, func, *args, **kwargs):
332
        del func
333
        super().__init__(*args, **kwargs)
334
        self.matchers = []
335
336
    def __get__(self, instance, owner):
337
        if instance is None:
338
            return self
339
        return functools.partial(self, instance)
340
341
    def _bound_and_values(self, args, kwargs):
342
        # Then we figure out what signature we're giving the outside world.
343
        signature = inspect.signature(self)
344
        # The signature lets us regularize the call and apply any defaults
345
        bound_arguments = signature.bind(*args, **kwargs)
346
        bound_arguments.apply_defaults()
347
348
        # Extract the *args and **kwargs, if any.
349
        # These are never used in the matching, just passed to the underlying function
350
        bound_args = ()
351
        bound_kwargs = {}
352
        values = bound_arguments.arguments.copy()
353
        for parameter in signature.parameters.values():
354
            if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
355
                bound_args = values.pop(parameter.name)
356
            if parameter.kind is inspect.Parameter.VAR_KEYWORD:
357
                bound_kwargs = values.pop(parameter.name)
358
        return bound_args, bound_kwargs, values
359
360
    def __call__(*args, **kwargs):
361
        # Okay, so, this is a convoluted mess.
362
        # First, we extract self from the beginning of the argument list
363
        self, *args = args
364
365
        bound_args, bound_kwargs, values = self._bound_and_values(args, kwargs)
366
367
        matchable = Matchable(values)
368
        for structure, func in self.matchers:
369
            if matchable(structure):
370
                return _dispatch(func, matchable.matches, bound_args, bound_kwargs, values)
371
        raise ValueError(values)
372
373
    @pep_570_when
374
    def when(self, kwargs):
375
        """Add a binding for this function."""
376
        structure = DictPattern(kwargs, exhaustive=True)
377
        names(structure)  # Raise ValueError if there are duplicates
378
        return functools.partial(_decorate, self.matchers, structure)
379
380
381
def _make_args_positional(func, positional_until):
382
    signature = inspect.signature(func)
383
    new_parameters = []
384
    for index, parameter in enumerate(signature.parameters.values()):
385
        if positional_until and parameter.kind is inspect.Parameter.POSITIONAL_ONLY:
386
            raise ValueError("Signature already contains positional-only arguments")
387
        if index < positional_until:
388
            if parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD:
389
                raise ValueError("Cannot overwrite non-POSITIONAL_OR_KEYWORD kind")
390
            parameter = parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY)
391
        new_parameters.append(parameter)
392
    new_signature = signature.replace(parameters=new_parameters)
393
    if new_signature != signature:
394
        func.__signature__ = new_signature
395
396
397
# This wraps a function that, for reasons, can't be called directly by the code
398
# The function body should probably just be a docstring.
399
def function(_func=None, *, positional_until=0):
400
    """Convert a function to dispatch by value.
401
402
    The original function is not called when the dispatch function is invoked.
403
    """
404
    def wrap(func):
405
        _make_args_positional(func, positional_until)
406
        return Function(func)
407
408
    if _func is None:
409
        return wrap
410
411
    return wrap(_func)
412
413
414
def decorate_in_order(*args):
415
    """Apply decorators in the order they're passed to the function."""
416
    def decorator(func):
417
        for arg in args:
418
            func = arg(func)
419
        return func
420
421
    return decorator
422
423
424
__all__ = [
425
    "AttrPattern",
426
    "Bind",
427
    "DictPattern",
428
    "MatchDict",
429
    "Matchable",
430
    "Pattern",
431
    "Property",
432
    "decorate_in_order",
433
    "function",
434
    "names",
435
    "pat",
436
]
437