pkstruct 0.1.0__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.
Files changed (72) hide show
  1. pkstruct/__init__.py +167 -0
  2. pkstruct/graphs/__init__.py +127 -0
  3. pkstruct/graphs/connectivity.py +157 -0
  4. pkstruct/graphs/directed.py +95 -0
  5. pkstruct/graphs/exceptions.py +63 -0
  6. pkstruct/graphs/graph.py +262 -0
  7. pkstruct/graphs/mst.py +118 -0
  8. pkstruct/graphs/scc.py +138 -0
  9. pkstruct/graphs/shortest_path.py +250 -0
  10. pkstruct/graphs/topo_sort.py +108 -0
  11. pkstruct/graphs/traversal.py +175 -0
  12. pkstruct/graphs/visualization.py +90 -0
  13. pkstruct/graphs/weighted.py +37 -0
  14. pkstruct/linear/__init__.py +95 -0
  15. pkstruct/linear/deques/__init__.py +33 -0
  16. pkstruct/linear/deques/deque.py +194 -0
  17. pkstruct/linear/deques/linked_deque.py +198 -0
  18. pkstruct/linear/exceptions.py +26 -0
  19. pkstruct/linear/linked_lists/__init__.py +5 -0
  20. pkstruct/linear/linked_lists/_base.py +608 -0
  21. pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
  22. pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
  23. pkstruct/linear/linked_lists/nodes.py +68 -0
  24. pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
  25. pkstruct/linear/queues/__init__.py +44 -0
  26. pkstruct/linear/queues/circular_queue.py +258 -0
  27. pkstruct/linear/queues/linked_queue.py +186 -0
  28. pkstruct/linear/queues/priority_queue.py +202 -0
  29. pkstruct/linear/queues/queue.py +174 -0
  30. pkstruct/linear/stacks/__init__.py +38 -0
  31. pkstruct/linear/stacks/array_stack.py +165 -0
  32. pkstruct/linear/stacks/linked_stack.py +168 -0
  33. pkstruct/linear/stacks/stack.py +158 -0
  34. pkstruct/linear/utils/__init__.py +18 -0
  35. pkstruct/linear/utils/benchmark.py +255 -0
  36. pkstruct/linear/utils/debug_tools.py +239 -0
  37. pkstruct/linear/utils/helpers.py +143 -0
  38. pkstruct/linear/utils/iterators.py +148 -0
  39. pkstruct/linear/visualization/__init__.py +0 -0
  40. pkstruct/linear/visualization/ascii_visualizer.py +114 -0
  41. pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
  42. pkstruct/shared/__init__.py +67 -0
  43. pkstruct/shared/benchmarking/__init__.py +78 -0
  44. pkstruct/shared/debugging/__init__.py +69 -0
  45. pkstruct/shared/exceptions/__init__.py +59 -0
  46. pkstruct/shared/serializers/__init__.py +65 -0
  47. pkstruct/shared/threading/__init__.py +43 -0
  48. pkstruct/shared/validators/__init__.py +98 -0
  49. pkstruct/shared/visualization/__init__.py +21 -0
  50. pkstruct/trees/__init__.py +92 -0
  51. pkstruct/trees/avl.py +321 -0
  52. pkstruct/trees/balancing.py +253 -0
  53. pkstruct/trees/bplus.py +425 -0
  54. pkstruct/trees/bst.py +948 -0
  55. pkstruct/trees/btree.py +504 -0
  56. pkstruct/trees/exceptions.py +96 -0
  57. pkstruct/trees/fenwick_tree.py +312 -0
  58. pkstruct/trees/interval_tree.py +541 -0
  59. pkstruct/trees/node.py +356 -0
  60. pkstruct/trees/red_black.py +710 -0
  61. pkstruct/trees/segment_tree.py +398 -0
  62. pkstruct/trees/traversal.py +456 -0
  63. pkstruct/trees/tree_helpers.py +366 -0
  64. pkstruct/trees/utils/__init__.py +15 -0
  65. pkstruct/trees/utils/complexity_helpers.py +231 -0
  66. pkstruct/trees/visualization/__init__.py +0 -0
  67. pkstruct/trees/visualization/ascii_renderer.py +220 -0
  68. pkstruct/trees/visualization/tree_printer.py +129 -0
  69. pkstruct-0.1.0.dist-info/METADATA +482 -0
  70. pkstruct-0.1.0.dist-info/RECORD +72 -0
  71. pkstruct-0.1.0.dist-info/WHEEL +4 -0
  72. pkstruct-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,608 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable, Iterator
5
+ from typing import Any, Generic, TypeVar, cast
6
+
7
+ from pkstruct.linear.exceptions import (
8
+ EmptyStructureError,
9
+ IndexOutOfRangeError,
10
+ ValidationError,
11
+ ValueNotFoundError,
12
+ )
13
+ from pkstruct.shared.debugging import DebugTracer
14
+ from pkstruct.shared.serializers import deserialize_from_json, serialize_to_json
15
+ from pkstruct.shared.threading import StructureLock
16
+ from pkstruct.shared.validators import validate_index, validate_range
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ class _LinkedListBase(Generic[T], ABC):
22
+ __slots__ = ("_head", "_tail", "_size", "_lock", "_tracer")
23
+
24
+ def __init__(self) -> None:
25
+ self._head: Any = None
26
+ self._tail: Any = None
27
+ self._size: int = 0
28
+ self._lock: StructureLock = StructureLock()
29
+ self._tracer: DebugTracer = DebugTracer()
30
+
31
+ # ------------------------------------------------------------------ #
32
+ # Abstract node operations #
33
+ # ------------------------------------------------------------------ #
34
+
35
+ @abstractmethod
36
+ def _append(self, value: T) -> None: ...
37
+
38
+ @abstractmethod
39
+ def _prepend(self, value: T) -> None: ...
40
+
41
+ @abstractmethod
42
+ def _node_at(self, index: int) -> Any: ...
43
+
44
+ @abstractmethod
45
+ def _find_node(self, value: object) -> Any | None: ...
46
+
47
+ @abstractmethod
48
+ def _remove_node(self, node: Any) -> T: ...
49
+
50
+ @abstractmethod
51
+ def _insert_before_node(self, node: Any, value: T) -> None: ...
52
+
53
+ @abstractmethod
54
+ def _insert_after_node(self, node: Any, value: T) -> None: ...
55
+
56
+ @abstractmethod
57
+ def _to_list_unsafe(self) -> list[T]: ...
58
+
59
+ # ------------------------------------------------------------------ #
60
+ # Construction helpers #
61
+ # ------------------------------------------------------------------ #
62
+
63
+ @classmethod
64
+ def create(cls) -> _LinkedListBase[T]:
65
+ return cls()
66
+
67
+ @classmethod
68
+ def from_list(cls, items: list[T]) -> _LinkedListBase[T]:
69
+ if not isinstance(items, list):
70
+ raise ValidationError(f"'items' must be a list, got {type(items).__name__!r}.")
71
+ obj: _LinkedListBase[T] = cls()
72
+ for item in items:
73
+ obj._append(item)
74
+ return obj
75
+
76
+ @classmethod
77
+ def from_json(cls, json_str: str) -> _LinkedListBase[T]:
78
+ items: list[T] = deserialize_from_json(json_str)
79
+ return cls.from_list(items)
80
+
81
+ def to_json(self) -> str:
82
+ return serialize_to_json(self._to_list_unsafe())
83
+
84
+ def copy(self) -> _LinkedListBase[T]:
85
+ with self._lock:
86
+ return self.from_list(self._to_list_unsafe())
87
+
88
+ # ------------------------------------------------------------------ #
89
+ # Insertion #
90
+ # ------------------------------------------------------------------ #
91
+
92
+ def insert(
93
+ self,
94
+ value: T,
95
+ position: int | None = None,
96
+ before: T | None = None,
97
+ after: T | None = None,
98
+ ) -> None:
99
+ provided = sum(x is not None for x in (position, before, after))
100
+ if provided > 1:
101
+ raise ValidationError("Provide at most one of 'position', 'before', or 'after'.")
102
+ with self._lock:
103
+ self._tracer.record("insert", value=value, position=position, before=before, after=after)
104
+
105
+ if before is not None:
106
+ target = self._find_node(before)
107
+ if target is None:
108
+ raise ValueNotFoundError(before)
109
+ if target is self._head:
110
+ self._prepend(value)
111
+ else:
112
+ self._insert_before_node(target, value)
113
+ return
114
+
115
+ if after is not None:
116
+ target = self._find_node(after)
117
+ if target is None:
118
+ raise ValueNotFoundError(after)
119
+ self._insert_after_node(target, value)
120
+ return
121
+
122
+ if position is None:
123
+ self._append(value)
124
+ return
125
+
126
+ if position < 0:
127
+ raise IndexOutOfRangeError(position, self._size)
128
+ if position == 0:
129
+ self._prepend(value)
130
+ return
131
+ if position > self._size:
132
+ raise IndexOutOfRangeError(position, self._size)
133
+ if position == self._size:
134
+ self._append(value)
135
+ return
136
+
137
+ target = self._node_at(position)
138
+ self._insert_before_node(target, value)
139
+
140
+ def extend(self, values: list[T] | _LinkedListBase[T]) -> None:
141
+ if isinstance(values, _LinkedListBase):
142
+ items: list[T] = values._to_list_unsafe()
143
+ elif isinstance(values, list):
144
+ items = values
145
+ else:
146
+ raise ValidationError(
147
+ f"'values' must be a list or linked list, got {type(values).__name__!r}."
148
+ )
149
+ with self._lock:
150
+ for item in items:
151
+ self._append(item)
152
+
153
+ def merge(self, *others: _LinkedListBase[T]) -> None:
154
+ with self._lock:
155
+ for other in others:
156
+ with other._lock:
157
+ values = other._to_list_unsafe()
158
+ for v in values:
159
+ self._append(v)
160
+
161
+ # ------------------------------------------------------------------ #
162
+ # Deletion #
163
+ # ------------------------------------------------------------------ #
164
+
165
+ def delete(
166
+ self,
167
+ value: T | None = None,
168
+ position: int | None = None,
169
+ rng: tuple[int, int] | None = None,
170
+ ) -> T | list[T] | None:
171
+ provided = sum(x is not None for x in (value, position, rng))
172
+ if provided == 0:
173
+ raise ValidationError("Provide one of 'value', 'position', or 'rng'.")
174
+ if provided > 1:
175
+ raise ValidationError("Provide only one of 'value', 'position', or 'rng'.")
176
+ with self._lock:
177
+ if self._size == 0:
178
+ raise EmptyStructureError("delete")
179
+ self._tracer.record("delete", value=value, position=position, rng=rng)
180
+
181
+ if value is not None:
182
+ node = self._find_node(value)
183
+ if node is None:
184
+ raise ValueNotFoundError(value)
185
+ return self._remove_node(node)
186
+
187
+ if position is not None:
188
+ normalised = position if position >= 0 else position + self._size
189
+ if not (0 <= normalised < self._size):
190
+ raise IndexOutOfRangeError(position, self._size)
191
+ node = self._node_at(normalised)
192
+ return self._remove_node(node)
193
+
194
+ if rng is None:
195
+ raise RuntimeError("internal invariant violated: rng should not be None in the range-delete branch")
196
+ start, end = rng
197
+ validate_range(start, end, self._size)
198
+ removed: list[T] = []
199
+ for _ in range(end - start + 1):
200
+ node = self._node_at(start)
201
+ removed.append(self._remove_node(node))
202
+ return removed
203
+
204
+ def clear(self) -> None:
205
+ with self._lock:
206
+ self._head = None
207
+ self._tail = None
208
+ self._size = 0
209
+ self._tracer.record("clear")
210
+
211
+ # ------------------------------------------------------------------ #
212
+ # Access #
213
+ # ------------------------------------------------------------------ #
214
+
215
+ def get(self, position: int, from_end: bool = False) -> T:
216
+ with self._lock:
217
+ if self._size == 0:
218
+ raise EmptyStructureError("get")
219
+ if from_end:
220
+ if position < 0:
221
+ raise IndexOutOfRangeError(position, self._size)
222
+ validate_index(position, self._size)
223
+ idx = self._size - 1 - position
224
+ return cast(T, self._node_at(idx).value)
225
+ if position < 0:
226
+ raise IndexOutOfRangeError(position, self._size)
227
+ validate_index(position, self._size)
228
+ return cast(T, self._node_at(position).value)
229
+
230
+ def to_list(self) -> list[T]:
231
+ with self._lock:
232
+ return self._to_list_unsafe()
233
+
234
+ def size(self) -> int:
235
+ with self._lock:
236
+ return self._size
237
+
238
+ def is_empty(self) -> bool:
239
+ with self._lock:
240
+ return self._size == 0
241
+
242
+ def count(self, value: T) -> int:
243
+ with self._lock:
244
+ n = 0
245
+ node = self._head
246
+ for _ in range(self._size):
247
+ if node.value == value:
248
+ n += 1
249
+ node = node.next
250
+ return n
251
+
252
+ # ------------------------------------------------------------------ #
253
+ # Search #
254
+ # ------------------------------------------------------------------ #
255
+
256
+ def index(self, value: T) -> int:
257
+ with self._lock:
258
+ node = self._head
259
+ for i in range(self._size):
260
+ if node.value == value:
261
+ return i
262
+ node = node.next
263
+ raise ValueNotFoundError(value)
264
+
265
+ # ------------------------------------------------------------------ #
266
+ # Replacement #
267
+ # ------------------------------------------------------------------ #
268
+
269
+ def replace(
270
+ self,
271
+ new_value: T,
272
+ old_value: T | None = None,
273
+ position: int | None = None,
274
+ replace_all: bool = False,
275
+ ) -> int:
276
+ if new_value is None:
277
+ raise ValidationError("'new_value' is required.")
278
+ with self._lock:
279
+ self._tracer.record(
280
+ "replace", old_value=old_value, new_value=new_value, position=position
281
+ )
282
+ if position is not None:
283
+ idx = position if position >= 0 else position + self._size
284
+ validate_index(idx, self._size)
285
+ self._node_at(idx).value = new_value
286
+ return 1
287
+ if old_value is None:
288
+ raise ValidationError("Provide 'old_value' or 'position'.")
289
+ count = 0
290
+ node = self._head
291
+ for _ in range(self._size):
292
+ if node.value == old_value:
293
+ node.value = new_value
294
+ count += 1
295
+ if not replace_all:
296
+ break
297
+ node = node.next
298
+ if count == 0:
299
+ raise ValueNotFoundError(old_value)
300
+ return count
301
+
302
+ # ------------------------------------------------------------------ #
303
+ # Reverse & Rotation #
304
+ # ------------------------------------------------------------------ #
305
+
306
+ def reverse(self, start: int | None = None, end: int | None = None) -> None:
307
+ with self._lock:
308
+ if self._size == 0:
309
+ raise EmptyStructureError("reverse an empty list")
310
+ lo = 0 if start is None else start
311
+ hi = self._size - 1 if end is None else end
312
+ if lo >= hi:
313
+ return
314
+ validate_range(lo, hi, self._size)
315
+ self._tracer.record("reverse", start=lo, end=hi)
316
+ nodes: list[Any] = []
317
+ node = self._node_at(lo)
318
+ for _ in range(hi - lo + 1):
319
+ nodes.append(node)
320
+ node = node.next
321
+ left, right = 0, len(nodes) - 1
322
+ while left < right:
323
+ nodes[left].value, nodes[right].value = nodes[right].value, nodes[left].value
324
+ left += 1
325
+ right -= 1
326
+
327
+ def rotate(
328
+ self,
329
+ shift: int = 1,
330
+ start: int | None = None,
331
+ end: int | None = None,
332
+ direction: bool = True,
333
+ ) -> None:
334
+ with self._lock:
335
+ if self._size == 0:
336
+ raise EmptyStructureError("rotate an empty list")
337
+
338
+ actual_shift = shift
339
+ if shift < 0:
340
+ actual_shift = abs(shift)
341
+ direction = False
342
+
343
+ actual_start = 0 if start is None else start
344
+ actual_end = self._size - 1 if end is None else end
345
+
346
+ if actual_start < 0:
347
+ actual_start = self._size + actual_start
348
+ if actual_end < 0:
349
+ actual_end = self._size + actual_end
350
+
351
+ validate_range(actual_start, actual_end, self._size)
352
+
353
+ length = actual_end - actual_start + 1
354
+ if length <= 1:
355
+ return
356
+
357
+ effective_shift = actual_shift % length
358
+ if effective_shift == 0:
359
+ return
360
+
361
+ if not direction:
362
+ effective_shift = length - effective_shift
363
+
364
+ self._tracer.record(
365
+ "rotate", shift=actual_shift, start=actual_start, end=actual_end, direction=direction
366
+ )
367
+
368
+ if actual_start == 0 and actual_end == self._size - 1:
369
+ self._rotate_full_list(effective_shift)
370
+ else:
371
+ self._rotate_subrange(actual_start, actual_end, effective_shift)
372
+
373
+ def _rotate_full_list(self, shift: int) -> None:
374
+ if shift == 0 or self._size <= 1:
375
+ return
376
+ values = self._to_list_unsafe()
377
+ rotated = values[-shift:] + values[:-shift]
378
+ node = self._head
379
+ for v in rotated:
380
+ node.value = v
381
+ node = node.next
382
+
383
+ def _rotate_subrange(self, start: int, end: int, shift: int) -> None:
384
+ nodes: list[Any] = []
385
+ node = self._node_at(start)
386
+ for _ in range(end - start + 1):
387
+ nodes.append(node)
388
+ node = node.next
389
+ values = [n.value for n in nodes]
390
+ rotated = values[-shift:] + values[:-shift]
391
+ for n, v in zip(nodes, rotated, strict=True):
392
+ n.value = v
393
+
394
+ # ------------------------------------------------------------------ #
395
+ # Swapping #
396
+ # ------------------------------------------------------------------ #
397
+
398
+ def swap(
399
+ self,
400
+ value1: T | None = None,
401
+ value2: T | None = None,
402
+ pos1: int | None = None,
403
+ pos2: int | None = None,
404
+ pairwise: bool = False,
405
+ ) -> None:
406
+ with self._lock:
407
+ self._tracer.record(
408
+ "swap", value1=value1, value2=value2, pos1=pos1, pos2=pos2, pairwise=pairwise
409
+ )
410
+ if pairwise:
411
+ node = self._head
412
+ for _ in range(self._size // 2):
413
+ node.value, node.next.value = node.next.value, node.value
414
+ node = node.next.next
415
+ return
416
+ if self._size == 0:
417
+ raise EmptyStructureError("swap")
418
+ if value1 is not None and value2 is not None:
419
+ n1 = self._find_node(value1)
420
+ n2 = self._find_node(value2)
421
+ if n1 is None:
422
+ raise ValueNotFoundError(value1)
423
+ if n2 is None:
424
+ raise ValueNotFoundError(value2)
425
+ n1.value, n2.value = n2.value, n1.value
426
+ return
427
+ if pos1 is not None and pos2 is not None:
428
+ i1 = pos1 if pos1 >= 0 else pos1 + self._size
429
+ i2 = pos2 if pos2 >= 0 else pos2 + self._size
430
+ validate_index(i1, self._size)
431
+ validate_index(i2, self._size)
432
+ n1 = self._node_at(i1)
433
+ n2 = self._node_at(i2)
434
+ n1.value, n2.value = n2.value, n1.value
435
+ return
436
+ raise ValidationError("Provide (value1, value2), (pos1, pos2), or pairwise=True.")
437
+
438
+ # ------------------------------------------------------------------ #
439
+ # Sort & Partition #
440
+ # ------------------------------------------------------------------ #
441
+
442
+ def sort(
443
+ self,
444
+ reverse: bool = False,
445
+ key: Callable[[T], Any] | None = None,
446
+ ) -> None:
447
+ with self._lock:
448
+ if self._size <= 1:
449
+ return
450
+ self._tracer.record("sort", reverse=reverse)
451
+ values = self._to_list_unsafe()
452
+ values.sort(key=key, reverse=reverse)
453
+ node = self._head
454
+ for v in values:
455
+ node.value = v
456
+ node = node.next
457
+
458
+ def partition(self, predicate_or_pivot: Callable[[T], bool] | T) -> None:
459
+ with self._lock:
460
+ if self._size <= 1:
461
+ return
462
+ if callable(predicate_or_pivot):
463
+ pred = predicate_or_pivot
464
+ else:
465
+ pivot = predicate_or_pivot
466
+ def pred(x: T) -> bool:
467
+ return cast(bool, x < pivot) # type: ignore[operator]
468
+ values = self._to_list_unsafe()
469
+ left = [v for v in values if pred(v)]
470
+ right = [v for v in values if not pred(v)]
471
+ new_values = left + right
472
+ node = self._head
473
+ for v in new_values:
474
+ node.value = v
475
+ node = node.next
476
+
477
+ # ------------------------------------------------------------------ #
478
+ # Interview problems #
479
+ # ------------------------------------------------------------------ #
480
+
481
+ def detect_cycle(self, return_start: bool = False) -> bool | tuple[bool, Any]:
482
+ with self._lock:
483
+ slow = self._head
484
+ fast = self._head
485
+ while fast is not None and fast.next is not None:
486
+ slow = slow.next
487
+ fast = fast.next.next
488
+ if slow is fast:
489
+ if not return_start:
490
+ return True
491
+ slow = self._head
492
+ while slow is not fast:
493
+ slow = slow.next
494
+ fast = fast.next
495
+ return True, slow.value
496
+ if return_start:
497
+ return False, None
498
+ return False
499
+
500
+ def palindrome(self) -> bool:
501
+ with self._lock:
502
+ if self._size == 0:
503
+ raise EmptyStructureError("palindrome on empty list")
504
+ values = self._to_list_unsafe()
505
+ return values == values[::-1]
506
+
507
+ def reorder(self, mode: str = "odd_even") -> None:
508
+ with self._lock:
509
+ if self._size <= 1:
510
+ return
511
+ self._tracer.record("reorder", mode=mode)
512
+ if mode == "odd_even":
513
+ values = self._to_list_unsafe()
514
+ odd_vals = [v for i, v in enumerate(values) if i % 2 == 1]
515
+ even_vals = [v for i, v in enumerate(values) if i % 2 == 0]
516
+ reordered = odd_vals + even_vals
517
+ node = self._head
518
+ for v in reordered:
519
+ node.value = v
520
+ node = node.next
521
+ elif mode == "zigzag":
522
+ nodes: list[Any] = []
523
+ cur = self._head
524
+ while cur is not None:
525
+ nodes.append(cur)
526
+ cur = cur.next
527
+ for i in range(len(nodes) - 1):
528
+ a, b = nodes[i].value, nodes[i + 1].value
529
+ if i % 2 == 0:
530
+ if a > b:
531
+ nodes[i].value, nodes[i + 1].value = b, a
532
+ else:
533
+ if a < b:
534
+ nodes[i].value, nodes[i + 1].value = b, a
535
+ else:
536
+ raise ValidationError(
537
+ f"Unknown reorder mode {mode!r}. Choose 'odd_even' or 'zigzag'."
538
+ )
539
+
540
+ def segregate_even_odd(self) -> None:
541
+ with self._lock:
542
+ if self._size <= 1:
543
+ return
544
+ self._tracer.record("segregate_even_odd")
545
+ evens: list[Any] = []
546
+ odds: list[Any] = []
547
+ node = self._head
548
+ for _ in range(self._size):
549
+ try:
550
+ if isinstance(node.value, int) and node.value % 2 == 0:
551
+ evens.append(node.value)
552
+ else:
553
+ odds.append(node.value)
554
+ except (TypeError, ValueError):
555
+ odds.append(node.value)
556
+ node = node.next
557
+ all_values = evens + odds
558
+ node = self._head
559
+ for v in all_values:
560
+ node.value = v
561
+ node = node.next
562
+
563
+ # ------------------------------------------------------------------ #
564
+ # Public properties #
565
+ # ------------------------------------------------------------------ #
566
+
567
+ @property
568
+ def head(self) -> Any:
569
+ with self._lock:
570
+ return self._head
571
+
572
+ @property
573
+ def tail(self) -> Any:
574
+ with self._lock:
575
+ return self._tail
576
+
577
+ # ------------------------------------------------------------------ #
578
+ # Dunder methods #
579
+ # ------------------------------------------------------------------ #
580
+
581
+ def __iter__(self) -> Iterator[T]:
582
+ with self._lock:
583
+ snapshot = self._to_list_unsafe()
584
+ return iter(snapshot)
585
+
586
+ def __len__(self) -> int:
587
+ return self.size()
588
+
589
+ def __bool__(self) -> bool:
590
+ return self.size() > 0
591
+
592
+ def __repr__(self) -> str:
593
+ return f"{type(self).__name__}({self.to_list()!r})"
594
+
595
+ def __contains__(self, item: object) -> bool:
596
+ with self._lock:
597
+ return self._find_node(item) is not None
598
+
599
+ def __getitem__(self, index: int) -> T:
600
+ return self.get(index)
601
+
602
+ def __setitem__(self, index: int, value: T) -> None:
603
+ self.replace(new_value=value, position=index)
604
+
605
+ def __eq__(self, other: object) -> bool:
606
+ if type(other) is not type(self):
607
+ return NotImplemented
608
+ return self.to_list() == other.to_list()