diff options
author | rekby <rekby@ydb.tech> | 2024-08-16 13:53:46 +0300 |
---|---|---|
committer | rekby <rekby@ydb.tech> | 2024-08-16 14:02:56 +0300 |
commit | f23ace9b7c0c2e8578421e3e640e3d1cc0fe381b (patch) | |
tree | b8e99314de0a9340e442e5fa1a5289cf1a6a8704 /contrib/python/deepmerge | |
parent | a63920bcb9be4fb351b4e4e85c7cadc6b331ba18 (diff) | |
download | ydb-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')
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 +) |