Repository: aio-libs/multidict

Issue:
Cython and Python versions of CIMultiDict treat key case differently
First noticed this in 3.3.2 (probably existed before that), still exists in 4.0.0:

As one would expect (IMO) the Cython version uses the title-ized key for matching and preserves the original key for output/serialization. The Python version throws away the original key completely and uses the title-ized key for both purposes, breaking output. Again, that's my opinion, but in any case, the behavior should be the same.

```python
In [1]: from multidict._multidict import CIMultiDict  # Cython

In [2]: md = CIMultiDict()
   ...: md['some_key'] = 1
   ...: list(md.items())
   ...:
Out[2]: [('some_key', 1)]  # outputs original key

In [3]: from multidict._multidict_py import CIMultiDict  # Python

In [4]: md = CIMultiDict()
   ...: md['some_key'] = 1
   ...: list(md.items())
   ...:
Out[4]: [('Some_Key', 1)]  # outputs title-ized key
```

Comments:

Patch:
diff --git a/multidict/_multidict_py.py b/multidict/_multidict_py.py
index b698e25..62eaca2 100644
--- a/multidict/_multidict_py.py
+++ b/multidict/_multidict_py.py
@@ -243,7 +243,7 @@ class MultiDict(_Base, MutableMultiMapping):
 
             method(items)
 
-        method([(self._title(key), key, value)
+        method([(self._title(key), self._key(key), value)
                for key, value in kwargs.items()])
 
     def _extend_items(self, items):
@@ -258,15 +258,14 @@ class MultiDict(_Base, MutableMultiMapping):
     # Mapping interface #
 
     def __setitem__(self, key, value):
-        key = self._title(key)
         self._replace(key, value)
 
     def __delitem__(self, key):
-        key = self._title(key)
+        identity = self._title(key)
         items = self._impl._items
         found = False
         for i in range(len(items) - 1, -1, -1):
-            if items[i][0] == key:
+            if items[i][0] == identity:
                 del items[i]
                 found = True
         if not found:
@@ -276,9 +275,9 @@ class MultiDict(_Base, MutableMultiMapping):
 
     def setdefault(self, key, default=None):
         """Return value for key, set value to default if key is not present."""
-        key = self._title(key)
+        identity = self._title(key)
         for i, k, v in self._impl._items:
-            if i == key:
+            if i == identity:
                 return v
         self.add(key, default)
         return default
@@ -290,9 +289,9 @@ class MultiDict(_Base, MutableMultiMapping):
         KeyError is raised.
 
         """
-        key = self._title(key)
+        identity = self._title(key)
         for i in range(len(self._impl._items)):
-            if self._impl._items[i][0] == key:
+            if self._impl._items[i][0] == identity:
                 value = self._impl._items[i][2]
                 del self._impl._items[i]
                 self._impl.incr_version()


Test patch:
diff --git a/tests/test_multidict.py b/tests/test_multidict.py
index b8a3391..85f4cc2 100644
--- a/tests/test_multidict.py
+++ b/tests/test_multidict.py
@@ -118,9 +118,9 @@ class BaseMultiDictTest:
         assert d.get('key') == 'value1'
         assert d['key'] == 'value1'
 
-        with pytest.raises(KeyError, matches='key2'):
+        with pytest.raises(KeyError, match='key2'):
             d['key2']
-        with pytest.raises(KeyError, matches='key2'):
+        with pytest.raises(KeyError, match='key2'):
             d.getone('key2')
 
         assert d.getone('key2', 'default') == 'default'
@@ -326,7 +326,7 @@ class TestMultiDict(BaseMultiDictTest):
 
         assert d.getall('key') == ['value1', 'value2']
 
-        with pytest.raises(KeyError, matches='some_key'):
+        with pytest.raises(KeyError, match='some_key'):
             d.getall('some_key')
 
         default = object()
@@ -374,9 +374,9 @@ class TestCIMultiDict(BaseMultiDictTest):
         assert d['key'] == 'value1'
         assert 'key' in d
 
-        with pytest.raises(KeyError):
+        with pytest.raises(KeyError, match='key2'):
             d['key2']
-        with pytest.raises(KeyError):
+        with pytest.raises(KeyError, match='key2'):
             d.getone('key2')
 
     def test_getall(self, cls):
@@ -387,9 +387,8 @@ class TestCIMultiDict(BaseMultiDictTest):
 
         assert d.getall('key') == ['value1', 'value2']
 
-        with pytest.raises(KeyError) as excinfo:
+        with pytest.raises(KeyError, match='some_key'):
             d.getall('some_key')
-        assert "some_key" in str(excinfo.value)
 
     def test_get(self, cls):
         d = cls([('A', 1), ('a', 2)])
diff --git a/tests/test_mutable_multidict.py b/tests/test_mutable_multidict.py
index bab9270..b6bf60d 100644
--- a/tests/test_mutable_multidict.py
+++ b/tests/test_mutable_multidict.py
@@ -37,9 +37,8 @@ class TestMutableMultiDict:
 
         assert d.getall('key') == ['value1', 'value2']
 
-        with pytest.raises(KeyError) as excinfo:
+        with pytest.raises(KeyError, match='some_key'):
             d.getall('some_key')
-        assert 'some_key' in str(excinfo.value)
 
         default = object()
         assert d.getall('some_key', default) is default
@@ -116,7 +115,7 @@ class TestMutableMultiDict:
         assert d == {'foo': 'bar'}
         assert list(d.items()) == [('foo', 'bar')]
 
-        with pytest.raises(KeyError):
+        with pytest.raises(KeyError, match='key'):
             del d['key']
 
     def test_set_default(self, cls):
@@ -166,7 +165,7 @@ class TestMutableMultiDict:
     def test_pop_raises(self, cls):
         d = cls(other='val')
 
-        with pytest.raises(KeyError):
+        with pytest.raises(KeyError, match='key'):
             d.pop('key')
 
         assert 'other' in d
@@ -218,7 +217,7 @@ class TestMutableMultiDict:
 
     def test_popall_key_error(self, cls):
         d = cls()
-        with pytest.raises(KeyError):
+        with pytest.raises(KeyError, match='key'):
             d.popall('key')
 
 
@@ -244,18 +243,19 @@ class TestCIMutableMultiDict:
 
         assert d.getall('key') == ['value1', 'value2']
 
-        with pytest.raises(KeyError) as excinfo:
+        with pytest.raises(KeyError, match='some_key'):
             d.getall('some_key')
-        assert 'some_key' in str(excinfo.value)
 
     def test_ctor(self, cls):
         d = cls(k1='v1')
         assert 'v1' == d['K1']
+        assert ('k1', 'v1') in d.items()
 
     def test_setitem(self, cls):
         d = cls()
         d['k1'] = 'v1'
         assert 'v1' == d['K1']
+        assert ('k1', 'v1') in d.items()
 
     def test_delitem(self, cls):
         d = cls()
@@ -269,6 +269,7 @@ class TestCIMutableMultiDict:
 
         d2 = d1.copy()
         assert d1 == d2
+        assert d1.items() == d2.items()
         assert d1 is not d2
 
     def test__repr__(self, cls):
@@ -285,18 +286,22 @@ class TestCIMutableMultiDict:
 
         assert d == {}
         d['KEY'] = 'one'
+        assert ('KEY', 'one') in d.items()
         assert d == cls({'Key': 'one'})
         assert d.getall('key') == ['one']
 
         d['KEY'] = 'two'
+        assert ('KEY', 'two') in d.items()
         assert d == cls({'Key': 'two'})
         assert d.getall('key') == ['two']
 
         d.add('KEY', 'one')
+        assert ('KEY', 'one') in d.items()
         assert 2 == len(d)
         assert d.getall('key') == ['two', 'one']
 
         d.add('FOO', 'bar')
+        assert ('FOO', 'bar') in d.items()
         assert 3 == len(d)
         assert d.getall('foo') == ['bar']
 
@@ -353,7 +358,7 @@ class TestCIMutableMultiDict:
         assert d == {'foo': 'bar'}
         assert list(d.items()) == [('foo', 'bar')]
 
-        with pytest.raises(KeyError):
+        with pytest.raises(KeyError, match='key'):
             del d['key']
 
     def test_set_default(self, cls):
@@ -361,6 +366,7 @@ class TestCIMutableMultiDict:
         assert 'one' == d.setdefault('key', 'three')
         assert 'three' == d.setdefault('otherkey', 'three')
         assert 'otherkey' in d
+        assert ('otherkey', 'three') in d.items()
         assert 'three' == d['OTHERKEY']
 
     def test_popitem(self, cls):
@@ -404,7 +410,7 @@ class TestCIMutableMultiDict:
     def test_pop_raises(self, cls):
         d = cls(OTHER='val')
 
-        with pytest.raises(KeyError):
+        with pytest.raises(KeyError, match="KEY"):
             d.pop('KEY')
 
         assert 'other' in d