[#5193] Replaced implementation of CaseInsensitiveDict with one based on ABC

Signed-off-by: Cory Johns <johnsca@geek.net>
diff --git a/Allura/allura/lib/utils.py b/Allura/allura/lib/utils.py
index 285cbfb..285e7e4 100644
--- a/Allura/allura/lib/utils.py
+++ b/Allura/allura/lib/utils.py
@@ -11,6 +11,7 @@
 import re
 import magic
 from itertools import groupby
+import collections
 
 import tg
 import pylons
@@ -348,44 +349,43 @@
     def __nonzero__(self):
         return self.callable()
 
-class CaseInsensitiveDict(dict):
+
+class TransformedDict(collections.MutableMapping):
+    """
+    A dictionary which applies an arbitrary
+    key-altering function before accessing the keys.
+
+    From: http://stackoverflow.com/questions/3387691/python-how-to-perfectly-override-a-dict
+    """
 
     def __init__(self, *args, **kwargs):
-        super(CaseInsensitiveDict, self).__init__(*args, **kwargs)
-        self._reindex()
+        self.store = dict()
+        self.update(dict(*args, **kwargs)) # use the free update to set keys
 
-    def _reindex(self):
-        items = self.items()
-        self.clear()
-        self._index = {}
-        for k,v in items:
-            self[k] = v
-        assert len(self) == len(items), 'Duplicate (case-insensitive) key'
+    def __getitem__(self, key):
+        return self.store[self.__keytransform__(key)]
 
-    def __getitem__(self, name):
-        return super(CaseInsensitiveDict, self).__getitem__(name.lower())
+    def __setitem__(self, key, value):
+        self.store[self.__keytransform__(key)] = value
 
-    def __setitem__(self, name, value):
-        lname = name.lower()
-        super(CaseInsensitiveDict, self).__setitem__(lname, value)
-        self._index[lname] = name
+    def __delitem__(self, key):
+        del self.store[self.__keytransform__(key)]
 
-    def __delitem__(self, name):
-        super(CaseInsensitiveDict, self).__delitem__(name.lower())
+    def __iter__(self):
+        return iter(self.store)
 
-    def __contains__(self, name):
-        return super(CaseInsensitiveDict, self).__contains__(name.lower())
+    def __len__(self):
+        return len(self.store)
 
-    def pop(self, k, *args):
-        return super(CaseInsensitiveDict, self).pop(k.lower(), *args)
+    def __keytransform__(self, key):
+        return key
 
-    def popitem(self):
-        k,v = super(CaseInsensitiveDict, self).popitem()
-        return self._index[k], v
 
-    def update(self, *args, **kwargs):
-        super(CaseInsensitiveDict, self).update(*args, **kwargs)
-        self._reindex()
+class CaseInsensitiveDict(TransformedDict):
+
+    def __keytransform__(self, key):
+        return key.lower()
+
 
 def postmortem_hook(etype, value, tb): # pragma no cover
     import sys, pdb, traceback
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index 69f26c6..62a01b8 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -111,12 +111,11 @@
         assert d['bar'] == d['Bar'] == 6
         d['bar'] = 7
         assert d['bar'] == d['bAr'] == 7
-        self.assertRaises(AssertionError, utils.CaseInsensitiveDict, foo=1, Foo=2)
         del d['bar']
         assert len(d) == 1, d
-        assert d.popitem() == ('Foo', 5)
-        self.assertRaises(AssertionError, d.update, foo=1, Foo=2)
+        assert d.get('foo') == 5
         d.update(foo=1, Bar=2)
+        assert d.get('FOO') == 1
         assert d == dict(foo=1, bar=2)
         assert d != dict(Foo=1, bar=2)
         assert d == utils.CaseInsensitiveDict(Foo=1, bar=2)