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.
Files changed (20) hide show
  1. {multicollections-0.1.2 → multicollections-0.2.0}/PKG-INFO +1 -1
  2. {multicollections-0.1.2 → multicollections-0.2.0}/mkdocs.yml +0 -1
  3. {multicollections-0.1.2 → multicollections-0.2.0}/multicollections/__init__.py +25 -17
  4. {multicollections-0.1.2 → multicollections-0.2.0}/multicollections/abc.py +71 -109
  5. {multicollections-0.1.2 → multicollections-0.2.0}/pyproject.toml +1 -1
  6. multicollections-0.2.0/tests/minimalimpl.py +80 -0
  7. {multicollections-0.1.2 → multicollections-0.2.0}/tests/test_multicollections.py +18 -75
  8. {multicollections-0.1.2 → multicollections-0.2.0}/.github/dependabot.yml +0 -0
  9. {multicollections-0.1.2 → multicollections-0.2.0}/.github/workflows/ci.yml +0 -0
  10. {multicollections-0.1.2 → multicollections-0.2.0}/.github/workflows/pypi-publish.yml +0 -0
  11. {multicollections-0.1.2 → multicollections-0.2.0}/.gitignore +0 -0
  12. {multicollections-0.1.2 → multicollections-0.2.0}/.readthedocs.yaml +0 -0
  13. {multicollections-0.1.2 → multicollections-0.2.0}/LICENSE.txt +0 -0
  14. {multicollections-0.1.2 → multicollections-0.2.0}/README.md +0 -0
  15. {multicollections-0.1.2 → multicollections-0.2.0}/docs/api/abc.md +0 -0
  16. {multicollections-0.1.2 → multicollections-0.2.0}/docs/api/multicollections.md +0 -0
  17. {multicollections-0.1.2 → multicollections-0.2.0}/docs/index.md +0 -0
  18. {multicollections-0.1.2 → multicollections-0.2.0}/multicollections/py.typed +0 -0
  19. {multicollections-0.1.2 → multicollections-0.2.0}/tests/__init__.py +0 -0
  20. {multicollections-0.1.2 → multicollections-0.2.0}/tests/ruff.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multicollections
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Fully generic MultiDict class
5
5
  Project-URL: Homepage, https://github.com/gerlero/multicollections
6
6
  Project-URL: Repository, https://github.com/gerlero/multicollections
@@ -21,7 +21,6 @@ plugins:
21
21
  show_signature_annotations: true
22
22
  show_symbol_type_toc: true
23
23
  inherited_members: true
24
- filters: false
25
24
 
26
25
  nav:
27
26
  - Home: 'index.md'
@@ -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
- K = TypeVar("K")
26
- V = TypeVar("V")
25
+ _K = TypeVar("_K")
26
+ _V = TypeVar("_V")
27
27
 
28
28
 
29
- class MultiDict(MutableMultiMapping[K, V]):
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[K, V] | Iterable[Sequence[K | V]] = (), **kwargs: V
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[K, V]] = []
40
- self._key_indices: dict[K, list[int]] = {}
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: K, value: V) -> None:
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
- def _getall(self, key: K) -> list[V]:
59
- """Get all values for a key."""
60
- return [self._items[i][1] for i in self._key_indices.get(key, [])]
58
+ @with_default
59
+ def getall(self, key: _K) -> list[_V]:
60
+ """Get all values for a key.
61
61
 
62
- def __setitem__(self, key: K, value: V) -> None:
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: K, value: V) -> None:
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
- def _popone(self, key: K) -> V:
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: K) -> None:
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[K]:
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, overload
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
- K = TypeVar("K")
33
- V = TypeVar("V")
34
- D = TypeVar("D")
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[K, V]) -> None:
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: K) -> bool:
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[K]:
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[K, V]) -> bool:
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[K, V]]:
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: V) -> bool:
83
+ def __contains__(self, value: _V) -> bool:
80
84
  """Check if the value is in the mapping."""
81
- return any(value in self._mapping.getall(key) for key in set(self._mapping))
85
+ return value in iter(self)
82
86
 
83
- def __iter__(self) -> Iterator[V]:
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
- class MultiMapping(Mapping[K, V], Generic[K, V]):
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
- def _getall(self, key: K) -> list[V]:
126
+ @with_default
127
+ def getall(self, key: _K) -> list[_V]:
104
128
  """Get all values for a key.
105
129
 
106
- Returns an empty list if no values are found.
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[K]:
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
- @overload
124
- def getone(self, key: K) -> V: ...
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
- try:
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: K) -> V:
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
- @overload
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[K, V]:
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[V]:
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[K, V], MutableMapping[K, V]):
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: K, value: V) -> None:
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: K, value: V) -> None:
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
- def _popone(self, key: K) -> V:
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
- @overload
214
- def popone(self, key: K) -> V: ...
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
- try:
243
- return [self.popone(key) for _ in range(len(self.getall(key)))]
244
- except KeyError:
245
- if default is _NO_DEFAULT:
246
- raise
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
- def pop(self, key: K, default: D | _NoDefault = _NO_DEFAULT) -> V | D:
218
+ @with_default
219
+ def pop(self, key: _K) -> _V:
256
220
  """Same as `popone`."""
257
- if default is _NO_DEFAULT:
258
- return self.popone(key)
259
- return self.popone(key, default)
221
+ return self.popone(key)
260
222
 
261
- def popitem(self) -> tuple[K, V]:
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: K) -> None:
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
- return self.popall(key)
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[K, V] | Iterable[Sequence[K | V]] = (),
282
- **kwargs: V,
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[K, V] | Iterable[Sequence[K | V]] = (),
293
- **kwargs: V,
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[K, V] | Iterable[Sequence[K | V]] = (),
309
- **kwargs: V,
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "multicollections"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  authors = [
9
9
  { name = "Gabriel S. Gerlero", email = "ggerlero@cimec.unl.edu.ar" },
10
10
  ]
@@ -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 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
- )
4
+ from typing import TYPE_CHECKING
20
5
 
21
6
  import multidict
22
7
  import pytest
23
8
  from multicollections import MultiDict
24
- from multicollections.abc import MutableMultiMapping
25
-
26
- K = TypeVar("K")
27
- V = TypeVar("V")
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) == 3
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: