1
|
|
|
# SPDX-License-Identifier Apache-2.0 |
2
|
|
|
# Source: https://github.com/dmyersturnbull/typed-dfs |
3
|
|
|
# |
4
|
|
|
""" |
5
|
|
|
Hashable and ordered collections. |
6
|
|
|
""" |
7
|
|
|
from __future__ import annotations |
8
|
|
|
|
9
|
|
|
import functools |
10
|
|
|
from collections.abc import ( |
11
|
|
|
Hashable, |
12
|
|
|
Iterator, |
13
|
|
|
Mapping, |
14
|
|
|
MutableMapping, |
15
|
|
|
Sequence, |
16
|
|
|
ValuesView, |
17
|
|
|
) |
18
|
|
|
from typing import TypeVar |
19
|
|
|
|
20
|
|
|
T_co = TypeVar("T_co", covariant=True) |
21
|
|
|
K_contra = TypeVar("K_contra", contravariant=True) |
22
|
|
|
V_co = TypeVar("V_co", covariant=True) |
23
|
|
|
|
24
|
|
|
|
25
|
|
|
@functools.total_ordering |
26
|
|
|
class FrozeList(Sequence[T_co], Hashable): |
27
|
|
|
""" |
28
|
|
|
An immutable list. |
29
|
|
|
Hashable and ordered. |
30
|
|
|
""" |
31
|
|
|
|
32
|
|
|
EMPTY: FrozeList = NotImplemented # delayed |
33
|
|
|
|
34
|
|
|
def __init__(self, lst: Sequence[T_co]) -> None: |
35
|
|
|
self.__lst = lst if isinstance(lst, list) else list(lst) |
36
|
|
|
try: |
37
|
|
|
self.__hash = hash(tuple(lst)) |
38
|
|
|
except AttributeError: |
39
|
|
|
self.__hash = 0 |
40
|
|
|
|
41
|
|
|
@property |
42
|
|
|
def is_empty(self) -> bool: # pragma: no cover |
43
|
|
|
return len(self.__lst) == 0 |
44
|
|
|
|
45
|
|
|
@property |
46
|
|
|
def length(self) -> int: # pragma: no cover |
47
|
|
|
return len(self.__lst) |
48
|
|
|
|
49
|
|
|
def __iter__(self) -> Iterator[T_co]: # pragma: no cover |
50
|
|
|
return iter(self.__lst) |
51
|
|
|
|
52
|
|
|
def __getitem__(self, item: int): # pragma: no cover |
53
|
|
|
return self.__lst[item] |
54
|
|
|
|
55
|
|
|
def __hash__(self) -> int: |
56
|
|
|
return self.__hash |
57
|
|
|
|
58
|
|
|
def __eq__(self, other: FrozeList[T_co] | Sequence[T_co]) -> bool: |
59
|
|
|
return self.__lst == self.__make_other(other) |
60
|
|
|
|
61
|
|
|
def __lt__(self, other: FrozeList[T_co] | Sequence[T_co]): |
62
|
|
|
return self.__lst < self.__make_other(other) |
63
|
|
|
|
64
|
|
|
def __len__(self) -> int: |
65
|
|
|
return len(self.__lst) |
66
|
|
|
|
67
|
|
|
def __str__(self) -> str: |
68
|
|
|
return str(self.__lst) |
69
|
|
|
|
70
|
|
|
def __repr__(self) -> str: |
71
|
|
|
return repr(self.__lst) |
72
|
|
|
|
73
|
|
|
def to_list(self) -> list[T_co]: |
74
|
|
|
return list(self.__lst) |
75
|
|
|
|
76
|
|
|
def get(self, item: T_co, default: T_co | None = None) -> T_co | None: |
77
|
|
|
if item in self.__lst: |
78
|
|
|
return item |
79
|
|
|
return default |
80
|
|
|
|
81
|
|
|
def req(self, item: T_co, default: T_co | None = None) -> T_co: |
82
|
|
|
""" |
83
|
|
|
Returns the requested list item, falling back to a default. |
84
|
|
|
Short for "require". |
85
|
|
|
|
86
|
|
|
Raise: |
87
|
|
|
KeyError: If ``item`` is not in this list and ``default`` is ``None`` |
88
|
|
|
""" |
89
|
|
|
if item in self.__lst: |
90
|
|
|
return item |
91
|
|
|
if default is None: |
92
|
|
|
msg = f"Item {item} not found" |
93
|
|
|
raise KeyError(msg) |
94
|
|
|
return default |
95
|
|
|
|
96
|
|
|
def __make_other(self, other: FrozeList[T_co] | Sequence[T_co]) -> list[T_co]: |
97
|
|
|
if isinstance(other, FrozeList): |
98
|
|
|
other = other.__lst |
99
|
|
|
if isinstance(other, list): |
100
|
|
|
return other |
101
|
|
|
elif isinstance(other, Sequence): |
102
|
|
|
return list(other) |
103
|
|
|
msg = f"Cannot compare to {type(other)}" |
104
|
|
|
raise TypeError(msg) |
105
|
|
|
|
106
|
|
|
|
107
|
|
|
class FrozeSet(frozenset[T_co], Hashable): |
108
|
|
|
""" |
109
|
|
|
An immutable set. |
110
|
|
|
Hashable and ordered. |
111
|
|
|
This is almost identical to ``typing.FrozenSet``, but it's behavior was made |
112
|
|
|
equivalent to those of FrozeDict and FrozeList. |
113
|
|
|
""" |
114
|
|
|
|
115
|
|
|
EMPTY: FrozeSet = NotImplemented # delayed |
116
|
|
|
|
117
|
|
|
def __init__(self, lst: frozenset[T_co]) -> None: |
118
|
|
|
self.__lst = lst if isinstance(lst, set) else set(lst) |
119
|
|
|
try: |
120
|
|
|
self.__hash = hash(tuple(lst)) |
121
|
|
|
except AttributeError: |
122
|
|
|
# the hashes will collide, making sets slow |
123
|
|
|
# but at least we'll have a hash and thereby not violate the constraint |
124
|
|
|
self.__hash = 0 |
125
|
|
|
|
126
|
|
|
def get(self, item: T_co, default: T_co | None = None) -> T_co | None: |
127
|
|
|
if item in self.__lst: |
128
|
|
|
return item |
129
|
|
|
return default |
130
|
|
|
|
131
|
|
|
def req(self, item: T_co, default: T_co | None = None) -> T_co: |
132
|
|
|
""" |
133
|
|
|
Returns ``item`` if it is in this set. |
134
|
|
|
Short for "require". |
135
|
|
|
Falls back to ``default`` if ``default`` is not ``None``. |
136
|
|
|
|
137
|
|
|
Raises: |
138
|
|
|
KeyError: If ``item`` is not in this set and ``default`` is ``None`` |
139
|
|
|
""" |
140
|
|
|
if item in self.__lst: |
141
|
|
|
return item |
142
|
|
|
if default is None: |
143
|
|
|
msg = f"Item {item} not found" |
144
|
|
|
raise KeyError(msg) |
145
|
|
|
return default |
146
|
|
|
|
147
|
|
|
def __getitem__(self, item: T_co) -> T_co: |
148
|
|
|
if item in self.__lst: |
149
|
|
|
return item |
150
|
|
|
msg = f"Item {item} not found" |
151
|
|
|
raise KeyError(msg) |
152
|
|
|
|
153
|
|
|
def __contains__(self, x: T_co) -> bool: # pragma: no cover |
154
|
|
|
return x in self.__lst |
155
|
|
|
|
156
|
|
|
def __iter__(self) -> Iterator[T_co]: # pragma: no cover |
157
|
|
|
return iter(self.__lst) |
158
|
|
|
|
159
|
|
|
def __hash__(self) -> int: |
160
|
|
|
return self.__hash |
161
|
|
|
|
162
|
|
|
def __eq__(self, other: FrozeSet[T_co]) -> bool: |
163
|
|
|
return self.__lst == self.__make_other(other) |
164
|
|
|
|
165
|
|
|
def __lt__(self, other: FrozeSet[T_co] | frozenset[T_co]): |
166
|
|
|
""" |
167
|
|
|
Compares ``self`` and ``other`` for partial ordering. |
168
|
|
|
Sorts ``self`` and ``other``, then compares the two sorted sets. |
169
|
|
|
|
170
|
|
|
Approximately:: |
171
|
|
|
return list(sorted(self)) < list(sorted(other)) |
172
|
|
|
""" |
173
|
|
|
other = sorted(self.__make_other(other)) |
174
|
|
|
me = sorted(self.__lst) |
175
|
|
|
return me < other |
176
|
|
|
|
177
|
|
|
@property |
178
|
|
|
def is_empty(self) -> bool: # pragma: no cover |
179
|
|
|
return len(self.__lst) == 0 |
180
|
|
|
|
181
|
|
|
@property |
182
|
|
|
def length(self) -> int: # pragma: no cover |
183
|
|
|
return len(self.__lst) |
184
|
|
|
|
185
|
|
|
def __len__(self) -> int: # pragma: no cover |
186
|
|
|
return len(self.__lst) |
187
|
|
|
|
188
|
|
|
def __str__(self) -> str: |
189
|
|
|
return str(self.__lst) |
190
|
|
|
|
191
|
|
|
def __repr__(self) -> str: |
192
|
|
|
return repr(self.__lst) |
193
|
|
|
|
194
|
|
|
def to_set(self) -> frozenset[T_co]: |
195
|
|
|
return set(self.__lst) |
196
|
|
|
|
197
|
|
|
def to_frozenset(self) -> frozenset[T_co]: |
198
|
|
|
return frozenset(self.__lst) |
199
|
|
|
|
200
|
|
|
def __make_other(self, other: FrozeSet[T_co] | frozenset[T_co]) -> set[T_co]: |
201
|
|
|
if isinstance(other, FrozeSet): |
202
|
|
|
other = other.__lst |
203
|
|
|
if isinstance(other, set): |
204
|
|
|
return other |
205
|
|
|
elif isinstance(other, frozenset): |
206
|
|
|
return set(other) |
207
|
|
|
msg = f"Cannot compare to {type(other)}" |
208
|
|
|
raise TypeError(msg) |
209
|
|
|
|
210
|
|
|
|
211
|
|
|
class FrozeDict(Mapping[K_contra, V_co], Hashable): |
212
|
|
|
""" |
213
|
|
|
An immutable dictionary/mapping. |
214
|
|
|
Hashable and ordered. |
215
|
|
|
""" |
216
|
|
|
|
217
|
|
|
EMPTY: FrozeDict = NotImplemented # delayed |
218
|
|
|
|
219
|
|
|
def __init__(self, dct: Mapping[K_contra, V_co]) -> None: |
220
|
|
|
self.__dct = dct if isinstance(dct, dict) else dict(dct) |
221
|
|
|
self.__hash = hash(tuple(dct.items())) |
222
|
|
|
|
223
|
|
|
def get(self, key: K_contra, default: V_co | None = None) -> V_co | None: # pragma: no cover |
224
|
|
|
return self.__dct.get(key, default) |
225
|
|
|
|
226
|
|
|
def req(self, key: K_contra, default: V_co | None = None) -> V_co: |
227
|
|
|
""" |
228
|
|
|
Returns the value corresponding to ``key``. |
229
|
|
|
Short for "require". |
230
|
|
|
Falls back to ``default`` if ``default`` is not None and ``key`` is not in this dict. |
231
|
|
|
|
232
|
|
|
Raise: |
233
|
|
|
KeyError: If ``key`` is not in this dict and ``default`` is ``None`` |
234
|
|
|
""" |
235
|
|
|
if default is None: |
236
|
|
|
return self.__dct[key] |
237
|
|
|
return self.__dct.get(key, default) |
238
|
|
|
|
239
|
|
|
def items(self) -> frozenset[tuple[K_contra, V_co]]: # pragma: no cover |
240
|
|
|
return self.__dct.items() |
241
|
|
|
|
242
|
|
|
def keys(self) -> frozenset[K_contra]: # pragma: no cover |
243
|
|
|
return self.__dct.keys() |
244
|
|
|
|
245
|
|
|
def values(self) -> ValuesView[V_co]: # pragma: no cover |
246
|
|
|
return self.__dct.values() |
247
|
|
|
|
248
|
|
|
def __iter__(self): # pragma: no cover |
249
|
|
|
return iter(self.__dct) |
250
|
|
|
|
251
|
|
|
def __contains__(self, item: K_contra) -> bool: # pragma: no cover |
252
|
|
|
return item in self.__dct |
253
|
|
|
|
254
|
|
|
def __getitem__(self, item: K_contra) -> T_co: # pragma: no cover |
255
|
|
|
return self.__dct[item] |
256
|
|
|
|
257
|
|
|
def __hash__(self) -> int: |
258
|
|
|
return self.__hash |
259
|
|
|
|
260
|
|
|
def __eq__(self, other: FrozeDict[K_contra, V_co]) -> bool: |
261
|
|
|
if isinstance(self, FrozeDict): |
262
|
|
|
return self.__dct == other.__dct |
263
|
|
|
elif isinstance(self, dict): |
264
|
|
|
return self == other.__dct |
265
|
|
|
elif isinstance(self, Mapping): |
266
|
|
|
return self == dict(other.__dct) |
267
|
|
|
msg = f"Cannot compare to {type(other)}" |
268
|
|
|
raise TypeError(msg) |
269
|
|
|
|
270
|
|
|
def __lt__(self, other: Mapping[K_contra, V_co]): |
271
|
|
|
""" |
272
|
|
|
Compares this dict to another, with partial ordering. |
273
|
|
|
|
274
|
|
|
The algorithm is: |
275
|
|
|
1. Sort ``self`` and ``other`` by keys |
276
|
|
|
2. If ``sorted_self < sorted_other``, return ``False`` |
277
|
|
|
3. If the reverse is true (``sorted_other < sorted_self``), return ``True`` |
278
|
|
|
4. (The keys are now known to be the same.) |
279
|
|
|
For each key, in order: If ``self[key] < other[key]``, return ``True`` |
280
|
|
|
5. Return ``False`` |
281
|
|
|
""" |
282
|
|
|
other = self.__make_other(other) |
283
|
|
|
me = self.__dct |
284
|
|
|
o_keys = sorted(other.keys()) |
285
|
|
|
s_keys = sorted(me.keys()) |
286
|
|
|
if o_keys < s_keys: |
287
|
|
|
return False |
288
|
|
|
if o_keys > s_keys: |
289
|
|
|
return True |
290
|
|
|
# keys are equal |
291
|
|
|
return any(other[k] > me[k] for k in o_keys) |
292
|
|
|
|
293
|
|
|
@property |
294
|
|
|
def is_empty(self) -> bool: # pragma: no cover |
295
|
|
|
return len(self.__dct) == 0 |
296
|
|
|
|
297
|
|
|
@property |
298
|
|
|
def length(self) -> int: # pragma: no cover |
299
|
|
|
return len(self.__dct) |
300
|
|
|
|
301
|
|
|
def __len__(self) -> int: |
302
|
|
|
return len(self.__dct) |
303
|
|
|
|
304
|
|
|
def __str__(self) -> str: |
305
|
|
|
return str(self.__dct) |
306
|
|
|
|
307
|
|
|
def __repr__(self) -> str: |
308
|
|
|
return repr(self.__dct) |
309
|
|
|
|
310
|
|
|
def to_dict(self) -> MutableMapping[K_contra, V_co]: # pragma: no cover |
311
|
|
|
return dict(self.__dct) |
312
|
|
|
|
313
|
|
|
def __make_other( |
314
|
|
|
self, |
315
|
|
|
other: FrozeDict[K_contra, V_co] | Mapping[K_contra, V_co], |
316
|
|
|
) -> dict[K_contra, V_co]: |
317
|
|
|
if isinstance(other, FrozeDict): |
318
|
|
|
other = other.__dct |
319
|
|
|
if isinstance(other, dict): |
320
|
|
|
return other |
321
|
|
|
elif isinstance(other, Mapping): |
322
|
|
|
return dict(other) |
323
|
|
|
msg = f"Cannot compare to {type(other)}" |
324
|
|
|
raise TypeError(msg) |
325
|
|
|
|
326
|
|
|
|
327
|
|
|
# for performance, only make these once: |
328
|
|
|
FrozeList.EMPTY = FrozeList([]) |
329
|
|
|
FrozeSet.EMPTY = FrozeSet(set()) |
330
|
|
|
FrozeDict.EMPTY = FrozeDict({}) |
331
|
|
|
|
332
|
|
|
|
333
|
|
|
__all__ = ["FrozeList", "FrozeSet", "FrozeDict"] |
334
|
|
|
|