multicollections 0.2.0__tar.gz → 0.2.1__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.
- {multicollections-0.2.0 → multicollections-0.2.1}/PKG-INFO +1 -1
- multicollections-0.2.1/multicollections/__init__.py +315 -0
- multicollections-0.2.1/multicollections/_util.py +16 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/multicollections/abc.py +49 -9
- {multicollections-0.2.0 → multicollections-0.2.1}/pyproject.toml +6 -2
- {multicollections-0.2.0 → multicollections-0.2.1}/tests/minimalimpl.py +7 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/tests/test_multicollections.py +211 -0
- multicollections-0.2.0/multicollections/__init__.py +0 -156
- {multicollections-0.2.0 → multicollections-0.2.1}/.github/dependabot.yml +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/.github/workflows/ci.yml +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/.github/workflows/pypi-publish.yml +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/.gitignore +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/.readthedocs.yaml +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/LICENSE.txt +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/README.md +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/docs/api/abc.md +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/docs/api/multicollections.md +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/docs/index.md +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/mkdocs.yml +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/multicollections/py.typed +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/tests/__init__.py +0 -0
- {multicollections-0.2.0 → multicollections-0.2.1}/tests/ruff.toml +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Fully generic `MultiDict` class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
import sys
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 9):
|
|
10
|
+
from collections.abc import (
|
|
11
|
+
Iterable,
|
|
12
|
+
Iterator,
|
|
13
|
+
Mapping,
|
|
14
|
+
Sequence,
|
|
15
|
+
)
|
|
16
|
+
else:
|
|
17
|
+
from typing import (
|
|
18
|
+
Iterable,
|
|
19
|
+
Iterator,
|
|
20
|
+
Mapping,
|
|
21
|
+
Sequence,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from ._util import override
|
|
25
|
+
from .abc import MutableMultiMapping, with_default
|
|
26
|
+
|
|
27
|
+
_K = TypeVar("_K")
|
|
28
|
+
_V = TypeVar("_V")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MultiDict(MutableMultiMapping[_K, _V]):
|
|
32
|
+
"""A fully generic dictionary that allows multiple values with the same key.
|
|
33
|
+
|
|
34
|
+
Preserves insertion order.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self, iterable: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (), **kwargs: _V
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Create a MultiDict."""
|
|
41
|
+
self._items: list[tuple[_K, _V]] = []
|
|
42
|
+
self._key_indices: dict[_K, list[int]] = {}
|
|
43
|
+
|
|
44
|
+
# Batch initialization: collect all items first, then build indices once
|
|
45
|
+
if isinstance(iterable, Mapping):
|
|
46
|
+
self._items.extend((key, value) for key, value in iterable.items())
|
|
47
|
+
else:
|
|
48
|
+
self._items.extend((key, value) for key, value in iterable)
|
|
49
|
+
|
|
50
|
+
# Add kwargs items
|
|
51
|
+
self._items.extend((key, value) for key, value in kwargs.items())
|
|
52
|
+
|
|
53
|
+
# Build indices in one pass for better performance
|
|
54
|
+
if self._items:
|
|
55
|
+
self._rebuild_indices()
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
@with_default
|
|
59
|
+
def getall(self, key: _K) -> list[_V]:
|
|
60
|
+
"""Get all values for a key.
|
|
61
|
+
|
|
62
|
+
Raises a `KeyError` if the key is not found and no default is provided.
|
|
63
|
+
"""
|
|
64
|
+
ret = [self._items[i][1] for i in self._key_indices.get(key, [])]
|
|
65
|
+
if not ret:
|
|
66
|
+
raise KeyError(key)
|
|
67
|
+
return ret
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
def __setitem__(self, key: _K, value: _V) -> None:
|
|
71
|
+
"""Set the value for a key.
|
|
72
|
+
|
|
73
|
+
Replaces the first value for a key if it exists; otherwise, it adds a new item.
|
|
74
|
+
Any other items with the same key are removed.
|
|
75
|
+
"""
|
|
76
|
+
if key in self._key_indices:
|
|
77
|
+
# Key exists, replace first occurrence and remove others
|
|
78
|
+
indices = self._key_indices[key]
|
|
79
|
+
first_index = indices[0]
|
|
80
|
+
|
|
81
|
+
# Update the first occurrence
|
|
82
|
+
self._items[first_index] = (key, value)
|
|
83
|
+
|
|
84
|
+
if len(indices) > 1:
|
|
85
|
+
# Remove duplicates efficiently by marking items as None and filtering
|
|
86
|
+
for idx in indices[1:]:
|
|
87
|
+
self._items[idx] = None
|
|
88
|
+
|
|
89
|
+
# Filter out None items and rebuild indices
|
|
90
|
+
self._items = [item for item in self._items if item is not None]
|
|
91
|
+
self._rebuild_indices()
|
|
92
|
+
else:
|
|
93
|
+
# Key doesn't exist, add it
|
|
94
|
+
self.add(key, value)
|
|
95
|
+
|
|
96
|
+
def _rebuild_indices(self) -> None:
|
|
97
|
+
"""Rebuild the key indices after items list has been modified."""
|
|
98
|
+
self._key_indices = {}
|
|
99
|
+
for i, (key, _) in enumerate(self._items):
|
|
100
|
+
if key not in self._key_indices:
|
|
101
|
+
self._key_indices[key] = []
|
|
102
|
+
self._key_indices[key].append(i)
|
|
103
|
+
|
|
104
|
+
@override
|
|
105
|
+
def add(self, key: _K, value: _V) -> None:
|
|
106
|
+
"""Add a new value for a key."""
|
|
107
|
+
index = len(self._items)
|
|
108
|
+
self._items.append((key, value))
|
|
109
|
+
if key not in self._key_indices:
|
|
110
|
+
self._key_indices[key] = []
|
|
111
|
+
self._key_indices[key].append(index)
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
@with_default
|
|
115
|
+
def popone(self, key: _K) -> _V:
|
|
116
|
+
"""Remove and return the first value for a key."""
|
|
117
|
+
if key not in self._key_indices:
|
|
118
|
+
raise KeyError(key)
|
|
119
|
+
|
|
120
|
+
indices = self._key_indices[key]
|
|
121
|
+
first_index = indices[0]
|
|
122
|
+
value = self._items[first_index][1]
|
|
123
|
+
|
|
124
|
+
# Mark the first item for removal
|
|
125
|
+
self._items[first_index] = None
|
|
126
|
+
|
|
127
|
+
# Filter out None items and rebuild indices
|
|
128
|
+
self._items = [item for item in self._items if item is not None]
|
|
129
|
+
self._rebuild_indices()
|
|
130
|
+
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
def __delitem__(self, key: _K) -> None:
|
|
135
|
+
"""Remove all values for a key.
|
|
136
|
+
|
|
137
|
+
Raises a `KeyError` if the key is not found.
|
|
138
|
+
"""
|
|
139
|
+
if key not in self._key_indices:
|
|
140
|
+
raise KeyError(key)
|
|
141
|
+
|
|
142
|
+
# Mark items for removal
|
|
143
|
+
indices_to_remove = self._key_indices[key]
|
|
144
|
+
for idx in indices_to_remove:
|
|
145
|
+
self._items[idx] = None
|
|
146
|
+
|
|
147
|
+
# Filter out None items and rebuild indices
|
|
148
|
+
self._items = [item for item in self._items if item is not None]
|
|
149
|
+
self._rebuild_indices()
|
|
150
|
+
|
|
151
|
+
@override
|
|
152
|
+
def __iter__(self) -> Iterator[_K]:
|
|
153
|
+
"""Return an iterator over the keys, in insertion order.
|
|
154
|
+
|
|
155
|
+
Keys with multiple values will be yielded multiple times.
|
|
156
|
+
"""
|
|
157
|
+
return (k for k, _ in self._items)
|
|
158
|
+
|
|
159
|
+
@override
|
|
160
|
+
def __len__(self) -> int:
|
|
161
|
+
"""Return the total number of items."""
|
|
162
|
+
return len(self._items)
|
|
163
|
+
|
|
164
|
+
@override
|
|
165
|
+
def clear(self) -> None:
|
|
166
|
+
"""Remove all items from the multi-mapping."""
|
|
167
|
+
self._items.clear()
|
|
168
|
+
self._key_indices.clear()
|
|
169
|
+
|
|
170
|
+
def _collect_update_items(
|
|
171
|
+
self,
|
|
172
|
+
all_items: list[tuple[_K, _V]],
|
|
173
|
+
existing_keys: set[_K],
|
|
174
|
+
) -> tuple[dict[_K, list[_V]], list[tuple[_K, _V]]]:
|
|
175
|
+
"""Separate items into updates and additions."""
|
|
176
|
+
updates_by_key = {} # key -> list of values to replace with
|
|
177
|
+
additions = [] # list of (key, value) for new keys
|
|
178
|
+
|
|
179
|
+
for key, value in all_items:
|
|
180
|
+
if key in existing_keys:
|
|
181
|
+
if key not in updates_by_key:
|
|
182
|
+
updates_by_key[key] = []
|
|
183
|
+
updates_by_key[key].append(value)
|
|
184
|
+
else:
|
|
185
|
+
additions.append((key, value))
|
|
186
|
+
|
|
187
|
+
return updates_by_key, additions
|
|
188
|
+
|
|
189
|
+
def _process_updates(self, updates_by_key: dict[_K, list[_V]]) -> None:
|
|
190
|
+
"""Process updates efficiently by batch removing and adding."""
|
|
191
|
+
# Mark items for removal that need to be replaced
|
|
192
|
+
items_to_remove = set()
|
|
193
|
+
for key in updates_by_key:
|
|
194
|
+
items_to_remove.update(self._key_indices[key])
|
|
195
|
+
|
|
196
|
+
# Mark items for removal
|
|
197
|
+
for idx in items_to_remove:
|
|
198
|
+
self._items[idx] = None
|
|
199
|
+
|
|
200
|
+
# Filter out None items
|
|
201
|
+
self._items = [item for item in self._items if item is not None]
|
|
202
|
+
|
|
203
|
+
# Add updated items (all values for each key)
|
|
204
|
+
for key, values in updates_by_key.items():
|
|
205
|
+
for value in values:
|
|
206
|
+
self._items.append((key, value))
|
|
207
|
+
|
|
208
|
+
@override
|
|
209
|
+
def update(
|
|
210
|
+
self,
|
|
211
|
+
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
212
|
+
**kwargs: _V,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Update the multi-mapping with items from another object.
|
|
215
|
+
|
|
216
|
+
This replaces existing values for keys found in the other object.
|
|
217
|
+
This is optimized for batch operations.
|
|
218
|
+
"""
|
|
219
|
+
# Collect all items first
|
|
220
|
+
items = other.items() if isinstance(other, Mapping) else other
|
|
221
|
+
items = itertools.chain(items, kwargs.items())
|
|
222
|
+
all_items = list(items)
|
|
223
|
+
|
|
224
|
+
if not all_items:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Get existing keys once for efficiency
|
|
228
|
+
existing_keys = set(self._key_indices.keys())
|
|
229
|
+
|
|
230
|
+
# Separate items into updates and additions
|
|
231
|
+
updates_by_key, additions = self._collect_update_items(all_items, existing_keys)
|
|
232
|
+
|
|
233
|
+
# Process updates efficiently
|
|
234
|
+
if updates_by_key:
|
|
235
|
+
self._process_updates(updates_by_key)
|
|
236
|
+
|
|
237
|
+
# Add new items
|
|
238
|
+
if additions:
|
|
239
|
+
self._items.extend(additions)
|
|
240
|
+
|
|
241
|
+
# Rebuild indices once at the end
|
|
242
|
+
if updates_by_key or additions:
|
|
243
|
+
self._rebuild_indices()
|
|
244
|
+
|
|
245
|
+
@override
|
|
246
|
+
def merge(
|
|
247
|
+
self,
|
|
248
|
+
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
249
|
+
**kwargs: _V,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Merge another object into the multi-mapping.
|
|
252
|
+
|
|
253
|
+
Keys from `other` that already exist in the multi-mapping will not be added.
|
|
254
|
+
This is optimized for batch operations.
|
|
255
|
+
"""
|
|
256
|
+
# Get existing keys once for efficiency
|
|
257
|
+
existing_keys = set(self._key_indices.keys())
|
|
258
|
+
|
|
259
|
+
# Collect all items and filter out existing keys
|
|
260
|
+
items = other.items() if isinstance(other, Mapping) else other
|
|
261
|
+
items = itertools.chain(items, kwargs.items())
|
|
262
|
+
new_items = [(key, value) for key, value in items if key not in existing_keys]
|
|
263
|
+
|
|
264
|
+
if not new_items:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# Add all items to the list at once
|
|
268
|
+
start_index = len(self._items)
|
|
269
|
+
self._items.extend(new_items)
|
|
270
|
+
|
|
271
|
+
# Update indices incrementally for better performance
|
|
272
|
+
for i, (key, _) in enumerate(new_items, start_index):
|
|
273
|
+
if key not in self._key_indices:
|
|
274
|
+
self._key_indices[key] = []
|
|
275
|
+
self._key_indices[key].append(i)
|
|
276
|
+
|
|
277
|
+
@override
|
|
278
|
+
def extend(
|
|
279
|
+
self,
|
|
280
|
+
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
281
|
+
**kwargs: _V,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Extend the multi-mapping with items from another object.
|
|
284
|
+
|
|
285
|
+
This is optimized for batch operations to avoid rebuilding indices
|
|
286
|
+
multiple times.
|
|
287
|
+
"""
|
|
288
|
+
# Collect all new items first
|
|
289
|
+
items = other.items() if isinstance(other, Mapping) else other
|
|
290
|
+
items = itertools.chain(items, kwargs.items())
|
|
291
|
+
new_items = list(items)
|
|
292
|
+
|
|
293
|
+
if not new_items:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# Add all items to the list at once
|
|
297
|
+
start_index = len(self._items)
|
|
298
|
+
self._items.extend(new_items)
|
|
299
|
+
|
|
300
|
+
# Update indices incrementally for better performance
|
|
301
|
+
for i, (key, _) in enumerate(new_items, start_index):
|
|
302
|
+
if key not in self._key_indices:
|
|
303
|
+
self._key_indices[key] = []
|
|
304
|
+
self._key_indices[key].append(i)
|
|
305
|
+
|
|
306
|
+
def copy(self) -> MultiDict[_K, _V]:
|
|
307
|
+
"""Return a shallow copy of the MultiDict."""
|
|
308
|
+
new_md = MultiDict.__new__(MultiDict)
|
|
309
|
+
new_md._items = self._items.copy() # noqa: SLF001
|
|
310
|
+
new_md._key_indices = {k: v.copy() for k, v in self._key_indices.items()} # noqa: SLF001
|
|
311
|
+
return new_md
|
|
312
|
+
|
|
313
|
+
def __repr__(self) -> str:
|
|
314
|
+
"""Return a string representation of the MultiDict."""
|
|
315
|
+
return f"{self.__class__.__name__}({list(self._items)!r})"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if sys.version_info >= (3, 12):
|
|
6
|
+
from typing import override
|
|
7
|
+
else:
|
|
8
|
+
try:
|
|
9
|
+
from typing_extensions import override
|
|
10
|
+
except ImportError: # pragma: nocover
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
raise
|
|
13
|
+
|
|
14
|
+
def override(func: Callable) -> Callable:
|
|
15
|
+
"""Fallback override decorator that does nothing."""
|
|
16
|
+
return func
|
|
@@ -8,7 +8,7 @@ import itertools
|
|
|
8
8
|
import sys
|
|
9
9
|
from abc import abstractmethod
|
|
10
10
|
from collections import defaultdict
|
|
11
|
-
from typing import Generic, TypeVar
|
|
11
|
+
from typing import TYPE_CHECKING, Generic, TypeVar, overload
|
|
12
12
|
|
|
13
13
|
if sys.version_info >= (3, 9):
|
|
14
14
|
from collections.abc import (
|
|
@@ -33,14 +33,21 @@ else:
|
|
|
33
33
|
Sequence,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
37
|
+
from typing import Protocol
|
|
38
|
+
|
|
39
|
+
from ._util import override
|
|
40
|
+
|
|
36
41
|
_K = TypeVar("_K")
|
|
37
42
|
_V = TypeVar("_V")
|
|
38
43
|
_D = TypeVar("_D")
|
|
44
|
+
_Self = TypeVar("_Self")
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
class MultiMappingView(MappingView, Collection):
|
|
42
48
|
"""Base class for MultiMapping views."""
|
|
43
49
|
|
|
50
|
+
@override
|
|
44
51
|
def __init__(self, mapping: MultiMapping[_K, _V]) -> None:
|
|
45
52
|
"""Initialize the view with the given mapping."""
|
|
46
53
|
super().__init__(mapping)
|
|
@@ -49,10 +56,12 @@ class MultiMappingView(MappingView, Collection):
|
|
|
49
56
|
class KeysView(MultiMappingView):
|
|
50
57
|
"""View for the keys in a MultiMapping."""
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
@override
|
|
60
|
+
def __contains__(self, key: object) -> bool:
|
|
53
61
|
"""Check if the key is in the mapping."""
|
|
54
62
|
return key in self._mapping
|
|
55
63
|
|
|
64
|
+
@override
|
|
56
65
|
def __iter__(self) -> Iterator[_K]:
|
|
57
66
|
"""Return an iterator over the keys."""
|
|
58
67
|
return iter(self._mapping)
|
|
@@ -61,17 +70,22 @@ class KeysView(MultiMappingView):
|
|
|
61
70
|
class ItemsView(MultiMappingView):
|
|
62
71
|
"""View for the items (key-value pairs) in a MultiMapping."""
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
@override
|
|
74
|
+
def __contains__(self, item: object) -> bool:
|
|
65
75
|
"""Check if the item is in the mapping."""
|
|
66
|
-
|
|
76
|
+
try:
|
|
77
|
+
key, value = item # ty: ignore[not-iterable]
|
|
78
|
+
except TypeError:
|
|
79
|
+
return False
|
|
67
80
|
try:
|
|
68
81
|
return value in self._mapping.getall(key)
|
|
69
82
|
except KeyError:
|
|
70
83
|
return False
|
|
71
84
|
|
|
85
|
+
@override
|
|
72
86
|
def __iter__(self) -> Iterator[tuple[_K, _V]]:
|
|
73
87
|
"""Return an iterator over the items (key-value pairs)."""
|
|
74
|
-
counts = defaultdict(int)
|
|
88
|
+
counts: defaultdict[_K, int] = defaultdict(int)
|
|
75
89
|
for k in self._mapping:
|
|
76
90
|
yield (k, self._mapping.getall(k)[counts[k]])
|
|
77
91
|
counts[k] += 1
|
|
@@ -80,10 +94,12 @@ class ItemsView(MultiMappingView):
|
|
|
80
94
|
class ValuesView(MultiMappingView):
|
|
81
95
|
"""View for the values in a MultiMapping."""
|
|
82
96
|
|
|
83
|
-
|
|
97
|
+
@override
|
|
98
|
+
def __contains__(self, value: object) -> bool:
|
|
84
99
|
"""Check if the value is in the mapping."""
|
|
85
100
|
return value in iter(self)
|
|
86
101
|
|
|
102
|
+
@override
|
|
87
103
|
def __iter__(self) -> Iterator[_V]:
|
|
88
104
|
"""Return an iterator over the values."""
|
|
89
105
|
yield from (v for _, v in self._mapping.items())
|
|
@@ -95,15 +111,30 @@ class _NoDefault:
|
|
|
95
111
|
|
|
96
112
|
_NO_DEFAULT = _NoDefault()
|
|
97
113
|
|
|
114
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
115
|
+
|
|
116
|
+
class _CallableWithDefault(Protocol[_Self, _K, _V, _D]):
|
|
117
|
+
@overload
|
|
118
|
+
def __call__(self: _Self, key: _K) -> _V: ...
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
def __call__(self: _Self, key: _K, default: _D) -> _V | _D: ...
|
|
122
|
+
|
|
98
123
|
|
|
99
124
|
def with_default(
|
|
100
|
-
meth: Callable[[
|
|
101
|
-
) ->
|
|
125
|
+
meth: Callable[[_Self, _K], _V],
|
|
126
|
+
) -> _CallableWithDefault[_Self, _K, _V, _D]:
|
|
102
127
|
"""Add a default value argument to a method that can raise a `KeyError`."""
|
|
103
128
|
|
|
129
|
+
@overload
|
|
130
|
+
def wrapper(self: _Self, key: _K) -> _V: ... # pragma: no cover
|
|
131
|
+
|
|
132
|
+
@overload
|
|
133
|
+
def wrapper(self: _Self, key: _K, default: _D) -> _V | _D: ... # pragma: no cover
|
|
134
|
+
|
|
104
135
|
@functools.wraps(meth)
|
|
105
136
|
def wrapper(
|
|
106
|
-
self:
|
|
137
|
+
self: _Self, key: _K, default: _D | _NoDefault = _NO_DEFAULT
|
|
107
138
|
) -> _V | _D:
|
|
108
139
|
try:
|
|
109
140
|
return meth(self, key)
|
|
@@ -152,6 +183,7 @@ class MultiMapping(Mapping[_K, _V], Generic[_K, _V]):
|
|
|
152
183
|
"""
|
|
153
184
|
return self.getall(key)[0]
|
|
154
185
|
|
|
186
|
+
@override
|
|
155
187
|
def __getitem__(self, key: _K) -> _V:
|
|
156
188
|
"""Get the first value for a key.
|
|
157
189
|
|
|
@@ -159,14 +191,17 @@ class MultiMapping(Mapping[_K, _V], Generic[_K, _V]):
|
|
|
159
191
|
"""
|
|
160
192
|
return self.getone(key)
|
|
161
193
|
|
|
194
|
+
@override
|
|
162
195
|
def keys(self) -> KeysView[_K]:
|
|
163
196
|
"""Return a view of the keys in the MultiMapping."""
|
|
164
197
|
return KeysView(self)
|
|
165
198
|
|
|
199
|
+
@override
|
|
166
200
|
def items(self) -> ItemsView[_K, _V]:
|
|
167
201
|
"""Return a view of the items (key-value pairs) in the MultiMapping."""
|
|
168
202
|
return ItemsView(self)
|
|
169
203
|
|
|
204
|
+
@override
|
|
170
205
|
def values(self) -> ValuesView[_V]:
|
|
171
206
|
"""Return a view of the values in the MultiMapping."""
|
|
172
207
|
return ValuesView(self)
|
|
@@ -226,6 +261,7 @@ class MutableMultiMapping(MultiMapping[_K, _V], MutableMapping[_K, _V]):
|
|
|
226
261
|
value = self.popone(key)
|
|
227
262
|
return key, value
|
|
228
263
|
|
|
264
|
+
@override
|
|
229
265
|
def __delitem__(self, key: _K) -> None:
|
|
230
266
|
"""Remove all values for a key.
|
|
231
267
|
|
|
@@ -233,11 +269,13 @@ class MutableMultiMapping(MultiMapping[_K, _V], MutableMapping[_K, _V]):
|
|
|
233
269
|
"""
|
|
234
270
|
self.popall(key)
|
|
235
271
|
|
|
272
|
+
@override
|
|
236
273
|
def clear(self) -> None:
|
|
237
274
|
"""Remove all items from the multi-mapping."""
|
|
238
275
|
for key in set(self.keys()):
|
|
239
276
|
self.popall(key)
|
|
240
277
|
|
|
278
|
+
@override
|
|
241
279
|
def extend(
|
|
242
280
|
self,
|
|
243
281
|
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
@@ -249,6 +287,7 @@ class MutableMultiMapping(MultiMapping[_K, _V], MutableMapping[_K, _V]):
|
|
|
249
287
|
for key, value in items:
|
|
250
288
|
self.add(key, value)
|
|
251
289
|
|
|
290
|
+
@override
|
|
252
291
|
def merge(
|
|
253
292
|
self,
|
|
254
293
|
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
@@ -265,6 +304,7 @@ class MutableMultiMapping(MultiMapping[_K, _V], MutableMapping[_K, _V]):
|
|
|
265
304
|
if key not in existing_keys:
|
|
266
305
|
self.add(key, value)
|
|
267
306
|
|
|
307
|
+
@override
|
|
268
308
|
def update(
|
|
269
309
|
self,
|
|
270
310
|
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "multicollections"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Gabriel S. Gerlero", email = "ggerlero@cimec.unl.edu.ar" },
|
|
10
10
|
]
|
|
@@ -31,9 +31,13 @@ requires-python = ">=3.7"
|
|
|
31
31
|
|
|
32
32
|
[dependency-groups]
|
|
33
33
|
lint = ["ruff"]
|
|
34
|
-
typing = [
|
|
34
|
+
typing = [
|
|
35
|
+
"ty; python_version>='3.8'",
|
|
36
|
+
"typing_extensions >= 4.4.0; python_version<'3.12'"
|
|
37
|
+
]
|
|
35
38
|
test = [
|
|
36
39
|
"multidict>=6,<7",
|
|
40
|
+
"typing_extensions >= 4.4.0; python_version<'3.12'",
|
|
37
41
|
"pytest>=7,<9",
|
|
38
42
|
"pytest-cov"
|
|
39
43
|
]
|
|
@@ -18,6 +18,7 @@ else:
|
|
|
18
18
|
Sequence,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
from multicollections._util import override
|
|
21
22
|
from multicollections.abc import MutableMultiMapping, with_default
|
|
22
23
|
|
|
23
24
|
_K = TypeVar("_K")
|
|
@@ -38,6 +39,7 @@ class ListMultiDict(MutableMultiMapping[_K, _V]):
|
|
|
38
39
|
for key, value in kwargs.items():
|
|
39
40
|
self._items.append((key, value))
|
|
40
41
|
|
|
42
|
+
@override
|
|
41
43
|
@with_default
|
|
42
44
|
def getall(self, key: _K) -> list[_V]:
|
|
43
45
|
ret = [v for k, v in self._items if k == key]
|
|
@@ -45,6 +47,7 @@ class ListMultiDict(MutableMultiMapping[_K, _V]):
|
|
|
45
47
|
raise KeyError(key)
|
|
46
48
|
return ret
|
|
47
49
|
|
|
50
|
+
@override
|
|
48
51
|
def __setitem__(self, key: _K, value: _V) -> None:
|
|
49
52
|
replaced: int | None = None
|
|
50
53
|
for i, (k, _) in enumerate(self._items):
|
|
@@ -62,9 +65,11 @@ class ListMultiDict(MutableMultiMapping[_K, _V]):
|
|
|
62
65
|
else:
|
|
63
66
|
self._items.append((key, value))
|
|
64
67
|
|
|
68
|
+
@override
|
|
65
69
|
def add(self, key: _K, value: _V) -> None:
|
|
66
70
|
self._items.append((key, value))
|
|
67
71
|
|
|
72
|
+
@override
|
|
68
73
|
@with_default
|
|
69
74
|
def popone(self, key: _K) -> _V:
|
|
70
75
|
for i, (k, v) in enumerate(self._items):
|
|
@@ -73,8 +78,10 @@ class ListMultiDict(MutableMultiMapping[_K, _V]):
|
|
|
73
78
|
return v
|
|
74
79
|
raise KeyError(key)
|
|
75
80
|
|
|
81
|
+
@override
|
|
76
82
|
def __iter__(self) -> Iterator[_K]:
|
|
77
83
|
return (k for k, _ in self._items)
|
|
78
84
|
|
|
85
|
+
@override
|
|
79
86
|
def __len__(self) -> int:
|
|
80
87
|
return len(self._items)
|
|
@@ -213,6 +213,9 @@ def test_items_view_contains(cls: type[MutableMultiMapping]) -> None:
|
|
|
213
213
|
assert ("c", 1) not in items # Key doesn't exist
|
|
214
214
|
assert ("b", 1) not in items # Wrong value for existing key
|
|
215
215
|
|
|
216
|
+
# Test different type
|
|
217
|
+
assert None not in items
|
|
218
|
+
|
|
216
219
|
# Test empty case
|
|
217
220
|
empty_md = cls()
|
|
218
221
|
empty_items = empty_md.items()
|
|
@@ -267,6 +270,8 @@ def test_contains(cls: type[MutableMultiMapping]) -> None:
|
|
|
267
270
|
assert "a" in md
|
|
268
271
|
assert "b" in md
|
|
269
272
|
assert "missing" not in md
|
|
273
|
+
if cls is not multidict.MultiDict or sys.version_info >= (3, 9):
|
|
274
|
+
assert None not in md
|
|
270
275
|
|
|
271
276
|
# Test with empty MultiDict
|
|
272
277
|
empty_md = cls()
|
|
@@ -805,3 +810,209 @@ def test_edge_cases_multidict_specific() -> None:
|
|
|
805
810
|
|
|
806
811
|
assert len(md._items) == 2 # b and c # noqa: SLF001
|
|
807
812
|
assert md._key_indices["c"] == [1] # Only one index for c # noqa: SLF001
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@pytest.mark.parametrize("cls", [MultiDict, multidict.MultiDict])
|
|
816
|
+
def test_copy_method(cls: type[MultiDict | multidict.MultiDict]) -> None:
|
|
817
|
+
"""Test copy() method."""
|
|
818
|
+
# Test copying empty MultiDict
|
|
819
|
+
empty_md = cls()
|
|
820
|
+
empty_copy = empty_md.copy()
|
|
821
|
+
assert len(empty_copy) == 0
|
|
822
|
+
assert list(empty_copy.items()) == []
|
|
823
|
+
assert empty_md is not empty_copy # Different objects
|
|
824
|
+
|
|
825
|
+
# Test copying MultiDict with single item
|
|
826
|
+
single_md = cls([("a", 1)]) # ty: ignore [too-many-positional-arguments]
|
|
827
|
+
single_copy = single_md.copy()
|
|
828
|
+
assert len(single_copy) == 1
|
|
829
|
+
assert list(single_copy.items()) == [("a", 1)]
|
|
830
|
+
assert single_md is not single_copy # Different objects
|
|
831
|
+
|
|
832
|
+
# Test copying MultiDict with multiple items including duplicates
|
|
833
|
+
md = cls([("a", 1), ("b", 2), ("a", 3), ("c", 4)]) # ty: ignore [too-many-positional-arguments]
|
|
834
|
+
copied = md.copy()
|
|
835
|
+
|
|
836
|
+
# Test that content is identical
|
|
837
|
+
assert len(copied) == len(md)
|
|
838
|
+
assert list(copied.items()) == list(md.items())
|
|
839
|
+
assert list(copied.keys()) == list(md.keys())
|
|
840
|
+
assert list(copied.values()) == list(md.values())
|
|
841
|
+
|
|
842
|
+
# Test that duplicate keys are preserved
|
|
843
|
+
assert copied.getall("a") == md.getall("a") == [1, 3]
|
|
844
|
+
|
|
845
|
+
# Test that they are different objects
|
|
846
|
+
assert md is not copied
|
|
847
|
+
|
|
848
|
+
# Test that changes to original don't affect copy
|
|
849
|
+
md.add("d", 5)
|
|
850
|
+
assert len(md) == 5
|
|
851
|
+
assert len(copied) == 4
|
|
852
|
+
assert "d" not in copied
|
|
853
|
+
|
|
854
|
+
# Test that changes to copy don't affect original
|
|
855
|
+
copied.add("e", 6)
|
|
856
|
+
assert len(copied) == 5
|
|
857
|
+
assert len(md) == 5
|
|
858
|
+
assert "e" not in md
|
|
859
|
+
|
|
860
|
+
# Test that copy() creates a shallow copy (values are not deep copied)
|
|
861
|
+
value_list = [1, 2, 3]
|
|
862
|
+
md_with_mutable = cls([("key", value_list)]) # ty: ignore [too-many-positional-arguments]
|
|
863
|
+
copied_with_mutable = md_with_mutable.copy()
|
|
864
|
+
|
|
865
|
+
# The list should be the same object (shallow copy)
|
|
866
|
+
assert copied_with_mutable["key"] is value_list
|
|
867
|
+
assert md_with_mutable["key"] is value_list
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
# Additional tests for uncovered lines
|
|
871
|
+
def test_empty_init_no_rebuild() -> None:
|
|
872
|
+
"""Test that empty MultiDict initialization doesn't call _rebuild_indices."""
|
|
873
|
+
# This tests line 53-54: if self._items: and the else branch
|
|
874
|
+
md = MultiDict()
|
|
875
|
+
assert len(md._items) == 0 # noqa: SLF001
|
|
876
|
+
assert len(md._key_indices) == 0 # noqa: SLF001
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def test_update_with_empty_input() -> None:
|
|
880
|
+
"""Test update with empty input to cover early return."""
|
|
881
|
+
# This tests line 214-215: if not all_items: return
|
|
882
|
+
md = MultiDict([("a", 1)])
|
|
883
|
+
md.update() # Empty input
|
|
884
|
+
assert len(md) == 1
|
|
885
|
+
assert md["a"] == 1
|
|
886
|
+
|
|
887
|
+
md.update([]) # Empty list
|
|
888
|
+
assert len(md) == 1
|
|
889
|
+
assert md["a"] == 1
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def test_merge_with_empty_input() -> None:
|
|
893
|
+
"""Test merge with empty input to cover early return."""
|
|
894
|
+
# This tests line 253-254: if not new_items: return
|
|
895
|
+
md = MultiDict([("a", 1)])
|
|
896
|
+
md.merge() # Empty input
|
|
897
|
+
assert len(md) == 1
|
|
898
|
+
assert md["a"] == 1
|
|
899
|
+
|
|
900
|
+
md.merge([]) # Empty list
|
|
901
|
+
assert len(md) == 1
|
|
902
|
+
assert md["a"] == 1
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def test_extend_with_empty_input() -> None:
|
|
906
|
+
"""Test extend with empty input to cover early return."""
|
|
907
|
+
# This tests line 281-282: if not new_items: return
|
|
908
|
+
md = MultiDict([("a", 1)])
|
|
909
|
+
md.extend() # Empty input
|
|
910
|
+
assert len(md) == 1
|
|
911
|
+
assert md["a"] == 1
|
|
912
|
+
|
|
913
|
+
md.extend([]) # Empty list
|
|
914
|
+
assert len(md) == 1
|
|
915
|
+
assert md["a"] == 1
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def test_merge_all_existing_keys() -> None:
|
|
919
|
+
"""Test merge where all keys already exist."""
|
|
920
|
+
# This tests the case where new_items becomes empty after filtering
|
|
921
|
+
md = MultiDict([("a", 1), ("b", 2)])
|
|
922
|
+
md.merge([("a", 999), ("b", 888)]) # All keys exist, should be filtered out
|
|
923
|
+
assert len(md) == 2
|
|
924
|
+
assert md["a"] == 1 # Should remain unchanged
|
|
925
|
+
assert md["b"] == 2 # Should remain unchanged
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def test_merge_existing_key_in_indices() -> None:
|
|
929
|
+
"""Test merge with key that already exists in indices."""
|
|
930
|
+
# This tests line 262-263: if key not in self._key_indices
|
|
931
|
+
md = MultiDict([("a", 1)])
|
|
932
|
+
existing_keys = set(md._key_indices.keys()) # noqa: SLF001
|
|
933
|
+
assert "a" in existing_keys
|
|
934
|
+
|
|
935
|
+
# Since merge filters out existing keys, we test the else branch
|
|
936
|
+
# by adding a new key
|
|
937
|
+
md.merge([("b", 2)])
|
|
938
|
+
assert "b" in md._key_indices # noqa: SLF001
|
|
939
|
+
assert md["b"] == 2
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def test_extend_existing_key_in_indices() -> None:
|
|
943
|
+
"""Test extend with key that already exists in indices."""
|
|
944
|
+
# This tests line 290-291: if key not in self._key_indices
|
|
945
|
+
md = MultiDict([("a", 1)])
|
|
946
|
+
|
|
947
|
+
# Extend with existing key - should add to existing key's index list
|
|
948
|
+
md.extend([("a", 2)])
|
|
949
|
+
assert len(md._key_indices["a"]) == 2 # noqa: SLF001
|
|
950
|
+
assert md._key_indices["a"] == [0, 1] # noqa: SLF001
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def test_update_only_updates_no_additions() -> None:
|
|
954
|
+
"""Test update with only existing keys (no new keys)."""
|
|
955
|
+
# This tests line 228-229: if additions: branch when additions is empty
|
|
956
|
+
md = MultiDict([("a", 1), ("b", 2)])
|
|
957
|
+
md.update([("a", 999), ("b", 888)]) # Only existing keys
|
|
958
|
+
|
|
959
|
+
assert len(md) == 2
|
|
960
|
+
assert md["a"] == 999
|
|
961
|
+
assert md["b"] == 888
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def test_update_only_additions_no_updates() -> None:
|
|
965
|
+
"""Test update with only new keys (no existing keys)."""
|
|
966
|
+
# This tests line 224-225: if updates_by_key: branch when updates_by_key is empty
|
|
967
|
+
md = MultiDict([("a", 1)])
|
|
968
|
+
md.update([("b", 2), ("c", 3)]) # Only new keys
|
|
969
|
+
|
|
970
|
+
assert len(md) == 3
|
|
971
|
+
assert md["a"] == 1 # Original unchanged
|
|
972
|
+
assert md["b"] == 2
|
|
973
|
+
assert md["c"] == 3
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def test_init_with_no_items_and_no_kwargs() -> None:
|
|
977
|
+
"""Test initialization with completely empty input."""
|
|
978
|
+
# This tests the case where _items remains empty after all initialization
|
|
979
|
+
md = MultiDict(()) # Empty tuple
|
|
980
|
+
assert len(md._items) == 0 # noqa: SLF001
|
|
981
|
+
assert len(md._key_indices) == 0 # noqa: SLF001
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def test_collect_update_items_edge_cases() -> None:
|
|
985
|
+
"""Test _collect_update_items helper method edge cases."""
|
|
986
|
+
md = MultiDict([("a", 1)])
|
|
987
|
+
|
|
988
|
+
# Test with empty all_items
|
|
989
|
+
updates, additions = md._collect_update_items([], set()) # noqa: SLF001
|
|
990
|
+
assert updates == {}
|
|
991
|
+
assert additions == []
|
|
992
|
+
|
|
993
|
+
# Test with all new keys
|
|
994
|
+
updates, additions = md._collect_update_items([("b", 2), ("c", 3)], set()) # noqa: SLF001
|
|
995
|
+
assert updates == {}
|
|
996
|
+
assert additions == [("b", 2), ("c", 3)]
|
|
997
|
+
|
|
998
|
+
# Test with all existing keys
|
|
999
|
+
updates, additions = md._collect_update_items([("a", 999)], {"a"}) # noqa: SLF001
|
|
1000
|
+
assert updates == {"a": [999]}
|
|
1001
|
+
assert additions == []
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def test_process_updates_edge_cases() -> None:
|
|
1005
|
+
"""Test _process_updates helper method edge cases."""
|
|
1006
|
+
md = MultiDict([("a", 1), ("b", 2), ("a", 3)])
|
|
1007
|
+
original_len = len(md)
|
|
1008
|
+
|
|
1009
|
+
# Test with empty updates
|
|
1010
|
+
md._process_updates({}) # noqa: SLF001
|
|
1011
|
+
assert len(md) == original_len # Should remain unchanged
|
|
1012
|
+
|
|
1013
|
+
# Test with single key update - need to rebuild indices after _process_updates
|
|
1014
|
+
md = MultiDict([("a", 1), ("b", 2), ("a", 3)])
|
|
1015
|
+
md._process_updates({"a": [999]}) # noqa: SLF001
|
|
1016
|
+
md._rebuild_indices() # _process_updates doesn't rebuild indices # noqa: SLF001
|
|
1017
|
+
assert md["a"] == 999
|
|
1018
|
+
assert md["b"] == 2
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
"""Fully generic `MultiDict` class."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
from typing import TypeVar
|
|
7
|
-
|
|
8
|
-
if sys.version_info >= (3, 9):
|
|
9
|
-
from collections.abc import (
|
|
10
|
-
Iterable,
|
|
11
|
-
Iterator,
|
|
12
|
-
Mapping,
|
|
13
|
-
Sequence,
|
|
14
|
-
)
|
|
15
|
-
else:
|
|
16
|
-
from typing import (
|
|
17
|
-
Iterable,
|
|
18
|
-
Iterator,
|
|
19
|
-
Mapping,
|
|
20
|
-
Sequence,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
from .abc import MutableMultiMapping, with_default
|
|
24
|
-
|
|
25
|
-
_K = TypeVar("_K")
|
|
26
|
-
_V = TypeVar("_V")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class MultiDict(MutableMultiMapping[_K, _V]):
|
|
30
|
-
"""A fully generic dictionary that allows multiple values with the same key.
|
|
31
|
-
|
|
32
|
-
Preserves insertion order.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
def __init__(
|
|
36
|
-
self, iterable: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (), **kwargs: _V
|
|
37
|
-
) -> None:
|
|
38
|
-
"""Create a MultiDict."""
|
|
39
|
-
self._items: list[tuple[_K, _V]] = []
|
|
40
|
-
self._key_indices: dict[_K, list[int]] = {}
|
|
41
|
-
if isinstance(iterable, Mapping):
|
|
42
|
-
for key, value in iterable.items():
|
|
43
|
-
self._add_item(key, value)
|
|
44
|
-
else:
|
|
45
|
-
for key, value in iterable:
|
|
46
|
-
self._add_item(key, value)
|
|
47
|
-
for key, value in kwargs.items():
|
|
48
|
-
self._add_item(key, value)
|
|
49
|
-
|
|
50
|
-
def _add_item(self, key: _K, value: _V) -> None:
|
|
51
|
-
"""Add an item and update the key index."""
|
|
52
|
-
index = len(self._items)
|
|
53
|
-
self._items.append((key, value))
|
|
54
|
-
if key not in self._key_indices:
|
|
55
|
-
self._key_indices[key] = []
|
|
56
|
-
self._key_indices[key].append(index)
|
|
57
|
-
|
|
58
|
-
@with_default
|
|
59
|
-
def getall(self, key: _K) -> list[_V]:
|
|
60
|
-
"""Get all values for a key.
|
|
61
|
-
|
|
62
|
-
Raises a `KeyError` if the key is not found and no default is provided.
|
|
63
|
-
"""
|
|
64
|
-
ret = [self._items[i][1] for i in self._key_indices.get(key, [])]
|
|
65
|
-
if not ret:
|
|
66
|
-
raise KeyError(key)
|
|
67
|
-
return ret
|
|
68
|
-
|
|
69
|
-
def __setitem__(self, key: _K, value: _V) -> None:
|
|
70
|
-
"""Set the value for a key.
|
|
71
|
-
|
|
72
|
-
Replaces the first value for a key if it exists; otherwise, it adds a new item.
|
|
73
|
-
Any other items with the same key are removed.
|
|
74
|
-
"""
|
|
75
|
-
if key in self._key_indices:
|
|
76
|
-
# Key exists, replace first occurrence and remove others
|
|
77
|
-
indices = self._key_indices[key]
|
|
78
|
-
first_index = indices[0]
|
|
79
|
-
|
|
80
|
-
# Update the first occurrence
|
|
81
|
-
self._items[first_index] = (key, value)
|
|
82
|
-
|
|
83
|
-
if len(indices) > 1:
|
|
84
|
-
# Remove duplicates efficiently by marking items as None and filtering
|
|
85
|
-
for idx in indices[1:]:
|
|
86
|
-
self._items[idx] = None
|
|
87
|
-
|
|
88
|
-
# Filter out None items and rebuild indices
|
|
89
|
-
self._items = [item for item in self._items if item is not None]
|
|
90
|
-
self._rebuild_indices()
|
|
91
|
-
else:
|
|
92
|
-
# Key doesn't exist, add it
|
|
93
|
-
self._add_item(key, value)
|
|
94
|
-
|
|
95
|
-
def _rebuild_indices(self) -> None:
|
|
96
|
-
"""Rebuild the key indices after items list has been modified."""
|
|
97
|
-
self._key_indices = {}
|
|
98
|
-
for i, (key, _) in enumerate(self._items):
|
|
99
|
-
if key not in self._key_indices:
|
|
100
|
-
self._key_indices[key] = []
|
|
101
|
-
self._key_indices[key].append(i)
|
|
102
|
-
|
|
103
|
-
def add(self, key: _K, value: _V) -> None:
|
|
104
|
-
"""Add a new value for a key."""
|
|
105
|
-
self._add_item(key, value)
|
|
106
|
-
|
|
107
|
-
@with_default
|
|
108
|
-
def popone(self, key: _K) -> _V:
|
|
109
|
-
"""Remove and return the first value for a key."""
|
|
110
|
-
if key not in self._key_indices:
|
|
111
|
-
raise KeyError(key)
|
|
112
|
-
|
|
113
|
-
indices = self._key_indices[key]
|
|
114
|
-
first_index = indices[0]
|
|
115
|
-
value = self._items[first_index][1]
|
|
116
|
-
|
|
117
|
-
# Mark the first item for removal
|
|
118
|
-
self._items[first_index] = None
|
|
119
|
-
|
|
120
|
-
# Filter out None items and rebuild indices
|
|
121
|
-
self._items = [item for item in self._items if item is not None]
|
|
122
|
-
self._rebuild_indices()
|
|
123
|
-
|
|
124
|
-
return value
|
|
125
|
-
|
|
126
|
-
def __delitem__(self, key: _K) -> None:
|
|
127
|
-
"""Remove all values for a key.
|
|
128
|
-
|
|
129
|
-
Raises a `KeyError` if the key is not found.
|
|
130
|
-
"""
|
|
131
|
-
if key not in self._key_indices:
|
|
132
|
-
raise KeyError(key)
|
|
133
|
-
|
|
134
|
-
# Mark items for removal
|
|
135
|
-
indices_to_remove = self._key_indices[key]
|
|
136
|
-
for idx in indices_to_remove:
|
|
137
|
-
self._items[idx] = None
|
|
138
|
-
|
|
139
|
-
# Filter out None items and rebuild indices
|
|
140
|
-
self._items = [item for item in self._items if item is not None]
|
|
141
|
-
self._rebuild_indices()
|
|
142
|
-
|
|
143
|
-
def __iter__(self) -> Iterator[_K]:
|
|
144
|
-
"""Return an iterator over the keys, in insertion order.
|
|
145
|
-
|
|
146
|
-
Keys with multiple values will be yielded multiple times.
|
|
147
|
-
"""
|
|
148
|
-
return (k for k, _ in self._items)
|
|
149
|
-
|
|
150
|
-
def __len__(self) -> int:
|
|
151
|
-
"""Return the total number of items."""
|
|
152
|
-
return len(self._items)
|
|
153
|
-
|
|
154
|
-
def __repr__(self) -> str:
|
|
155
|
-
"""Return a string representation of the MultiDict."""
|
|
156
|
-
return f"{self.__class__.__name__}({list(self._items)!r})"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|