Skip to content

Commit

Permalink
Merge pull request #22 from toumorokoshi/toum/append_unique_unhashable
Browse files Browse the repository at this point in the history
make strategy_append_unique work for unhashables
  • Loading branch information
toumorokoshi authored Oct 25, 2022
2 parents 4ac5ff6 + 40239c0 commit c96610a
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ build: .venv/deps

# only works with python 3+
lint: .venv/deps
.venv/bin/python -m pip install black==21.12b0
.venv/bin/python -m pip install black==22.3.0
.venv/bin/python -m black --check .

test: .venv/deps
Expand Down
25 changes: 25 additions & 0 deletions deepmerge/extended_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class ExtendedSet(set):
"""
ExtendedSet is an extension of set, which allows for usage
of types that are typically not allowed in a set
(e.g. unhashable).
The following types that cannot be used in a set are supported:
- unhashable types
"""

def __init__(self, elements):
self._values_by_hash = {self._hash(e): e for e in elements}

def _insert(self, element):
self._values_by_hash[self._hash(element)] = element

def _hash(self, element):
if getattr(element, "__hash__") is not None:
return hash(element)
else:
return hash(str(element))

def __contains__(self, obj):
return self._hash(obj) in self._values_by_hash
3 changes: 2 additions & 1 deletion deepmerge/strategy/list.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .core import StrategyList
from ..extended_set import ExtendedSet


class ListStrategies(StrategyList):
Expand Down Expand Up @@ -26,5 +27,5 @@ def strategy_append(config, path, base, nxt):
@staticmethod
def strategy_append_unique(config, path, base, nxt):
"""append items without duplicates in nxt to base."""
base_as_set = set(base)
base_as_set = ExtendedSet(base)
return base + [n for n in nxt if n not in base_as_set]
12 changes: 12 additions & 0 deletions deepmerge/tests/strategy/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ def test_strategy_append_unique(custom_merger):
expected = [1, 3, 2, 5, 4]
actual = custom_merger.merge(base, nxt)
assert actual == expected


def test_strategy_append_unique_nested_dict(custom_merger):
"""append_unique should work even with unhashable objects
Like dicts.
"""
base = [{"bar": ["bob"]}]
nxt = [{"bar": ["baz"]}]

result = custom_merger.merge(base, nxt)

assert result == [{"bar": ["bob"]}, {"bar": ["baz"]}]
18 changes: 9 additions & 9 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@
master_doc = "index"

# General information about the project.
project = u"deepmerge"
copyright = u"2016, Yusuke Tsutsumi"
author = u"Yusuke Tsutsumi"
project = "deepmerge"
copyright = "2016, Yusuke Tsutsumi"
author = "Yusuke Tsutsumi"

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u"0.1"
version = "0.1"
# The full version, including alpha/beta/rc tags.
release = u"0.1"
release = "0.1"

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down Expand Up @@ -271,8 +271,8 @@
(
master_doc,
"deepmerge.tex",
u"deepmerge Documentation",
u"Yusuke Tsutsumi",
"deepmerge Documentation",
"Yusuke Tsutsumi",
"manual",
),
]
Expand Down Expand Up @@ -308,7 +308,7 @@

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "deepmerge", u"deepmerge Documentation", [author], 1)]
man_pages = [(master_doc, "deepmerge", "deepmerge Documentation", [author], 1)]

# If true, show URL addresses after external links.
#
Expand All @@ -324,7 +324,7 @@
(
master_doc,
"deepmerge",
u"deepmerge Documentation",
"deepmerge Documentation",
author,
"deepmerge",
"One line description of project.",
Expand Down
13 changes: 11 additions & 2 deletions docs/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ it's recommended to choose your own strategies, deepmerge does
provided some preconfigured mergers for a common situations:

* deepmerge.always_merger: always try to merge. in the case of mismatches, the value from the second object overrides the first o ne.
* deepmerge.merge_or_raise: try to merge, raise an exception if an unmergable situation is encountered.
* deepmerge.merge_or_raise: try to merge, raise an exception if an unmergable situation is encountered.
* deepmerge.conservative_merger: similar to always_merger, but in the case of a conflict, use the existing value.

Once a merger is constructed, it then has a merge() method that can be called:
Expand All @@ -33,7 +33,6 @@ Once a merger is constructed, it then has a merge() method that can be called:
Merges are Destructive
======================


You may have noticed from the example, but merging is a destructive behavior: it will modify the first argument passed in (the base) as part of the merge.

This is intentional, as an implicit copy would result in a significant performance slowdown for deep data structures. If you need to keep the original objects unmodified, you can use the deepcopy method:
Expand Down Expand Up @@ -96,3 +95,13 @@ Example:
If a strategy fails, an exception should not be raised. This is to
ensure it can be chained with other strategies, or the fall-back.

Uniqueness of elements when merging
===================================

Some strategies require determining the uniqueness
of the elements. Since deepmerge primarily deals with nested
types, this includes structures that are not hashable such as
dictionaries.

In those cases, built-in deepmerge strategies will call repr()
on the object and hash that value instead.

0 comments on commit c96610a

Please sign in to comment.