python-utils 4.0.1__py3-none-any.whl → 4.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_utils/aio.py +5 -2
- python_utils/containers.py +537 -41
- python_utils/converters.py +70 -15
- python_utils/decorators.py +29 -3
- python_utils/formatters.py +34 -12
- python_utils/generators.py +82 -34
- python_utils/import_.py +49 -8
- python_utils/logger.py +89 -30
- python_utils/loguru.py +34 -1
- python_utils/time.py +55 -26
- {python_utils-4.0.1.dist-info → python_utils-4.1.1.dist-info}/METADATA +22 -9
- python_utils-4.1.1.dist-info/RECORD +21 -0
- {python_utils-4.0.1.dist-info → python_utils-4.1.1.dist-info}/WHEEL +1 -1
- python_utils-4.0.1.dist-info/RECORD +0 -21
- {python_utils-4.0.1.dist-info → python_utils-4.1.1.dist-info}/licenses/LICENSE +0 -0
python_utils/containers.py
CHANGED
|
@@ -61,10 +61,13 @@ Examples:
|
|
|
61
61
|
import abc
|
|
62
62
|
import collections
|
|
63
63
|
import collections.abc
|
|
64
|
+
import contextlib
|
|
65
|
+
import operator
|
|
64
66
|
import typing
|
|
65
67
|
|
|
66
68
|
if typing.TYPE_CHECKING:
|
|
67
69
|
import _typeshed # noqa: F401
|
|
70
|
+
import typing_extensions
|
|
68
71
|
|
|
69
72
|
#: A type alias for a type that can be used as a key in a dictionary.
|
|
70
73
|
KT = typing.TypeVar('KT')
|
|
@@ -82,20 +85,65 @@ HT = typing.TypeVar('HT', bound=collections.abc.Hashable)
|
|
|
82
85
|
T = typing.TypeVar('T')
|
|
83
86
|
|
|
84
87
|
#: Argument shapes accepted when updating a casted dict: a mapping, an iterable
|
|
85
|
-
#: of key/value pairs,
|
|
88
|
+
#: of key/value pairs, or a keys-and-getitem object.
|
|
86
89
|
# Kept as `typing.Union` (not PEP 604 `|`): one member is a string forward
|
|
87
90
|
# reference, and `|` evaluates its operands eagerly, raising `TypeError` on a
|
|
88
91
|
# `str` operand at runtime. `typing.Union` accepts it as a lazy `ForwardRef`.
|
|
89
92
|
DictUpdateArgs = typing.Union[
|
|
90
93
|
collections.abc.Mapping[KT, VT],
|
|
91
94
|
collections.abc.Iterable[tuple[KT, VT]],
|
|
92
|
-
collections.abc.Iterable[collections.abc.Mapping[KT, VT]],
|
|
93
95
|
'_typeshed.SupportsKeysAndGetItem[KT, VT]',
|
|
94
96
|
]
|
|
95
97
|
|
|
96
98
|
#: Policy for ``UniqueList`` duplicates: silently ``'ignore'`` or ``'raise'``.
|
|
97
99
|
OnDuplicate = typing.Literal['ignore', 'raise']
|
|
98
100
|
|
|
101
|
+
#: Marks the state that ``CastedDictBase.__reduce_ex__`` writes, to tell it
|
|
102
|
+
#: apart from the default state of an instance with slots.
|
|
103
|
+
_CASTED_DICT_STATE: str = 'python_utils.containers.CastedDictBase'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _restore_attributes(
|
|
107
|
+
instance: object,
|
|
108
|
+
state: typing.Any,
|
|
109
|
+
inherited: collections.abc.Callable[[typing.Any], None] | None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Restores the attributes of an instance from its default pickle state.
|
|
113
|
+
|
|
114
|
+
The default state is the instance dictionary, or a tuple of that
|
|
115
|
+
dictionary and the slot values when the class has slots. `pickle` and
|
|
116
|
+
`copy` apply both forms themselves, but only for a class without a
|
|
117
|
+
`__setstate__` of its own.
|
|
118
|
+
|
|
119
|
+
A `__setstate__` that follows the calling class in the method resolution
|
|
120
|
+
order gets the state when there is one. It would have received it if the
|
|
121
|
+
calling class did not define `__setstate__`.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
instance (object): The instance to restore.
|
|
125
|
+
state (typing.Any): The default state of the instance.
|
|
126
|
+
inherited (Callable[[typing.Any], None] | None): The `__setstate__`
|
|
127
|
+
that follows the calling class, or None without one.
|
|
128
|
+
"""
|
|
129
|
+
if inherited is not None:
|
|
130
|
+
inherited(state)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
slots: dict[str, typing.Any] | None = None
|
|
134
|
+
if isinstance(state, tuple):
|
|
135
|
+
state, slots = typing.cast(
|
|
136
|
+
'tuple[dict[str, typing.Any] | None, dict[str, typing.Any]]',
|
|
137
|
+
state,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if state:
|
|
141
|
+
vars(instance).update(state)
|
|
142
|
+
|
|
143
|
+
if slots:
|
|
144
|
+
for name, value in slots.items():
|
|
145
|
+
setattr(instance, name, value)
|
|
146
|
+
|
|
99
147
|
|
|
100
148
|
class CastedDictBase(dict[KT, VT], abc.ABC):
|
|
101
149
|
"""
|
|
@@ -119,8 +167,10 @@ class CastedDictBase(dict[KT, VT], abc.ABC):
|
|
|
119
167
|
callable is provided.
|
|
120
168
|
"""
|
|
121
169
|
|
|
122
|
-
|
|
123
|
-
|
|
170
|
+
# The casts default to `None` on the class so that a dict which `pickle`
|
|
171
|
+
# created without calling `__init__` can already store items.
|
|
172
|
+
_key_cast: KT_cast[KT] = None
|
|
173
|
+
_value_cast: VT_cast[VT] = None
|
|
124
174
|
|
|
125
175
|
def __init__(
|
|
126
176
|
self,
|
|
@@ -160,12 +210,73 @@ class CastedDictBase(dict[KT, VT], abc.ABC):
|
|
|
160
210
|
the dictionary.
|
|
161
211
|
**kwargs (typing.Any): Keyword arguments to update the dictionary.
|
|
162
212
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
213
|
+
# Keyword arguments are applied last so that they win, as they do
|
|
214
|
+
# for `dict.update`.
|
|
215
|
+
positional: dict[typing.Any, typing.Any] = {}
|
|
216
|
+
positional.update(*args)
|
|
217
|
+
for key, value in positional.items():
|
|
218
|
+
self[key] = value
|
|
219
|
+
|
|
220
|
+
for key, value in kwargs.items():
|
|
221
|
+
self[key] = value
|
|
222
|
+
|
|
223
|
+
def setdefault(self, key: typing.Any, default: typing.Any = None) -> VT:
|
|
224
|
+
"""
|
|
225
|
+
Stores the default through the casts when the key is missing.
|
|
226
|
+
|
|
227
|
+
A default of `None` is stored as `None` without casting it, so that
|
|
228
|
+
`setdefault(key)` works for every value cast.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
key (typing.Any): The key to look up, before casting.
|
|
232
|
+
default (typing.Any, optional): The value to store when the key
|
|
233
|
+
is missing. Defaults to None.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
VT: The value that is stored under the key.
|
|
237
|
+
"""
|
|
238
|
+
if self._key_cast is not None:
|
|
239
|
+
key = self._key_cast(key)
|
|
240
|
+
|
|
241
|
+
if not super().__contains__(key):
|
|
242
|
+
stored: typing.Any = default
|
|
243
|
+
if default is not None:
|
|
244
|
+
stored = self._cast_stored_value(default)
|
|
245
|
+
|
|
246
|
+
super().__setitem__(key, stored)
|
|
247
|
+
|
|
248
|
+
return super().__getitem__(key)
|
|
249
|
+
|
|
250
|
+
def _cast_stored_value(self, value: typing.Any) -> typing.Any:
|
|
251
|
+
"""
|
|
252
|
+
Casts a value on its way into the dictionary.
|
|
253
|
+
|
|
254
|
+
The base class stores values as they are. A subclass that casts when
|
|
255
|
+
it stores a value overrides this method.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
value (typing.Any): The value to store.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
typing.Any: The value as it is stored.
|
|
262
|
+
"""
|
|
263
|
+
return value
|
|
264
|
+
|
|
265
|
+
def __ior__( # type: ignore[override, misc]
|
|
266
|
+
self, other: DictUpdateArgs[typing.Any, typing.Any]
|
|
267
|
+
) -> 'typing_extensions.Self':
|
|
268
|
+
"""
|
|
269
|
+
Updates the dictionary in place through the casts, see `update`.
|
|
165
270
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
271
|
+
Args:
|
|
272
|
+
other (DictUpdateArgs[typing.Any, typing.Any]): The items to
|
|
273
|
+
merge into the dictionary.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
typing_extensions.Self: The dictionary itself.
|
|
277
|
+
"""
|
|
278
|
+
self.update(other)
|
|
279
|
+
return self
|
|
169
280
|
|
|
170
281
|
def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
|
|
171
282
|
"""
|
|
@@ -181,6 +292,81 @@ class CastedDictBase(dict[KT, VT], abc.ABC):
|
|
|
181
292
|
|
|
182
293
|
return super().__setitem__(key, value)
|
|
183
294
|
|
|
295
|
+
def __reduce_ex__(self, protocol: typing.SupportsIndex) -> typing.Any:
|
|
296
|
+
"""
|
|
297
|
+
Describes the dictionary for `pickle` and `copy` with its raw items.
|
|
298
|
+
|
|
299
|
+
By default the items are restored through `__setitem__`. `pickle`
|
|
300
|
+
does that before the casts are back and `copy` does it after, which
|
|
301
|
+
casts every value a second time. The items travel inside the state
|
|
302
|
+
to avoid both.
|
|
303
|
+
|
|
304
|
+
The default description is returned unchanged where it already
|
|
305
|
+
works or where other code relies on its form, see the comment below.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
protocol (typing.SupportsIndex): The pickle protocol.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
typing.Any: The default description, with the items moved into
|
|
312
|
+
the state for protocol 2 and up.
|
|
313
|
+
"""
|
|
314
|
+
reduced: typing.Any = super().__reduce_ex__(protocol)
|
|
315
|
+
if (
|
|
316
|
+
operator.index(protocol) < 2
|
|
317
|
+
or len(self) == 0
|
|
318
|
+
or type(self).__reduce__ is not object.__reduce__
|
|
319
|
+
or type(self).__reduce_ex__ is not _BASE_REDUCE_EX
|
|
320
|
+
or type(self).__setstate__ is not _BASE_SETSTATE
|
|
321
|
+
):
|
|
322
|
+
# Protocol 0 and 1 restore the items through `dict` itself and
|
|
323
|
+
# an empty dictionary has none. A subclass with its own
|
|
324
|
+
# `__reduce__` describes itself. One with its own `__reduce_ex__`
|
|
325
|
+
# or `__setstate__` expects the default state and items.
|
|
326
|
+
return reduced
|
|
327
|
+
|
|
328
|
+
state: tuple[str, typing.Any, dict[KT, VT]] = (
|
|
329
|
+
_CASTED_DICT_STATE,
|
|
330
|
+
reduced[2],
|
|
331
|
+
super().copy(),
|
|
332
|
+
)
|
|
333
|
+
return reduced[0], reduced[1], state, None, None
|
|
334
|
+
|
|
335
|
+
def __setstate__(self, state: object) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Restores the attributes and the raw items without casting them.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
state (object): The state from `__reduce_ex__`, or the default
|
|
341
|
+
state for a pickle that already restored its items.
|
|
342
|
+
"""
|
|
343
|
+
parts: tuple[object, ...] = ()
|
|
344
|
+
if isinstance(state, tuple):
|
|
345
|
+
parts = typing.cast('tuple[object, ...]', state)
|
|
346
|
+
|
|
347
|
+
# A mixin after this class can have a `__setstate__` of its own.
|
|
348
|
+
inherited: typing.Any = getattr(super(), '__setstate__', None)
|
|
349
|
+
if len(parts) == 3 and parts[0] == _CASTED_DICT_STATE:
|
|
350
|
+
_restore_attributes(self, parts[1], inherited)
|
|
351
|
+
super().update(typing.cast('dict[KT, VT]', parts[2]))
|
|
352
|
+
else:
|
|
353
|
+
_restore_attributes(self, state, inherited)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
#: The `__setstate__` that understands the state of
|
|
357
|
+
#: `CastedDictBase.__reduce_ex__`. A subclass that replaces it gets the default
|
|
358
|
+
#: state.
|
|
359
|
+
# Both are taken from the class dictionary. `CastedDictBase[...].__reduce_ex__`
|
|
360
|
+
# would be the method of the generic alias itself.
|
|
361
|
+
_BASE_SETSTATE: collections.abc.Callable[..., None] = vars(CastedDictBase)[
|
|
362
|
+
'__setstate__'
|
|
363
|
+
]
|
|
364
|
+
#: The `__reduce_ex__` that writes that state. A subclass that replaces it
|
|
365
|
+
#: gets the default description to build on.
|
|
366
|
+
_BASE_REDUCE_EX: collections.abc.Callable[..., typing.Any] = vars(
|
|
367
|
+
CastedDictBase
|
|
368
|
+
)['__reduce_ex__']
|
|
369
|
+
|
|
184
370
|
|
|
185
371
|
class CastedDict(CastedDictBase[KT, VT]):
|
|
186
372
|
"""
|
|
@@ -223,10 +409,22 @@ class CastedDict(CastedDictBase[KT, VT]):
|
|
|
223
409
|
The key itself is cast by ``CastedDictBase.__setitem__`` when a key
|
|
224
410
|
cast is configured.
|
|
225
411
|
"""
|
|
412
|
+
super().__setitem__(key, self._cast_stored_value(value))
|
|
413
|
+
|
|
414
|
+
def _cast_stored_value(self, value: typing.Any) -> typing.Any:
|
|
415
|
+
"""
|
|
416
|
+
Casts a value on its way into the dictionary.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
value (typing.Any): The value to store.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
typing.Any: The value, cast when a value cast is set.
|
|
423
|
+
"""
|
|
226
424
|
if self._value_cast is not None:
|
|
227
425
|
value = self._value_cast(value)
|
|
228
426
|
|
|
229
|
-
|
|
427
|
+
return value
|
|
230
428
|
|
|
231
429
|
|
|
232
430
|
class LazyCastedDict(CastedDictBase[KT, VT]):
|
|
@@ -278,15 +476,14 @@ class LazyCastedDict(CastedDictBase[KT, VT]):
|
|
|
278
476
|
def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
|
|
279
477
|
"""
|
|
280
478
|
Sets the item in the dictionary, casting the key if a key cast
|
|
281
|
-
callable is provided.
|
|
479
|
+
callable is provided. The value is stored as it is.
|
|
282
480
|
|
|
283
481
|
Args:
|
|
284
482
|
key (typing.Any): The key to set in the dictionary.
|
|
285
483
|
value (typing.Any): The value to set in the dictionary.
|
|
286
484
|
"""
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
485
|
+
# The base class casts the key. Casting it here as well would cast
|
|
486
|
+
# it twice.
|
|
290
487
|
super().__setitem__(key, value)
|
|
291
488
|
|
|
292
489
|
def __getitem__(self, key: typing.Any) -> VT:
|
|
@@ -357,6 +554,16 @@ class UniqueList(list[HT]):
|
|
|
357
554
|
>>> l
|
|
358
555
|
[5, 10, 2, 3, 4]
|
|
359
556
|
|
|
557
|
+
A value that was removed or replaced can be added again:
|
|
558
|
+
|
|
559
|
+
>>> l = UniqueList(1, 2, 3)
|
|
560
|
+
>>> l[0] = 4
|
|
561
|
+
>>> l.pop()
|
|
562
|
+
3
|
|
563
|
+
>>> l.extend([1, 2, 3])
|
|
564
|
+
>>> l
|
|
565
|
+
[4, 2, 1, 3]
|
|
566
|
+
|
|
360
567
|
>>> l = UniqueList(1, 2, 3, on_duplicate='raise')
|
|
361
568
|
>>> l.append(4)
|
|
362
569
|
>>> l.append(4)
|
|
@@ -378,6 +585,30 @@ class UniqueList(list[HT]):
|
|
|
378
585
|
"""
|
|
379
586
|
|
|
380
587
|
_set: set[HT]
|
|
588
|
+
#: The default lives on the class, so that a subclass can set its own
|
|
589
|
+
#: and a list that `pickle` created without `__init__` has one.
|
|
590
|
+
on_duplicate: OnDuplicate = 'ignore'
|
|
591
|
+
|
|
592
|
+
def __new__(
|
|
593
|
+
cls, *args: typing.Any, **kwargs: typing.Any
|
|
594
|
+
) -> 'typing_extensions.Self':
|
|
595
|
+
"""
|
|
596
|
+
Creates the list with an empty membership set.
|
|
597
|
+
|
|
598
|
+
`pickle` and `copy` create the list through `__new__` and refill it
|
|
599
|
+
through `extend` or `append` without calling `__init__`, so the
|
|
600
|
+
membership set has to exist before `__init__` runs.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
*args (typing.Any): Ignored, handled by `__init__`.
|
|
604
|
+
**kwargs (typing.Any): Ignored, handled by `__init__`.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
typing_extensions.Self: The new, empty list.
|
|
608
|
+
"""
|
|
609
|
+
instance: typing_extensions.Self = super().__new__(cls)
|
|
610
|
+
instance._set = set()
|
|
611
|
+
return instance
|
|
381
612
|
|
|
382
613
|
def __init__(
|
|
383
614
|
self,
|
|
@@ -398,6 +629,44 @@ class UniqueList(list[HT]):
|
|
|
398
629
|
for arg in args:
|
|
399
630
|
self.append(arg)
|
|
400
631
|
|
|
632
|
+
def __setstate__(self, state: typing.Any) -> None:
|
|
633
|
+
"""
|
|
634
|
+
Restores the attributes and rebuilds the membership from the items.
|
|
635
|
+
|
|
636
|
+
`copy` restores the attributes before the items and `pickle` restores
|
|
637
|
+
them after the items. A stored membership set is only right in the
|
|
638
|
+
second case, so the set is always derived from the items that are in
|
|
639
|
+
the list at this point.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
state (typing.Any): The instance attributes, with the slot values
|
|
643
|
+
when a subclass has slots.
|
|
644
|
+
"""
|
|
645
|
+
# A mixin after this class can have a `__setstate__` of its own.
|
|
646
|
+
inherited: typing.Any = getattr(super(), '__setstate__', None)
|
|
647
|
+
_restore_attributes(self, state, inherited)
|
|
648
|
+
self._set = set(self)
|
|
649
|
+
|
|
650
|
+
def _release(self, value: HT) -> None:
|
|
651
|
+
"""
|
|
652
|
+
Drops a value that left the list from the membership set.
|
|
653
|
+
|
|
654
|
+
The set is rebuilt from the list when the value cannot be removed
|
|
655
|
+
from it. That happens when the value changed its hash after it was
|
|
656
|
+
added, and when its `__hash__` or `__eq__` raises.
|
|
657
|
+
|
|
658
|
+
The value has left the list by now, so nothing here may raise. When
|
|
659
|
+
the rebuild fails as well the set stays as it is.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
value (HT): The value that was removed from the list.
|
|
663
|
+
"""
|
|
664
|
+
try:
|
|
665
|
+
self._set.remove(value)
|
|
666
|
+
except Exception: # noqa: BLE001
|
|
667
|
+
with contextlib.suppress(Exception):
|
|
668
|
+
self._set = set(self)
|
|
669
|
+
|
|
401
670
|
def insert(self, index: typing.SupportsIndex, value: HT) -> None:
|
|
402
671
|
"""
|
|
403
672
|
Inserts a value at the specified index, ensuring uniqueness.
|
|
@@ -416,8 +685,15 @@ class UniqueList(list[HT]):
|
|
|
416
685
|
else:
|
|
417
686
|
return
|
|
418
687
|
|
|
688
|
+
# The membership first, right after the check above. Another thread
|
|
689
|
+
# that inserts the same value in between would get past its own
|
|
690
|
+
# check as well. A failed insert takes the membership back.
|
|
419
691
|
self._set.add(value)
|
|
420
|
-
|
|
692
|
+
try:
|
|
693
|
+
super().insert(index, value)
|
|
694
|
+
except BaseException:
|
|
695
|
+
self._set.discard(value)
|
|
696
|
+
raise
|
|
421
697
|
|
|
422
698
|
def append(self, value: HT) -> None:
|
|
423
699
|
"""
|
|
@@ -439,6 +715,133 @@ class UniqueList(list[HT]):
|
|
|
439
715
|
self._set.add(value)
|
|
440
716
|
super().append(value)
|
|
441
717
|
|
|
718
|
+
def extend(self, values: collections.abc.Iterable[HT]) -> None:
|
|
719
|
+
"""
|
|
720
|
+
Extends the list with the values that are not in it yet.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
values (Iterable[HT]): The values to append.
|
|
724
|
+
|
|
725
|
+
Raises:
|
|
726
|
+
ValueError: If `on_duplicate` is set to 'raise' and a value is
|
|
727
|
+
already in the list or occurs more than once in `values`.
|
|
728
|
+
The list is left unchanged in that case.
|
|
729
|
+
"""
|
|
730
|
+
new_values: list[HT] = list(values)
|
|
731
|
+
if self.on_duplicate == 'raise':
|
|
732
|
+
duplicates: set[HT] = self._find_duplicates(new_values)
|
|
733
|
+
if duplicates:
|
|
734
|
+
raise ValueError(f'Duplicate values: {duplicates}')
|
|
735
|
+
|
|
736
|
+
for value in new_values:
|
|
737
|
+
self.append(value)
|
|
738
|
+
|
|
739
|
+
# `list.__iadd__` accepts any iterable while `list.__add__` only accepts
|
|
740
|
+
# a list. Typeshed ignores the same mismatch.
|
|
741
|
+
def __iadd__( # type: ignore[misc, override]
|
|
742
|
+
self, values: collections.abc.Iterable[HT]
|
|
743
|
+
) -> 'typing_extensions.Self':
|
|
744
|
+
"""
|
|
745
|
+
Extends the list in place, see `extend`.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
values (Iterable[HT]): The values to append.
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
typing_extensions.Self: The list itself.
|
|
752
|
+
"""
|
|
753
|
+
self.extend(values)
|
|
754
|
+
return self
|
|
755
|
+
|
|
756
|
+
def __imul__(
|
|
757
|
+
self, value: typing.SupportsIndex
|
|
758
|
+
) -> 'typing_extensions.Self':
|
|
759
|
+
"""
|
|
760
|
+
Multiplies the list in place without ever repeating an item.
|
|
761
|
+
|
|
762
|
+
A count below 1 empties the list, as it does for a regular list. A
|
|
763
|
+
count above 1 would repeat every item, so the repeat goes through
|
|
764
|
+
`extend` and follows `on_duplicate`.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
value (typing.SupportsIndex): The number of times to repeat.
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
typing_extensions.Self: The list itself.
|
|
771
|
+
|
|
772
|
+
Raises:
|
|
773
|
+
ValueError: If `on_duplicate` is set to 'raise' and a non-empty
|
|
774
|
+
list is multiplied by more than 1.
|
|
775
|
+
"""
|
|
776
|
+
count: int = operator.index(value)
|
|
777
|
+
if count < 1:
|
|
778
|
+
self.clear()
|
|
779
|
+
elif count > 1:
|
|
780
|
+
self.extend(self)
|
|
781
|
+
|
|
782
|
+
return self
|
|
783
|
+
|
|
784
|
+
def pop(self, index: typing.SupportsIndex = -1) -> HT:
|
|
785
|
+
"""
|
|
786
|
+
Removes and returns the item at the given index.
|
|
787
|
+
|
|
788
|
+
Args:
|
|
789
|
+
index (typing.SupportsIndex, optional): The index to pop.
|
|
790
|
+
Defaults to the last item.
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
HT: The removed item.
|
|
794
|
+
"""
|
|
795
|
+
value: HT = super().pop(index)
|
|
796
|
+
self._release(value)
|
|
797
|
+
return value
|
|
798
|
+
|
|
799
|
+
def remove(self, value: HT) -> None:
|
|
800
|
+
"""
|
|
801
|
+
Removes a value from the list.
|
|
802
|
+
|
|
803
|
+
Args:
|
|
804
|
+
value (HT): The value to remove.
|
|
805
|
+
|
|
806
|
+
Raises:
|
|
807
|
+
ValueError: If the value is not in the list.
|
|
808
|
+
"""
|
|
809
|
+
# One list operation, so that another thread cannot get between
|
|
810
|
+
# finding the value and removing it.
|
|
811
|
+
super().remove(value)
|
|
812
|
+
self._release(value)
|
|
813
|
+
|
|
814
|
+
def clear(self) -> None:
|
|
815
|
+
"""Removes all items from the list."""
|
|
816
|
+
super().clear()
|
|
817
|
+
self._set.clear()
|
|
818
|
+
|
|
819
|
+
def _find_duplicates(
|
|
820
|
+
self,
|
|
821
|
+
values: list[HT],
|
|
822
|
+
replaced: collections.abc.Set[HT] = frozenset(),
|
|
823
|
+
) -> set[HT]:
|
|
824
|
+
"""
|
|
825
|
+
Finds the values that would break uniqueness when added.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
values (list[HT]): The values to add.
|
|
829
|
+
replaced (Set[HT], optional): Items that leave the list in the
|
|
830
|
+
same operation, so that adding them again is allowed.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
set[HT]: The values that occur more than once in `values` or
|
|
834
|
+
that are already in the list and not in `replaced`.
|
|
835
|
+
"""
|
|
836
|
+
seen: set[HT] = set()
|
|
837
|
+
duplicates: set[HT] = set()
|
|
838
|
+
for value in values:
|
|
839
|
+
if value in seen or (value in self._set and value not in replaced):
|
|
840
|
+
duplicates.add(value)
|
|
841
|
+
seen.add(value)
|
|
842
|
+
|
|
843
|
+
return duplicates
|
|
844
|
+
|
|
442
845
|
def __contains__(self, item: HT) -> bool: # type: ignore[override]
|
|
443
846
|
"""
|
|
444
847
|
Checks if the list contains the specified item.
|
|
@@ -449,7 +852,12 @@ class UniqueList(list[HT]):
|
|
|
449
852
|
Returns:
|
|
450
853
|
bool: True if the item is in the list, False otherwise.
|
|
451
854
|
"""
|
|
452
|
-
|
|
855
|
+
try:
|
|
856
|
+
return item in self._set
|
|
857
|
+
except TypeError:
|
|
858
|
+
# An unhashable item cannot be in the set, but it can still be
|
|
859
|
+
# equal to an item in the list.
|
|
860
|
+
return super().__contains__(item)
|
|
453
861
|
|
|
454
862
|
@typing.overload
|
|
455
863
|
def __setitem__(self, indices: typing.SupportsIndex, values: HT) -> None:
|
|
@@ -480,31 +888,93 @@ class UniqueList(list[HT]):
|
|
|
480
888
|
set to 'raise'.
|
|
481
889
|
"""
|
|
482
890
|
if isinstance(indices, slice):
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
'ignore mode while setting slices introduces ambiguous '
|
|
487
|
-
'behaviour and is therefore not supported'
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
duplicates: set[HT] = set(values) & self._set
|
|
491
|
-
if duplicates and values != list(self[indices]):
|
|
492
|
-
raise ValueError(f'Duplicate values: {duplicates}')
|
|
493
|
-
|
|
494
|
-
self._set.update(values)
|
|
891
|
+
self._set_slice(
|
|
892
|
+
indices, typing.cast(collections.abc.Iterable[HT], values)
|
|
893
|
+
)
|
|
495
894
|
else:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
895
|
+
self._set_index(indices, typing.cast(HT, values))
|
|
896
|
+
|
|
897
|
+
def _set_slice(
|
|
898
|
+
self, indices: slice, values: collections.abc.Iterable[HT]
|
|
899
|
+
) -> None:
|
|
900
|
+
"""
|
|
901
|
+
Replaces a slice of the list and keeps the membership in sync.
|
|
902
|
+
|
|
903
|
+
The new values can reuse the items they replace. They cannot repeat
|
|
904
|
+
each other or an item that stays in the list.
|
|
502
905
|
|
|
503
|
-
|
|
906
|
+
Args:
|
|
907
|
+
indices (slice): The slice to replace.
|
|
908
|
+
values (Iterable[HT]): The values to store.
|
|
909
|
+
|
|
910
|
+
Raises:
|
|
911
|
+
RuntimeError: If `on_duplicate` is 'ignore'.
|
|
912
|
+
ValueError: If storing the values would create a duplicate.
|
|
913
|
+
"""
|
|
914
|
+
if self.on_duplicate == 'ignore':
|
|
915
|
+
raise RuntimeError(
|
|
916
|
+
'ignore mode while setting slices introduces ambiguous '
|
|
917
|
+
'behaviour and is therefore not supported'
|
|
918
|
+
)
|
|
504
919
|
|
|
505
|
-
|
|
506
|
-
|
|
920
|
+
new_values: list[HT] = list(values)
|
|
921
|
+
old_values: list[HT] = self[indices]
|
|
922
|
+
duplicates: set[HT] = self._find_duplicates(
|
|
923
|
+
new_values, replaced=set(old_values)
|
|
507
924
|
)
|
|
925
|
+
if duplicates:
|
|
926
|
+
raise ValueError(f'Duplicate values: {duplicates}')
|
|
927
|
+
|
|
928
|
+
# The membership first, for the same reason as in `insert`.
|
|
929
|
+
fresh: set[HT] = set(new_values).difference(self._set)
|
|
930
|
+
self._set.update(fresh)
|
|
931
|
+
try:
|
|
932
|
+
super().__setitem__(indices, new_values)
|
|
933
|
+
except BaseException:
|
|
934
|
+
self._set.difference_update(fresh)
|
|
935
|
+
raise
|
|
936
|
+
|
|
937
|
+
# Only what really left the list. A value that the slice reuses
|
|
938
|
+
# stays a member the whole time.
|
|
939
|
+
self._set.difference_update(set(old_values).difference(new_values))
|
|
940
|
+
|
|
941
|
+
def _set_index(self, index: typing.SupportsIndex, value: HT) -> None:
|
|
942
|
+
"""
|
|
943
|
+
Replaces a single item and keeps the membership in sync.
|
|
944
|
+
|
|
945
|
+
Args:
|
|
946
|
+
index (typing.SupportsIndex): The index to replace.
|
|
947
|
+
value (HT): The value to store.
|
|
948
|
+
|
|
949
|
+
Raises:
|
|
950
|
+
ValueError: If the value is a duplicate of another item and
|
|
951
|
+
`on_duplicate` is set to 'raise'.
|
|
952
|
+
"""
|
|
953
|
+
old_value: HT = self[index]
|
|
954
|
+
known: bool = value in self._set
|
|
955
|
+
if known and value != old_value:
|
|
956
|
+
if self.on_duplicate == 'raise':
|
|
957
|
+
raise ValueError(f'Duplicate value: {value}')
|
|
958
|
+
else:
|
|
959
|
+
return
|
|
960
|
+
|
|
961
|
+
# The membership first, for the same reason as in `insert`.
|
|
962
|
+
self._set.add(value)
|
|
963
|
+
try:
|
|
964
|
+
super().__setitem__(index, value)
|
|
965
|
+
except BaseException:
|
|
966
|
+
if not known:
|
|
967
|
+
self._set.discard(value)
|
|
968
|
+
|
|
969
|
+
raise
|
|
970
|
+
|
|
971
|
+
if not known:
|
|
972
|
+
# A known value replaces itself and stays a member the whole
|
|
973
|
+
# time. For a new value the old one leaves.
|
|
974
|
+
self._release(old_value)
|
|
975
|
+
# The release dropped the new value as well when it is the old
|
|
976
|
+
# object with a changed hash.
|
|
977
|
+
self._set.add(value)
|
|
508
978
|
|
|
509
979
|
def __delitem__(self, index: typing.SupportsIndex | slice) -> None:
|
|
510
980
|
"""
|
|
@@ -514,13 +984,16 @@ class UniqueList(list[HT]):
|
|
|
514
984
|
index (typing.SupportsIndex | slice): The index or slice
|
|
515
985
|
to delete the item(s) at.
|
|
516
986
|
"""
|
|
987
|
+
removed: list[HT]
|
|
517
988
|
if isinstance(index, slice):
|
|
518
|
-
|
|
519
|
-
self._set.remove(value)
|
|
989
|
+
removed = self[index]
|
|
520
990
|
else:
|
|
521
|
-
self
|
|
991
|
+
removed = [self[index]]
|
|
522
992
|
|
|
993
|
+
# The list first, then the membership, like every other mutator.
|
|
523
994
|
super().__delitem__(index)
|
|
995
|
+
for value in removed:
|
|
996
|
+
self._release(value)
|
|
524
997
|
|
|
525
998
|
|
|
526
999
|
# Type hinting `collections.deque` does not work consistently between Python
|
|
@@ -595,10 +1068,33 @@ class SliceableDeque(typing.Generic[T], collections.deque[T]):
|
|
|
595
1068
|
elif isinstance(other, tuple):
|
|
596
1069
|
return tuple(self) == other
|
|
597
1070
|
elif isinstance(other, set):
|
|
598
|
-
|
|
1071
|
+
try:
|
|
1072
|
+
return set(self) == other
|
|
1073
|
+
except TypeError:
|
|
1074
|
+
# An unhashable item cannot be in a set.
|
|
1075
|
+
return False
|
|
599
1076
|
else:
|
|
600
1077
|
return super().__eq__(other)
|
|
601
1078
|
|
|
1079
|
+
def __ne__(self, other: typing.Any) -> bool:
|
|
1080
|
+
"""
|
|
1081
|
+
Checks inequality as the opposite of `__eq__`.
|
|
1082
|
+
|
|
1083
|
+
`collections.deque` has its own `__ne__`, which does not know about
|
|
1084
|
+
the lists, tuples and sets that `__eq__` accepts.
|
|
1085
|
+
|
|
1086
|
+
Args:
|
|
1087
|
+
other (typing.Any): The object to compare with.
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
bool: False if the objects are equal, True otherwise.
|
|
1091
|
+
"""
|
|
1092
|
+
equal: typing.Any = self.__eq__(other)
|
|
1093
|
+
if equal is NotImplemented:
|
|
1094
|
+
return NotImplemented
|
|
1095
|
+
|
|
1096
|
+
return not equal
|
|
1097
|
+
|
|
602
1098
|
def pop(self, index: int = -1) -> T:
|
|
603
1099
|
"""
|
|
604
1100
|
Removes and returns the item at the given index. Only supports index 0
|