Passed
Push — master ( 8ea497...820800 )
by Max
01:11
created

structured_data.match._match_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 3
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
import collections
16
import operator
17
import typing
18
19
from ._attribute_constructor import AttributeConstructor
20
from ._destructure import DESTRUCTURERS
21
from ._match_failure import MatchFailure
22
from ._not_in import not_in
23
from ._patterns.basic_patterns import DISCARD
24
from ._patterns.basic_patterns import Pattern
25
from ._patterns.bind import Bind
26
from ._patterns.guard import Guard
27
from ._patterns.mapping_match import AttrPattern
28
from ._patterns.mapping_match import DictPattern
29
from ._stack_iter import Action
30
from ._stack_iter import Extend
31
from ._stack_iter import Yield
32
from ._stack_iter import stack_iter
33
34
35
def names(target) -> typing.List[str]:
36
    """Return every name bound by a target."""
37
    return DESTRUCTURERS.names(target)
38
39
40
def _as_name(key):
41
    if isinstance(key, Pattern):
42
        return key.name
43
    return key
44
45
46
def _multi_index(dct, key):
47
    if isinstance(key, tuple):
48
        return tuple(dct[sub_key] for sub_key in key)
49
    if isinstance(key, dict):
50
        return {name: dct[value] for (name, value) in key.items()}
51
    raise KeyError(key)
52
53
54
class MatchDict(collections.abc.MutableMapping):
55
    def __init__(self) -> None:
56
        self.data: typing.Dict[str, typing.Any] = {}
57
58
    def __getitem__(self, key):
59
        key = _as_name(key)
60
        if isinstance(key, str):
61
            return self.data[key]
62
        return _multi_index(self, key)
63
64
    def __setitem__(self, key, value):
65
        key = _as_name(key)
66
        if not isinstance(key, str):
67
            raise TypeError
68
        self.data[key] = value
69
70
    def __delitem__(self, key):
71
        del self.data[_as_name(key)]
72
73
    def __iter__(self):
74
        yield from self.data
75
76
    def __len__(self):
77
        return len(self.data)
78
79
80
def _stack_iteration(item) -> typing.Optional[Action]:
81
    target, value = item
82
    if target is DISCARD:
83
        return
84
    if isinstance(target, Pattern):
85
        return Yield((target, value))
86
    destructurer = DESTRUCTURERS.get_destructurer(target)
87
    if destructurer:
88
        return Extend(zip(destructurer(target), destructurer(value)))
89
    elif target != value:
90
        raise MatchFailure
91
92
93
def _match(target, value) -> MatchDict:
94
    match_dict = MatchDict()
95
    for target, value in stack_iter((target, value), _stack_iteration):
96
        not_in(match_dict, target.name)
97
        match_dict[target.name] = value
98
    return match_dict
99
100
101
class Matchable:
102
    """Given a value, attempt to match against a target."""
103
104
    def __init__(self, value):
105
        self.value = value
106
        self.matches = None
107
108
    def match(self, target) -> "Matchable":
109
        """Match against target, generating a set of bindings."""
110
        try:
111
            self.matches = _match(target, self.value)
112
        except MatchFailure:
113
            self.matches = None
114
        return self
115
116
    def __call__(self, target):
117
        return self.match(target)
118
119
    def __getitem__(self, key):
120
        if self.matches is None:
121
            raise ValueError
122
        return self.matches[key]
123
124
    def __bool__(self):
125
        return self.matches is not None
126
127
128
pat = AttributeConstructor(Pattern)
129
130
TRUTHY = Guard(operator.truth)
131
FALSY = Guard(operator.not_)
132
133
134
__all__ = [
135
    "AttrPattern",
136
    "Bind",
137
    "DictPattern",
138
    "FALSY",
139
    "Guard",
140
    "Matchable",
141
    "Pattern",
142
    "TRUTHY",
143
    "names",
144
    "pat",
145
]
146