Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1903,10 +1903,35 @@ def test_hash(self):
self.assertEqual(hash(frozendict(x=1, y=2)),
hash(frozendict(y=2, x=1)))

# Check that hash() computes the hash of (key, value) pairs
cases = [
frozendict(a=False, b=True, c=True),
frozendict(a=True, b=False, c=True),
frozendict(a=True, b=True, c=False),
frozendict({False: "a", "b": True, "c": True}),
frozendict({"a": "b", False: True, True: "c"}),
]
hashes = {hash(fd) for fd in cases}
self.assertEqual(len(hashes), len(cases))

fd = frozendict(x=[1], y=[2])
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"):
hash(fd)

@support.cpython_only
def test_hash_cpython(self):
# Check that hash(frozendict) implementation is:
# hash(frozenset(fd.items()))
for fd in (
frozendict(),
frozendict(x=1, y=2),
frozendict(y=2, x=1),
frozendict(a=False, b=True, c=True),
frozendict.fromkeys('abc'),
):
with self.subTest(fd=fd):
self.assertEqual(hash(fd), hash(frozenset(fd.items())))

def test_fromkeys(self):
self.assertEqual(frozendict.fromkeys('abc'),
frozendict(a=None, b=None, c=None))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``hash(frozendict)``: compute the hash of each ``(key, value)`` pair
correctly. Patch by Victor Stinner.
50 changes: 39 additions & 11 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -8230,6 +8230,39 @@ _shuffle_bits(Py_uhash_t h)
return ((h ^ 89869747UL) ^ (h << 16)) * 3644798167UL;
}

// Compute hash((key, value)).
// Code copied from tuple_hash().
static Py_hash_t
frozendict_pair_hash(Py_hash_t key_hash, PyObject *value)
{
assert(key_hash != -1);

const Py_ssize_t len = 2;
Py_uhash_t acc = _PyTuple_HASH_XXPRIME_5;

Py_uhash_t lane = key_hash;
acc += lane * _PyTuple_HASH_XXPRIME_2;
acc = _PyTuple_HASH_XXROTATE(acc);
acc *= _PyTuple_HASH_XXPRIME_1;

lane = PyObject_Hash(value);
if (lane == (Py_uhash_t)-1) {
return -1;
}
acc += lane * _PyTuple_HASH_XXPRIME_2;
acc = _PyTuple_HASH_XXROTATE(acc);
acc *= _PyTuple_HASH_XXPRIME_1;

/* Add input length, mangled to keep the historical value of hash(()). */
acc += len ^ (_PyTuple_HASH_XXPRIME_5 ^ 3527539UL);

if (acc == (Py_uhash_t)-1) {
acc = 1546275796;
}
return acc;
}


// Code copied from frozenset_hash()
static Py_hash_t
frozendict_hash(PyObject *op)
Expand All @@ -8243,20 +8276,15 @@ frozendict_hash(PyObject *op)
PyDictObject *mp = _PyAnyDict_CAST(op);
Py_uhash_t hash = 0;

PyObject *key, *value; // borrowed refs
PyObject *value; // borrowed ref
Py_ssize_t pos = 0;
while (PyDict_Next(op, &pos, &key, &value)) {
Py_hash_t key_hash = PyObject_Hash(key);
if (key_hash == -1) {
return -1;
}
hash ^= _shuffle_bits(key_hash);

Py_hash_t value_hash = PyObject_Hash(value);
if (value_hash == -1) {
Py_hash_t key_hash;
while (_PyDict_Next(op, &pos, NULL, &value, &key_hash)) {
Py_hash_t pair_hash = frozendict_pair_hash(key_hash, value);
if (pair_hash == -1) {
return -1;
}
hash ^= _shuffle_bits(value_hash);
hash ^= _shuffle_bits(pair_hash);
}

/* Factor in the number of active entries */
Expand Down
3 changes: 3 additions & 0 deletions Objects/tupleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,9 @@ tuple_repr(PyObject *self)
https://github.com/Cyan4973/xxHash/blob/master/doc/xxhash_spec.md
The constants for the hash function are defined in pycore_tuple.h.
If you update this code, update also frozendict_pair_hash() which copied
this code.
*/

static Py_hash_t
Expand Down
Loading