Completed
Push — master ( b59976...579f49 )
by Max
02:41
created

structured_data.match._stack_iteration()   A

Complexity

Conditions 5

Size

Total Lines 12
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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