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.
This commit is contained in:
aspalding 2018-10-16 10:02:36 -05:00 committed by Tim Graham
parent 834c4ec8e4
commit 217f82d713
2 changed files with 23 additions and 3 deletions

View File

@ -1,9 +1,19 @@
from django.utils.itercompat import is_iterable
def make_hashable(value): def make_hashable(value):
if isinstance(value, list):
return tuple(map(make_hashable, value))
if isinstance(value, dict): if isinstance(value, dict):
return tuple([ return tuple([
(key, make_hashable(nested_value)) (key, make_hashable(nested_value))
for key, nested_value in value.items() 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 return value

View File

@ -8,9 +8,11 @@ class TestHashable(SimpleTestCase):
([], ()), ([], ()),
(['a', 1], ('a', 1)), (['a', 1], ('a', 1)),
({}, ()), ({}, ()),
({'a'}, {'a'}), ({'a'}, ('a',)),
(frozenset({'a'}), {'a'}), (frozenset({'a'}), {'a'}),
({'a': 1}, (('a', 1),)), ({'a': 1}, (('a', 1),)),
(('a', ['b', 1]), ('a', ('b', 1))),
(('a', {'b': 1}), ('a', (('b', 1),))),
) )
for value, expected in tests: for value, expected in tests:
with self.subTest(value=value): with self.subTest(value=value):
@ -19,7 +21,15 @@ class TestHashable(SimpleTestCase):
def test_count_equal(self): def test_count_equal(self):
tests = ( tests = (
({'a': 1, 'b': ['a', 1]}, (('a', 1), ('b', ('a', 1)))), ({'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: for value, expected in tests:
with self.subTest(value=value): with self.subTest(value=value):
self.assertCountEqual(make_hashable(value), expected) self.assertCountEqual(make_hashable(value), expected)
def test_unhashable(self):
class Unhashable:
__hash__ = None
with self.assertRaisesMessage(TypeError, "unhashable type: 'Unhashable'"):
make_hashable(Unhashable())