dequedict 0.1.0__tar.gz

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.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: dequedict
3
+ Version: 0.1.0
4
+ Summary: Fast ordered dict with deque-like operations at both ends
5
+ Author: sebastian
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: C
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: typing_extensions>=4.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == "dev"
21
+ Requires-Dist: pytest-benchmark; extra == "dev"
22
+
23
+ # DequeDict
24
+
25
+ Ordered dictionary with O(1) deque operations at both ends.
26
+
27
+ ## Features
28
+
29
+ - **O(1) key lookup** — like `dict`
30
+ - **O(1) pop/peek from both ends** — like `deque`
31
+ - **O(1) appendleft, move_to_end, delete by key**
32
+ - **Full typing support** with `.pyi` stubs
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install dequedict
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ from dequedict import DequeDict, DefaultDequeDict
44
+
45
+ dd = DequeDict([("a", 1), ("b", 2), ("c", 3)])
46
+
47
+ dd["d"] = 4 # Set
48
+ value = dd["a"] # Get
49
+ del dd["b"] # Delete
50
+
51
+ dd.peekleft() # First value
52
+ dd.popleft() # Remove first
53
+ dd.appendleft("z", 0) # Insert at front
54
+ dd.move_to_end("a") # Move to back
55
+
56
+ # DefaultDequeDict: auto-create values like defaultdict
57
+ groups = DefaultDequeDict(list)
58
+ groups["a"].append(1)
59
+ groups["a"].append(2)
60
+ ```
61
+
62
+ ## API
63
+
64
+ | Method | Description |
65
+ |--------|-------------|
66
+ | `peekleft()` / `peek()` | First/last value |
67
+ | `peekleftitem()` / `peekitem()` | First/last (key, value) |
68
+ | `popleft()` / `pop()` | Remove and return first/last |
69
+ | `popleftitem()` / `popitem()` | Remove and return first/last pair |
70
+ | `appendleft(key, value)` | Insert at front |
71
+ | `move_to_end(key, last=True)` | Move to front or back |
72
+ | `get`, `keys`, `values`, `items`, `clear`, `copy`, `update`, `setdefault` | Standard dict ops |
73
+
74
+ ## Performance
75
+
76
+ Mac M1, Python 3.11, C extension:
77
+
78
+ | Operation | DequeDict | dict | deque | OrderedDict |
79
+ |-----------|-----------|------|-------|-------------|
80
+ | Key lookup | 32 ns | **23 ns** | O(n) | 24 ns |
81
+ | Peek left | **22 ns** | N/A | 23 ns | 59 ns |
82
+ | Peek right | **22 ns** | N/A | 23 ns | 70 ns |
83
+ | Pop left ×100 | 3.7 µs | N/A | **2.1 µs** | 7.7 µs |
84
+ | Delete by key ×100 | 4.3 µs | **2.8 µs** | O(n) | 6.0 µs |
85
+ | Insert ×100 | **6.6 µs** | 9.5 µs | N/A | 6.8 µs |
86
+
87
+ Pop is ~1.8x slower than deque due to dict maintenance, but provides O(1) key lookup.
88
+
89
+ ## Benchmarks
90
+
91
+ ```bash
92
+ python benchmarks/benchmark.py
93
+ ```
94
+
95
+ ## Tests
96
+
97
+ ```bash
98
+ pip install pytest
99
+ python -m pytest tests/ -v
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,82 @@
1
+ # DequeDict
2
+
3
+ Ordered dictionary with O(1) deque operations at both ends.
4
+
5
+ ## Features
6
+
7
+ - **O(1) key lookup** — like `dict`
8
+ - **O(1) pop/peek from both ends** — like `deque`
9
+ - **O(1) appendleft, move_to_end, delete by key**
10
+ - **Full typing support** with `.pyi` stubs
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install dequedict
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```python
21
+ from dequedict import DequeDict, DefaultDequeDict
22
+
23
+ dd = DequeDict([("a", 1), ("b", 2), ("c", 3)])
24
+
25
+ dd["d"] = 4 # Set
26
+ value = dd["a"] # Get
27
+ del dd["b"] # Delete
28
+
29
+ dd.peekleft() # First value
30
+ dd.popleft() # Remove first
31
+ dd.appendleft("z", 0) # Insert at front
32
+ dd.move_to_end("a") # Move to back
33
+
34
+ # DefaultDequeDict: auto-create values like defaultdict
35
+ groups = DefaultDequeDict(list)
36
+ groups["a"].append(1)
37
+ groups["a"].append(2)
38
+ ```
39
+
40
+ ## API
41
+
42
+ | Method | Description |
43
+ |--------|-------------|
44
+ | `peekleft()` / `peek()` | First/last value |
45
+ | `peekleftitem()` / `peekitem()` | First/last (key, value) |
46
+ | `popleft()` / `pop()` | Remove and return first/last |
47
+ | `popleftitem()` / `popitem()` | Remove and return first/last pair |
48
+ | `appendleft(key, value)` | Insert at front |
49
+ | `move_to_end(key, last=True)` | Move to front or back |
50
+ | `get`, `keys`, `values`, `items`, `clear`, `copy`, `update`, `setdefault` | Standard dict ops |
51
+
52
+ ## Performance
53
+
54
+ Mac M1, Python 3.11, C extension:
55
+
56
+ | Operation | DequeDict | dict | deque | OrderedDict |
57
+ |-----------|-----------|------|-------|-------------|
58
+ | Key lookup | 32 ns | **23 ns** | O(n) | 24 ns |
59
+ | Peek left | **22 ns** | N/A | 23 ns | 59 ns |
60
+ | Peek right | **22 ns** | N/A | 23 ns | 70 ns |
61
+ | Pop left ×100 | 3.7 µs | N/A | **2.1 µs** | 7.7 µs |
62
+ | Delete by key ×100 | 4.3 µs | **2.8 µs** | O(n) | 6.0 µs |
63
+ | Insert ×100 | **6.6 µs** | 9.5 µs | N/A | 6.8 µs |
64
+
65
+ Pop is ~1.8x slower than deque due to dict maintenance, but provides O(1) key lookup.
66
+
67
+ ## Benchmarks
68
+
69
+ ```bash
70
+ python benchmarks/benchmark.py
71
+ ```
72
+
73
+ ## Tests
74
+
75
+ ```bash
76
+ pip install pytest
77
+ python -m pytest tests/ -v
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,387 @@
1
+ """DequeDict - Ordered dictionary with O(1) deque operations at both ends."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from collections.abc import Iterable, Mapping
6
+ from contextlib import suppress
7
+ from typing import TYPE_CHECKING, Callable, Generic, ItemsView, Iterator, KeysView, TypeVar, ValuesView, overload
8
+
9
+ from typing_extensions import TypeIs
10
+
11
+ __all__ = ["DequeDict", "DefaultDequeDict"]
12
+
13
+ K = TypeVar("K")
14
+ V = TypeVar("V")
15
+
16
+
17
+ def _is_iterable_of_pairs(items: object) -> TypeIs[Iterable[tuple[K, V]]]:
18
+ return not isinstance(items, Mapping) and getattr(items, "__iter__", None) is not None
19
+
20
+
21
+ class DequeDict(Mapping[K, V]):
22
+ """Ordered dictionary with O(1) deque operations at both ends.
23
+
24
+ Combines dict interface with deque-like operations:
25
+ - O(1) key lookup, contains, delete
26
+ - O(1) popleft/popleftitem/peekleft/peekleftitem
27
+ - O(1) pop/popitem/peek/peekitem
28
+ - O(1) appendleft
29
+ - O(1) move_to_end
30
+ """
31
+
32
+ __slots__ = ("_dict", "_head", "_tail")
33
+ __hash__ = None # type: ignore[assignment]
34
+
35
+ class _Node:
36
+ __slots__ = ("key", "value", "prev", "next")
37
+
38
+ def __init__(self, key: K, value: V) -> None:
39
+ self.key = key
40
+ self.value = value
41
+ self.prev: DequeDict._Node | None = None
42
+ self.next: DequeDict._Node | None = None
43
+
44
+ def __init__(self, items: Mapping[K, V] | Iterable[tuple[K, V]] | None = None) -> None:
45
+ self._dict: dict[K, DequeDict._Node] = {}
46
+ self._head: DequeDict._Node | None = None
47
+ self._tail: DequeDict._Node | None = None
48
+ if items is not None:
49
+ if _is_iterable_of_pairs(items):
50
+ for k, v in items:
51
+ self[k] = v
52
+ else:
53
+ for k, v in items.items():
54
+ self[k] = v
55
+
56
+ def __len__(self) -> int:
57
+ return len(self._dict)
58
+
59
+ def __contains__(self, key: object) -> bool:
60
+ return key in self._dict
61
+
62
+ def __getitem__(self, key: K) -> V:
63
+ if key not in self._dict:
64
+ raise KeyError(key)
65
+ return self._dict[key].value
66
+
67
+ def __setitem__(self, key: K, value: V) -> None:
68
+ if key in self._dict:
69
+ self._dict[key].value = value
70
+ return
71
+ node = self._Node(key, value)
72
+ self._dict[key] = node
73
+ if self._tail is None:
74
+ self._head = self._tail = node
75
+ else:
76
+ node.prev = self._tail
77
+ self._tail.next = node
78
+ self._tail = node
79
+
80
+ def __delitem__(self, key: K) -> None:
81
+ if key not in self._dict:
82
+ raise KeyError(key)
83
+ node = self._dict.pop(key)
84
+ self._unlink(node)
85
+
86
+ def _unlink(self, node: _Node) -> None:
87
+ if node.prev:
88
+ node.prev.next = node.next
89
+ else:
90
+ self._head = node.next
91
+ if node.next:
92
+ node.next.prev = node.prev
93
+ else:
94
+ self._tail = node.prev
95
+
96
+ def __iter__(self) -> Iterator[K]:
97
+ node = self._head
98
+ while node:
99
+ yield node.key
100
+ node = node.next
101
+
102
+ def __reversed__(self) -> Iterator[K]:
103
+ node = self._tail
104
+ while node:
105
+ yield node.key
106
+ node = node.prev
107
+
108
+ def __repr__(self) -> str:
109
+ if not self._dict:
110
+ return "DequeDict()"
111
+ items = [(n.key, n.value) for n in self._iter_nodes()]
112
+ return f"DequeDict({items!r})"
113
+
114
+ def __eq__(self, other: object) -> bool:
115
+ if not isinstance(other, (dict, DequeDict)):
116
+ return NotImplemented
117
+ if len(self) != len(other):
118
+ return False
119
+ return all(not (k not in other or other[k] != v) for k, v in self.items())
120
+
121
+ def _iter_nodes(self) -> Iterator[_Node]:
122
+ node = self._head
123
+ while node:
124
+ yield node
125
+ node = node.next
126
+
127
+ def peekleft(self) -> V:
128
+ """Return first value without removing."""
129
+ if self._head is None:
130
+ raise IndexError("peek from an empty DequeDict")
131
+ return self._head.value
132
+
133
+ def peekleftitem(self) -> tuple[K, V]:
134
+ """Return first (key, value) without removing."""
135
+ if self._head is None:
136
+ raise IndexError("peek from an empty DequeDict")
137
+ return (self._head.key, self._head.value)
138
+
139
+ def peekleftkey(self) -> K:
140
+ """Return first key without removing."""
141
+ if self._head is None:
142
+ raise IndexError("peek from an empty DequeDict")
143
+ return self._head.key
144
+
145
+ def peek(self) -> V:
146
+ """Return last value without removing."""
147
+ if self._tail is None:
148
+ raise IndexError("peek from an empty DequeDict")
149
+ return self._tail.value
150
+
151
+ def peekitem(self) -> tuple[K, V]:
152
+ """Return last (key, value) without removing."""
153
+ if self._tail is None:
154
+ raise IndexError("peek from an empty DequeDict")
155
+ return (self._tail.key, self._tail.value)
156
+
157
+ def popleft(self) -> V:
158
+ """Remove and return first value."""
159
+ if self._head is None:
160
+ raise IndexError("pop from an empty DequeDict")
161
+ node = self._head
162
+ del self._dict[node.key]
163
+ self._unlink(node)
164
+ return node.value
165
+
166
+ def popleftitem(self) -> tuple[K, V]:
167
+ """Remove and return first (key, value)."""
168
+ if self._head is None:
169
+ raise KeyError("popleftitem from an empty DequeDict")
170
+ node = self._head
171
+ del self._dict[node.key]
172
+ self._unlink(node)
173
+ return (node.key, node.value)
174
+
175
+ @overload
176
+ def pop(self) -> V: ...
177
+ @overload
178
+ def pop(self, key: K) -> V: ...
179
+ @overload
180
+ def pop(self, key: K, default: V) -> V: ...
181
+
182
+ def pop(self, key: K | None = None, default: V | None = None) -> V: # type: ignore[misc]
183
+ """Remove and return value by key, or from end if no key given."""
184
+ if key is None:
185
+ if self._tail is None:
186
+ if default is not None:
187
+ return default
188
+ raise IndexError("pop from an empty DequeDict")
189
+ node = self._tail
190
+ del self._dict[node.key]
191
+ self._unlink(node)
192
+ return node.value
193
+ if key not in self._dict:
194
+ if default is not None:
195
+ return default
196
+ raise KeyError(key)
197
+ node = self._dict.pop(key)
198
+ self._unlink(node)
199
+ return node.value
200
+
201
+ def popitem(self) -> tuple[K, V]:
202
+ """Remove and return last (key, value)."""
203
+ if self._tail is None:
204
+ raise KeyError("popitem from an empty DequeDict")
205
+ node = self._tail
206
+ del self._dict[node.key]
207
+ self._unlink(node)
208
+ return (node.key, node.value)
209
+
210
+ def appendleft(self, key: K, value: V) -> None:
211
+ """Insert (key, value) at front."""
212
+ if key in self._dict:
213
+ raise KeyError("key already exists")
214
+ node = self._Node(key, value)
215
+ self._dict[key] = node
216
+ if self._head is None:
217
+ self._head = self._tail = node
218
+ else:
219
+ node.next = self._head
220
+ self._head.prev = node
221
+ self._head = node
222
+
223
+ def move_to_end(self, key: K, last: bool = True) -> None:
224
+ """Move existing key to front (last=False) or back (last=True)."""
225
+ if key not in self._dict:
226
+ raise KeyError(key)
227
+ node = self._dict[key]
228
+ if (last and node is self._tail) or (not last and node is self._head):
229
+ return
230
+ self._unlink(node)
231
+ if last:
232
+ node.prev = self._tail
233
+ node.next = None
234
+ if self._tail:
235
+ self._tail.next = node
236
+ else:
237
+ self._head = node
238
+ self._tail = node
239
+ else:
240
+ node.prev = None
241
+ node.next = self._head
242
+ if self._head:
243
+ self._head.prev = node
244
+ else:
245
+ self._tail = node
246
+ self._head = node
247
+
248
+ def get(self, key: K, default: V | None = None) -> V | None:
249
+ """Return value for key, or default if key not present."""
250
+ if key not in self._dict:
251
+ return default
252
+ return self._dict[key].value
253
+
254
+ def keys(self) -> KeysView[K]:
255
+ """Return view of keys in insertion order."""
256
+ return _DequeDictKeysView(self)
257
+
258
+ def values(self) -> ValuesView[V]:
259
+ """Return view of values in insertion order."""
260
+ return _DequeDictValuesView(self)
261
+
262
+ def items(self) -> ItemsView[K, V]:
263
+ """Return view of (key, value) pairs in insertion order."""
264
+ return _DequeDictItemsView(self)
265
+
266
+ def clear(self) -> None:
267
+ """Remove all items."""
268
+ self._dict.clear()
269
+ self._head = None
270
+ self._tail = None
271
+
272
+ def copy(self) -> DequeDict[K, V]:
273
+ """Return a shallow copy."""
274
+ return DequeDict(self.items())
275
+
276
+ def update(self, other: Mapping[K, V] | Iterable[tuple[K, V]] | None = None, **kwargs: V) -> None:
277
+ """Update from dict, iterable of pairs, or keyword arguments."""
278
+ if other is not None:
279
+ if isinstance(other, Mapping):
280
+ for k, v in other.items():
281
+ self[k] = v
282
+ else:
283
+ for k, v in other:
284
+ self[k] = v
285
+ for k, v in kwargs.items():
286
+ self[k] = v # type: ignore[index]
287
+
288
+ def setdefault(self, key: K, default: V | None = None) -> V | None:
289
+ """Return value for key, setting default if not present."""
290
+ if key in self._dict:
291
+ return self._dict[key].value
292
+ self[key] = default # type: ignore[assignment]
293
+ return default
294
+
295
+
296
+ class _DequeDictKeysView(KeysView[K]):
297
+ __slots__ = ("_dd",)
298
+
299
+ def __init__(self, dd: DequeDict[K, V]) -> None:
300
+ self._dd = dd
301
+
302
+ def __len__(self) -> int:
303
+ return len(self._dd)
304
+
305
+ def __iter__(self) -> Iterator[K]:
306
+ return iter(self._dd)
307
+
308
+ def __contains__(self, key: object) -> bool:
309
+ return key in self._dd
310
+
311
+
312
+ class _DequeDictValuesView(ValuesView[V]):
313
+ __slots__ = ("_dd",)
314
+
315
+ def __init__(self, dd: DequeDict[K, V]) -> None:
316
+ self._dd = dd
317
+
318
+ def __len__(self) -> int:
319
+ return len(self._dd)
320
+
321
+ def __iter__(self) -> Iterator[V]:
322
+ for node in self._dd._iter_nodes():
323
+ yield node.value
324
+
325
+ def __contains__(self, value: object) -> bool:
326
+ return any(node.value == value for node in self._dd._iter_nodes())
327
+
328
+
329
+ class _DequeDictItemsView(ItemsView[K, V]):
330
+ __slots__ = ("_dd",)
331
+
332
+ def __init__(self, dd: DequeDict[K, V]) -> None:
333
+ self._dd = dd
334
+
335
+ def __len__(self) -> int:
336
+ return len(self._dd)
337
+
338
+ def __iter__(self) -> Iterator[tuple[K, V]]:
339
+ for node in self._dd._iter_nodes():
340
+ yield (node.key, node.value)
341
+
342
+ def __contains__(self, item: object) -> bool:
343
+ if not isinstance(item, tuple) or len(item) != 2:
344
+ return False
345
+ k, v = item
346
+ if k not in self._dd:
347
+ return False
348
+ return self._dd[k] == v
349
+
350
+
351
+ class DefaultDequeDict(DequeDict[K, V]):
352
+ """DequeDict with default_factory for missing keys, like collections.defaultdict."""
353
+
354
+ __slots__ = ("default_factory",)
355
+
356
+ def __init__(
357
+ self,
358
+ default_factory: Callable[[], V] | None = None,
359
+ items: Mapping[K, V] | Iterable[tuple[K, V]] | None = None,
360
+ ) -> None:
361
+ self.default_factory = default_factory
362
+ super().__init__(items)
363
+
364
+ def __missing__(self, key: K) -> V:
365
+ if self.default_factory is None:
366
+ raise KeyError(key)
367
+ value = self.default_factory()
368
+ self[key] = value
369
+ return value
370
+
371
+ def __getitem__(self, key: K) -> V:
372
+ try:
373
+ return super().__getitem__(key)
374
+ except KeyError:
375
+ return self.__missing__(key)
376
+
377
+ def __repr__(self) -> str:
378
+ return f"DefaultDequeDict({self.default_factory}, {list(self.items())!r})"
379
+
380
+ def copy(self) -> DefaultDequeDict[K, V]:
381
+ return DefaultDequeDict(self.default_factory, self.items())
382
+
383
+
384
+ # Use C extension if available (disable with NOC=1 environment variable)
385
+ if not TYPE_CHECKING and not os.getenv("NOC"):
386
+ with suppress(ImportError):
387
+ from dequedict._dequedict import DequeDict