Skip to content

Commit

Permalink
Fix update() method to accept multiple keys (#199)
Browse files Browse the repository at this point in the history
* Move annoying params from pytest to Makefile/appveyor conf

* Fix pure python multidict update

* Code cleanup

* Work on

* Make everything working

* Update CHANGES
  • Loading branch information
asvetlov committed Jan 14, 2018
1 parent 25430ee commit e7b9a58
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 81 deletions.
6 changes: 6 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
.. _changes:

4.0.0 (2018-01-14)
------------------

* Accept multiple keys in :py:meth:`MultiDict.update` and
:py:meth:`CIMultiDict.update` (:pr:`199`)

3.3.2 (2017-11-02)
------------------

Expand Down
164 changes: 126 additions & 38 deletions multidict/_multidict.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ cdef class MultiDict(_Base):
cdef _extend(self, tuple args, dict kwargs, name, bint do_add):
cdef _Pair item
cdef object key
cdef object value
cdef object arg
cdef object i

if len(args) > 1:
raise TypeError("{} takes at most 1 positional argument"
Expand All @@ -295,52 +298,137 @@ cdef class MultiDict(_Base):
if args:
arg = args[0]
if isinstance(arg, _Base):
for i in (<_Base>arg)._impl._items:
item = <_Pair>i
key = item._key
value = item._value
if do_add:
self._add(key, value)
else:
self._replace(key, value)
elif hasattr(arg, 'items'):
for i in arg.items():
if isinstance(i, _Pair):
item = <_Pair>i
key = item._key
value = item._value
else:
key = i[0]
value = i[1]
if do_add:
self._add(key, value)
else:
self._replace(key, value)
if do_add:
self._append_items((<_Base>arg)._impl)
else:
self._update_items((<_Base>arg)._impl)
else:
for i in arg:
if isinstance(i, _Pair):
item = <_Pair>i
key = item._key
value = item._value
else:
if not len(i) == 2:
raise TypeError(
"{} takes either dict or list of (key, value) "
"tuples".format(name))
key = i[0]
value = i[1]
if do_add:
self._add(key, value)
else:
self._replace(key, value)

if hasattr(arg, 'items'):
arg = arg.items()
if do_add:
self._append_items_seq(arg, name)
else:
self._update_items_seq(arg, name)

for key, value in kwargs.items():
if do_add:
self._add(key, value)
else:
self._replace(key, value)

cdef object _update_items(self, _Impl impl):
cdef _Pair item, item2
cdef object i
cdef dict used_keys = {}
cdef Py_ssize_t start
cdef Py_ssize_t post
cdef Py_ssize_t size = len(self._impl._items)
cdef Py_hash_t h

for i in impl._items:
item = <_Pair>i

start = used_keys.get(item._identity, 0)
for pos in range(start, size):
item2 = <_Pair>(self._impl._items[pos])
if item2._hash != item._hash:
continue
if item2._identity == item._identity:
used_keys[item._identity] = pos + 1
item2._key = item._key
item2._value = item._value
break
else:
self._impl._items.append(_Pair.__new__(
_Pair, item._identity, item._key, item._value))
size += 1
used_keys[item._identity] = size

self._post_update(used_keys)

cdef object _update_items_seq(self, object arg, object name):
cdef _Pair item
cdef object i
cdef object identity
cdef object key
cdef object value
cdef dict used_keys = {}
cdef Py_ssize_t start
cdef Py_ssize_t post
cdef Py_ssize_t size = len(self._impl._items)
cdef Py_hash_t h
for i in arg:
if not len(i) == 2:
raise TypeError(
"{} takes either dict or list of (key, value) "
"tuples".format(name))
key = _str(i[0])
value = i[1]
identity = self._title(key)
h = hash(identity)

start = used_keys.get(identity, 0)
for pos in range(start, size):
item = <_Pair>(self._impl._items[pos])
if item._hash != h:
continue
if item._identity == identity:
used_keys[identity] = pos + 1
item._key = key
item._value = value
break
else:
self._impl._items.append(_Pair.__new__(
_Pair, identity, key, value))
size += 1
used_keys[identity] = size

self._post_update(used_keys)

cdef object _post_update(self, dict used_keys):
cdef Py_ssize_t i = 0
cdef _Pair item
while i < len(self._impl._items):
item = <_Pair>self._impl._items[i]
pos = used_keys.get(item._identity)
if pos is None:
i += 1
continue
if i >= pos:
del self._impl._items[i]
else:
i += 1

self._impl.incr_version()

cdef object _append_items(self, _Impl impl):
cdef _Pair item
cdef object i
cdef str key
cdef object value
for i in impl._items:
item = <_Pair>i
key = item._key
value = item._value
self._impl._items.append(_Pair.__new__(
_Pair, self._title(key), key, value))
self._impl.incr_version()

cdef object _append_items_seq(self, object arg, object name):
cdef object i
cdef object key
cdef object value
for i in arg:
if not len(i) == 2:
raise TypeError(
"{} takes either dict or list of (key, value) "
"tuples".format(name))
key = i[0]
value = i[1]
self._impl._items.append(_Pair.__new__(
_Pair, self._title(key), _str(key), value))
self._impl.incr_version()

cdef _add(self, key, value):
self._impl._items.append(_Pair.__new__(
_Pair, self._title(key), _str(key), value))
Expand Down
62 changes: 49 additions & 13 deletions multidict/_multidict_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ class MultiDict(_Base, MutableMultiMapping):
def __init__(self, *args, **kwargs):
self._impl = _Impl()

self._extend(args, kwargs, self.__class__.__name__, self.add)
self._extend(args, kwargs, self.__class__.__name__,
self._extend_items)

def __reduce__(self):
return (self.__class__, (list(self.items()),))
Expand Down Expand Up @@ -217,34 +218,37 @@ def extend(self, *args, **kwargs):
This method must be used instead of update.
"""
self._extend(args, kwargs, 'extend', self.add)
self._extend(args, kwargs, 'extend', self._extend_items)

def _extend(self, args, kwargs, name, method):
if len(args) > 1:
raise TypeError("{} takes at most 1 positional argument"
" ({} given)".format(name, len(args)))
if args:
arg = args[0]
if isinstance(args[0], MultiDictProxy):
if isinstance(args[0], (MultiDict, MultiDictProxy)):
items = arg._impl._items
elif isinstance(args[0], MultiDict):
items = arg._impl._items
elif hasattr(arg, 'items'):
items = [(k, k, v) for k, v in arg.items()]
else:
if hasattr(arg, 'items'):
arg = arg.items()
items = []
for item in arg:
if not len(item) == 2:
raise TypeError(
"{} takes either dict or list of (key, value) "
"tuples".format(name))
items.append((item[0], item[0], item[1]))
items.append((self._title(item[0]),
self._key(item[0]),
item[1]))

method(items)

for identity, key, value in items:
method(key, value)
method([(self._title(key), key, value)
for key, value in kwargs.items()])

for key, value in kwargs.items():
method(key, value)
def _extend_items(self, items):
for identity, key, value in items:
self.add(key, value)

def clear(self):
"""Remove all items from MultiDict."""
Expand Down Expand Up @@ -338,7 +342,39 @@ def popitem(self):

def update(self, *args, **kwargs):
"""Update the dictionary from *other*, overwriting existing keys."""
self._extend(args, kwargs, 'update', self._replace)
self._extend(args, kwargs, 'update', self._update_items)

def _update_items(self, items):
if not items:
return
used_keys = {}
for identity, key, value in items:
start = used_keys.get(identity, 0)
for i in range(start, len(self._impl._items)):
item = self._impl._items[i]
if item[0] == identity:
used_keys[identity] = i + 1
self._impl._items[i] = (identity, key, value)
break
else:
self._impl._items.append((identity, key, value))
used_keys[identity] = len(self._impl._items)

# drop tails
i = 0
while i < len(self._impl._items):
item = self._impl._items[i]
identity = item[0]
pos = used_keys.get(identity)
if pos is None:
i += 1
continue
if i >= pos:
del self._impl._items[i]
else:
i += 1

self._impl.incr_version()

def _replace(self, key, value):
key = self._key(key)
Expand Down
30 changes: 0 additions & 30 deletions tests/test_mutable_multidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,6 @@ def test_pop_raises(self, cls):

assert 'other' in d

def test_update(self, cls):
d = cls()
d.add('key', 'val1')
d.add('key', 'val2')
d.add('key2', 'val3')

d.update(key='val')

assert [('key', 'val'), ('key2', 'val3')] == list(d.items())

def test_replacement_order(self, cls):
d = cls()
d.add('key1', 'val1')
Expand Down Expand Up @@ -419,33 +409,13 @@ def test_pop_raises(self, cls):

assert 'other' in d

def test_update(self, cls):
d = cls()
d.add('KEY', 'val1')
d.add('key', 'val2')
d.add('key2', 'val3')

d.update(Key='val')

assert [('Key', 'val'), ('key2', 'val3')] == list(d.items())

def test_extend_with_istr(self, cls, istr):
us = istr('a')
d = cls()

d.extend([(us, 'val')])
assert [('A', 'val')] == list(d.items())

def test_update_istr(self, cls, istr):
d = cls()
d.add(istr('KEY'), 'val1')
d.add('key', 'val2')
d.add('key2', 'val3')

d.update({istr('key'): 'val'})

assert [('Key', 'val'), ('key2', 'val3')] == list(d.items())

def test_copy_istr(self, cls, istr):
d = cls({istr('Foo'): 'bar'})
d2 = d.copy()
Expand Down

0 comments on commit e7b9a58

Please sign in to comment.