diff --git a/src/xmmutablemap/_core.py b/src/xmmutablemap/_core.py index 3632880..b6c5ac2 100644 --- a/src/xmmutablemap/_core.py +++ b/src/xmmutablemap/_core.py @@ -207,6 +207,10 @@ def __hash__(self) -> int: Normally, dictionaries are not hashable because they are mutable. However, this dictionary is immutable, so we can hash it. + The hash is computed from an order-independent view of the items, to + match the order-independent equality inherited from `Mapping`. Equal + maps therefore always hash equally, whatever their insertion order. + Examples -------- >>> from xmmutablemap import ImmutableMap @@ -214,8 +218,16 @@ def __hash__(self) -> int: >>> isinstance(hash(d), int) True + Insertion order does not affect equality, and so must not affect the + hash: + + >>> ImmutableMap(a=1, b=2) == ImmutableMap(b=2, a=1) + True + >>> hash(ImmutableMap(a=1, b=2)) == hash(ImmutableMap(b=2, a=1)) + True + """ - return hash(tuple(self._data.items())) + return hash(frozenset(self._data.items())) def __repr__(self) -> str: """Return the representation. diff --git a/tests/test_immutablemap.py b/tests/test_immutablemap.py index 766457a..e95b8de 100644 --- a/tests/test_immutablemap.py +++ b/tests/test_immutablemap.py @@ -57,13 +57,25 @@ def test_len(self, d: ImmutableMap[str, Any]) -> None: def test_hash(self, d: ImmutableMap[str, Any]) -> None: """Test `__hash__`.""" - assert hash(d) == hash(tuple(d.items())) + assert hash(d) == hash(frozenset(d.items())) # Not hashable if values aren't hashable. d = ImmutableMap(a=1, b={"c"}) with pytest.raises(TypeError, match="unhashable type: 'set'"): hash(d) + def test_hash_eq_invariant(self) -> None: + """Equal maps must hash equally, regardless of insertion order.""" + d1 = ImmutableMap(a=1, b=2) + d2 = ImmutableMap(b=2, a=1) + + assert d1 == d2 + assert hash(d1) == hash(d2) + + # The invariant is what makes set/dict membership work. + assert len({d1, d2}) == 1 + assert d2 in {d1: "value"} + def test_keys(self, d: ImmutableMap[str, Any]) -> None: """Test `keys`.""" assert list(d.keys()) == ["a", "b"]