multicollections 0.1.2__tar.gz → 0.2.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.
- {multicollections-0.1.2 → multicollections-0.2.0}/PKG-INFO +1 -1
- {multicollections-0.1.2 → multicollections-0.2.0}/mkdocs.yml +0 -1
- {multicollections-0.1.2 → multicollections-0.2.0}/multicollections/__init__.py +25 -17
- {multicollections-0.1.2 → multicollections-0.2.0}/multicollections/abc.py +71 -109
- {multicollections-0.1.2 → multicollections-0.2.0}/pyproject.toml +1 -1
- multicollections-0.2.0/tests/minimalimpl.py +80 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/tests/test_multicollections.py +18 -75
- {multicollections-0.1.2 → multicollections-0.2.0}/.github/dependabot.yml +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/.github/workflows/ci.yml +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/.github/workflows/pypi-publish.yml +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/.gitignore +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/.readthedocs.yaml +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/LICENSE.txt +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/README.md +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/docs/api/abc.md +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/docs/api/multicollections.md +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/docs/index.md +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/multicollections/py.typed +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/tests/__init__.py +0 -0
- {multicollections-0.1.2 → multicollections-0.2.0}/tests/ruff.toml +0 -0
|
@@ -20,24 +20,24 @@ else:
|
|
|
20
20
|
Sequence,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
-
from .abc import MutableMultiMapping
|
|
23
|
+
from .abc import MutableMultiMapping, with_default
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
_K = TypeVar("_K")
|
|
26
|
+
_V = TypeVar("_V")
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class MultiDict(MutableMultiMapping[
|
|
29
|
+
class MultiDict(MutableMultiMapping[_K, _V]):
|
|
30
30
|
"""A fully generic dictionary that allows multiple values with the same key.
|
|
31
31
|
|
|
32
32
|
Preserves insertion order.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
35
|
def __init__(
|
|
36
|
-
self, iterable: Mapping[
|
|
36
|
+
self, iterable: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (), **kwargs: _V
|
|
37
37
|
) -> None:
|
|
38
38
|
"""Create a MultiDict."""
|
|
39
|
-
self._items: list[tuple[
|
|
40
|
-
self._key_indices: dict[
|
|
39
|
+
self._items: list[tuple[_K, _V]] = []
|
|
40
|
+
self._key_indices: dict[_K, list[int]] = {}
|
|
41
41
|
if isinstance(iterable, Mapping):
|
|
42
42
|
for key, value in iterable.items():
|
|
43
43
|
self._add_item(key, value)
|
|
@@ -47,7 +47,7 @@ class MultiDict(MutableMultiMapping[K, V]):
|
|
|
47
47
|
for key, value in kwargs.items():
|
|
48
48
|
self._add_item(key, value)
|
|
49
49
|
|
|
50
|
-
def _add_item(self, key:
|
|
50
|
+
def _add_item(self, key: _K, value: _V) -> None:
|
|
51
51
|
"""Add an item and update the key index."""
|
|
52
52
|
index = len(self._items)
|
|
53
53
|
self._items.append((key, value))
|
|
@@ -55,11 +55,18 @@ class MultiDict(MutableMultiMapping[K, V]):
|
|
|
55
55
|
self._key_indices[key] = []
|
|
56
56
|
self._key_indices[key].append(index)
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
@with_default
|
|
59
|
+
def getall(self, key: _K) -> list[_V]:
|
|
60
|
+
"""Get all values for a key.
|
|
61
61
|
|
|
62
|
-
|
|
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:
|
|
63
70
|
"""Set the value for a key.
|
|
64
71
|
|
|
65
72
|
Replaces the first value for a key if it exists; otherwise, it adds a new item.
|
|
@@ -93,11 +100,12 @@ class MultiDict(MutableMultiMapping[K, V]):
|
|
|
93
100
|
self._key_indices[key] = []
|
|
94
101
|
self._key_indices[key].append(i)
|
|
95
102
|
|
|
96
|
-
def add(self, key:
|
|
97
|
-
"""Add a value for a key."""
|
|
103
|
+
def add(self, key: _K, value: _V) -> None:
|
|
104
|
+
"""Add a new value for a key."""
|
|
98
105
|
self._add_item(key, value)
|
|
99
106
|
|
|
100
|
-
|
|
107
|
+
@with_default
|
|
108
|
+
def popone(self, key: _K) -> _V:
|
|
101
109
|
"""Remove and return the first value for a key."""
|
|
102
110
|
if key not in self._key_indices:
|
|
103
111
|
raise KeyError(key)
|
|
@@ -115,7 +123,7 @@ class MultiDict(MutableMultiMapping[K, V]):
|
|
|
115
123
|
|
|
116
124
|
return value
|
|
117
125
|
|
|
118
|
-
def __delitem__(self, key:
|
|
126
|
+
def __delitem__(self, key: _K) -> None:
|
|
119
127
|
"""Remove all values for a key.
|
|
120
128
|
|
|
121
129
|
Raises a `KeyError` if the key is not found.
|
|
@@ -132,7 +140,7 @@ class MultiDict(MutableMultiMapping[K, V]):
|
|
|
132
140
|
self._items = [item for item in self._items if item is not None]
|
|
133
141
|
self._rebuild_indices()
|
|
134
142
|
|
|
135
|
-
def __iter__(self) -> Iterator[
|
|
143
|
+
def __iter__(self) -> Iterator[_K]:
|
|
136
144
|
"""Return an iterator over the keys, in insertion order.
|
|
137
145
|
|
|
138
146
|
Keys with multiple values will be yielded multiple times.
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
6
|
+
import functools
|
|
5
7
|
import itertools
|
|
6
8
|
import sys
|
|
7
9
|
from abc import abstractmethod
|
|
8
10
|
from collections import defaultdict
|
|
9
|
-
from typing import Generic, TypeVar
|
|
11
|
+
from typing import Generic, TypeVar
|
|
10
12
|
|
|
11
13
|
if sys.version_info >= (3, 9):
|
|
12
14
|
from collections.abc import (
|
|
15
|
+
Callable,
|
|
13
16
|
Collection,
|
|
14
17
|
Iterable,
|
|
15
18
|
Iterator,
|
|
@@ -20,6 +23,7 @@ if sys.version_info >= (3, 9):
|
|
|
20
23
|
)
|
|
21
24
|
else:
|
|
22
25
|
from typing import (
|
|
26
|
+
Callable,
|
|
23
27
|
Collection,
|
|
24
28
|
Iterable,
|
|
25
29
|
Iterator,
|
|
@@ -29,15 +33,15 @@ else:
|
|
|
29
33
|
Sequence,
|
|
30
34
|
)
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
_K = TypeVar("_K")
|
|
37
|
+
_V = TypeVar("_V")
|
|
38
|
+
_D = TypeVar("_D")
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
class MultiMappingView(MappingView, Collection):
|
|
38
42
|
"""Base class for MultiMapping views."""
|
|
39
43
|
|
|
40
|
-
def __init__(self, mapping: MultiMapping[
|
|
44
|
+
def __init__(self, mapping: MultiMapping[_K, _V]) -> None:
|
|
41
45
|
"""Initialize the view with the given mapping."""
|
|
42
46
|
super().__init__(mapping)
|
|
43
47
|
|
|
@@ -45,11 +49,11 @@ class MultiMappingView(MappingView, Collection):
|
|
|
45
49
|
class KeysView(MultiMappingView):
|
|
46
50
|
"""View for the keys in a MultiMapping."""
|
|
47
51
|
|
|
48
|
-
def __contains__(self, key:
|
|
52
|
+
def __contains__(self, key: _K) -> bool:
|
|
49
53
|
"""Check if the key is in the mapping."""
|
|
50
54
|
return key in self._mapping
|
|
51
55
|
|
|
52
|
-
def __iter__(self) -> Iterator[
|
|
56
|
+
def __iter__(self) -> Iterator[_K]:
|
|
53
57
|
"""Return an iterator over the keys."""
|
|
54
58
|
return iter(self._mapping)
|
|
55
59
|
|
|
@@ -57,7 +61,7 @@ class KeysView(MultiMappingView):
|
|
|
57
61
|
class ItemsView(MultiMappingView):
|
|
58
62
|
"""View for the items (key-value pairs) in a MultiMapping."""
|
|
59
63
|
|
|
60
|
-
def __contains__(self, item: tuple[
|
|
64
|
+
def __contains__(self, item: tuple[_K, _V]) -> bool:
|
|
61
65
|
"""Check if the item is in the mapping."""
|
|
62
66
|
key, value = item
|
|
63
67
|
try:
|
|
@@ -65,7 +69,7 @@ class ItemsView(MultiMappingView):
|
|
|
65
69
|
except KeyError:
|
|
66
70
|
return False
|
|
67
71
|
|
|
68
|
-
def __iter__(self) -> Iterator[tuple[
|
|
72
|
+
def __iter__(self) -> Iterator[tuple[_K, _V]]:
|
|
69
73
|
"""Return an iterator over the items (key-value pairs)."""
|
|
70
74
|
counts = defaultdict(int)
|
|
71
75
|
for k in self._mapping:
|
|
@@ -76,11 +80,11 @@ class ItemsView(MultiMappingView):
|
|
|
76
80
|
class ValuesView(MultiMappingView):
|
|
77
81
|
"""View for the values in a MultiMapping."""
|
|
78
82
|
|
|
79
|
-
def __contains__(self, value:
|
|
83
|
+
def __contains__(self, value: _V) -> bool:
|
|
80
84
|
"""Check if the value is in the mapping."""
|
|
81
|
-
return
|
|
85
|
+
return value in iter(self)
|
|
82
86
|
|
|
83
|
-
def __iter__(self) -> Iterator[
|
|
87
|
+
def __iter__(self) -> Iterator[_V]:
|
|
84
88
|
"""Return an iterator over the values."""
|
|
85
89
|
yield from (v for _, v in self._mapping.items())
|
|
86
90
|
|
|
@@ -92,7 +96,26 @@ class _NoDefault:
|
|
|
92
96
|
_NO_DEFAULT = _NoDefault()
|
|
93
97
|
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
def with_default(
|
|
100
|
+
meth: Callable[[MultiMappingView[_K, _V], _K], _V],
|
|
101
|
+
) -> Callable[[MultiMappingView[_K, _V], _K, _D], _V | _D]:
|
|
102
|
+
"""Add a default value argument to a method that can raise a `KeyError`."""
|
|
103
|
+
|
|
104
|
+
@functools.wraps(meth)
|
|
105
|
+
def wrapper(
|
|
106
|
+
self: MultiMappingView[_K, _V], key: _K, default: _D | _NoDefault = _NO_DEFAULT
|
|
107
|
+
) -> _V | _D:
|
|
108
|
+
try:
|
|
109
|
+
return meth(self, key)
|
|
110
|
+
except KeyError:
|
|
111
|
+
if default is _NO_DEFAULT:
|
|
112
|
+
raise
|
|
113
|
+
return default # ty: ignore [invalid-return-type]
|
|
114
|
+
|
|
115
|
+
return wrapper
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class MultiMapping(Mapping[_K, _V], Generic[_K, _V]):
|
|
96
119
|
"""Abstract base class for multi-mapping collections.
|
|
97
120
|
|
|
98
121
|
A multi-mapping is a mapping that can hold multiple values for the same key.
|
|
@@ -100,15 +123,16 @@ class MultiMapping(Mapping[K, V], Generic[K, V]):
|
|
|
100
123
|
"""
|
|
101
124
|
|
|
102
125
|
@abstractmethod
|
|
103
|
-
|
|
126
|
+
@with_default
|
|
127
|
+
def getall(self, key: _K) -> list[_V]:
|
|
104
128
|
"""Get all values for a key.
|
|
105
129
|
|
|
106
|
-
|
|
130
|
+
Raises a `KeyError` if the key is not found and no default is provided.
|
|
107
131
|
"""
|
|
108
132
|
raise NotImplementedError # pragma: no cover
|
|
109
133
|
|
|
110
134
|
@abstractmethod
|
|
111
|
-
def __iter__(self) -> Iterator[
|
|
135
|
+
def __iter__(self) -> Iterator[_K]:
|
|
112
136
|
"""Return an iterator over the keys.
|
|
113
137
|
|
|
114
138
|
Keys with multiple values will be yielded multiple times.
|
|
@@ -120,74 +144,42 @@ class MultiMapping(Mapping[K, V], Generic[K, V]):
|
|
|
120
144
|
"""Return the total number of items (key-value pairs)."""
|
|
121
145
|
raise NotImplementedError # pragma: no cover
|
|
122
146
|
|
|
123
|
-
@
|
|
124
|
-
def getone(self, key:
|
|
125
|
-
|
|
126
|
-
@overload
|
|
127
|
-
def getone(self, key: K, default: D) -> V | D: ...
|
|
128
|
-
|
|
129
|
-
def getone(self, key: K, default: D | _NoDefault = _NO_DEFAULT) -> V | D:
|
|
147
|
+
@with_default
|
|
148
|
+
def getone(self, key: _K) -> _V:
|
|
130
149
|
"""Get the first value for a key.
|
|
131
150
|
|
|
132
151
|
Raises a `KeyError` if the key is not found and no default is provided.
|
|
133
152
|
"""
|
|
134
|
-
|
|
135
|
-
return self.getall(key)[0]
|
|
136
|
-
except KeyError:
|
|
137
|
-
if default is _NO_DEFAULT:
|
|
138
|
-
raise
|
|
139
|
-
return default # ty: ignore[invalid-return-type]
|
|
153
|
+
return self.getall(key)[0]
|
|
140
154
|
|
|
141
|
-
def __getitem__(self, key:
|
|
155
|
+
def __getitem__(self, key: _K) -> _V:
|
|
142
156
|
"""Get the first value for a key.
|
|
143
157
|
|
|
144
158
|
Raises a `KeyError` if the key is not found.
|
|
145
159
|
"""
|
|
146
160
|
return self.getone(key)
|
|
147
161
|
|
|
148
|
-
|
|
149
|
-
def getall(self, key: K) -> list[V]: ...
|
|
150
|
-
|
|
151
|
-
@overload
|
|
152
|
-
def getall(self, key: K, default: D) -> list[V] | D: ...
|
|
153
|
-
|
|
154
|
-
def getall(self, key: K, default: D | _NoDefault = _NO_DEFAULT) -> list[V] | D:
|
|
155
|
-
"""Get all values for a key as a list.
|
|
156
|
-
|
|
157
|
-
Raises a `KeyError` if the key is not found and no default is provided.
|
|
158
|
-
"""
|
|
159
|
-
try:
|
|
160
|
-
ret = self._getall(key)
|
|
161
|
-
except KeyError as e: # pragma: no cover
|
|
162
|
-
msg = "_getall must return an empty list instead of raising KeyError"
|
|
163
|
-
raise RuntimeError(msg) from e
|
|
164
|
-
if not ret:
|
|
165
|
-
if default is _NO_DEFAULT:
|
|
166
|
-
raise KeyError(key)
|
|
167
|
-
return default # ty: ignore[invalid-return-type]
|
|
168
|
-
return ret
|
|
169
|
-
|
|
170
|
-
def keys(self) -> KeysView[K]:
|
|
162
|
+
def keys(self) -> KeysView[_K]:
|
|
171
163
|
"""Return a view of the keys in the MultiMapping."""
|
|
172
164
|
return KeysView(self)
|
|
173
165
|
|
|
174
|
-
def items(self) -> ItemsView[
|
|
166
|
+
def items(self) -> ItemsView[_K, _V]:
|
|
175
167
|
"""Return a view of the items (key-value pairs) in the MultiMapping."""
|
|
176
168
|
return ItemsView(self)
|
|
177
169
|
|
|
178
|
-
def values(self) -> ValuesView[
|
|
170
|
+
def values(self) -> ValuesView[_V]:
|
|
179
171
|
"""Return a view of the values in the MultiMapping."""
|
|
180
172
|
return ValuesView(self)
|
|
181
173
|
|
|
182
174
|
|
|
183
|
-
class MutableMultiMapping(MultiMapping[
|
|
175
|
+
class MutableMultiMapping(MultiMapping[_K, _V], MutableMapping[_K, _V]):
|
|
184
176
|
"""Abstract base class for mutable multi-mapping collections.
|
|
185
177
|
|
|
186
178
|
A mutable multi-mapping extends MultiMapping with methods to modify the collection.
|
|
187
179
|
"""
|
|
188
180
|
|
|
189
181
|
@abstractmethod
|
|
190
|
-
def __setitem__(self, key:
|
|
182
|
+
def __setitem__(self, key: _K, value: _V) -> None:
|
|
191
183
|
"""Set the value for a key.
|
|
192
184
|
|
|
193
185
|
If the key does not exist, it is added with the specified value.
|
|
@@ -198,78 +190,48 @@ class MutableMultiMapping(MultiMapping[K, V], MutableMapping[K, V]):
|
|
|
198
190
|
raise NotImplementedError # pragma: no cover
|
|
199
191
|
|
|
200
192
|
@abstractmethod
|
|
201
|
-
def add(self, key:
|
|
193
|
+
def add(self, key: _K, value: _V) -> None:
|
|
202
194
|
"""Add a new value for a key."""
|
|
203
195
|
raise NotImplementedError # pragma: no cover
|
|
204
196
|
|
|
205
197
|
@abstractmethod
|
|
206
|
-
|
|
198
|
+
@with_default
|
|
199
|
+
def popone(self, key: _K) -> _V:
|
|
207
200
|
"""Remove and return the first value for a key.
|
|
208
201
|
|
|
209
202
|
Raises a `KeyError` if the key is not found.
|
|
210
203
|
"""
|
|
211
204
|
raise NotImplementedError # pragma: no cover
|
|
212
205
|
|
|
213
|
-
@
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
@overload
|
|
217
|
-
def popone(self, key: K, default: D) -> V | D: ...
|
|
218
|
-
|
|
219
|
-
def popone(self, key: K, default: D | _NoDefault = _NO_DEFAULT) -> V | D:
|
|
220
|
-
"""Remove and return the first value for a key.
|
|
221
|
-
|
|
222
|
-
Raises a `KeyError` if the key is not found and no default is provided.
|
|
223
|
-
"""
|
|
224
|
-
try:
|
|
225
|
-
return self._popone(key)
|
|
226
|
-
except KeyError:
|
|
227
|
-
if default is _NO_DEFAULT:
|
|
228
|
-
raise
|
|
229
|
-
return default # type: ignore[invalid-return-type]
|
|
230
|
-
|
|
231
|
-
@overload
|
|
232
|
-
def popall(self, key: K) -> list[V]: ...
|
|
233
|
-
|
|
234
|
-
@overload
|
|
235
|
-
def popall(self, key: K, default: D) -> list[V] | D: ...
|
|
236
|
-
|
|
237
|
-
def popall(self, key: K, default: D | _NoDefault = _NO_DEFAULT) -> list[V] | D:
|
|
206
|
+
@with_default
|
|
207
|
+
def popall(self, key: _K) -> list[_V]:
|
|
238
208
|
"""Remove and return all values for a key as a list.
|
|
239
209
|
|
|
240
210
|
Raises a `KeyError` if the key is not found and no default is provided.
|
|
241
211
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return default # type: ignore[invalid-return-type]
|
|
248
|
-
|
|
249
|
-
@overload
|
|
250
|
-
def pop(self, key: K) -> V: ...
|
|
251
|
-
|
|
252
|
-
@overload
|
|
253
|
-
def pop(self, key: K, default: D) -> V | D: ...
|
|
212
|
+
ret = [self.popone(key)]
|
|
213
|
+
with contextlib.suppress(KeyError):
|
|
214
|
+
while True:
|
|
215
|
+
ret.append(self.popone(key))
|
|
216
|
+
return ret
|
|
254
217
|
|
|
255
|
-
|
|
218
|
+
@with_default
|
|
219
|
+
def pop(self, key: _K) -> _V:
|
|
256
220
|
"""Same as `popone`."""
|
|
257
|
-
|
|
258
|
-
return self.popone(key)
|
|
259
|
-
return self.popone(key, default)
|
|
221
|
+
return self.popone(key)
|
|
260
222
|
|
|
261
|
-
def popitem(self) -> tuple[
|
|
223
|
+
def popitem(self) -> tuple[_K, _V]:
|
|
262
224
|
"""Remove and return a (key, value) pair."""
|
|
263
225
|
key = next(iter(self))
|
|
264
226
|
value = self.popone(key)
|
|
265
227
|
return key, value
|
|
266
228
|
|
|
267
|
-
def __delitem__(self, key:
|
|
229
|
+
def __delitem__(self, key: _K) -> None:
|
|
268
230
|
"""Remove all values for a key.
|
|
269
231
|
|
|
270
232
|
Raises a `KeyError` if the key is not found.
|
|
271
233
|
"""
|
|
272
|
-
|
|
234
|
+
self.popall(key)
|
|
273
235
|
|
|
274
236
|
def clear(self) -> None:
|
|
275
237
|
"""Remove all items from the multi-mapping."""
|
|
@@ -278,8 +240,8 @@ class MutableMultiMapping(MultiMapping[K, V], MutableMapping[K, V]):
|
|
|
278
240
|
|
|
279
241
|
def extend(
|
|
280
242
|
self,
|
|
281
|
-
other: Mapping[
|
|
282
|
-
**kwargs:
|
|
243
|
+
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
244
|
+
**kwargs: _V,
|
|
283
245
|
) -> None:
|
|
284
246
|
"""Extend the multi-mapping with items from another object."""
|
|
285
247
|
items = other.items() if isinstance(other, Mapping) else other
|
|
@@ -289,8 +251,8 @@ class MutableMultiMapping(MultiMapping[K, V], MutableMapping[K, V]):
|
|
|
289
251
|
|
|
290
252
|
def merge(
|
|
291
253
|
self,
|
|
292
|
-
other: Mapping[
|
|
293
|
-
**kwargs:
|
|
254
|
+
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
255
|
+
**kwargs: _V,
|
|
294
256
|
) -> None:
|
|
295
257
|
"""Merge another object into the multi-mapping.
|
|
296
258
|
|
|
@@ -305,8 +267,8 @@ class MutableMultiMapping(MultiMapping[K, V], MutableMapping[K, V]):
|
|
|
305
267
|
|
|
306
268
|
def update(
|
|
307
269
|
self,
|
|
308
|
-
other: Mapping[
|
|
309
|
-
**kwargs:
|
|
270
|
+
other: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (),
|
|
271
|
+
**kwargs: _V,
|
|
310
272
|
) -> None:
|
|
311
273
|
"""Update the multi-mapping with items from another object.
|
|
312
274
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
if sys.version_info >= (3, 9):
|
|
7
|
+
from collections.abc import (
|
|
8
|
+
Iterable,
|
|
9
|
+
Iterator,
|
|
10
|
+
Mapping,
|
|
11
|
+
Sequence,
|
|
12
|
+
)
|
|
13
|
+
else:
|
|
14
|
+
from typing import (
|
|
15
|
+
Iterable,
|
|
16
|
+
Iterator,
|
|
17
|
+
Mapping,
|
|
18
|
+
Sequence,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from multicollections.abc import MutableMultiMapping, with_default
|
|
22
|
+
|
|
23
|
+
_K = TypeVar("_K")
|
|
24
|
+
_V = TypeVar("_V")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ListMultiDict(MutableMultiMapping[_K, _V]):
|
|
28
|
+
def __init__(
|
|
29
|
+
self, iterable: Mapping[_K, _V] | Iterable[Sequence[_K | _V]] = (), **kwargs: _V
|
|
30
|
+
) -> None:
|
|
31
|
+
self._items: list[tuple[_K, _V]] = []
|
|
32
|
+
if isinstance(iterable, Mapping):
|
|
33
|
+
for key, value in iterable.items():
|
|
34
|
+
self._items.append((key, value))
|
|
35
|
+
else:
|
|
36
|
+
for key, value in iterable:
|
|
37
|
+
self._items.append((key, value))
|
|
38
|
+
for key, value in kwargs.items():
|
|
39
|
+
self._items.append((key, value))
|
|
40
|
+
|
|
41
|
+
@with_default
|
|
42
|
+
def getall(self, key: _K) -> list[_V]:
|
|
43
|
+
ret = [v for k, v in self._items if k == key]
|
|
44
|
+
if not ret:
|
|
45
|
+
raise KeyError(key)
|
|
46
|
+
return ret
|
|
47
|
+
|
|
48
|
+
def __setitem__(self, key: _K, value: _V) -> None:
|
|
49
|
+
replaced: int | None = None
|
|
50
|
+
for i, (k, _) in enumerate(self._items):
|
|
51
|
+
if k == key:
|
|
52
|
+
self._items[i] = (key, value)
|
|
53
|
+
replaced = i
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if replaced is not None:
|
|
57
|
+
self._items = [
|
|
58
|
+
(k, v)
|
|
59
|
+
for i, (k, v) in enumerate(self._items)
|
|
60
|
+
if i == replaced or k != key
|
|
61
|
+
]
|
|
62
|
+
else:
|
|
63
|
+
self._items.append((key, value))
|
|
64
|
+
|
|
65
|
+
def add(self, key: _K, value: _V) -> None:
|
|
66
|
+
self._items.append((key, value))
|
|
67
|
+
|
|
68
|
+
@with_default
|
|
69
|
+
def popone(self, key: _K) -> _V:
|
|
70
|
+
for i, (k, v) in enumerate(self._items):
|
|
71
|
+
if k == key:
|
|
72
|
+
del self._items[i]
|
|
73
|
+
return v
|
|
74
|
+
raise KeyError(key)
|
|
75
|
+
|
|
76
|
+
def __iter__(self) -> Iterator[_K]:
|
|
77
|
+
return (k for k, _ in self._items)
|
|
78
|
+
|
|
79
|
+
def __len__(self) -> int:
|
|
80
|
+
return len(self._items)
|
|
@@ -1,83 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
from typing import
|
|
5
|
-
|
|
6
|
-
if sys.version_info >= (3, 9):
|
|
7
|
-
from collections.abc import (
|
|
8
|
-
Iterable,
|
|
9
|
-
Iterator,
|
|
10
|
-
Mapping,
|
|
11
|
-
Sequence,
|
|
12
|
-
)
|
|
13
|
-
else:
|
|
14
|
-
from typing import (
|
|
15
|
-
Iterable,
|
|
16
|
-
Iterator,
|
|
17
|
-
Mapping,
|
|
18
|
-
Sequence,
|
|
19
|
-
)
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
20
5
|
|
|
21
6
|
import multidict
|
|
22
7
|
import pytest
|
|
23
8
|
from multicollections import MultiDict
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class ListMultiDict(MutableMultiMapping[K, V]):
|
|
31
|
-
def __init__(
|
|
32
|
-
self, iterable: Mapping[K, V] | Iterable[Sequence[K | V]] = (), **kwargs: V
|
|
33
|
-
) -> None:
|
|
34
|
-
self._items: list[tuple[K, V]] = []
|
|
35
|
-
if isinstance(iterable, Mapping):
|
|
36
|
-
for key, value in iterable.items():
|
|
37
|
-
self._items.append((key, value))
|
|
38
|
-
else:
|
|
39
|
-
for key, value in iterable:
|
|
40
|
-
self._items.append((key, value))
|
|
41
|
-
for key, value in kwargs.items():
|
|
42
|
-
self._items.append((key, value))
|
|
43
|
-
|
|
44
|
-
def _getall(self, key: K) -> list[V]:
|
|
45
|
-
return [v for k, v in self._items if k == key]
|
|
46
|
-
|
|
47
|
-
def __setitem__(self, key: K, value: V) -> None:
|
|
48
|
-
replaced: int | None = None
|
|
49
|
-
for i, (k, _) in enumerate(self._items):
|
|
50
|
-
if k == key:
|
|
51
|
-
self._items[i] = (key, value)
|
|
52
|
-
replaced = i
|
|
53
|
-
break
|
|
54
|
-
|
|
55
|
-
if replaced is not None:
|
|
56
|
-
# Key existed, remove any duplicates
|
|
57
|
-
self._items = [
|
|
58
|
-
(k, v)
|
|
59
|
-
for i, (k, v) in enumerate(self._items)
|
|
60
|
-
if i == replaced or k != key
|
|
61
|
-
]
|
|
62
|
-
else:
|
|
63
|
-
# Key didn't exist, add it
|
|
64
|
-
self._items.append((key, value))
|
|
65
|
-
|
|
66
|
-
def add(self, key: K, value: V) -> None:
|
|
67
|
-
self._items.append((key, value))
|
|
68
|
-
|
|
69
|
-
def _popone(self, key: K) -> V:
|
|
70
|
-
for i, (k, v) in enumerate(self._items):
|
|
71
|
-
if k == key:
|
|
72
|
-
del self._items[i]
|
|
73
|
-
return v
|
|
74
|
-
raise KeyError(key)
|
|
75
|
-
|
|
76
|
-
def __iter__(self) -> Iterator[K]:
|
|
77
|
-
return (k for k, _ in self._items)
|
|
78
|
-
|
|
79
|
-
def __len__(self) -> int:
|
|
80
|
-
return len(self._items)
|
|
9
|
+
|
|
10
|
+
from .minimalimpl import ListMultiDict
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from multicollections.abc import MutableMultiMapping
|
|
81
14
|
|
|
82
15
|
|
|
83
16
|
@pytest.mark.parametrize("cls", [MultiDict, ListMultiDict, multidict.MultiDict])
|
|
@@ -649,11 +582,12 @@ def test_update_method(cls: type[MutableMultiMapping]) -> None:
|
|
|
649
582
|
md = cls([("a", 1), ("b", 2), ("a", 3)]) # ty: ignore [too-many-positional-arguments]
|
|
650
583
|
|
|
651
584
|
# Test updating with pairs (should replace existing keys)
|
|
652
|
-
md.update([("a", 999), ("c", 4)])
|
|
653
|
-
assert len(md) ==
|
|
585
|
+
md.update([("a", 999), ("c", 4), ("a", 5)])
|
|
586
|
+
assert len(md) == 4
|
|
654
587
|
assert md["a"] == 999 # Replaced (duplicates removed)
|
|
655
588
|
assert md["b"] == 2 # Unchanged
|
|
656
589
|
assert md["c"] == 4 # New key added
|
|
590
|
+
assert md.getall("a") == [999, 5] # Both 'a' values present
|
|
657
591
|
|
|
658
592
|
# Test updating with dict
|
|
659
593
|
md2 = cls([("x", 10), ("y", 20)]) # ty: ignore [too-many-positional-arguments]
|
|
@@ -671,6 +605,15 @@ def test_update_method(cls: type[MutableMultiMapping]) -> None:
|
|
|
671
605
|
assert md3["b"] == 2 # Unchanged
|
|
672
606
|
assert md3["c"] == 3 # New key added
|
|
673
607
|
|
|
608
|
+
# Test updating with args and kwargs
|
|
609
|
+
md4 = cls([("a", 1), ("b", 2)]) # ty: ignore [too-many-positional-arguments]
|
|
610
|
+
md4.update([("a", 999), ("c", 3)], a=4)
|
|
611
|
+
assert len(md4) == 4
|
|
612
|
+
assert md4["a"] == 999 # Replaced
|
|
613
|
+
assert md4["b"] == 2 # Unchanged
|
|
614
|
+
assert md4["c"] == 3 # New key added
|
|
615
|
+
assert md4.getall("a") == [999, 4] # Both 'a' values present
|
|
616
|
+
|
|
674
617
|
|
|
675
618
|
@pytest.mark.parametrize("cls", [MultiDict, ListMultiDict, multidict.MultiDict])
|
|
676
619
|
def test_edge_cases_none_values(cls: type[MutableMultiMapping]) -> None:
|
|
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
|