aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/deepmerge
diff options
context:
space:
mode:
authorrekby <rekby@ydb.tech>2024-08-16 13:53:46 +0300
committerrekby <rekby@ydb.tech>2024-08-16 14:02:56 +0300
commitf23ace9b7c0c2e8578421e3e640e3d1cc0fe381b (patch)
treeb8e99314de0a9340e442e5fa1a5289cf1a6a8704 /contrib/python/deepmerge
parenta63920bcb9be4fb351b4e4e85c7cadc6b331ba18 (diff)
downloadydb-f23ace9b7c0c2e8578421e3e640e3d1cc0fe381b.tar.gz
Export python deepmerge library instead of mergedeep to github.com/ydb-platform/ydb for support python2
98bbe613ba94337077da6f6bb9b519768fdef800
Diffstat (limited to 'contrib/python/deepmerge')
-rw-r--r--contrib/python/deepmerge/py2/.dist-info/METADATA80
-rw-r--r--contrib/python/deepmerge/py2/.dist-info/top_level.txt1
-rw-r--r--contrib/python/deepmerge/py2/LICENSE21
-rw-r--r--contrib/python/deepmerge/py2/README.rst70
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/__init__.py34
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/_version.py5
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/compat.py4
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/exception.py18
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/extended_set.py25
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/merger.py44
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/strategy/__init__.py0
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/strategy/core.py38
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/strategy/dict.py32
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/strategy/fallback.py19
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/strategy/list.py31
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/strategy/set.py30
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/strategy/type_conflict.py22
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/__init__.py0
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/strategy/__init__.py0
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_core.py41
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_list.py33
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_set_merge.py13
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_type_conflict.py22
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/test_full.py50
-rw-r--r--contrib/python/deepmerge/py2/deepmerge/tests/test_merger.py30
-rw-r--r--contrib/python/deepmerge/py2/tests/ya.make25
-rw-r--r--contrib/python/deepmerge/py2/ya.make40
-rw-r--r--contrib/python/deepmerge/py3/.dist-info/METADATA80
-rw-r--r--contrib/python/deepmerge/py3/.dist-info/top_level.txt1
-rw-r--r--contrib/python/deepmerge/py3/LICENSE21
-rw-r--r--contrib/python/deepmerge/py3/README.rst70
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/__init__.py34
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/_version.py16
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/compat.py4
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/exception.py18
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/extended_set.py25
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/merger.py44
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py0
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/core.py38
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/dict.py32
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py19
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/list.py31
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/set.py30
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/type_conflict.py22
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/__init__.py0
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/strategy/__init__.py0
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_core.py41
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py33
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_set_merge.py13
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_type_conflict.py22
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/test_full.py60
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/test_merger.py30
-rw-r--r--contrib/python/deepmerge/py3/tests/ya.make25
-rw-r--r--contrib/python/deepmerge/py3/ya.make40
-rw-r--r--contrib/python/deepmerge/ya.make22
55 files changed, 1499 insertions, 0 deletions
diff --git a/contrib/python/deepmerge/py2/.dist-info/METADATA b/contrib/python/deepmerge/py2/.dist-info/METADATA
new file mode 100644
index 0000000000..dd6807cf68
--- /dev/null
+++ b/contrib/python/deepmerge/py2/.dist-info/METADATA
@@ -0,0 +1,80 @@
+Metadata-Version: 2.1
+Name: deepmerge
+Version: 1.1.0
+Summary: a toolset to deeply merge python dictionaries.
+Home-page: http://deepmerge.readthedocs.io/en/latest/
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 3
+License-File: LICENSE
+
+=========
+deepmerge
+=========
+
+.. image:: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml/badge.svg
+ :target: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml
+
+A tools to handle merging of
+nested data structures in python.
+
+------------
+Installation
+------------
+
+deepmerge is available on `pypi <https://pypi.python.org/>`_:
+
+.. code-block:: bash
+
+ pip install deepmerge
+
+-------
+Example
+-------
+
+**Generic Strategy**
+
+.. code-block:: python
+
+ from deepmerge import always_merger
+
+ base = {"foo": ["bar"]}
+ next = {"foo": ["baz"]}
+
+ expected_result = {'foo': ['bar', 'baz']}
+ result = always_merger.merge(base, next)
+
+ assert expected_result == result
+
+
+**Custom Strategy**
+
+.. code-block:: python
+
+ from deepmerge import Merger
+
+ my_merger = Merger(
+ # pass in a list of tuple, with the
+ # strategies you are looking to apply
+ # to each type.
+ [
+ (list, ["append"]),
+ (dict, ["merge"]),
+ (set, ["union"])
+ ],
+ # next, choose the fallback strategies,
+ # applied to all other types:
+ ["override"],
+ # finally, choose the strategies in
+ # the case where the types conflict:
+ ["override"]
+ )
+ base = {"foo": ["bar"]}
+ next = {"bar": "baz"}
+ my_merger.merge(base, next)
+ assert base == {"foo": ["bar"], "bar": "baz"}
+
+
+You can also pass in your own merge functions, instead of a string.
+
+For more information, see the `docs <https://deepmerge.readthedocs.io/en/latest/>`_
diff --git a/contrib/python/deepmerge/py2/.dist-info/top_level.txt b/contrib/python/deepmerge/py2/.dist-info/top_level.txt
new file mode 100644
index 0000000000..046e27c3a8
--- /dev/null
+++ b/contrib/python/deepmerge/py2/.dist-info/top_level.txt
@@ -0,0 +1 @@
+deepmerge
diff --git a/contrib/python/deepmerge/py2/LICENSE b/contrib/python/deepmerge/py2/LICENSE
new file mode 100644
index 0000000000..7e0b51be12
--- /dev/null
+++ b/contrib/python/deepmerge/py2/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Yusuke Tsutsumi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/contrib/python/deepmerge/py2/README.rst b/contrib/python/deepmerge/py2/README.rst
new file mode 100644
index 0000000000..24050eb556
--- /dev/null
+++ b/contrib/python/deepmerge/py2/README.rst
@@ -0,0 +1,70 @@
+=========
+deepmerge
+=========
+
+.. image:: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml/badge.svg
+ :target: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml
+
+A tools to handle merging of
+nested data structures in python.
+
+------------
+Installation
+------------
+
+deepmerge is available on `pypi <https://pypi.python.org/>`_:
+
+.. code-block:: bash
+
+ pip install deepmerge
+
+-------
+Example
+-------
+
+**Generic Strategy**
+
+.. code-block:: python
+
+ from deepmerge import always_merger
+
+ base = {"foo": ["bar"]}
+ next = {"foo": ["baz"]}
+
+ expected_result = {'foo': ['bar', 'baz']}
+ result = always_merger.merge(base, next)
+
+ assert expected_result == result
+
+
+**Custom Strategy**
+
+.. code-block:: python
+
+ from deepmerge import Merger
+
+ my_merger = Merger(
+ # pass in a list of tuple, with the
+ # strategies you are looking to apply
+ # to each type.
+ [
+ (list, ["append"]),
+ (dict, ["merge"]),
+ (set, ["union"])
+ ],
+ # next, choose the fallback strategies,
+ # applied to all other types:
+ ["override"],
+ # finally, choose the strategies in
+ # the case where the types conflict:
+ ["override"]
+ )
+ base = {"foo": ["bar"]}
+ next = {"bar": "baz"}
+ my_merger.merge(base, next)
+ assert base == {"foo": ["bar"], "bar": "baz"}
+
+
+You can also pass in your own merge functions, instead of a string.
+
+For more information, see the `docs <https://deepmerge.readthedocs.io/en/latest/>`_ \ No newline at end of file
diff --git a/contrib/python/deepmerge/py2/deepmerge/__init__.py b/contrib/python/deepmerge/py2/deepmerge/__init__.py
new file mode 100644
index 0000000000..b7686cf91c
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/__init__.py
@@ -0,0 +1,34 @@
+from .merger import Merger
+from .strategy.core import STRATEGY_END # noqa
+
+# some standard mergers available
+
+DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES = [
+ (list, "append"),
+ (dict, "merge"),
+ (set, "union"),
+]
+
+# this merge will never raise an exception.
+# in the case of type mismatches,
+# the value from the second object
+# will override the previous one.
+always_merger = Merger(
+ DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES, ["override"], ["override"]
+)
+
+# this merge strategies attempts
+# to merge (append for list, unify for dicts)
+# if possible, but raises an exception
+# in the case of type conflicts.
+merge_or_raise = Merger(DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES, [], [])
+
+# a conservative merge tactic:
+# for data structures with a specific
+# strategy, keep the existing value.
+# similar to always_merger but instead
+# keeps existing values when faced
+# with a type conflict.
+conservative_merger = Merger(
+ DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES, ["use_existing"], ["use_existing"]
+)
diff --git a/contrib/python/deepmerge/py2/deepmerge/_version.py b/contrib/python/deepmerge/py2/deepmerge/_version.py
new file mode 100644
index 0000000000..bef9f66601
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/_version.py
@@ -0,0 +1,5 @@
+# coding: utf-8
+# file generated by setuptools_scm
+# don't change, don't track in version control
+__version__ = version = '1.1.0'
+__version_tuple__ = version_tuple = (1, 1, 0)
diff --git a/contrib/python/deepmerge/py2/deepmerge/compat.py b/contrib/python/deepmerge/py2/deepmerge/compat.py
new file mode 100644
index 0000000000..d2872a6edf
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/compat.py
@@ -0,0 +1,4 @@
+try:
+ string_type = basestring
+except:
+ string_type = str
diff --git a/contrib/python/deepmerge/py2/deepmerge/exception.py b/contrib/python/deepmerge/py2/deepmerge/exception.py
new file mode 100644
index 0000000000..ea36dac3ba
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/exception.py
@@ -0,0 +1,18 @@
+class DeepMergeException(Exception):
+ pass
+
+
+class StrategyNotFound(DeepMergeException):
+ pass
+
+
+class InvalidMerge(DeepMergeException):
+ def __init__(self, strategy_list_name, merge_args, merge_kwargs):
+ super(InvalidMerge, self).__init__(
+ "no more strategies found for {0} and arguments {1}, {2}".format(
+ strategy_list_name, merge_args, merge_kwargs
+ )
+ )
+ self.strategy_list_name = strategy_list_name
+ self.merge_args = merge_args
+ self.merge_kwargs = merge_kwargs
diff --git a/contrib/python/deepmerge/py2/deepmerge/extended_set.py b/contrib/python/deepmerge/py2/deepmerge/extended_set.py
new file mode 100644
index 0000000000..1d51b431b9
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/extended_set.py
@@ -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
diff --git a/contrib/python/deepmerge/py2/deepmerge/merger.py b/contrib/python/deepmerge/py2/deepmerge/merger.py
new file mode 100644
index 0000000000..c1f747ef3d
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/merger.py
@@ -0,0 +1,44 @@
+from .strategy.list import ListStrategies
+from .strategy.dict import DictStrategies
+from .strategy.set import SetStrategies
+from .strategy.type_conflict import TypeConflictStrategies
+from .strategy.fallback import FallbackStrategies
+
+
+class Merger(object):
+ """
+ :param type_strategies, List[Tuple]: a list of (Type, Strategy) pairs
+ that should be used against incoming types. For example: (dict, "override").
+ """
+
+ PROVIDED_TYPE_STRATEGIES = {
+ list: ListStrategies,
+ dict: DictStrategies,
+ set: SetStrategies,
+ }
+
+ def __init__(self, type_strategies, fallback_strategies, type_conflict_strategies):
+ self._fallback_strategy = FallbackStrategies(fallback_strategies)
+
+ expanded_type_strategies = []
+ for typ, strategy in type_strategies:
+ if typ in self.PROVIDED_TYPE_STRATEGIES:
+ strategy = self.PROVIDED_TYPE_STRATEGIES[typ](strategy)
+ expanded_type_strategies.append((typ, strategy))
+ self._type_strategies = expanded_type_strategies
+
+ self._type_conflict_strategy = TypeConflictStrategies(type_conflict_strategies)
+
+ def merge(self, base, nxt):
+ return self.value_strategy([], base, nxt)
+
+ def type_conflict_strategy(self, *args):
+ return self._type_conflict_strategy(self, *args)
+
+ def value_strategy(self, path, base, nxt):
+ if not (isinstance(base, type(nxt)) or isinstance(nxt, type(base))):
+ return self.type_conflict_strategy(path, base, nxt)
+ for typ, strategy in self._type_strategies:
+ if isinstance(nxt, typ):
+ return strategy(self, path, base, nxt)
+ return self._fallback_strategy(self, path, base, nxt)
diff --git a/contrib/python/deepmerge/py2/deepmerge/strategy/__init__.py b/contrib/python/deepmerge/py2/deepmerge/strategy/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/strategy/__init__.py
diff --git a/contrib/python/deepmerge/py2/deepmerge/strategy/core.py b/contrib/python/deepmerge/py2/deepmerge/strategy/core.py
new file mode 100644
index 0000000000..91fcfbb070
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/strategy/core.py
@@ -0,0 +1,38 @@
+from ..exception import StrategyNotFound, InvalidMerge
+from ..compat import string_type
+
+STRATEGY_END = object()
+
+
+class StrategyList(object):
+
+ NAME = None
+
+ def __init__(self, strategy_list):
+ if not isinstance(strategy_list, list):
+ strategy_list = [strategy_list]
+ self._strategies = [self._expand_strategy(s) for s in strategy_list]
+
+ @classmethod
+ def _expand_strategy(cls, strategy):
+ """
+ :param strategy: string or function
+
+ If the strategy is a string, attempt to resolve it
+ among the built in strategies.
+
+ Otherwise, return the value, implicitly assuming it's a function.
+ """
+ if isinstance(strategy, string_type):
+ method_name = "strategy_{0}".format(strategy)
+ if not hasattr(cls, method_name):
+ raise StrategyNotFound(strategy)
+ return getattr(cls, method_name)
+ return strategy
+
+ def __call__(self, *args, **kwargs):
+ for s in self._strategies:
+ ret_val = s(*args, **kwargs)
+ if ret_val is not STRATEGY_END:
+ return ret_val
+ raise InvalidMerge(self.NAME, args, kwargs)
diff --git a/contrib/python/deepmerge/py2/deepmerge/strategy/dict.py b/contrib/python/deepmerge/py2/deepmerge/strategy/dict.py
new file mode 100644
index 0000000000..8f09eb984f
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/strategy/dict.py
@@ -0,0 +1,32 @@
+from .core import StrategyList
+
+
+class DictStrategies(StrategyList):
+ """
+ Contains the strategies provided for dictionaries.
+
+ """
+
+ NAME = "dict"
+
+ @staticmethod
+ def strategy_merge(config, path, base, nxt):
+ """
+ for keys that do not exists,
+ use them directly. if the key exists
+ in both dictionaries, attempt a value merge.
+ """
+ for k, v in nxt.items():
+ if k not in base:
+ base[k] = v
+ else:
+ base[k] = config.value_strategy(path + [k], base[k], v)
+ return base
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """
+ move all keys in nxt into base, overriding
+ conflicts.
+ """
+ return nxt
diff --git a/contrib/python/deepmerge/py2/deepmerge/strategy/fallback.py b/contrib/python/deepmerge/py2/deepmerge/strategy/fallback.py
new file mode 100644
index 0000000000..714e8f9f59
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/strategy/fallback.py
@@ -0,0 +1,19 @@
+from .core import StrategyList
+
+
+class FallbackStrategies(StrategyList):
+ """
+ The StrategyList containing fallback strategies.
+ """
+
+ NAME = "fallback"
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """use nxt, and ignore base."""
+ return nxt
+
+ @staticmethod
+ def strategy_use_existing(config, path, base, nxt):
+ """use base, and ignore next."""
+ return base
diff --git a/contrib/python/deepmerge/py2/deepmerge/strategy/list.py b/contrib/python/deepmerge/py2/deepmerge/strategy/list.py
new file mode 100644
index 0000000000..2e42519fd5
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/strategy/list.py
@@ -0,0 +1,31 @@
+from .core import StrategyList
+from ..extended_set import ExtendedSet
+
+
+class ListStrategies(StrategyList):
+ """
+ Contains the strategies provided for lists.
+ """
+
+ NAME = "list"
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """use the list nxt."""
+ return nxt
+
+ @staticmethod
+ def strategy_prepend(config, path, base, nxt):
+ """prepend nxt to base."""
+ return nxt + base
+
+ @staticmethod
+ def strategy_append(config, path, base, nxt):
+ """append nxt to base."""
+ return base + nxt
+
+ @staticmethod
+ def strategy_append_unique(config, path, base, nxt):
+ """append items without duplicates in nxt to base."""
+ base_as_set = ExtendedSet(base)
+ return base + [n for n in nxt if n not in base_as_set]
diff --git a/contrib/python/deepmerge/py2/deepmerge/strategy/set.py b/contrib/python/deepmerge/py2/deepmerge/strategy/set.py
new file mode 100644
index 0000000000..55b26e3995
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/strategy/set.py
@@ -0,0 +1,30 @@
+from .core import StrategyList
+
+
+class SetStrategies(StrategyList):
+ """
+ Contains the strategies provided for sets.
+ """
+
+ NAME = "set"
+
+ @staticmethod
+ def strategy_union(config, path, base, nxt):
+ """
+ use all values in either base or nxt.
+ """
+ return base | nxt
+
+ @staticmethod
+ def strategy_intersect(config, path, base, nxt):
+ """
+ use all values in both base and nxt.
+ """
+ return base & nxt
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """
+ use the set nxt.
+ """
+ return nxt
diff --git a/contrib/python/deepmerge/py2/deepmerge/strategy/type_conflict.py b/contrib/python/deepmerge/py2/deepmerge/strategy/type_conflict.py
new file mode 100644
index 0000000000..ebbc77a4ee
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/strategy/type_conflict.py
@@ -0,0 +1,22 @@
+from .core import StrategyList
+
+
+class TypeConflictStrategies(StrategyList):
+ """contains the strategies provided for type conflicts."""
+
+ NAME = "type conflict"
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """overrides the new object over the old object"""
+ return nxt
+
+ @staticmethod
+ def strategy_use_existing(config, path, base, nxt):
+ """uses the old object instead of the new object"""
+ return base
+
+ @staticmethod
+ def strategy_override_if_not_empty(config, path, base, nxt):
+ """overrides the new object over the old object only if the new object is not empty or null"""
+ return nxt if nxt else base
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/__init__.py b/contrib/python/deepmerge/py2/deepmerge/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/__init__.py
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/strategy/__init__.py b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/__init__.py
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_core.py b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_core.py
new file mode 100644
index 0000000000..afa76d7299
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_core.py
@@ -0,0 +1,41 @@
+from deepmerge.strategy.core import StrategyList
+from deepmerge import STRATEGY_END
+
+
+def return_true_if_foo(config, path, base, nxt):
+ if base == "foo":
+ return True
+ return STRATEGY_END
+
+
+def always_return_custom(config, path, base, nxt):
+ return "custom"
+
+
+def test_single_value_allowed():
+ """ """
+
+ def strat(name):
+ return name
+
+ sl = StrategyList(strat)
+ assert sl("foo") == "foo"
+
+
+def test_first_working_strategy_is_used():
+ """
+ In the case where the StrategyList has multiple values,
+ the first strategy which returns a valid value (i.e. not STRATEGY_END)
+ should be returned.
+ """
+ sl = StrategyList(
+ [
+ return_true_if_foo,
+ always_return_custom,
+ ]
+ )
+ # return_true_if_foo will take.
+ assert sl({}, [], "foo", "bar") is True
+ # return_true_if_foo will fail,
+ # which will then activea always_return_custom
+ assert sl({}, [], "bar", "baz") == "custom"
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_list.py b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_list.py
new file mode 100644
index 0000000000..7eb2d3bb9d
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_list.py
@@ -0,0 +1,33 @@
+import pytest
+from deepmerge.strategy.list import ListStrategies
+from deepmerge import Merger
+
+
+@pytest.fixture
+def custom_merger():
+ return Merger(
+ [(list, ListStrategies.strategy_append_unique)],
+ [],
+ [],
+ )
+
+
+def test_strategy_append_unique(custom_merger):
+ base = [1, 3, 2]
+ nxt = [3, 5, 4, 1, 2]
+
+ 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"]}]
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_set_merge.py b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_set_merge.py
new file mode 100644
index 0000000000..72df386cc0
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_set_merge.py
@@ -0,0 +1,13 @@
+from deepmerge.strategy.set import SetStrategies
+
+
+def test_union_unions():
+ assert SetStrategies.strategy_union({}, [], set("abc"), set("bcd")) == set("abcd")
+
+
+def test_intersect_intersects():
+ assert SetStrategies.strategy_intersect({}, [], set("abc"), set("bcd")) == set("bc")
+
+
+def test_override_overrides():
+ assert SetStrategies.strategy_override({}, [], set("abc"), set("bcd")) == set("bcd")
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_type_conflict.py b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_type_conflict.py
new file mode 100644
index 0000000000..a366351e4d
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/strategy/test_type_conflict.py
@@ -0,0 +1,22 @@
+from deepmerge.strategy.type_conflict import TypeConflictStrategies
+
+EMPTY_DICT = {}
+
+CONTENT_AS_LIST = [{"key": "val"}]
+
+
+def test_merge_if_not_empty():
+ strategy = TypeConflictStrategies.strategy_override_if_not_empty(
+ {}, [], EMPTY_DICT, CONTENT_AS_LIST
+ )
+ assert strategy == CONTENT_AS_LIST
+
+ strategy = TypeConflictStrategies.strategy_override_if_not_empty(
+ {}, [], CONTENT_AS_LIST, EMPTY_DICT
+ )
+ assert strategy == CONTENT_AS_LIST
+
+ strategy = TypeConflictStrategies.strategy_override_if_not_empty(
+ {}, [], CONTENT_AS_LIST, None
+ )
+ assert strategy == CONTENT_AS_LIST
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/test_full.py b/contrib/python/deepmerge/py2/deepmerge/tests/test_full.py
new file mode 100644
index 0000000000..c7710cdf91
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/test_full.py
@@ -0,0 +1,50 @@
+from deepmerge.exception import *
+import pytest
+
+
+from deepmerge import (
+ always_merger,
+ conservative_merger,
+ merge_or_raise,
+)
+
+
+def test_fill_missing_value():
+ base = {"foo": 0, "baz": 2}
+ nxt = {"bar": 1}
+ always_merger.merge(base, nxt)
+ assert base == {"foo": 0, "bar": 1, "baz": 2}
+
+
+def test_handles_set_values_via_union():
+ base = {"a": set("123"), "b": 3}
+ nxt = {"a": set("2345"), "c": 1}
+ always_merger.merge(base, nxt)
+ assert base == {"a": set("12345"), "b": 3, "c": 1}
+
+
+def test_merge_or_raise_raises_exception():
+ base = {"foo": 0, "baz": 2}
+ nxt = {"bar": 1, "foo": "a string!"}
+ with pytest.raises(InvalidMerge) as exc_info:
+ merge_or_raise.merge(base, nxt)
+ exc = exc_info.value
+ assert exc.strategy_list_name == "type conflict"
+ assert exc.merge_args == (merge_or_raise, ["foo"], 0, "a string!")
+ assert exc.merge_kwargs == {}
+
+
+@pytest.mark.parametrize(
+ "base, nxt, expected", [("dooby", "fooby", "dooby"), (-10, "goo", -10)]
+)
+def test_use_existing(base, nxt, expected):
+ assert conservative_merger.merge(base, nxt) == expected
+
+
+def test_example():
+ base = {"foo": "value", "baz": ["a"]}
+ next = {"bar": "value2", "baz": ["b"]}
+
+ always_merger.merge(base, next)
+
+ assert base == {"foo": "value", "bar": "value2", "baz": ["a", "b"]}
diff --git a/contrib/python/deepmerge/py2/deepmerge/tests/test_merger.py b/contrib/python/deepmerge/py2/deepmerge/tests/test_merger.py
new file mode 100644
index 0000000000..e28b571d6c
--- /dev/null
+++ b/contrib/python/deepmerge/py2/deepmerge/tests/test_merger.py
@@ -0,0 +1,30 @@
+import pytest
+from deepmerge import Merger
+
+
+@pytest.fixture
+def custom_merger():
+ def merge_sets(merger, path, base, nxt):
+ base |= nxt
+ return base
+
+ def merge_list(merger, path, base, nxt):
+ if len(nxt) > 0:
+ base.append(nxt[-1])
+ return base
+
+ return Merger(
+ [(list, merge_list), (dict, "merge"), (set, merge_sets)],
+ [],
+ [],
+ )
+
+
+def test_custom_merger_applied(custom_merger):
+ result = custom_merger.merge({"foo"}, {"bar"})
+ assert result == {"foo", "bar"}
+
+
+def test_custom_merger_list(custom_merger):
+ result = custom_merger.merge([1, 2, 3], [4, 5, 6])
+ assert result == [1, 2, 3, 6]
diff --git a/contrib/python/deepmerge/py2/tests/ya.make b/contrib/python/deepmerge/py2/tests/ya.make
new file mode 100644
index 0000000000..c0d3cfaeda
--- /dev/null
+++ b/contrib/python/deepmerge/py2/tests/ya.make
@@ -0,0 +1,25 @@
+PY2TEST()
+
+SUBSCRIBER(g:python-contrib)
+
+PEERDIR(
+ contrib/python/deepmerge
+)
+
+SRCDIR(
+ contrib/python/deepmerge/py2/deepmerge/tests
+)
+
+TEST_SRCS(
+ __init__.py
+ strategy/__init__.py
+ strategy/test_core.py
+ strategy/test_set_merge.py
+ strategy/test_type_conflict.py
+ test_full.py
+ test_merger.py
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/deepmerge/py2/ya.make b/contrib/python/deepmerge/py2/ya.make
new file mode 100644
index 0000000000..5ae850071c
--- /dev/null
+++ b/contrib/python/deepmerge/py2/ya.make
@@ -0,0 +1,40 @@
+# Generated by devtools/yamaker (pypi).
+
+PY2_LIBRARY()
+
+SUBSCRIBER(g:python-contrib)
+
+VERSION(1.1.0)
+
+LICENSE(MIT)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ deepmerge/__init__.py
+ deepmerge/_version.py
+ deepmerge/compat.py
+ deepmerge/exception.py
+ deepmerge/extended_set.py
+ deepmerge/merger.py
+ deepmerge/strategy/__init__.py
+ deepmerge/strategy/core.py
+ deepmerge/strategy/dict.py
+ deepmerge/strategy/fallback.py
+ deepmerge/strategy/list.py
+ deepmerge/strategy/set.py
+ deepmerge/strategy/type_conflict.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/deepmerge/py2/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)
diff --git a/contrib/python/deepmerge/py3/.dist-info/METADATA b/contrib/python/deepmerge/py3/.dist-info/METADATA
new file mode 100644
index 0000000000..9312291976
--- /dev/null
+++ b/contrib/python/deepmerge/py3/.dist-info/METADATA
@@ -0,0 +1,80 @@
+Metadata-Version: 2.1
+Name: deepmerge
+Version: 1.1.1
+Summary: a toolset to deeply merge python dictionaries.
+Home-page: http://deepmerge.readthedocs.io/en/latest/
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 3
+License-File: LICENSE
+
+=========
+deepmerge
+=========
+
+.. image:: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml/badge.svg
+ :target: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml
+
+A tools to handle merging of
+nested data structures in python.
+
+------------
+Installation
+------------
+
+deepmerge is available on `pypi <https://pypi.python.org/>`_:
+
+.. code-block:: bash
+
+ pip install deepmerge
+
+-------
+Example
+-------
+
+**Generic Strategy**
+
+.. code-block:: python
+
+ from deepmerge import always_merger
+
+ base = {"foo": ["bar"]}
+ next = {"foo": ["baz"]}
+
+ expected_result = {'foo': ['bar', 'baz']}
+ result = always_merger.merge(base, next)
+
+ assert expected_result == result
+
+
+**Custom Strategy**
+
+.. code-block:: python
+
+ from deepmerge import Merger
+
+ my_merger = Merger(
+ # pass in a list of tuple, with the
+ # strategies you are looking to apply
+ # to each type.
+ [
+ (list, ["append"]),
+ (dict, ["merge"]),
+ (set, ["union"])
+ ],
+ # next, choose the fallback strategies,
+ # applied to all other types:
+ ["override"],
+ # finally, choose the strategies in
+ # the case where the types conflict:
+ ["override"]
+ )
+ base = {"foo": ["bar"]}
+ next = {"bar": "baz"}
+ my_merger.merge(base, next)
+ assert base == {"foo": ["bar"], "bar": "baz"}
+
+
+You can also pass in your own merge functions, instead of a string.
+
+For more information, see the `docs <https://deepmerge.readthedocs.io/en/latest/>`_
diff --git a/contrib/python/deepmerge/py3/.dist-info/top_level.txt b/contrib/python/deepmerge/py3/.dist-info/top_level.txt
new file mode 100644
index 0000000000..046e27c3a8
--- /dev/null
+++ b/contrib/python/deepmerge/py3/.dist-info/top_level.txt
@@ -0,0 +1 @@
+deepmerge
diff --git a/contrib/python/deepmerge/py3/LICENSE b/contrib/python/deepmerge/py3/LICENSE
new file mode 100644
index 0000000000..7e0b51be12
--- /dev/null
+++ b/contrib/python/deepmerge/py3/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Yusuke Tsutsumi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/contrib/python/deepmerge/py3/README.rst b/contrib/python/deepmerge/py3/README.rst
new file mode 100644
index 0000000000..24050eb556
--- /dev/null
+++ b/contrib/python/deepmerge/py3/README.rst
@@ -0,0 +1,70 @@
+=========
+deepmerge
+=========
+
+.. image:: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml/badge.svg
+ :target: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml
+
+A tools to handle merging of
+nested data structures in python.
+
+------------
+Installation
+------------
+
+deepmerge is available on `pypi <https://pypi.python.org/>`_:
+
+.. code-block:: bash
+
+ pip install deepmerge
+
+-------
+Example
+-------
+
+**Generic Strategy**
+
+.. code-block:: python
+
+ from deepmerge import always_merger
+
+ base = {"foo": ["bar"]}
+ next = {"foo": ["baz"]}
+
+ expected_result = {'foo': ['bar', 'baz']}
+ result = always_merger.merge(base, next)
+
+ assert expected_result == result
+
+
+**Custom Strategy**
+
+.. code-block:: python
+
+ from deepmerge import Merger
+
+ my_merger = Merger(
+ # pass in a list of tuple, with the
+ # strategies you are looking to apply
+ # to each type.
+ [
+ (list, ["append"]),
+ (dict, ["merge"]),
+ (set, ["union"])
+ ],
+ # next, choose the fallback strategies,
+ # applied to all other types:
+ ["override"],
+ # finally, choose the strategies in
+ # the case where the types conflict:
+ ["override"]
+ )
+ base = {"foo": ["bar"]}
+ next = {"bar": "baz"}
+ my_merger.merge(base, next)
+ assert base == {"foo": ["bar"], "bar": "baz"}
+
+
+You can also pass in your own merge functions, instead of a string.
+
+For more information, see the `docs <https://deepmerge.readthedocs.io/en/latest/>`_ \ No newline at end of file
diff --git a/contrib/python/deepmerge/py3/deepmerge/__init__.py b/contrib/python/deepmerge/py3/deepmerge/__init__.py
new file mode 100644
index 0000000000..b7686cf91c
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/__init__.py
@@ -0,0 +1,34 @@
+from .merger import Merger
+from .strategy.core import STRATEGY_END # noqa
+
+# some standard mergers available
+
+DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES = [
+ (list, "append"),
+ (dict, "merge"),
+ (set, "union"),
+]
+
+# this merge will never raise an exception.
+# in the case of type mismatches,
+# the value from the second object
+# will override the previous one.
+always_merger = Merger(
+ DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES, ["override"], ["override"]
+)
+
+# this merge strategies attempts
+# to merge (append for list, unify for dicts)
+# if possible, but raises an exception
+# in the case of type conflicts.
+merge_or_raise = Merger(DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES, [], [])
+
+# a conservative merge tactic:
+# for data structures with a specific
+# strategy, keep the existing value.
+# similar to always_merger but instead
+# keeps existing values when faced
+# with a type conflict.
+conservative_merger = Merger(
+ DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES, ["use_existing"], ["use_existing"]
+)
diff --git a/contrib/python/deepmerge/py3/deepmerge/_version.py b/contrib/python/deepmerge/py3/deepmerge/_version.py
new file mode 100644
index 0000000000..e602b7bb58
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/_version.py
@@ -0,0 +1,16 @@
+# file generated by setuptools_scm
+# don't change, don't track in version control
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Tuple, Union
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
+else:
+ VERSION_TUPLE = object
+
+version: str
+__version__: str
+__version_tuple__: VERSION_TUPLE
+version_tuple: VERSION_TUPLE
+
+__version__ = version = '1.1.1'
+__version_tuple__ = version_tuple = (1, 1, 1)
diff --git a/contrib/python/deepmerge/py3/deepmerge/compat.py b/contrib/python/deepmerge/py3/deepmerge/compat.py
new file mode 100644
index 0000000000..d2872a6edf
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/compat.py
@@ -0,0 +1,4 @@
+try:
+ string_type = basestring
+except:
+ string_type = str
diff --git a/contrib/python/deepmerge/py3/deepmerge/exception.py b/contrib/python/deepmerge/py3/deepmerge/exception.py
new file mode 100644
index 0000000000..ea36dac3ba
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/exception.py
@@ -0,0 +1,18 @@
+class DeepMergeException(Exception):
+ pass
+
+
+class StrategyNotFound(DeepMergeException):
+ pass
+
+
+class InvalidMerge(DeepMergeException):
+ def __init__(self, strategy_list_name, merge_args, merge_kwargs):
+ super(InvalidMerge, self).__init__(
+ "no more strategies found for {0} and arguments {1}, {2}".format(
+ strategy_list_name, merge_args, merge_kwargs
+ )
+ )
+ self.strategy_list_name = strategy_list_name
+ self.merge_args = merge_args
+ self.merge_kwargs = merge_kwargs
diff --git a/contrib/python/deepmerge/py3/deepmerge/extended_set.py b/contrib/python/deepmerge/py3/deepmerge/extended_set.py
new file mode 100644
index 0000000000..1d51b431b9
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/extended_set.py
@@ -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
diff --git a/contrib/python/deepmerge/py3/deepmerge/merger.py b/contrib/python/deepmerge/py3/deepmerge/merger.py
new file mode 100644
index 0000000000..5ccd568755
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/merger.py
@@ -0,0 +1,44 @@
+from .strategy.list import ListStrategies
+from .strategy.dict import DictStrategies
+from .strategy.set import SetStrategies
+from .strategy.type_conflict import TypeConflictStrategies
+from .strategy.fallback import FallbackStrategies
+
+
+class Merger(object):
+ """
+ :param type_strategies, List[Tuple]: a list of (Type, Strategy) pairs
+ that should be used against incoming types. For example: (dict, "override").
+ """
+
+ PROVIDED_TYPE_STRATEGIES = {
+ list: ListStrategies,
+ dict: DictStrategies,
+ set: SetStrategies,
+ }
+
+ def __init__(self, type_strategies, fallback_strategies, type_conflict_strategies):
+ self._fallback_strategy = FallbackStrategies(fallback_strategies)
+
+ expanded_type_strategies = []
+ for typ, strategy in type_strategies:
+ if typ in self.PROVIDED_TYPE_STRATEGIES:
+ strategy = self.PROVIDED_TYPE_STRATEGIES[typ](strategy)
+ expanded_type_strategies.append((typ, strategy))
+ self._type_strategies = expanded_type_strategies
+
+ self._type_conflict_strategy = TypeConflictStrategies(type_conflict_strategies)
+
+ def merge(self, base, nxt):
+ return self.value_strategy([], base, nxt)
+
+ def type_conflict_strategy(self, *args):
+ return self._type_conflict_strategy(self, *args)
+
+ def value_strategy(self, path, base, nxt):
+ for typ, strategy in self._type_strategies:
+ if isinstance(base, typ) and isinstance(nxt, typ):
+ return strategy(self, path, base, nxt)
+ if not (isinstance(base, type(nxt)) or isinstance(nxt, type(base))):
+ return self.type_conflict_strategy(path, base, nxt)
+ return self._fallback_strategy(self, path, base, nxt)
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py b/contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/core.py b/contrib/python/deepmerge/py3/deepmerge/strategy/core.py
new file mode 100644
index 0000000000..91fcfbb070
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/core.py
@@ -0,0 +1,38 @@
+from ..exception import StrategyNotFound, InvalidMerge
+from ..compat import string_type
+
+STRATEGY_END = object()
+
+
+class StrategyList(object):
+
+ NAME = None
+
+ def __init__(self, strategy_list):
+ if not isinstance(strategy_list, list):
+ strategy_list = [strategy_list]
+ self._strategies = [self._expand_strategy(s) for s in strategy_list]
+
+ @classmethod
+ def _expand_strategy(cls, strategy):
+ """
+ :param strategy: string or function
+
+ If the strategy is a string, attempt to resolve it
+ among the built in strategies.
+
+ Otherwise, return the value, implicitly assuming it's a function.
+ """
+ if isinstance(strategy, string_type):
+ method_name = "strategy_{0}".format(strategy)
+ if not hasattr(cls, method_name):
+ raise StrategyNotFound(strategy)
+ return getattr(cls, method_name)
+ return strategy
+
+ def __call__(self, *args, **kwargs):
+ for s in self._strategies:
+ ret_val = s(*args, **kwargs)
+ if ret_val is not STRATEGY_END:
+ return ret_val
+ raise InvalidMerge(self.NAME, args, kwargs)
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/dict.py b/contrib/python/deepmerge/py3/deepmerge/strategy/dict.py
new file mode 100644
index 0000000000..8f09eb984f
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/dict.py
@@ -0,0 +1,32 @@
+from .core import StrategyList
+
+
+class DictStrategies(StrategyList):
+ """
+ Contains the strategies provided for dictionaries.
+
+ """
+
+ NAME = "dict"
+
+ @staticmethod
+ def strategy_merge(config, path, base, nxt):
+ """
+ for keys that do not exists,
+ use them directly. if the key exists
+ in both dictionaries, attempt a value merge.
+ """
+ for k, v in nxt.items():
+ if k not in base:
+ base[k] = v
+ else:
+ base[k] = config.value_strategy(path + [k], base[k], v)
+ return base
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """
+ move all keys in nxt into base, overriding
+ conflicts.
+ """
+ return nxt
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py b/contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py
new file mode 100644
index 0000000000..714e8f9f59
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py
@@ -0,0 +1,19 @@
+from .core import StrategyList
+
+
+class FallbackStrategies(StrategyList):
+ """
+ The StrategyList containing fallback strategies.
+ """
+
+ NAME = "fallback"
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """use nxt, and ignore base."""
+ return nxt
+
+ @staticmethod
+ def strategy_use_existing(config, path, base, nxt):
+ """use base, and ignore next."""
+ return base
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/list.py b/contrib/python/deepmerge/py3/deepmerge/strategy/list.py
new file mode 100644
index 0000000000..2e42519fd5
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/list.py
@@ -0,0 +1,31 @@
+from .core import StrategyList
+from ..extended_set import ExtendedSet
+
+
+class ListStrategies(StrategyList):
+ """
+ Contains the strategies provided for lists.
+ """
+
+ NAME = "list"
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """use the list nxt."""
+ return nxt
+
+ @staticmethod
+ def strategy_prepend(config, path, base, nxt):
+ """prepend nxt to base."""
+ return nxt + base
+
+ @staticmethod
+ def strategy_append(config, path, base, nxt):
+ """append nxt to base."""
+ return base + nxt
+
+ @staticmethod
+ def strategy_append_unique(config, path, base, nxt):
+ """append items without duplicates in nxt to base."""
+ base_as_set = ExtendedSet(base)
+ return base + [n for n in nxt if n not in base_as_set]
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/set.py b/contrib/python/deepmerge/py3/deepmerge/strategy/set.py
new file mode 100644
index 0000000000..55b26e3995
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/set.py
@@ -0,0 +1,30 @@
+from .core import StrategyList
+
+
+class SetStrategies(StrategyList):
+ """
+ Contains the strategies provided for sets.
+ """
+
+ NAME = "set"
+
+ @staticmethod
+ def strategy_union(config, path, base, nxt):
+ """
+ use all values in either base or nxt.
+ """
+ return base | nxt
+
+ @staticmethod
+ def strategy_intersect(config, path, base, nxt):
+ """
+ use all values in both base and nxt.
+ """
+ return base & nxt
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """
+ use the set nxt.
+ """
+ return nxt
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/type_conflict.py b/contrib/python/deepmerge/py3/deepmerge/strategy/type_conflict.py
new file mode 100644
index 0000000000..ebbc77a4ee
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/type_conflict.py
@@ -0,0 +1,22 @@
+from .core import StrategyList
+
+
+class TypeConflictStrategies(StrategyList):
+ """contains the strategies provided for type conflicts."""
+
+ NAME = "type conflict"
+
+ @staticmethod
+ def strategy_override(config, path, base, nxt):
+ """overrides the new object over the old object"""
+ return nxt
+
+ @staticmethod
+ def strategy_use_existing(config, path, base, nxt):
+ """uses the old object instead of the new object"""
+ return base
+
+ @staticmethod
+ def strategy_override_if_not_empty(config, path, base, nxt):
+ """overrides the new object over the old object only if the new object is not empty or null"""
+ return nxt if nxt else base
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/__init__.py b/contrib/python/deepmerge/py3/deepmerge/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/__init__.py
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/strategy/__init__.py b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/__init__.py
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_core.py b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_core.py
new file mode 100644
index 0000000000..afa76d7299
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_core.py
@@ -0,0 +1,41 @@
+from deepmerge.strategy.core import StrategyList
+from deepmerge import STRATEGY_END
+
+
+def return_true_if_foo(config, path, base, nxt):
+ if base == "foo":
+ return True
+ return STRATEGY_END
+
+
+def always_return_custom(config, path, base, nxt):
+ return "custom"
+
+
+def test_single_value_allowed():
+ """ """
+
+ def strat(name):
+ return name
+
+ sl = StrategyList(strat)
+ assert sl("foo") == "foo"
+
+
+def test_first_working_strategy_is_used():
+ """
+ In the case where the StrategyList has multiple values,
+ the first strategy which returns a valid value (i.e. not STRATEGY_END)
+ should be returned.
+ """
+ sl = StrategyList(
+ [
+ return_true_if_foo,
+ always_return_custom,
+ ]
+ )
+ # return_true_if_foo will take.
+ assert sl({}, [], "foo", "bar") is True
+ # return_true_if_foo will fail,
+ # which will then activea always_return_custom
+ assert sl({}, [], "bar", "baz") == "custom"
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py
new file mode 100644
index 0000000000..7eb2d3bb9d
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py
@@ -0,0 +1,33 @@
+import pytest
+from deepmerge.strategy.list import ListStrategies
+from deepmerge import Merger
+
+
+@pytest.fixture
+def custom_merger():
+ return Merger(
+ [(list, ListStrategies.strategy_append_unique)],
+ [],
+ [],
+ )
+
+
+def test_strategy_append_unique(custom_merger):
+ base = [1, 3, 2]
+ nxt = [3, 5, 4, 1, 2]
+
+ 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"]}]
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_set_merge.py b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_set_merge.py
new file mode 100644
index 0000000000..72df386cc0
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_set_merge.py
@@ -0,0 +1,13 @@
+from deepmerge.strategy.set import SetStrategies
+
+
+def test_union_unions():
+ assert SetStrategies.strategy_union({}, [], set("abc"), set("bcd")) == set("abcd")
+
+
+def test_intersect_intersects():
+ assert SetStrategies.strategy_intersect({}, [], set("abc"), set("bcd")) == set("bc")
+
+
+def test_override_overrides():
+ assert SetStrategies.strategy_override({}, [], set("abc"), set("bcd")) == set("bcd")
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_type_conflict.py b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_type_conflict.py
new file mode 100644
index 0000000000..a366351e4d
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_type_conflict.py
@@ -0,0 +1,22 @@
+from deepmerge.strategy.type_conflict import TypeConflictStrategies
+
+EMPTY_DICT = {}
+
+CONTENT_AS_LIST = [{"key": "val"}]
+
+
+def test_merge_if_not_empty():
+ strategy = TypeConflictStrategies.strategy_override_if_not_empty(
+ {}, [], EMPTY_DICT, CONTENT_AS_LIST
+ )
+ assert strategy == CONTENT_AS_LIST
+
+ strategy = TypeConflictStrategies.strategy_override_if_not_empty(
+ {}, [], CONTENT_AS_LIST, EMPTY_DICT
+ )
+ assert strategy == CONTENT_AS_LIST
+
+ strategy = TypeConflictStrategies.strategy_override_if_not_empty(
+ {}, [], CONTENT_AS_LIST, None
+ )
+ assert strategy == CONTENT_AS_LIST
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/test_full.py b/contrib/python/deepmerge/py3/deepmerge/tests/test_full.py
new file mode 100644
index 0000000000..799db192c1
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/test_full.py
@@ -0,0 +1,60 @@
+from deepmerge.exception import *
+from collections import OrderedDict, defaultdict
+import pytest
+
+
+from deepmerge import (
+ always_merger,
+ conservative_merger,
+ merge_or_raise,
+)
+
+
+def test_fill_missing_value():
+ base = {"foo": 0, "baz": 2}
+ nxt = {"bar": 1}
+ always_merger.merge(base, nxt)
+ assert base == {"foo": 0, "bar": 1, "baz": 2}
+
+
+def test_handles_set_values_via_union():
+ base = {"a": set("123"), "b": 3}
+ nxt = {"a": set("2345"), "c": 1}
+ always_merger.merge(base, nxt)
+ assert base == {"a": set("12345"), "b": 3, "c": 1}
+
+
+def test_merge_or_raise_raises_exception():
+ base = {"foo": 0, "baz": 2}
+ nxt = {"bar": 1, "foo": "a string!"}
+ with pytest.raises(InvalidMerge) as exc_info:
+ merge_or_raise.merge(base, nxt)
+ exc = exc_info.value
+ assert exc.strategy_list_name == "type conflict"
+ assert exc.merge_args == (merge_or_raise, ["foo"], 0, "a string!")
+ assert exc.merge_kwargs == {}
+
+
+@pytest.mark.parametrize(
+ "base, nxt, expected", [("dooby", "fooby", "dooby"), (-10, "goo", -10)]
+)
+def test_use_existing(base, nxt, expected):
+ assert conservative_merger.merge(base, nxt) == expected
+
+
+def test_example():
+ base = {"foo": "value", "baz": ["a"]}
+ next = {"bar": "value2", "baz": ["b"]}
+
+ always_merger.merge(base, next)
+
+ assert base == {"foo": "value", "bar": "value2", "baz": ["a", "b"]}
+
+
+def test_subtypes():
+ base = OrderedDict({"foo": "value", "baz": ["a"]})
+ next = defaultdict(str, {"bar": "value2", "baz": ["b"]})
+
+ result = always_merger.merge(base, next)
+
+ assert dict(result) == {"foo": "value", "bar": "value2", "baz": ["a", "b"]}
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/test_merger.py b/contrib/python/deepmerge/py3/deepmerge/tests/test_merger.py
new file mode 100644
index 0000000000..e28b571d6c
--- /dev/null
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/test_merger.py
@@ -0,0 +1,30 @@
+import pytest
+from deepmerge import Merger
+
+
+@pytest.fixture
+def custom_merger():
+ def merge_sets(merger, path, base, nxt):
+ base |= nxt
+ return base
+
+ def merge_list(merger, path, base, nxt):
+ if len(nxt) > 0:
+ base.append(nxt[-1])
+ return base
+
+ return Merger(
+ [(list, merge_list), (dict, "merge"), (set, merge_sets)],
+ [],
+ [],
+ )
+
+
+def test_custom_merger_applied(custom_merger):
+ result = custom_merger.merge({"foo"}, {"bar"})
+ assert result == {"foo", "bar"}
+
+
+def test_custom_merger_list(custom_merger):
+ result = custom_merger.merge([1, 2, 3], [4, 5, 6])
+ assert result == [1, 2, 3, 6]
diff --git a/contrib/python/deepmerge/py3/tests/ya.make b/contrib/python/deepmerge/py3/tests/ya.make
new file mode 100644
index 0000000000..6117ded073
--- /dev/null
+++ b/contrib/python/deepmerge/py3/tests/ya.make
@@ -0,0 +1,25 @@
+PY3TEST()
+
+SUBSCRIBER(g:python-contrib)
+
+PEERDIR(
+ contrib/python/deepmerge
+)
+
+SRCDIR(
+ contrib/python/deepmerge/py3/deepmerge/tests
+)
+
+TEST_SRCS(
+ __init__.py
+ strategy/__init__.py
+ strategy/test_core.py
+ strategy/test_set_merge.py
+ strategy/test_type_conflict.py
+ test_full.py
+ test_merger.py
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/deepmerge/py3/ya.make b/contrib/python/deepmerge/py3/ya.make
new file mode 100644
index 0000000000..e30cae0333
--- /dev/null
+++ b/contrib/python/deepmerge/py3/ya.make
@@ -0,0 +1,40 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+SUBSCRIBER(g:python-contrib)
+
+VERSION(1.1.1)
+
+LICENSE(MIT)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ deepmerge/__init__.py
+ deepmerge/_version.py
+ deepmerge/compat.py
+ deepmerge/exception.py
+ deepmerge/extended_set.py
+ deepmerge/merger.py
+ deepmerge/strategy/__init__.py
+ deepmerge/strategy/core.py
+ deepmerge/strategy/dict.py
+ deepmerge/strategy/fallback.py
+ deepmerge/strategy/list.py
+ deepmerge/strategy/set.py
+ deepmerge/strategy/type_conflict.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/deepmerge/py3/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)
diff --git a/contrib/python/deepmerge/ya.make b/contrib/python/deepmerge/ya.make
new file mode 100644
index 0000000000..1e596fbb3e
--- /dev/null
+++ b/contrib/python/deepmerge/ya.make
@@ -0,0 +1,22 @@
+PY23_LIBRARY()
+
+LICENSE(Service-Py23-Proxy)
+
+VERSION(Service-proxy-version)
+
+SUBSCRIBER(g:python-contrib)
+
+IF (PYTHON2)
+ PEERDIR(contrib/python/deepmerge/py2)
+ELSE()
+ PEERDIR(contrib/python/deepmerge/py3)
+ENDIF()
+
+NO_LINT()
+
+END()
+
+RECURSE(
+ py2
+ py3
+)