Repository: scrapy/scrapy
Pull request #2646 (2017-03-13 11:30:37)

[MRG+1] Allowed passing objects of Mapping class or its subclass to the CaselessDict initializer
CaselessDict can process sequence of general Mapping/MutableMapping class or subclass of Mapping.

Documentations both for Python 2 and Python 3 declare that all instances of `Mapping` class have `items()` method, so it's safely to generalize check from `isinstance(seq, dict)`. 

Real usage example: `requests` package returns responses which headers are instances of `requests.structures.CaseInsensitiveDict` that is subclass of `MutableMapping` and this patch allows to pass these headers directly to `scrapy.Response` initializer.


Patch:
diff --git a/scrapy/utils/datatypes.py b/scrapy/utils/datatypes.py
index e516185bd..eb373c501 100644
--- a/scrapy/utils/datatypes.py
+++ b/scrapy/utils/datatypes.py
@@ -8,7 +8,7 @@ This module must not depend on any module outside the Standard Library.
 import copy
 import six
 import warnings
-from collections import OrderedDict
+from collections import OrderedDict, Mapping
 
 from scrapy.exceptions import ScrapyDeprecationWarning
 
@@ -224,7 +224,7 @@ class CaselessDict(dict):
         return dict.setdefault(self, self.normkey(key), self.normvalue(def_val))
 
     def update(self, seq):
-        seq = seq.items() if isinstance(seq, dict) else seq
+        seq = seq.items() if isinstance(seq, Mapping) else seq
         iseq = ((self.normkey(k), self.normvalue(v)) for k, v in seq)
         super(CaselessDict, self).update(iseq)
 


Test patch:
diff --git a/tests/test_utils_datatypes.py b/tests/test_utils_datatypes.py
index 80f797227..49323f0ff 100644
--- a/tests/test_utils_datatypes.py
+++ b/tests/test_utils_datatypes.py
@@ -1,5 +1,6 @@
 import copy
 import unittest
+from collections import Mapping, MutableMapping
 
 from scrapy.utils.datatypes import CaselessDict, SequenceExclude
 
@@ -7,17 +8,62 @@ __doctests__ = ['scrapy.utils.datatypes']
 
 class CaselessDictTest(unittest.TestCase):
 
-    def test_init(self):
+    def test_init_dict(self):
         seq = {'red': 1, 'black': 3}
         d = CaselessDict(seq)
         self.assertEqual(d['red'], 1)
         self.assertEqual(d['black'], 3)
 
+    def test_init_pair_sequence(self):
         seq = (('red', 1), ('black', 3))
         d = CaselessDict(seq)
         self.assertEqual(d['red'], 1)
         self.assertEqual(d['black'], 3)
 
+    def test_init_mapping(self):
+        class MyMapping(Mapping):
+            def __init__(self, **kwargs):
+                self._d = kwargs
+
+            def __getitem__(self, key):
+                return self._d[key]
+
+            def __iter__(self):
+                return iter(self._d)
+
+            def __len__(self):
+                return len(self._d)
+
+        seq = MyMapping(red=1, black=3)
+        d = CaselessDict(seq)
+        self.assertEqual(d['red'], 1)
+        self.assertEqual(d['black'], 3)
+
+    def test_init_mutable_mapping(self):
+        class MyMutableMapping(MutableMapping):
+            def __init__(self, **kwargs):
+                self._d = kwargs
+
+            def __getitem__(self, key):
+                return self._d[key]
+
+            def __setitem__(self, key, value):
+                self._d[key] = value
+
+            def __delitem__(self, key):
+                del self._d[key]
+
+            def __iter__(self):
+                return iter(self._d)
+
+            def __len__(self):
+                return len(self._d)
+
+        seq = MyMutableMapping(red=1, black=3)
+        d = CaselessDict(seq)
+        self.assertEqual(d['red'], 1)
+        self.assertEqual(d['black'], 3)
+
     def test_caseless(self):
         d = CaselessDict()
         d['key_Lower'] = 1