Passed
Push — master ( 49a75c...deac8e )
by Max
01:10
created

structured_data.match._DocWrapper.__set__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 3
dl 0
loc 2
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 functools
18
import inspect
19
import typing
20
21
from . import _attribute_constructor
22
from . import _destructure
23
from . import _doc_wrapper
24
from . import _match_dict
25
from . import _match_failure
26
from . import _pep_570_when
27
from ._match_dict import MatchDict
28
from ._patterns.basic_patterns import Pattern
29
from ._patterns.bind import Bind
30
from ._patterns.mapping_match import AttrPattern
31
from ._patterns.mapping_match import DictPattern
32
33
34
def names(target) -> typing.List[str]:
35
    """Return every name bound by a target."""
36
    return _destructure.DESTRUCTURERS.names(target)
37
38
39
class Matchable:
40
    """Given a value, attempt to match against a target.
41
42
    The truthiness of ``Matchable`` values varies on whether they have bindings
43
    associated with them. They are truthy exactly when they have bindings.
44
45
    ``Matchable`` values provide two basic forms of syntactic sugar.
46
    ``m_able(target)`` is equivalent to ``m_able.match(target)``, and
47
    ``m_able[k]`` will return ``m_able.matches[k]`` if the ``Matchable`` is
48
    truthy, and raise a ``ValueError`` otherwise.
49
    """
50
51
    value: typing.Any
52
    matches: typing.Optional[MatchDict]
53
54
    def __init__(self, value: typing.Any):
55
        self.value = value
56
        self.matches = None
57
58
    def match(self, target) -> Matchable:
59
        """Match against target, generating a set of bindings."""
60
        try:
61
            self.matches = _match_dict.match(target, self.value)
62
        except _match_failure.MatchFailure:
63
            self.matches = None
64
        return self
65
66
    def __call__(self, target) -> Matchable:
67
        return self.match(target)
68
69
    def __getitem__(self, key):
70
        if self.matches is None:
71
            raise ValueError
72
        return self.matches[key]
73
74
    def __bool__(self):
75
        return self.matches is not None
76
77
78
# In lower-case for aesthetics.
79
pat = _attribute_constructor.AttributeConstructor(  # pylint: disable=invalid-name
80
    Pattern
81
)
82
83
84
def _decorate(matchers, structure, func):
85
    matchers.append((structure, func))
86
    return func
87
88
89
class Descriptor:
90
    """Base class for decorator classes."""
91
92
    __wrapped__ = None
93
94
    def __new__(cls, func, *args, **kwargs):
95
        new = super().__new__(cls, *args, **kwargs)
96
        new.__doc__ = None
97
        if func is None:
98
            return new
99
        return functools.wraps(func)(new)
100
101
102
@_doc_wrapper.DocWrapper.wrap_class
103
class Property(Descriptor):
104
    """Decorator with value-based dispatch. Acts as a property."""
105
106
    fset = None
107
    fdel = None
108
109
    protected = False
110
111
    def __new__(cls, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
112
        del fset, fdel, doc
113
        return super().__new__(cls, func, *args, **kwargs)
114
115
    def __init__(self, func=None, fset=None, fdel=None, doc=None, *args, **kwargs):
116
        del func
117
        super().__init__(*args, **kwargs)
118
        self.fset = fset
119
        self.fdel = fdel
120
        if doc is not None:
121
            self.__doc__ = doc
122
        self.get_matchers = []
123
        self.set_matchers = []
124
        self.delete_matchers = []
125
        self.protected = True
126
127
    def __setattr__(self, name, value):
128
        if self.protected and name != "__doc__":
129
            raise AttributeError
130
        super().__setattr__(name, value)
131
132
    def __delattr__(self, name):
133
        if self.protected and name != "__doc__":
134
            raise AttributeError
135
        super().__delattr__(name)
136
137
    def getter(self, getter):
138
        """Return a copy of self with the getter replaced."""
139
        new = Property(getter, self.fset, self.fdel, self.__doc__)
140
        new.get_matchers.extend(self.get_matchers)
141
        new.set_matchers.extend(self.set_matchers)
142
        new.delete_matchers.extend(self.delete_matchers)
143
        return new
144
145
    def setter(self, setter):
146
        """Return a copy of self with the setter replaced."""
147
        new = Property(self.__wrapped__, setter, self.fdel, self.__doc__)
148
        new.get_matchers.extend(self.get_matchers)
149
        new.set_matchers.extend(self.set_matchers)
150
        new.delete_matchers.extend(self.delete_matchers)
151
        return new
152
153
    def deleter(self, deleter):
154
        """Return a copy of self with the deleter replaced."""
155
        new = Property(self.__wrapped__, self.fset, deleter, self.__doc__)
156
        new.get_matchers.extend(self.get_matchers)
157
        new.set_matchers.extend(self.set_matchers)
158
        new.delete_matchers.extend(self.delete_matchers)
159
        return new
160
161
    def __get__(self, instance, owner):
162
        if instance is None:
163
            return self
164
        matchable = Matchable(instance)
165
        for (structure, func) in self.get_matchers:
166
            if matchable(structure):
167
                return func(**matchable.matches)
168
        if self.__wrapped__ is None:
169
            raise ValueError(self)
170
        return self.__wrapped__(instance)
171
172
    def __set__(self, instance, value):
173
        matchable = Matchable((instance, value))
174
        for (structure, func) in self.set_matchers:
175
            if matchable(structure):
176
                func(**matchable.matches)
177
                return
178
        if self.fset is None:
179
            raise ValueError((instance, value))
180
        self.fset(instance, value)
181
182
    def __delete__(self, instance):
183
        matchable = Matchable(instance)
184
        for (structure, func) in self.delete_matchers:
185
            if matchable(structure):
186
                func(**matchable.matches)
187
                return
188
        if self.fdel is None:
189
            raise ValueError(instance)
190
        self.fdel(instance)
191
192
    def get_when(self, instance):
193
        """Add a binding to the getter."""
194
        structure = instance
195
        names(structure)  # Raise ValueError if there are duplicates
196
        return functools.partial(_decorate, self.get_matchers, structure)
197
198
    def set_when(self, instance, value):
199
        """Add a binding to the setter."""
200
        structure = (instance, value)
201
        names(structure)  # Raise ValueError if there are duplicates
202
        return functools.partial(_decorate, self.set_matchers, structure)
203
204
    def delete_when(self, instance):
205
        """Add a binding to the deleter."""
206
        structure = instance
207
        names(structure)  # Raise ValueError if there are duplicates
208
        return functools.partial(_decorate, self.delete_matchers, structure)
209
210
211
def _dispatch(func, matches, bound_args, bound_kwargs):
212
    for key, value in matches.items():
213
        if key in bound_kwargs:
214
            raise TypeError
215
        bound_kwargs[key] = value
216
    function_sig = inspect.signature(func)
217
    function_args = function_sig.bind(**bound_kwargs)
218
    for parameter in function_sig.parameters.values():
219
        if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
220
            function_args.arguments[parameter.name] = bound_args
221
    function_args.apply_defaults()
222
    return func(*function_args.args, **function_args.kwargs)
223
224
225
class Function(Descriptor):
226
    """Decorator with value-based dispatch. Acts as a function."""
227
228
    def __init__(self, func, *args, **kwargs):
229
        del func
230
        super().__init__(*args, **kwargs)
231
        self.matchers = []
232
233
    def __get__(self, instance, owner):
234
        if instance is None:
235
            return self
236
        return functools.partial(self, instance)
237
238
    def _bound_and_values(self, args, kwargs):
239
        # Then we figure out what signature we're giving the outside world.
240
        signature = inspect.signature(self)
241
        # The signature lets us regularize the call and apply any defaults
242
        bound_arguments = signature.bind(*args, **kwargs)
243
        bound_arguments.apply_defaults()
244
245
        # Extract the *args and **kwargs, if any.
246
        # These are never used in the matching, just passed to the underlying function
247
        bound_args = ()
248
        bound_kwargs = {}
249
        values = bound_arguments.arguments.copy()
250
        for parameter in signature.parameters.values():
251
            if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
252
                bound_args = values.pop(parameter.name)
253
            if parameter.kind is inspect.Parameter.VAR_KEYWORD:
254
                bound_kwargs = values.pop(parameter.name)
255
        return bound_args, bound_kwargs, values
256
257
    def __call__(*args, **kwargs):
258
        # Okay, so, this is a convoluted mess.
259
        # First, we extract self from the beginning of the argument list
260
        self, *args = args
261
262
        bound_args, bound_kwargs, values = self._bound_and_values(args, kwargs)
263
264
        matchable = Matchable(values)
265
        for structure, func in self.matchers:
266
            if matchable(structure):
267
                return _dispatch(func, matchable.matches, bound_args, bound_kwargs)
268
        raise ValueError(values)
269
270
    @_pep_570_when.pep_570_when
271
    def when(self, kwargs):
272
        """Add a binding for this function."""
273
        structure = DictPattern(kwargs, exhaustive=True)
274
        names(structure)  # Raise ValueError if there are duplicates
275
        return functools.partial(_decorate, self.matchers, structure)
276
277
278
def _make_args_positional(func, positional_until):
279
    signature = inspect.signature(func)
280
    new_parameters = []
281
    for index, parameter in enumerate(signature.parameters.values()):
282
        if positional_until and parameter.kind is inspect.Parameter.POSITIONAL_ONLY:
283
            raise ValueError("Signature already contains positional-only arguments")
284
        if index < positional_until:
285
            if parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD:
286
                raise ValueError("Cannot overwrite non-POSITIONAL_OR_KEYWORD kind")
287
            parameter = parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY)
288
        new_parameters.append(parameter)
289
    new_signature = signature.replace(parameters=new_parameters)
290
    if new_signature != signature:
291
        func.__signature__ = new_signature
292
293
294
# This wraps a function that, for reasons, can't be called directly by the code
295
# The function body should probably just be a docstring.
296
def function(_func=None, *, positional_until=0):
297
    """Convert a function to dispatch by value.
298
299
    The original function is not called when the dispatch function is invoked.
300
    """
301
302
    def wrap(func):
303
        _make_args_positional(func, positional_until)
304
        return Function(func)
305
306
    if _func is None:
307
        return wrap
308
309
    return wrap(_func)
310
311
312
def decorate_in_order(*args):
313
    """Apply decorators in the order they're passed to the function."""
314
315
    def decorator(func):
316
        for arg in args:
317
            func = arg(func)
318
        return func
319
320
    return decorator
321
322
323
__all__ = [
324
    "AttrPattern",
325
    "Bind",
326
    "DictPattern",
327
    "MatchDict",
328
    "Matchable",
329
    "Pattern",
330
    "Property",
331
    "decorate_in_order",
332
    "function",
333
    "names",
334
    "pat",
335
]
336