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.
- pkstruct/__init__.py +167 -0
- pkstruct/graphs/__init__.py +127 -0
- pkstruct/graphs/connectivity.py +157 -0
- pkstruct/graphs/directed.py +95 -0
- pkstruct/graphs/exceptions.py +63 -0
- pkstruct/graphs/graph.py +262 -0
- pkstruct/graphs/mst.py +118 -0
- pkstruct/graphs/scc.py +138 -0
- pkstruct/graphs/shortest_path.py +250 -0
- pkstruct/graphs/topo_sort.py +108 -0
- pkstruct/graphs/traversal.py +175 -0
- pkstruct/graphs/visualization.py +90 -0
- pkstruct/graphs/weighted.py +37 -0
- pkstruct/linear/__init__.py +95 -0
- pkstruct/linear/deques/__init__.py +33 -0
- pkstruct/linear/deques/deque.py +194 -0
- pkstruct/linear/deques/linked_deque.py +198 -0
- pkstruct/linear/exceptions.py +26 -0
- pkstruct/linear/linked_lists/__init__.py +5 -0
- pkstruct/linear/linked_lists/_base.py +608 -0
- pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
- pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
- pkstruct/linear/linked_lists/nodes.py +68 -0
- pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
- pkstruct/linear/queues/__init__.py +44 -0
- pkstruct/linear/queues/circular_queue.py +258 -0
- pkstruct/linear/queues/linked_queue.py +186 -0
- pkstruct/linear/queues/priority_queue.py +202 -0
- pkstruct/linear/queues/queue.py +174 -0
- pkstruct/linear/stacks/__init__.py +38 -0
- pkstruct/linear/stacks/array_stack.py +165 -0
- pkstruct/linear/stacks/linked_stack.py +168 -0
- pkstruct/linear/stacks/stack.py +158 -0
- pkstruct/linear/utils/__init__.py +18 -0
- pkstruct/linear/utils/benchmark.py +255 -0
- pkstruct/linear/utils/debug_tools.py +239 -0
- pkstruct/linear/utils/helpers.py +143 -0
- pkstruct/linear/utils/iterators.py +148 -0
- pkstruct/linear/visualization/__init__.py +0 -0
- pkstruct/linear/visualization/ascii_visualizer.py +114 -0
- pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
- pkstruct/shared/__init__.py +67 -0
- pkstruct/shared/benchmarking/__init__.py +78 -0
- pkstruct/shared/debugging/__init__.py +69 -0
- pkstruct/shared/exceptions/__init__.py +59 -0
- pkstruct/shared/serializers/__init__.py +65 -0
- pkstruct/shared/threading/__init__.py +43 -0
- pkstruct/shared/validators/__init__.py +98 -0
- pkstruct/shared/visualization/__init__.py +21 -0
- pkstruct/trees/__init__.py +92 -0
- pkstruct/trees/avl.py +321 -0
- pkstruct/trees/balancing.py +253 -0
- pkstruct/trees/bplus.py +425 -0
- pkstruct/trees/bst.py +948 -0
- pkstruct/trees/btree.py +504 -0
- pkstruct/trees/exceptions.py +96 -0
- pkstruct/trees/fenwick_tree.py +312 -0
- pkstruct/trees/interval_tree.py +541 -0
- pkstruct/trees/node.py +356 -0
- pkstruct/trees/red_black.py +710 -0
- pkstruct/trees/segment_tree.py +398 -0
- pkstruct/trees/traversal.py +456 -0
- pkstruct/trees/tree_helpers.py +366 -0
- pkstruct/trees/utils/__init__.py +15 -0
- pkstruct/trees/utils/complexity_helpers.py +231 -0
- pkstruct/trees/visualization/__init__.py +0 -0
- pkstruct/trees/visualization/ascii_renderer.py +220 -0
- pkstruct/trees/visualization/tree_printer.py +129 -0
- pkstruct-0.1.0.dist-info/METADATA +482 -0
- pkstruct-0.1.0.dist-info/RECORD +72 -0
- pkstruct-0.1.0.dist-info/WHEEL +4 -0
- pkstruct-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Iterator utilities for pkstruct linked lists."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_list_types() -> tuple[type, type, type]:
|
|
9
|
+
from pkstruct.linear.linked_lists.circular_linked_list import CircularLinkedList
|
|
10
|
+
from pkstruct.linear.linked_lists.doubly_linked_list import DoublyLinkedList
|
|
11
|
+
from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
|
|
12
|
+
return SinglyLinkedList, DoublyLinkedList, CircularLinkedList
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ForwardIterator:
|
|
16
|
+
"""Iterate over all node values in forward order.
|
|
17
|
+
|
|
18
|
+
Works with SinglyLinkedList, DoublyLinkedList, and CircularLinkedList.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
list_instance: Any supported linked list instance.
|
|
22
|
+
|
|
23
|
+
Yields:
|
|
24
|
+
Node data values from head to tail.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, list_instance: Any) -> None:
|
|
28
|
+
self._list = list_instance
|
|
29
|
+
|
|
30
|
+
def __iter__(self) -> Iterator[Any]:
|
|
31
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
|
|
32
|
+
lst = self._list
|
|
33
|
+
|
|
34
|
+
if isinstance(lst, CircularLinkedList):
|
|
35
|
+
size = lst.size
|
|
36
|
+
current = lst.head
|
|
37
|
+
steps = 0
|
|
38
|
+
while current is not None and steps < size:
|
|
39
|
+
yield current.value
|
|
40
|
+
current = current.next
|
|
41
|
+
steps += 1
|
|
42
|
+
elif isinstance(lst, (SinglyLinkedList, DoublyLinkedList)):
|
|
43
|
+
current = lst.head
|
|
44
|
+
visited: set[int] = set()
|
|
45
|
+
while current is not None:
|
|
46
|
+
if id(current) in visited:
|
|
47
|
+
break
|
|
48
|
+
visited.add(id(current))
|
|
49
|
+
yield current.value
|
|
50
|
+
current = current.next
|
|
51
|
+
else:
|
|
52
|
+
raise TypeError(
|
|
53
|
+
f"ForwardIterator does not support {type(lst).__name__}. "
|
|
54
|
+
"Use SinglyLinkedList, DoublyLinkedList, or CircularLinkedList."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BackwardIterator:
|
|
59
|
+
"""Iterate over all node values in reverse order.
|
|
60
|
+
|
|
61
|
+
Only supported for DoublyLinkedList (which has .prev pointers).
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
list_instance: A DoublyLinkedList instance.
|
|
65
|
+
|
|
66
|
+
Yields:
|
|
67
|
+
Node data values from tail to head.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
TypeError: If *list_instance* is not a DoublyLinkedList.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, list_instance: Any) -> None:
|
|
74
|
+
self._list = list_instance
|
|
75
|
+
|
|
76
|
+
def __iter__(self) -> Iterator[Any]:
|
|
77
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
|
|
78
|
+
lst = self._list
|
|
79
|
+
|
|
80
|
+
if isinstance(lst, DoublyLinkedList):
|
|
81
|
+
current = lst.tail
|
|
82
|
+
visited: set[int] = set()
|
|
83
|
+
while current is not None:
|
|
84
|
+
if id(current) in visited:
|
|
85
|
+
break
|
|
86
|
+
visited.add(id(current))
|
|
87
|
+
yield current.value
|
|
88
|
+
current = current.prev
|
|
89
|
+
elif isinstance(lst, (SinglyLinkedList, CircularLinkedList)):
|
|
90
|
+
raise TypeError(
|
|
91
|
+
f"BackwardIterator is not supported for {type(lst).__name__}. "
|
|
92
|
+
"Only DoublyLinkedList supports backward iteration via .prev pointers."
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
raise TypeError(
|
|
96
|
+
f"BackwardIterator does not support {type(lst).__name__}."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CircularIterator:
|
|
101
|
+
"""Infinite (or bounded) circular iterator over a CircularLinkedList.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
list_instance: A CircularLinkedList instance.
|
|
105
|
+
max_cycles: Maximum full cycles to iterate before stopping.
|
|
106
|
+
``None`` means infinite (caller is responsible for breaking).
|
|
107
|
+
|
|
108
|
+
Yields:
|
|
109
|
+
Node data values, wrapping around indefinitely (or until *max_cycles*
|
|
110
|
+
full circuits have been completed).
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
TypeError: If *list_instance* is not a CircularLinkedList.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, list_instance: Any, max_cycles: int | None = None, max_steps: int | None = None) -> None:
|
|
117
|
+
self._list = list_instance
|
|
118
|
+
self._max_cycles = max_cycles
|
|
119
|
+
self._max_steps = max_steps
|
|
120
|
+
|
|
121
|
+
def __iter__(self) -> Iterator[Any]:
|
|
122
|
+
_, _, CircularLinkedList = _get_list_types()
|
|
123
|
+
lst = self._list
|
|
124
|
+
|
|
125
|
+
if not isinstance(lst, CircularLinkedList):
|
|
126
|
+
raise TypeError(
|
|
127
|
+
f"CircularIterator requires a CircularLinkedList, got {type(lst).__name__}."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if lst.head is None or lst.size == 0:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
size = lst.size
|
|
134
|
+
if self._max_steps is not None:
|
|
135
|
+
limit = self._max_steps
|
|
136
|
+
elif self._max_cycles is not None:
|
|
137
|
+
limit = self._max_cycles * size
|
|
138
|
+
else:
|
|
139
|
+
limit = None
|
|
140
|
+
|
|
141
|
+
current = lst.head
|
|
142
|
+
steps = 0
|
|
143
|
+
while limit is None or steps < limit:
|
|
144
|
+
yield current.value
|
|
145
|
+
current = current.next
|
|
146
|
+
steps += 1
|
|
147
|
+
if current is None:
|
|
148
|
+
break
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""ASCII art visualization for linked list structures."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AsciiVisualizer:
|
|
9
|
+
"""Static methods for rendering linked lists as ASCII strings."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def singly(
|
|
13
|
+
head: Any,
|
|
14
|
+
node_repr: Callable[[Any], str] = str,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Render a singly linked list.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
head: Head node (must have .value and .next attributes).
|
|
20
|
+
node_repr: Callable that converts a node's value to a string.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
String like ``"10 -> 20 -> 30 -> None"`` or ``"NULL (empty)"``
|
|
24
|
+
when the list is empty.
|
|
25
|
+
"""
|
|
26
|
+
if head is None:
|
|
27
|
+
return "NULL (empty)"
|
|
28
|
+
|
|
29
|
+
parts: list[str] = []
|
|
30
|
+
current = head
|
|
31
|
+
visited: set[int] = set()
|
|
32
|
+
|
|
33
|
+
while current is not None:
|
|
34
|
+
node_id = id(current)
|
|
35
|
+
if node_id in visited:
|
|
36
|
+
parts.append("(cycle detected)")
|
|
37
|
+
break
|
|
38
|
+
visited.add(node_id)
|
|
39
|
+
parts.append(node_repr(current.value))
|
|
40
|
+
current = current.next
|
|
41
|
+
|
|
42
|
+
parts.append("None")
|
|
43
|
+
return " -> ".join(parts)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def doubly(
|
|
47
|
+
head: Any,
|
|
48
|
+
tail: Any,
|
|
49
|
+
node_repr: Callable[[Any], str] = str,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Render a doubly linked list.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
head: Head node (must have .value, .next, .prev attributes).
|
|
55
|
+
tail: Tail node.
|
|
56
|
+
node_repr: Callable that converts a node's value to a string.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
String like ``"None <- 10 <-> 20 <-> 30 -> None"`` or
|
|
60
|
+
``"NULL <-> NULL"`` when the list is empty.
|
|
61
|
+
"""
|
|
62
|
+
if head is None:
|
|
63
|
+
return "NULL <-> NULL"
|
|
64
|
+
|
|
65
|
+
parts: list[str] = []
|
|
66
|
+
current = head
|
|
67
|
+
visited: set[int] = set()
|
|
68
|
+
|
|
69
|
+
while current is not None:
|
|
70
|
+
node_id = id(current)
|
|
71
|
+
if node_id in visited:
|
|
72
|
+
parts.append("(cycle detected)")
|
|
73
|
+
break
|
|
74
|
+
visited.add(node_id)
|
|
75
|
+
parts.append(node_repr(current.value))
|
|
76
|
+
current = current.next
|
|
77
|
+
|
|
78
|
+
if not parts:
|
|
79
|
+
return "NULL <-> NULL"
|
|
80
|
+
|
|
81
|
+
inner = " <-> ".join(parts)
|
|
82
|
+
return f"None <- {inner} -> None"
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def circular(
|
|
86
|
+
head: Any,
|
|
87
|
+
size: int,
|
|
88
|
+
node_repr: Callable[[Any], str] = str,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Render a circular linked list.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
head: Head node (must have .value and .next attributes).
|
|
94
|
+
size: Number of nodes (used as iteration guard).
|
|
95
|
+
node_repr: Callable that converts a node's value to a string.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
String like ``"10 -> 20 -> 30 -> (back to head)"`` or
|
|
99
|
+
``"empty circular list"`` when the list is empty.
|
|
100
|
+
"""
|
|
101
|
+
if head is None or size == 0:
|
|
102
|
+
return "empty circular list"
|
|
103
|
+
|
|
104
|
+
parts: list[str] = []
|
|
105
|
+
current = head
|
|
106
|
+
steps = 0
|
|
107
|
+
|
|
108
|
+
while steps < size:
|
|
109
|
+
parts.append(node_repr(current.value))
|
|
110
|
+
current = current.next
|
|
111
|
+
steps += 1
|
|
112
|
+
|
|
113
|
+
parts.append("(back to head)")
|
|
114
|
+
return " -> ".join(parts)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""High-level visualizer that dispatches to AsciiVisualizer based on list type."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .ascii_visualizer import AsciiVisualizer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LinkedListVisualizer:
|
|
10
|
+
"""Provides visualization and debug views for any pkstruct linked list."""
|
|
11
|
+
|
|
12
|
+
# Lazy imports to avoid circular dependencies at module load time.
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _list_types() -> tuple[type, type, type]:
|
|
15
|
+
from pkstruct.linear.linked_lists.circular_linked_list import CircularLinkedList
|
|
16
|
+
from pkstruct.linear.linked_lists.doubly_linked_list import DoublyLinkedList
|
|
17
|
+
from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
|
|
18
|
+
return SinglyLinkedList, DoublyLinkedList, CircularLinkedList
|
|
19
|
+
|
|
20
|
+
def visualize(self, list_instance: Any, style: str = "ascii") -> str:
|
|
21
|
+
"""Return a string representation of *list_instance*.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
list_instance: A SinglyLinkedList, DoublyLinkedList, or
|
|
25
|
+
CircularLinkedList instance.
|
|
26
|
+
style: Rendering style. Currently only ``"ascii"`` is supported.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ASCII art string describing the list.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
TypeError: If *list_instance* is not a recognised list type.
|
|
33
|
+
ValueError: If *style* is not ``"ascii"``.
|
|
34
|
+
"""
|
|
35
|
+
if style != "ascii":
|
|
36
|
+
raise ValueError(f"Unsupported visualisation style: {style!r}. Use 'ascii'.")
|
|
37
|
+
|
|
38
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = self._list_types()
|
|
39
|
+
|
|
40
|
+
if isinstance(list_instance, CircularLinkedList):
|
|
41
|
+
return AsciiVisualizer.circular(
|
|
42
|
+
list_instance.head,
|
|
43
|
+
list_instance.size,
|
|
44
|
+
)
|
|
45
|
+
if isinstance(list_instance, DoublyLinkedList):
|
|
46
|
+
return AsciiVisualizer.doubly(
|
|
47
|
+
list_instance.head,
|
|
48
|
+
list_instance.tail,
|
|
49
|
+
)
|
|
50
|
+
if isinstance(list_instance, SinglyLinkedList):
|
|
51
|
+
return AsciiVisualizer.singly(list_instance.head)
|
|
52
|
+
|
|
53
|
+
raise TypeError(
|
|
54
|
+
f"Unsupported list type: {type(list_instance).__name__}. "
|
|
55
|
+
"Expected SinglyLinkedList, DoublyLinkedList, or CircularLinkedList."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def debug_view(self, list_instance: Any) -> dict[str, Any]:
|
|
59
|
+
"""Return a diagnostic dictionary for *list_instance*.
|
|
60
|
+
|
|
61
|
+
The returned dict always contains:
|
|
62
|
+
- ``type``: class name string
|
|
63
|
+
- ``size``: integer node count
|
|
64
|
+
- ``head``: value of head node, or ``None``
|
|
65
|
+
- ``tail``: value of tail node, or ``None``
|
|
66
|
+
- ``values``: list of all values in forward order
|
|
67
|
+
- ``node_ids``: list of ``id()`` for each node
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
list_instance: Any supported linked list instance.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with debugging information.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TypeError: If *list_instance* is not a recognised list type.
|
|
77
|
+
"""
|
|
78
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = self._list_types()
|
|
79
|
+
|
|
80
|
+
if not isinstance(list_instance, (SinglyLinkedList, DoublyLinkedList, CircularLinkedList)):
|
|
81
|
+
raise TypeError(
|
|
82
|
+
f"Unsupported list type: {type(list_instance).__name__}."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
list_type = type(list_instance).__name__
|
|
86
|
+
size = list_instance.size
|
|
87
|
+
|
|
88
|
+
# Collect values and node ids safely
|
|
89
|
+
values: list[Any] = []
|
|
90
|
+
node_ids: list[int] = []
|
|
91
|
+
|
|
92
|
+
if isinstance(list_instance, CircularLinkedList):
|
|
93
|
+
current = list_instance.head
|
|
94
|
+
steps = 0
|
|
95
|
+
while current is not None and steps < size:
|
|
96
|
+
values.append(current.value)
|
|
97
|
+
node_ids.append(id(current))
|
|
98
|
+
current = current.next
|
|
99
|
+
steps += 1
|
|
100
|
+
else:
|
|
101
|
+
current = list_instance.head
|
|
102
|
+
visited: set[int] = set()
|
|
103
|
+
while current is not None:
|
|
104
|
+
if id(current) in visited:
|
|
105
|
+
break
|
|
106
|
+
visited.add(id(current))
|
|
107
|
+
values.append(current.value)
|
|
108
|
+
node_ids.append(id(current))
|
|
109
|
+
current = current.next
|
|
110
|
+
|
|
111
|
+
head_value = list_instance.head.value if list_instance.head is not None else None
|
|
112
|
+
|
|
113
|
+
# Tail: DoublyLinkedList has .tail; others we derive
|
|
114
|
+
if isinstance(list_instance, (DoublyLinkedList, CircularLinkedList)):
|
|
115
|
+
tail_value = list_instance.tail.value if list_instance.tail is not None else None
|
|
116
|
+
else:
|
|
117
|
+
tail_value = values[-1] if values else None
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"type": list_type,
|
|
121
|
+
"size": size,
|
|
122
|
+
"head": head_value,
|
|
123
|
+
"tail": tail_value,
|
|
124
|
+
"values": values,
|
|
125
|
+
"node_ids": node_ids,
|
|
126
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.shared
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
Reusable infrastructure layer shared across all pkstruct modules.
|
|
5
|
+
|
|
6
|
+
Sub-packages
|
|
7
|
+
------------
|
|
8
|
+
exceptions — Base exception hierarchy
|
|
9
|
+
validators — Input validation helpers
|
|
10
|
+
serializers — Safe JSON serialization
|
|
11
|
+
threading — StructureLock (RLock wrapper)
|
|
12
|
+
debugging — DebugTracer for step-tracing
|
|
13
|
+
benchmarking — timeit / compare helpers
|
|
14
|
+
visualization— Visualizable protocol
|
|
15
|
+
"""
|
|
16
|
+
from pkstruct.shared.benchmarking import compare, timeit
|
|
17
|
+
from pkstruct.shared.debugging import DebugTracer
|
|
18
|
+
from pkstruct.shared.exceptions import (
|
|
19
|
+
ConcurrencyError,
|
|
20
|
+
EmptyStructureError,
|
|
21
|
+
IndexOutOfRangeError,
|
|
22
|
+
InvalidRangeError,
|
|
23
|
+
PkstructError,
|
|
24
|
+
SerializationError,
|
|
25
|
+
ValidationError,
|
|
26
|
+
ValueNotFoundError,
|
|
27
|
+
)
|
|
28
|
+
from pkstruct.shared.serializers import deserialize_from_json, serialize_to_json
|
|
29
|
+
from pkstruct.shared.threading import StructureLock
|
|
30
|
+
from pkstruct.shared.validators import (
|
|
31
|
+
validate_index,
|
|
32
|
+
validate_non_empty_list,
|
|
33
|
+
validate_non_negative_int,
|
|
34
|
+
validate_positive_int,
|
|
35
|
+
validate_range,
|
|
36
|
+
)
|
|
37
|
+
from pkstruct.shared.visualization import Visualizable
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# exceptions
|
|
41
|
+
"PkstructError",
|
|
42
|
+
"ValidationError",
|
|
43
|
+
"IndexOutOfRangeError",
|
|
44
|
+
"ValueNotFoundError",
|
|
45
|
+
"EmptyStructureError",
|
|
46
|
+
"SerializationError",
|
|
47
|
+
"ConcurrencyError",
|
|
48
|
+
"InvalidRangeError",
|
|
49
|
+
# validators
|
|
50
|
+
"validate_index",
|
|
51
|
+
"validate_range",
|
|
52
|
+
"validate_non_empty_list",
|
|
53
|
+
"validate_positive_int",
|
|
54
|
+
"validate_non_negative_int",
|
|
55
|
+
# serializers
|
|
56
|
+
"serialize_to_json",
|
|
57
|
+
"deserialize_from_json",
|
|
58
|
+
# threading
|
|
59
|
+
"StructureLock",
|
|
60
|
+
# debugging
|
|
61
|
+
"DebugTracer",
|
|
62
|
+
# benchmarking
|
|
63
|
+
"timeit",
|
|
64
|
+
"compare",
|
|
65
|
+
# visualization
|
|
66
|
+
"Visualizable",
|
|
67
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.shared.benchmarking
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Micro-benchmarking utilities for pkstruct structures.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import statistics
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def timeit(
|
|
15
|
+
fn: Callable[..., Any],
|
|
16
|
+
*args: Any,
|
|
17
|
+
iterations: int = 1000,
|
|
18
|
+
**kwargs: Any,
|
|
19
|
+
) -> dict[str, float]:
|
|
20
|
+
"""Time *fn* over *iterations* calls and return statistics (in seconds).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
fn: The callable to benchmark.
|
|
24
|
+
*args: Positional arguments forwarded to *fn*.
|
|
25
|
+
iterations: Number of repetitions.
|
|
26
|
+
**kwargs: Keyword arguments forwarded to *fn*.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A dict with keys ``min``, ``max``, ``mean``, ``median``, ``stdev``,
|
|
30
|
+
``total``, and ``iterations``.
|
|
31
|
+
"""
|
|
32
|
+
times: list[float] = []
|
|
33
|
+
for _ in range(iterations):
|
|
34
|
+
t0 = time.perf_counter()
|
|
35
|
+
fn(*args, **kwargs)
|
|
36
|
+
times.append(time.perf_counter() - t0)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"min": min(times),
|
|
40
|
+
"max": max(times),
|
|
41
|
+
"mean": statistics.mean(times),
|
|
42
|
+
"median": statistics.median(times),
|
|
43
|
+
"stdev": statistics.pstdev(times),
|
|
44
|
+
"total": sum(times),
|
|
45
|
+
"iterations": iterations,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compare(
|
|
50
|
+
label_a: str,
|
|
51
|
+
fn_a: Callable[..., Any],
|
|
52
|
+
label_b: str,
|
|
53
|
+
fn_b: Callable[..., Any],
|
|
54
|
+
*,
|
|
55
|
+
iterations: int = 1000,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""Compare two callables head-to-head.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
label_a: Name for the first callable.
|
|
61
|
+
fn_a: First callable (no-arg).
|
|
62
|
+
label_b: Name for the second callable.
|
|
63
|
+
fn_b: Second callable (no-arg).
|
|
64
|
+
iterations: Repetitions per callable.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict with per-callable stats and a ``"winner"`` key.
|
|
68
|
+
"""
|
|
69
|
+
stats_a = timeit(fn_a, iterations=iterations)
|
|
70
|
+
stats_b = timeit(fn_b, iterations=iterations)
|
|
71
|
+
winner = label_a if stats_a["median"] <= stats_b["median"] else label_b
|
|
72
|
+
return {
|
|
73
|
+
label_a: stats_a,
|
|
74
|
+
label_b: stats_b,
|
|
75
|
+
"winner": winner,
|
|
76
|
+
"speedup": max(stats_a["median"], stats_b["median"])
|
|
77
|
+
/ max(min(stats_a["median"], stats_b["median"]), 1e-12),
|
|
78
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.shared.debugging
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Step-tracing and introspection helpers for pkstruct structures.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DebugTracer:
|
|
13
|
+
"""Lightweight operation tracer for debugging data structure mutations.
|
|
14
|
+
|
|
15
|
+
Keeps a bounded in-memory trace log (FIFO). Disabled by default to avoid
|
|
16
|
+
any overhead in production paths.
|
|
17
|
+
|
|
18
|
+
Example::
|
|
19
|
+
|
|
20
|
+
tracer = DebugTracer(enabled=True, max_entries=50)
|
|
21
|
+
tracer.record("insert", position=0, value=42)
|
|
22
|
+
for entry in tracer.entries:
|
|
23
|
+
print(entry)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__slots__ = ("enabled", "max_entries", "_entries")
|
|
27
|
+
|
|
28
|
+
def __init__(self, *, enabled: bool = False, max_entries: int = 200) -> None:
|
|
29
|
+
self.enabled = enabled
|
|
30
|
+
self.max_entries = max_entries
|
|
31
|
+
self._entries: list[dict[str, Any]] = []
|
|
32
|
+
|
|
33
|
+
def record(self, operation: str, **kwargs: Any) -> None:
|
|
34
|
+
"""Record a single operation event.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
operation: Human-readable operation name (e.g. ``"insert"``).
|
|
38
|
+
**kwargs: Arbitrary key-value context (e.g. ``position=2, value=99``).
|
|
39
|
+
"""
|
|
40
|
+
if not self.enabled:
|
|
41
|
+
return
|
|
42
|
+
entry: dict[str, Any] = {
|
|
43
|
+
"op": operation,
|
|
44
|
+
"ts": time.monotonic(),
|
|
45
|
+
**kwargs,
|
|
46
|
+
}
|
|
47
|
+
self._entries.append(entry)
|
|
48
|
+
if len(self._entries) > self.max_entries:
|
|
49
|
+
self._entries.pop(0)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def entries(self) -> list[dict[str, Any]]:
|
|
53
|
+
"""Return a shallow copy of the trace log."""
|
|
54
|
+
return list(self._entries)
|
|
55
|
+
|
|
56
|
+
def clear(self) -> None:
|
|
57
|
+
"""Clear the trace log."""
|
|
58
|
+
self._entries.clear()
|
|
59
|
+
|
|
60
|
+
def summary(self) -> dict[str, int]:
|
|
61
|
+
"""Return a count of each operation type."""
|
|
62
|
+
counts: dict[str, int] = {}
|
|
63
|
+
for e in self._entries:
|
|
64
|
+
counts[e["op"]] = counts.get(e["op"], 0) + 1
|
|
65
|
+
return counts
|
|
66
|
+
|
|
67
|
+
def get_events(self) -> list[dict[str, Any]]:
|
|
68
|
+
"""Return all recorded events."""
|
|
69
|
+
return self._entries
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.shared.exceptions
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Base exception hierarchy shared across all pkstruct modules.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PkstructError(Exception):
|
|
10
|
+
"""Root exception for all pkstruct errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ValidationError(PkstructError):
|
|
14
|
+
"""Raised when input validation fails."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IndexOutOfRangeError(PkstructError, IndexError):
|
|
18
|
+
"""Raised when a positional index is out of valid range."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, index: int, size: int) -> None:
|
|
21
|
+
super().__init__(f"Index {index} is out of range for list of size {size}.")
|
|
22
|
+
self.index = index
|
|
23
|
+
self.size = size
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ValueNotFoundError(PkstructError, ValueError):
|
|
27
|
+
"""Raised when a value is not found in the structure."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, value: object) -> None:
|
|
30
|
+
super().__init__(f"Value {value!r} not found in the structure.")
|
|
31
|
+
self.value = value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EmptyStructureError(PkstructError):
|
|
35
|
+
"""Raised when an operation requires a non-empty structure."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, operation: str = "operation") -> None:
|
|
38
|
+
super().__init__(f"Cannot perform '{operation}' on an empty structure.")
|
|
39
|
+
self.operation = operation
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SerializationError(PkstructError):
|
|
43
|
+
"""Raised when serialization or deserialization fails."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ConcurrencyError(PkstructError):
|
|
47
|
+
"""Raised on threading/locking issues."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InvalidRangeError(PkstructError, ValueError):
|
|
51
|
+
"""Raised when a range (start, end) is logically invalid."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, start: int, end: int, size: int) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
f"Range ({start}, {end}) is invalid for structure of size {size}."
|
|
56
|
+
)
|
|
57
|
+
self.start = start
|
|
58
|
+
self.end = end
|
|
59
|
+
self.size = size
|