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
|
|
|
|