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
19 changes: 15 additions & 4 deletions deepmerge/extended_set.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence, Any
from typing import Sequence, Any, Dict, List


class ExtendedSet(set):
Expand All @@ -13,10 +13,18 @@ class ExtendedSet(set):
"""

def __init__(self, elements: Sequence) -> None:
self._values_by_hash = {self._hash_element(e): e for e in elements}
# Elements are grouped into buckets keyed by their (possibly lossy)
# hash. Membership and insertion compare a candidate against the
# members of the matching bucket using equality, so two distinct
# elements that merely collide on the hash are not treated as equal.
self._buckets: Dict[int, List[Any]] = {}
for element in elements:
self._insert(element)

def _insert(self, element: Any) -> None:
self._values_by_hash[self._hash_element(element)] = element
bucket = self._buckets.setdefault(self._hash_element(element), [])
if not any(element == existing for existing in bucket):
bucket.append(element)
return

def _hash_element(self, element: Any) -> int:
Expand All @@ -29,4 +37,7 @@ def _hash_element(self, element: Any) -> int:
return hash(str(element))

def __contains__(self, obj: Any) -> bool:
return self._hash_element(obj) in self._values_by_hash
bucket = self._buckets.get(self._hash_element(obj))
if bucket is None:
return False
return any(obj == existing for existing in bucket)
1 change: 0 additions & 1 deletion deepmerge/strategy/fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import deepmerge.merger
from .core import StrategyList


T = TypeVar("T")


Expand Down
20 changes: 20 additions & 0 deletions deepmerge/tests/strategy/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,23 @@ def test_strategy_append_similar_dict(custom_merger):
result = custom_merger.merge(base, nxt)

assert result == [{"bar": "bob", "foo": "baz"}, {"x": "y"}]


def test_strategy_append_unique_keeps_hash_colliding_dicts(custom_merger):
"""append_unique must keep distinct dicts even when their lossy
"key:value" hash collides (e.g. ``{"a": "1", "b": "2"}`` and
``{"a": "1,b:2"}`` both hash to ``"a:1,b:2"``), and must not treat an
``int`` value as equal to its ``str`` form.
"""
assert custom_merger.merge([{"a": "1", "b": "2"}], [{"a": "1,b:2"}]) == [
{"a": "1", "b": "2"},
{"a": "1,b:2"},
]
assert custom_merger.merge([{"k": 1}], [{"k": "1"}]) == [{"k": 1}, {"k": "1"}]


def test_strategy_append_unique_hashable_hash_collision(custom_merger):
"""A hashable element must not be considered present just because a
different element shares its hash (e.g. ``hash(-1) == hash(-2)``).
"""
assert custom_merger.merge([-1], [-2]) == [-1, -2]
Loading