Skip to content

Commit 259ca8c

Browse files
authored
Implement md.merge() (#1204)
Closes #292
1 parent acdd2d1 commit 259ca8c

File tree

8 files changed

+260
-60
lines changed

8 files changed

+260
-60
lines changed

.codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ coverage:
4040
typing:
4141
flags:
4242
- MyPy
43-
target: 80%
43+
target: 40%
4444

4545
github_checks:
4646
annotations: false

CHANGES/292.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added :meth:`multidict.MultiDict.merge` which copies all items from arguments if its key
2+
not exist in the dictionary -- by :user:`asvetlov`.

docs/multidict.rst

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,30 @@ MultiDict
181181

182182
.. seealso::
183183

184-
:meth:`update`
184+
:meth:`merge` and :meth:`update`
185+
186+
.. method:: merge([other], **kwargs)
187+
188+
Merge the dictionary with the key/value pairs from *other* and *kwargs*,
189+
appending non-existing pairs to this dictionary. For existing keys,
190+
the addition is skipped.
191+
192+
Returns ``None``.
193+
194+
:meth:`merge` accepts either another dictionary object or an
195+
iterable of key/value pairs (as tuples or other iterables of
196+
length two). If keyword arguments are specified, the dictionary
197+
is then merged with those key/value pairs:
198+
``d.merge(red=1, blue=2)``.
199+
200+
Effectively the same as calling :meth:`add` for every
201+
``(key, value)`` pair where ``key not in self``.
202+
203+
.. seealso::
204+
205+
:meth:`extend` and :meth:`update`
206+
207+
.. versionadded:: 6.6
185208

186209
.. method:: update([other], **kwargs)
187210

@@ -198,7 +221,7 @@ MultiDict
198221

199222
.. seealso::
200223

201-
:meth:`extend`
224+
:meth:`extend` and :meth:`merge`
202225

203226
.. seealso::
204227

multidict/_abc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def add(self, key: str, value: _V) -> None:
5252
def extend(self, arg: MDArg[_V] = None, /, **kwargs: _V) -> None:
5353
"""Add everything from arg and kwargs to the mapping."""
5454

55+
@abc.abstractmethod
56+
def merge(self, arg: MDArg[_V] = None, /, **kwargs: _V) -> None:
57+
"""Merge into the mapping, adding non-existing keys."""
58+
5559
@overload
5660
def popone(self, key: str) -> _V: ...
5761
@overload

multidict/_multidict.c

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ _multidict_getone(MultiDictObject *self, PyObject *key, PyObject *_default)
6666

6767
static inline int
6868
_multidict_extend(MultiDictObject *self, PyObject *arg, PyObject *kwds,
69-
const char *name, bool update)
69+
const char *name, UpdateOp op)
7070
{
7171
mod_state *state = self->state;
7272
PyObject *seq = NULL;
@@ -78,24 +78,24 @@ _multidict_extend(MultiDictObject *self, PyObject *arg, PyObject *kwds,
7878
if (arg != NULL) {
7979
if (AnyMultiDict_Check(state, arg)) {
8080
MultiDictObject *other = (MultiDictObject *)arg;
81-
if (md_update_from_ht(self, other, update) < 0) {
81+
if (md_update_from_ht(self, other, op) < 0) {
8282
goto fail;
8383
}
8484
} else if (AnyMultiDictProxy_Check(state, arg)) {
8585
MultiDictObject *other = ((MultiDictProxyObject *)arg)->md;
86-
if (md_update_from_ht(self, other, update) < 0) {
86+
if (md_update_from_ht(self, other, op) < 0) {
8787
goto fail;
8888
}
8989
} else if (PyDict_CheckExact(arg)) {
90-
if (md_update_from_dict(self, arg, update) < 0) {
90+
if (md_update_from_dict(self, arg, op) < 0) {
9191
goto fail;
9292
}
9393
} else if (PyList_CheckExact(arg)) {
94-
if (md_update_from_seq(self, arg, update) < 0) {
94+
if (md_update_from_seq(self, arg, op) < 0) {
9595
goto fail;
9696
}
9797
} else if (PyTuple_CheckExact(arg)) {
98-
if (md_update_from_seq(self, arg, update) < 0) {
98+
if (md_update_from_seq(self, arg, op) < 0) {
9999
goto fail;
100100
}
101101
} else {
@@ -105,19 +105,19 @@ _multidict_extend(MultiDictObject *self, PyObject *arg, PyObject *kwds,
105105
seq = Py_NewRef(arg);
106106
}
107107

108-
if (md_update_from_seq(self, seq, update) < 0) {
108+
if (md_update_from_seq(self, seq, op) < 0) {
109109
goto fail;
110110
}
111111
}
112112
}
113113

114114
if (kwds != NULL) {
115-
if (md_update_from_dict(self, kwds, update) < 0) {
115+
if (md_update_from_dict(self, kwds, op) < 0) {
116116
goto fail;
117117
}
118118
}
119119

120-
if (update) {
120+
if (op != Extend) { // Update or Merge
121121
if (md_post_update(self) < 0) {
122122
goto fail;
123123
}
@@ -551,7 +551,7 @@ multidict_tp_init(MultiDictObject *self, PyObject *args, PyObject *kwds)
551551
if (md_init(self, state, false, size) < 0) {
552552
goto fail;
553553
}
554-
if (_multidict_extend(self, arg, kwds, "MultiDict", false) < 0) {
554+
if (_multidict_extend(self, arg, kwds, "MultiDict", Extend) < 0) {
555555
goto fail;
556556
}
557557
done:
@@ -592,7 +592,7 @@ multidict_extend(MultiDictObject *self, PyObject *args, PyObject *kwds)
592592
if (md_reserve(self, size) < 0) {
593593
goto fail;
594594
}
595-
if (_multidict_extend(self, arg, kwds, "extend", false) < 0) {
595+
if (_multidict_extend(self, arg, kwds, "extend", Extend) < 0) {
596596
goto fail;
597597
}
598598
Py_CLEAR(arg);
@@ -774,7 +774,30 @@ multidict_update(MultiDictObject *self, PyObject *args, PyObject *kwds)
774774
if (md_reserve(self, size) < 0) {
775775
goto fail;
776776
}
777-
if (_multidict_extend(self, arg, kwds, "update", true) < 0) {
777+
if (_multidict_extend(self, arg, kwds, "update", Update) < 0) {
778+
goto fail;
779+
}
780+
Py_CLEAR(arg);
781+
ASSERT_CONSISTENT(self, false);
782+
Py_RETURN_NONE;
783+
fail:
784+
Py_CLEAR(arg);
785+
return NULL;
786+
}
787+
788+
static PyObject *
789+
multidict_merge(MultiDictObject *self, PyObject *args, PyObject *kwds)
790+
{
791+
PyObject *arg = NULL;
792+
Py_ssize_t size =
793+
_multidict_extend_parse_args(self->state, args, kwds, "merge", &arg);
794+
if (size < 0) {
795+
goto fail;
796+
}
797+
if (md_reserve(self, size) < 0) {
798+
goto fail;
799+
}
800+
if (_multidict_extend(self, arg, kwds, "merge", Merge) < 0) {
778801
goto fail;
779802
}
780803
Py_CLEAR(arg);
@@ -824,6 +847,9 @@ PyDoc_STRVAR(multidict_popitem_doc,
824847
PyDoc_STRVAR(multidict_update_doc,
825848
"Update the dictionary, overwriting existing keys.");
826849

850+
PyDoc_STRVAR(multidict_merge_doc,
851+
"Merge into the dictionary, adding non-existing keys.");
852+
827853
PyDoc_STRVAR(sizeof__doc__, "D.__sizeof__() -> size of D in memory, in bytes");
828854

829855
static PyObject *
@@ -887,6 +913,10 @@ static PyMethodDef multidict_methods[] = {
887913
(PyCFunction)multidict_update,
888914
METH_VARARGS | METH_KEYWORDS,
889915
multidict_update_doc},
916+
{"merge",
917+
(PyCFunction)multidict_merge,
918+
METH_VARARGS | METH_KEYWORDS,
919+
multidict_merge_doc},
890920
{
891921
"__reduce__",
892922
(PyCFunction)multidict_reduce,
@@ -979,7 +1009,7 @@ cimultidict_tp_init(MultiDictObject *self, PyObject *args, PyObject *kwds)
9791009
if (md_init(self, state, true, size) < 0) {
9801010
goto fail;
9811011
}
982-
if (_multidict_extend(self, arg, kwds, "CIMultiDict", false) < 0) {
1012+
if (_multidict_extend(self, arg, kwds, "CIMultiDict", Extend) < 0) {
9831013
goto fail;
9841014
}
9851015
done:

multidict/_multidict_py.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -999,10 +999,9 @@ def update(self, arg: MDArg[_V] = None, /, **kwargs: _V) -> None:
999999
if log2_size > self._keys.log2_size:
10001000
self._resize(log2_size, False)
10011001
self._update_items(items)
1002+
self._post_update()
10021003

10031004
def _update_items(self, items: list[_Entry[_V]]) -> None:
1004-
if not items:
1005-
return
10061005
for entry in items:
10071006
found = False
10081007
hash_ = entry.hash
@@ -1019,6 +1018,7 @@ def _update_items(self, items: list[_Entry[_V]]) -> None:
10191018
if not found:
10201019
self._add_with_hash_for_upd(entry)
10211020

1021+
def _post_update(self) -> None:
10221022
keys = self._keys
10231023
indices = keys.indices
10241024
entries = keys.entries
@@ -1028,14 +1028,38 @@ def _update_items(self, items: list[_Entry[_V]]) -> None:
10281028
e2 = entries[idx]
10291029
assert e2 is not None
10301030
if e2.key is None:
1031-
entries[idx] = None # type: ignore[unreachable]
1031+
entries[idx] = None
10321032
indices[slot] = -2
10331033
self._used -= 1
10341034
if e2.hash == -1:
10351035
e2.hash = hash(e2.identity)
10361036

10371037
self._incr_version()
10381038

1039+
def merge(self, arg: MDArg[_V] = None, /, **kwargs: _V) -> None:
1040+
"""Merge into the dictionary, adding non-existing keys."""
1041+
items = self._parse_args(arg, kwargs)
1042+
newsize = self._used + len(items)
1043+
log2_size = estimate_log2_keysize(newsize)
1044+
if log2_size > 17: # pragma: no cover
1045+
# Don't overallocate really huge keys space in update,
1046+
# duplicate keys could reduce the resulting anount of entries
1047+
log2_size = 17
1048+
if log2_size > self._keys.log2_size:
1049+
self._resize(log2_size, False)
1050+
self._merge_items(items)
1051+
self._post_update()
1052+
1053+
def _merge_items(self, items: list[_Entry[_V]]) -> None:
1054+
for entry in items:
1055+
hash_ = entry.hash
1056+
identity = entry.identity
1057+
for slot, idx, e in self._keys.iter_hash(hash_):
1058+
if e.identity == identity: # pragma: no branch
1059+
break
1060+
else:
1061+
self._add_with_hash_for_upd(entry)
1062+
10391063
def _incr_version(self) -> None:
10401064
v = _version
10411065
v[0] += 1

0 commit comments

Comments
 (0)