From c5c015e9a1a666bd6c556cc4f21428d529984e5e Mon Sep 17 00:00:00 2001 From: medmunds Date: Tue, 5 Feb 2019 11:01:55 -0800 Subject: [PATCH] Internal: add CaseInsensitiveCasePreservingDict Like CaseInsensitiveDict (which we borrow from Requests), but preserves case of the first key set rather than the last. --- anymail/utils.py | 32 ++++++++++++++++++++++++++++++++ tests/test_utils.py | 28 +++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/anymail/utils.py b/anymail/utils.py index c543967..7a22d06 100644 --- a/anymail/utils.py +++ b/anymail/utils.py @@ -12,6 +12,7 @@ from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_T from django.utils.encoding import force_text from django.utils.functional import Promise from django.utils.timezone import utc, get_fixed_timezone +from requests.structures import CaseInsensitiveDict from six.moves.urllib.parse import urlsplit, urlunsplit try: @@ -571,3 +572,34 @@ def parse_rfc2822date(s): except (IndexError, TypeError, ValueError): # despite the docs, parsedate_to_datetime often dies on unparseable input return None + + +class CaseInsensitiveCasePreservingDict(CaseInsensitiveDict): + """A dict with case-insensitive keys, which preserves the *first* key set. + + >>> cicpd = CaseInsensitiveCasePreservingDict() + >>> cicpd["Accept"] = "application/text+xml" + >>> cicpd["accEPT"] = "application/json" + >>> cicpd["accept"] + "application/json" + >>> cicpd.keys() + ["Accept"] + + Compare to CaseInsensitiveDict, which preserves *last* key set: + >>> cid = CaseInsensitiveCasePreservingDict() + >>> cid["Accept"] = "application/text+xml" + >>> cid["accEPT"] = "application/json" + >>> cid.keys() + ["accEPT"] + """ + def __setitem__(self, key, value): + _k = key.lower() + try: + # retrieve earlier matching key, if any + key, _ = self._store[_k] + except KeyError: + pass + self._store[_k] = (key, value) + + def copy(self): + return self.__class__(self._store.values()) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5af0c8d..bc6c8af 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -25,7 +25,8 @@ from anymail.utils import ( Attachment, is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list, update_deep, - get_request_uri, get_request_basic_auth, parse_rfc2822date, querydict_getfirst) + get_request_uri, get_request_basic_auth, parse_rfc2822date, querydict_getfirst, + CaseInsensitiveCasePreservingDict) class ParseAddressListTests(SimpleTestCase): @@ -415,3 +416,28 @@ class LazyErrorTests(SimpleTestCase): lazy = _LazyError(ValueError("lazy failure")) # creating doesn't cause error with self.assertRaisesMessage(ValueError, "lazy failure"): self.unused = lazy() # call *does* cause error + + +class CaseInsensitiveCasePreservingDictTests(SimpleTestCase): + def setUp(self): + self.dict = CaseInsensitiveCasePreservingDict() + self.dict["Accept"] = "application/text+xml" + self.dict["accEPT"] = "application/json" + + def test_preserves_first_key(self): + self.assertEqual(list(self.dict.keys()), ["Accept"]) + + def test_copy(self): + copy = self.dict.copy() + self.assertIsNot(copy, self.dict) + self.assertEqual(copy, self.dict) + # Here's why the superclass CaseInsensitiveDict.copy is insufficient: + self.assertIsInstance(copy, CaseInsensitiveCasePreservingDict) + + def test_get_item(self): + self.assertEqual(self.dict["accept"], "application/json") + self.assertEqual(self.dict["Accept"], "application/json") + self.assertEqual(self.dict["accEPT"], "application/json") + + # The base CaseInsensitiveDict functionality is well-tested in Requests, + # so we don't repeat it here.