summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoraspalding <aspalding@procuredhealth.com>2018-10-16 10:02:36 -0500
committerTim Graham <timograham@gmail.com>2018-10-17 11:17:50 -0400
commit217f82d7139fd32f07adbfa92c5fb383d0ade577 (patch)
tree753bd98d3cf496b43f1f719bb800bc129a2f5bc5
parent834c4ec8e4cfc43acf0525e0a4d8c914534f6afd (diff)
Refs #29838 -- Fixed make_hashable() for values that have lists or dicts nested in tuples.
And for non-hashable values that are iterable, e.g. sets.
-rw-r--r--django/utils/hashable.py14
-rw-r--r--tests/utils_tests/test_hashable.py12
2 files changed, 23 insertions, 3 deletions
diff --git a/django/utils/hashable.py b/django/utils/hashable.py
index 859ea8073e..0bef5b003d 100644
--- a/django/utils/hashable.py
+++ b/django/utils/hashable.py
@@ -1,9 +1,19 @@
+from django.utils.itercompat import is_iterable
+
+
def make_hashable(value):
- if isinstance(value, list):
- return tuple(map(make_hashable, value))
if isinstance(value, dict):
return tuple([
(key, make_hashable(nested_value))
for key, nested_value in value.items()
])
+ # Try hash to avoid converting a hashable iterable (e.g. string, frozenset)
+ # to a tuple.
+ try:
+ hash(value)
+ except TypeError:
+ if is_iterable(value):
+ return tuple(map(make_hashable, value))
+ # Non-hashable, non-iterable.
+ raise
return value
diff --git a/tests/utils_tests/test_hashable.py b/tests/utils_tests/test_hashable.py
index 2310c4c605..b4db3ef7d7 100644
--- a/tests/utils_tests/test_hashable.py
+++ b/tests/utils_tests/test_hashable.py
@@ -8,9 +8,11 @@ class TestHashable(SimpleTestCase):
([], ()),
(['a', 1], ('a', 1)),
({}, ()),
- ({'a'}, {'a'}),
+ ({'a'}, ('a',)),
(frozenset({'a'}), {'a'}),
({'a': 1}, (('a', 1),)),
+ (('a', ['b', 1]), ('a', ('b', 1))),
+ (('a', {'b': 1}), ('a', (('b', 1),))),
)
for value, expected in tests:
with self.subTest(value=value):
@@ -19,7 +21,15 @@ class TestHashable(SimpleTestCase):
def test_count_equal(self):
tests = (
({'a': 1, 'b': ['a', 1]}, (('a', 1), ('b', ('a', 1)))),
+ ({'a': 1, 'b': ('a', [1, 2])}, (('a', 1), ('b', ('a', (1, 2))))),
)
for value, expected in tests:
with self.subTest(value=value):
self.assertCountEqual(make_hashable(value), expected)
+
+ def test_unhashable(self):
+ class Unhashable:
+ __hash__ = None
+
+ with self.assertRaisesMessage(TypeError, "unhashable type: 'Unhashable'"):
+ make_hashable(Unhashable())