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

structured_data.match._match()   A

Complexity

Conditions 2

Size

Total Lines 10
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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