Passed
Push — master ( 3c2934...90e26c )
by Max
01:17
created

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