python-utils 3.8.2__py2.py3-none-any.whl → 3.9.1__py2.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.
@@ -1,3 +1,59 @@
1
+ """
2
+ This module provides custom container classes with enhanced functionality.
3
+
4
+ Classes:
5
+ CastedDictBase: Abstract base class for dictionaries that cast keys and
6
+ values.
7
+ CastedDict: Dictionary that casts keys and values to specified types.
8
+ LazyCastedDict: Dictionary that lazily casts values to specified types upon
9
+ access.
10
+ UniqueList: List that only allows unique values, with configurable behavior
11
+ on duplicates.
12
+ SliceableDeque: Deque that supports slicing and enhanced equality checks.
13
+
14
+ Type Aliases:
15
+ KT: Type variable for dictionary keys.
16
+ VT: Type variable for dictionary values.
17
+ DT: Type alias for a dictionary with keys of type KT and values of type VT.
18
+ KT_cast: Type alias for a callable that casts dictionary keys.
19
+ VT_cast: Type alias for a callable that casts dictionary values.
20
+ HT: Type variable for hashable values in UniqueList.
21
+ T: Type variable for generic types.
22
+ DictUpdateArgs: Union type for arguments that can be used to update a
23
+ dictionary.
24
+ OnDuplicate: Literal type for handling duplicate values in UniqueList.
25
+
26
+ Usage:
27
+ - CastedDict and LazyCastedDict can be used to create dictionaries with
28
+ automatic type casting.
29
+ - UniqueList ensures all elements are unique and can raise an error on
30
+ duplicates.
31
+ - SliceableDeque extends deque with slicing support and enhanced equality
32
+ checks.
33
+
34
+ Examples:
35
+ >>> d = CastedDict(int, int)
36
+ >>> d[1] = 2
37
+ >>> d['3'] = '4'
38
+ >>> d.update({'5': '6'})
39
+ >>> d.update([('7', '8')])
40
+ >>> d
41
+ {1: 2, 3: 4, 5: 6, 7: 8}
42
+
43
+ >>> l = UniqueList(1, 2, 3)
44
+ >>> l.append(4)
45
+ >>> l.append(4)
46
+ >>> l.insert(0, 4)
47
+ >>> l.insert(0, 5)
48
+ >>> l[1] = 10
49
+ >>> l
50
+ [5, 10, 2, 3, 4]
51
+
52
+ >>> d = SliceableDeque([1, 2, 3, 4, 5])
53
+ >>> d[1:4]
54
+ SliceableDeque([2, 3, 4])
55
+ """
56
+
1
57
  # pyright: reportIncompatibleMethodOverride=false
2
58
  import abc
3
59
  import collections
@@ -35,6 +91,26 @@ OnDuplicate = types.Literal['ignore', 'raise']
35
91
 
36
92
 
37
93
  class CastedDictBase(types.Dict[KT, VT], abc.ABC):
94
+ """
95
+ Abstract base class for dictionaries that cast keys and values.
96
+
97
+ Attributes:
98
+ _key_cast (KT_cast[KT]): Callable to cast dictionary keys.
99
+ _value_cast (VT_cast[VT]): Callable to cast dictionary values.
100
+
101
+ Methods:
102
+ __init__(key_cast: KT_cast[KT] = None, value_cast: VT_cast[VT] = None,
103
+ *args: DictUpdateArgs[KT, VT], **kwargs: VT) -> None:
104
+ Initializes the dictionary with optional key and value casting
105
+ callables.
106
+ update(*args: DictUpdateArgs[types.Any, types.Any],
107
+ **kwargs: types.Any) -> None:
108
+ Updates the dictionary with the given arguments.
109
+ __setitem__(key: types.Any, value: types.Any) -> None:
110
+ Sets the item in the dictionary, casting the key if a key cast
111
+ callable is provided.
112
+ """
113
+
38
114
  _key_cast: KT_cast[KT]
39
115
  _value_cast: VT_cast[VT]
40
116
 
@@ -45,6 +121,20 @@ class CastedDictBase(types.Dict[KT, VT], abc.ABC):
45
121
  *args: DictUpdateArgs[KT, VT],
46
122
  **kwargs: VT,
47
123
  ) -> None:
124
+ """
125
+ Initializes the CastedDictBase with optional key and value
126
+ casting callables.
127
+
128
+ Args:
129
+ key_cast (KT_cast[KT], optional): Callable to cast
130
+ dictionary keys. Defaults to None.
131
+ value_cast (VT_cast[VT], optional): Callable to cast
132
+ dictionary values. Defaults to None.
133
+ *args (DictUpdateArgs[KT, VT]): Arguments to initialize
134
+ the dictionary.
135
+ **kwargs (VT): Keyword arguments to initialize the
136
+ dictionary.
137
+ """
48
138
  self._value_cast = value_cast
49
139
  self._key_cast = key_cast
50
140
  self.update(*args, **kwargs)
@@ -52,6 +142,14 @@ class CastedDictBase(types.Dict[KT, VT], abc.ABC):
52
142
  def update(
53
143
  self, *args: DictUpdateArgs[types.Any, types.Any], **kwargs: types.Any
54
144
  ) -> None:
145
+ """
146
+ Updates the dictionary with the given arguments.
147
+
148
+ Args:
149
+ *args (DictUpdateArgs[types.Any, types.Any]): Arguments to update
150
+ the dictionary.
151
+ **kwargs (types.Any): Keyword arguments to update the dictionary.
152
+ """
55
153
  if args:
56
154
  kwargs.update(*args)
57
155
 
@@ -60,6 +158,14 @@ class CastedDictBase(types.Dict[KT, VT], abc.ABC):
60
158
  self[key] = value
61
159
 
62
160
  def __setitem__(self, key: types.Any, value: types.Any) -> None:
161
+ """
162
+ Sets the item in the dictionary, casting the key if a key cast
163
+ callable is provided.
164
+
165
+ Args:
166
+ key (types.Any): The key to set in the dictionary.
167
+ value (types.Any): The value to set in the dictionary.
168
+ """
63
169
  if self._key_cast is not None:
64
170
  key = self._key_cast(key)
65
171
 
@@ -67,7 +173,7 @@ class CastedDictBase(types.Dict[KT, VT], abc.ABC):
67
173
 
68
174
 
69
175
  class CastedDict(CastedDictBase[KT, VT]):
70
- '''
176
+ """
71
177
  Custom dictionary that casts keys and values to the specified typing.
72
178
 
73
179
  Note that you can specify the types for mypy and type hinting with:
@@ -99,9 +205,10 @@ class CastedDict(CastedDictBase[KT, VT]):
99
205
  >>> d.update([('7', '8')])
100
206
  >>> d
101
207
  {1: 2, '3': '4', '5': '6', '7': '8'}
102
- '''
208
+ """
103
209
 
104
210
  def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
211
+ """Sets `key` to `cast(value)` in the dictionary."""
105
212
  if self._value_cast is not None:
106
213
  value = self._value_cast(value)
107
214
 
@@ -109,7 +216,7 @@ class CastedDict(CastedDictBase[KT, VT]):
109
216
 
110
217
 
111
218
  class LazyCastedDict(CastedDictBase[KT, VT]):
112
- '''
219
+ """
113
220
  Custom dictionary that casts keys and lazily casts values to the specified
114
221
  typing. Note that the values are cast only when they are accessed and
115
222
  are not cached between executions.
@@ -152,15 +259,33 @@ class LazyCastedDict(CastedDictBase[KT, VT]):
152
259
  [(1, 2), ('3', '4'), ('5', '6'), ('7', '8')]
153
260
  >>> d['3']
154
261
  '4'
155
- '''
262
+ """
156
263
 
157
- def __setitem__(self, key: types.Any, value: types.Any):
264
+ def __setitem__(self, key: types.Any, value: types.Any) -> None:
265
+ """
266
+ Sets the item in the dictionary, casting the key if a key cast
267
+ callable is provided.
268
+
269
+ Args:
270
+ key (types.Any): The key to set in the dictionary.
271
+ value (types.Any): The value to set in the dictionary.
272
+ """
158
273
  if self._key_cast is not None:
159
274
  key = self._key_cast(key)
160
275
 
161
276
  super().__setitem__(key, value)
162
277
 
163
278
  def __getitem__(self, key: types.Any) -> VT:
279
+ """
280
+ Gets the item from the dictionary, casting the value if a value cast
281
+ callable is provided.
282
+
283
+ Args:
284
+ key (types.Any): The key to get from the dictionary.
285
+
286
+ Returns:
287
+ VT: The value from the dictionary.
288
+ """
164
289
  if self._key_cast is not None:
165
290
  key = self._key_cast(key)
166
291
 
@@ -171,16 +296,32 @@ class LazyCastedDict(CastedDictBase[KT, VT]):
171
296
 
172
297
  return value
173
298
 
174
- def items( # type: ignore
299
+ def items( # type: ignore[override]
175
300
  self,
176
301
  ) -> types.Generator[types.Tuple[KT, VT], None, None]:
302
+ """
303
+ Returns a generator of the dictionary's items, casting the values if a
304
+ value cast callable is provided.
305
+
306
+ Yields:
307
+ types.Generator[types.Tuple[KT, VT], None, None]: A generator of
308
+ the dictionary's items.
309
+ """
177
310
  if self._value_cast is None:
178
311
  yield from super().items()
179
312
  else:
180
313
  for key, value in super().items():
181
314
  yield key, self._value_cast(value)
182
315
 
183
- def values(self) -> types.Generator[VT, None, None]: # type: ignore
316
+ def values(self) -> types.Generator[VT, None, None]: # type: ignore[override]
317
+ """
318
+ Returns a generator of the dictionary's values, casting the values if a
319
+ value cast callable is provided.
320
+
321
+ Yields:
322
+ types.Generator[VT, None, None]: A generator of the dictionary's
323
+ values.
324
+ """
184
325
  if self._value_cast is None:
185
326
  yield from super().values()
186
327
  else:
@@ -189,7 +330,7 @@ class LazyCastedDict(CastedDictBase[KT, VT]):
189
330
 
190
331
 
191
332
  class UniqueList(types.List[HT]):
192
- '''
333
+ """
193
334
  A list that only allows unique values. Duplicate values are ignored by
194
335
  default, but can be configured to raise an exception instead.
195
336
 
@@ -220,7 +361,7 @@ class UniqueList(types.List[HT]):
220
361
  Traceback (most recent call last):
221
362
  ...
222
363
  ValueError: Duplicate value: 4
223
- '''
364
+ """
224
365
 
225
366
  _set: types.Set[HT]
226
367
 
@@ -229,6 +370,14 @@ class UniqueList(types.List[HT]):
229
370
  *args: HT,
230
371
  on_duplicate: OnDuplicate = 'ignore',
231
372
  ):
373
+ """
374
+ Initializes the UniqueList with optional duplicate handling behavior.
375
+
376
+ Args:
377
+ *args (HT): Initial values for the list.
378
+ on_duplicate (OnDuplicate, optional): Behavior on duplicates.
379
+ Defaults to 'ignore'.
380
+ """
232
381
  self.on_duplicate = on_duplicate
233
382
  self._set = set()
234
383
  super().__init__()
@@ -236,6 +385,17 @@ class UniqueList(types.List[HT]):
236
385
  self.append(arg)
237
386
 
238
387
  def insert(self, index: types.SupportsIndex, value: HT) -> None:
388
+ """
389
+ Inserts a value at the specified index, ensuring uniqueness.
390
+
391
+ Args:
392
+ index (types.SupportsIndex): The index to insert the value at.
393
+ value (HT): The value to insert.
394
+
395
+ Raises:
396
+ ValueError: If the value is a duplicate and `on_duplicate` is set
397
+ to 'raise'.
398
+ """
239
399
  if value in self._set:
240
400
  if self.on_duplicate == 'raise':
241
401
  raise ValueError(f'Duplicate value: {value}')
@@ -246,6 +406,16 @@ class UniqueList(types.List[HT]):
246
406
  super().insert(index, value)
247
407
 
248
408
  def append(self, value: HT) -> None:
409
+ """
410
+ Appends a value to the list, ensuring uniqueness.
411
+
412
+ Args:
413
+ value (HT): The value to append.
414
+
415
+ Raises:
416
+ ValueError: If the value is a duplicate and `on_duplicate` is set
417
+ to 'raise'.
418
+ """
249
419
  if value in self._set:
250
420
  if self.on_duplicate == 'raise':
251
421
  raise ValueError(f'Duplicate value: {value}')
@@ -255,22 +425,46 @@ class UniqueList(types.List[HT]):
255
425
  self._set.add(value)
256
426
  super().append(value)
257
427
 
258
- def __contains__(self, item: HT) -> bool: # type: ignore
428
+ def __contains__(self, item: HT) -> bool: # type: ignore[override]
429
+ """
430
+ Checks if the list contains the specified item.
431
+
432
+ Args:
433
+ item (HT): The item to check for.
434
+
435
+ Returns:
436
+ bool: True if the item is in the list, False otherwise.
437
+ """
259
438
  return item in self._set
260
439
 
261
440
  @typing.overload
262
- def __setitem__(self, indices: types.SupportsIndex, values: HT) -> None:
263
- ...
441
+ def __setitem__(
442
+ self, indices: types.SupportsIndex, values: HT
443
+ ) -> None: ...
264
444
 
265
445
  @typing.overload
266
- def __setitem__(self, indices: slice, values: types.Iterable[HT]) -> None:
267
- ...
446
+ def __setitem__(
447
+ self, indices: slice, values: types.Iterable[HT]
448
+ ) -> None: ...
268
449
 
269
450
  def __setitem__(
270
451
  self,
271
452
  indices: types.Union[slice, types.SupportsIndex],
272
453
  values: types.Union[types.Iterable[HT], HT],
273
454
  ) -> None:
455
+ """
456
+ Sets the item(s) at the specified index/indices, ensuring uniqueness.
457
+
458
+ Args:
459
+ indices (types.Union[slice, types.SupportsIndex]): The index or
460
+ slice to set the value(s) at.
461
+ values (types.Union[types.Iterable[HT], HT]): The value(s) to set.
462
+
463
+ Raises:
464
+ RuntimeError: If `on_duplicate` is 'ignore' and setting slices.
465
+ ValueError: If the value(s) are duplicates and `on_duplicate` is
466
+ set to 'raise'.
467
+ """
274
468
  if isinstance(indices, slice):
275
469
  values = types.cast(types.Iterable[HT], values)
276
470
  if self.on_duplicate == 'ignore':
@@ -301,6 +495,13 @@ class UniqueList(types.List[HT]):
301
495
  def __delitem__(
302
496
  self, index: types.Union[types.SupportsIndex, slice]
303
497
  ) -> None:
498
+ """
499
+ Deletes the item(s) at the specified index/indices.
500
+
501
+ Args:
502
+ index (types.Union[types.SupportsIndex, slice]): The index or slice
503
+ to delete the item(s) at.
504
+ """
304
505
  if isinstance(index, slice):
305
506
  for value in self[index]:
306
507
  self._set.remove(value)
@@ -312,38 +513,68 @@ class UniqueList(types.List[HT]):
312
513
 
313
514
  # Type hinting `collections.deque` does not work consistently between Python
314
515
  # runtime, mypy and pyright currently so we have to ignore the errors
315
- class SliceableDeque(types.Generic[T], collections.deque): # type: ignore
516
+ class SliceableDeque(types.Generic[T], collections.deque[T]):
517
+ """
518
+ A deque that supports slicing and enhanced equality checks.
519
+
520
+ Methods:
521
+ __getitem__(index: types.Union[types.SupportsIndex, slice]) ->
522
+ types.Union[T, 'SliceableDeque[T]']:
523
+ Returns the item or slice at the given index.
524
+ __eq__(other: types.Any) -> bool:
525
+ Checks equality with another object, allowing for comparison with
526
+ lists, tuples, and sets.
527
+ pop(index: int = -1) -> T:
528
+ Removes and returns the item at the given index. Only supports
529
+ index 0 and the last index.
530
+ """
531
+
316
532
  @typing.overload
317
- def __getitem__(self, index: types.SupportsIndex) -> T:
318
- ...
533
+ def __getitem__(self, index: types.SupportsIndex) -> T: ...
319
534
 
320
535
  @typing.overload
321
- def __getitem__(self, index: slice) -> 'SliceableDeque[T]':
322
- ...
536
+ def __getitem__(self, index: slice) -> 'SliceableDeque[T]': ...
323
537
 
324
538
  def __getitem__(
325
539
  self, index: types.Union[types.SupportsIndex, slice]
326
540
  ) -> types.Union[T, 'SliceableDeque[T]']:
327
- '''
541
+ """
328
542
  Return the item or slice at the given index.
329
543
 
330
- >>> d = SliceableDeque[int]([1, 2, 3, 4, 5])
331
- >>> d[1:4]
332
- SliceableDeque([2, 3, 4])
544
+ Args:
545
+ index (types.Union[types.SupportsIndex, slice]): The index or
546
+ slice to retrieve.
547
+
548
+ Returns:
549
+ types.Union[T, 'SliceableDeque[T]']: The item or slice at the
550
+ given index.
333
551
 
334
- >>> d = SliceableDeque[str](['a', 'b', 'c'])
335
- >>> d[-2:]
336
- SliceableDeque(['b', 'c'])
552
+ Examples:
553
+ >>> d = SliceableDeque[int]([1, 2, 3, 4, 5])
554
+ >>> d[1:4]
555
+ SliceableDeque([2, 3, 4])
337
556
 
338
- '''
557
+ >>> d = SliceableDeque[str](['a', 'b', 'c'])
558
+ >>> d[-2:]
559
+ SliceableDeque(['b', 'c'])
560
+ """
339
561
  if isinstance(index, slice):
340
562
  start, stop, step = index.indices(len(self))
341
563
  return self.__class__(self[i] for i in range(start, stop, step))
342
564
  else:
343
- return types.cast(T, super().__getitem__(index))
565
+ return super().__getitem__(index)
344
566
 
345
567
  def __eq__(self, other: types.Any) -> bool:
346
- # Allow for comparison with a list or tuple
568
+ """
569
+ Checks equality with another object, allowing for comparison with
570
+ lists, tuples, and sets.
571
+
572
+ Args:
573
+ other (types.Any): The object to compare with.
574
+
575
+ Returns:
576
+ bool: True if the objects are equal, False otherwise.
577
+ """
347
578
  if isinstance(other, list):
348
579
  return list(self) == other
349
580
  elif isinstance(other, tuple):
@@ -354,12 +585,31 @@ class SliceableDeque(types.Generic[T], collections.deque): # type: ignore
354
585
  return super().__eq__(other)
355
586
 
356
587
  def pop(self, index: int = -1) -> T:
357
- # We need to allow for an index but a deque only allows the removal of
358
- # the first or last item.
588
+ """
589
+ Removes and returns the item at the given index. Only supports index 0
590
+ and the last index.
591
+
592
+ Args:
593
+ index (int, optional): The index of the item to remove. Defaults to
594
+ -1.
595
+
596
+ Returns:
597
+ T: The removed item.
598
+
599
+ Raises:
600
+ IndexError: If the index is not 0 or the last index.
601
+
602
+ Examples:
603
+ >>> d = SliceableDeque([1, 2, 3])
604
+ >>> d.pop(0)
605
+ 1
606
+ >>> d.pop()
607
+ 3
608
+ """
359
609
  if index == 0:
360
- return typing.cast(T, super().popleft())
610
+ return super().popleft()
361
611
  elif index in {-1, len(self) - 1}:
362
- return typing.cast(T, super().pop())
612
+ return super().pop()
363
613
  else:
364
614
  raise IndexError(
365
615
  'Only index 0 and the last index (`N-1` or `-1`) '