Passed
Push — master ( bcf14c...4a1f1b )
by Max
57s
created

structured_data._match.destructure   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 150
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 66
dl 0
loc 150
rs 10
c 0
b 0
f 0
wmc 23

12 Methods

Rating   Name   Duplication   Size   Complexity  
A ADTDestructurer.destructure() 0 5 2
A TupleDestructurer.destructure() 0 14 4
A DestructurerList.get_destructurer() 0 19 4
A DestructurerList.custom() 0 7 1
A DestructurerList.stack_iteration() 0 5 2
A DestructurerList.destructure() 0 5 2
A Destructurer.__call__() 0 2 1
A DestructurerList.__new__() 0 2 1
A DestructurerList.names() 0 10 2
A Destructurer.__init__() 0 2 1
A Destructurer.__init_subclass__() 0 4 1
A Destructurer.destructure() 0 3 1

1 Function

Rating   Name   Duplication   Size   Complexity  
A names() 0 3 1
1
"""Classes for destructuring complex data."""
2
3
import typing
4
5
from .. import _stack_iter
6
from .._adt.constructor import ADTConstructor
7
from .._not_in import not_in
8
from .._unpack import unpack
9
from .match_failure import MatchFailure
10
from .patterns.basic_patterns import Pattern
11
from .patterns.compound_match import CompoundMatch
12
13
14
class Destructurer:
15
    """Abstract base class for destructuring third-party code."""
16
17
    type: typing.ClassVar[type]
18
19
    def __init_subclass__(cls, **kwargs) -> None:
20
        type_: type = kwargs.pop("type")
21
        super().__init_subclass__(**kwargs)  # type: ignore
22
        cls.type = type_
23
24
    def __init__(self, target):
25
        self.target = target
26
27
    def __call__(self, value):
28
        return self.destructure(value)
29
30
    def destructure(self, value):
31
        """Return a sequence of subvalues, or raise MatchFailure."""
32
        raise NotImplementedError
33
34
35
class ADTDestructurer(Destructurer, type=ADTConstructor):
36
    """Unpack ADT instances into a sequence of values.
37
38
    While all ADT instances are tuples in practice, this is ignored.
39
    """
40
41
    def destructure(self, value):
42
        """Unpack a value into a sequence of instances if the classes match."""
43
        if value.__class__ is not self.target.__class__:
44
            raise MatchFailure
45
        return reversed(unpack(value))
46
47
48
class TupleDestructurer(Destructurer, type=tuple):
49
    """Unpack tuples into a sequence of values."""
50
51
    def destructure(self, value):
52
        """Match against non-ADT tuple subclasses.
53
54
        Fail outright when matching ADTs.
55
56
        Given a superclass Sup and a subclass Sub, a value of type Sub can be
57
        interpreted as a value of type Sup, but a value of type Sup can only be
58
        interpreted as a value of type Sub if Sub is Sup.
59
        """
60
        if isinstance(value, ADTConstructor):
61
            raise MatchFailure
62
        if isinstance(value, self.target.__class__) and len(self.target) == len(value):
63
            return reversed(value)
64
        raise MatchFailure
65
66
67
T = typing.TypeVar("T", bound="DestructurerList")  # pylint: disable=invalid-name
68
69
70
class DestructurerList(tuple):
71
    """A list of destructurers, which are tried in order.
72
73
    The order of resolution is:
74
75
    - First, check on the object to be destructured; some classes provide for
76
    custom destructuring. This is only classes under the control of the
77
    library, and explicit subclasses of those.
78
    - Second, iterate over any custom destructurers defined to deal with
79
    classes defined outside of the library. Currently, this functionality isn't
80
    really used.
81
    - Finally, iterate over the builtin custom destructurers, which deal with
82
    standard library classes, and ADT classes. (ADT classes do not provide
83
    their own destructurers because they don't auto-define methods beyond those
84
    needed to interact properly with the Python runtime.)
85
    """
86
87
    __slots__ = ()
88
89
    def __new__(cls, *destructurers):
90
        return super().__new__(cls, destructurers)
91
92
    def get_destructurer(
93
        self, item
94
    ) -> typing.Optional[typing.Callable[[typing.Any], typing.Sequence[typing.Any]]]:
95
        """Return the destructurer for the item, if any.
96
97
        In the first case, the item is an instance of ``CompoundMatch``, and
98
        provides its own destructurer.
99
        In the second case, the item is an instance of the associated type of
100
        one of the destructurers, and that destructurer is used to wrap it and
101
        provide the destructurer.
102
        In the third case, we assume it's not a structure and therefore can't
103
        be recursed into.
104
        """
105
        if isinstance(item, CompoundMatch):
106
            return item.destructure
107
        for destructurer in self:
108
            if isinstance(item, destructurer.type):
109
                return destructurer(item)
110
        return None
111
112
    @classmethod
113
    def custom(cls: typing.Type[T], *destructurers) -> T:
114
        """Construct a new ``DestructurerList``, with custom destructurers.
115
116
        Custom destructurers are tried before the builtins.
117
        """
118
        return cls(*destructurers, ADTDestructurer, TupleDestructurer)
119
120
    def destructure(self, item) -> typing.Generator:
121
        """If we can destructure ``item``, do so, otherwise ignore it."""
122
        destructurer = self.get_destructurer(item)
123
        if destructurer:
124
            yield from destructurer(item)
125
126
    def stack_iteration(self, item) -> _stack_iter.Action:
127
        """If ``item`` is a ``Pattern``, yield its name. Otherwise, recurse."""
128
        if isinstance(item, Pattern):
129
            return _stack_iter.Yield(item)
130
        return _stack_iter.Extend(self.destructure(item))
131
132
    def names(self, target) -> typing.List[str]:
133
        """Return a list of names bound by the given structure.
134
135
        Raise ValueError if there are duplicate names.
136
        """
137
        name_list: typing.List[str] = []
138
        for item in _stack_iter.stack_iter(target, self.stack_iteration):
139
            not_in(container=name_list, item=item.name)
140
            name_list.append(item.name)
141
        return name_list
142
143
144
DESTRUCTURERS = DestructurerList.custom()
145
146
147
def names(target) -> typing.List[str]:
148
    """Return every name bound by a target."""
149
    return DESTRUCTURERS.names(target)
150