aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrekby <rekby@ydb.tech>2024-08-06 16:45:38 +0300
committerrekby <rekby@ydb.tech>2024-08-06 17:08:00 +0300
commit4f495b080a6a0f72c77fa503b9f4a1b357969037 (patch)
tree8e1b1094ac2d92937715c3a5cd04a2cc89f276ed
parent0322f8aa6f5794b8dca79988173ee93dd29c49f6 (diff)
downloadydb-4f495b080a6a0f72c77fa503b9f4a1b357969037.tar.gz
Export python mergedeep library to in github.com/ydb-platform/ydb
d57ca70b751f8907c3621fa2935e0a9fcdba7972
-rw-r--r--contrib/python/mergedeep/.dist-info/METADATA154
-rw-r--r--contrib/python/mergedeep/.dist-info/top_level.txt1
-rw-r--r--contrib/python/mergedeep/.yandex_meta/yamaker.yaml2
-rw-r--r--contrib/python/mergedeep/LICENSE21
-rw-r--r--contrib/python/mergedeep/README.md133
-rw-r--r--contrib/python/mergedeep/mergedeep/__init__.py5
-rw-r--r--contrib/python/mergedeep/mergedeep/mergedeep.py100
-rw-r--r--contrib/python/mergedeep/mergedeep/test_mergedeep.py397
-rw-r--r--contrib/python/mergedeep/tests/ya.make17
-rw-r--r--contrib/python/mergedeep/ya.make29
10 files changed, 859 insertions, 0 deletions
diff --git a/contrib/python/mergedeep/.dist-info/METADATA b/contrib/python/mergedeep/.dist-info/METADATA
new file mode 100644
index 0000000000..23452d786b
--- /dev/null
+++ b/contrib/python/mergedeep/.dist-info/METADATA
@@ -0,0 +1,154 @@
+Metadata-Version: 2.1
+Name: mergedeep
+Version: 1.3.4
+Summary: A deep merge function for 🐍.
+Home-page: https://github.com/clarketm/mergedeep
+Author: Travis Clarke
+Author-email: travis.m.clarke@gmail.com
+License: UNKNOWN
+Platform: UNKNOWN
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Requires-Python: >=3.6
+Description-Content-Type: text/markdown
+
+# [mergedeep](https://mergedeep.readthedocs.io/en/latest/)
+
+[![PyPi release](https://img.shields.io/pypi/v/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![PyPi versions](https://img.shields.io/pypi/pyversions/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![Downloads](https://pepy.tech/badge/mergedeep)](https://pepy.tech/project/mergedeep)
+[![Conda Version](https://img.shields.io/conda/vn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Documentation Status](https://readthedocs.org/projects/mergedeep/badge/?version=latest)](https://mergedeep.readthedocs.io/en/latest/?badge=latest)
+
+A deep merge function for 🐍.
+
+[Check out the mergedeep docs](https://mergedeep.readthedocs.io/en/latest/)
+
+## Installation
+
+```bash
+$ pip install mergedeep
+```
+
+## Usage
+
+```text
+merge(destination: MutableMapping, *sources: Mapping, strategy: Strategy = Strategy.REPLACE) -> MutableMapping
+```
+
+Deep merge without mutating the source dicts.
+
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merged = merge({}, a, b, c)
+
+print(merged)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+Deep merge into an existing dict.
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merge(a, b, c)
+
+print(a)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+### Merge strategies:
+
+1. Replace (*default*)
+
+> `Strategy.REPLACE`
+
+```python3
+# When `destination` and `source` keys are the same, replace the `destination` value with one from `source` (default).
+
+# Note: with multiple sources, the `last` (i.e. rightmost) source value will be what appears in the merged result.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": [3, 4]}
+
+merge(dst, src, strategy=Strategy.REPLACE)
+# same as: merge(dst, src)
+
+print(dst)
+# {"key": [3, 4]}
+```
+
+2. Additive
+
+> `Strategy.ADDITIVE`
+
+```python3
+# When `destination` and `source` values are both the same additive collection type, extend `destination` by adding values from `source`.
+# Additive collection types include: `list`, `tuple`, `set`, and `Counter`
+
+# Note: if the values are not additive collections of the same type, then fallback to a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2], "count": Counter({"a": 1, "b": 1})}
+src = {"key": [3, 4], "count": Counter({"a": 1, "c": 1})}
+
+merge(dst, src, strategy=Strategy.ADDITIVE)
+
+print(dst)
+# {"key": [1, 2, 3, 4], "count": Counter({"a": 2, "b": 1, "c": 1})}
+```
+
+3. Typesafe replace
+
+> `Strategy.TYPESAFE_REPLACE` or `Strategy.TYPESAFE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_REPLACE) # same as: `Strategy.TYPESAFE`
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+4. Typesafe additive
+
+> `Strategy.TYPESAFE_ADDITIVE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `ADDITIVE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_ADDITIVE)
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+## License
+
+MIT &copy; [**Travis Clarke**](https://blog.travismclarke.com/)
+
+
diff --git a/contrib/python/mergedeep/.dist-info/top_level.txt b/contrib/python/mergedeep/.dist-info/top_level.txt
new file mode 100644
index 0000000000..5413932b26
--- /dev/null
+++ b/contrib/python/mergedeep/.dist-info/top_level.txt
@@ -0,0 +1 @@
+mergedeep
diff --git a/contrib/python/mergedeep/.yandex_meta/yamaker.yaml b/contrib/python/mergedeep/.yandex_meta/yamaker.yaml
new file mode 100644
index 0000000000..b88229ef48
--- /dev/null
+++ b/contrib/python/mergedeep/.yandex_meta/yamaker.yaml
@@ -0,0 +1,2 @@
+mark_as_tests:
+ - mergedeep/test_mergedeep.py
diff --git a/contrib/python/mergedeep/LICENSE b/contrib/python/mergedeep/LICENSE
new file mode 100644
index 0000000000..45c6440816
--- /dev/null
+++ b/contrib/python/mergedeep/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Travis Clarke <travis.m.clarke@gmail.com> (https://www.travismclarke.com/)
+
+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/mergedeep/README.md b/contrib/python/mergedeep/README.md
new file mode 100644
index 0000000000..86322730cd
--- /dev/null
+++ b/contrib/python/mergedeep/README.md
@@ -0,0 +1,133 @@
+# [mergedeep](https://mergedeep.readthedocs.io/en/latest/)
+
+[![PyPi release](https://img.shields.io/pypi/v/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![PyPi versions](https://img.shields.io/pypi/pyversions/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![Downloads](https://pepy.tech/badge/mergedeep)](https://pepy.tech/project/mergedeep)
+[![Conda Version](https://img.shields.io/conda/vn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Documentation Status](https://readthedocs.org/projects/mergedeep/badge/?version=latest)](https://mergedeep.readthedocs.io/en/latest/?badge=latest)
+
+A deep merge function for 🐍.
+
+[Check out the mergedeep docs](https://mergedeep.readthedocs.io/en/latest/)
+
+## Installation
+
+```bash
+$ pip install mergedeep
+```
+
+## Usage
+
+```text
+merge(destination: MutableMapping, *sources: Mapping, strategy: Strategy = Strategy.REPLACE) -> MutableMapping
+```
+
+Deep merge without mutating the source dicts.
+
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merged = merge({}, a, b, c)
+
+print(merged)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+Deep merge into an existing dict.
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merge(a, b, c)
+
+print(a)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+### Merge strategies:
+
+1. Replace (*default*)
+
+> `Strategy.REPLACE`
+
+```python3
+# When `destination` and `source` keys are the same, replace the `destination` value with one from `source` (default).
+
+# Note: with multiple sources, the `last` (i.e. rightmost) source value will be what appears in the merged result.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": [3, 4]}
+
+merge(dst, src, strategy=Strategy.REPLACE)
+# same as: merge(dst, src)
+
+print(dst)
+# {"key": [3, 4]}
+```
+
+2. Additive
+
+> `Strategy.ADDITIVE`
+
+```python3
+# When `destination` and `source` values are both the same additive collection type, extend `destination` by adding values from `source`.
+# Additive collection types include: `list`, `tuple`, `set`, and `Counter`
+
+# Note: if the values are not additive collections of the same type, then fallback to a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2], "count": Counter({"a": 1, "b": 1})}
+src = {"key": [3, 4], "count": Counter({"a": 1, "c": 1})}
+
+merge(dst, src, strategy=Strategy.ADDITIVE)
+
+print(dst)
+# {"key": [1, 2, 3, 4], "count": Counter({"a": 2, "b": 1, "c": 1})}
+```
+
+3. Typesafe replace
+
+> `Strategy.TYPESAFE_REPLACE` or `Strategy.TYPESAFE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_REPLACE) # same as: `Strategy.TYPESAFE`
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+4. Typesafe additive
+
+> `Strategy.TYPESAFE_ADDITIVE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `ADDITIVE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_ADDITIVE)
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+## License
+
+MIT &copy; [**Travis Clarke**](https://blog.travismclarke.com/)
diff --git a/contrib/python/mergedeep/mergedeep/__init__.py b/contrib/python/mergedeep/mergedeep/__init__.py
new file mode 100644
index 0000000000..92d12f6c94
--- /dev/null
+++ b/contrib/python/mergedeep/mergedeep/__init__.py
@@ -0,0 +1,5 @@
+__version__ = "1.3.4"
+
+from mergedeep.mergedeep import merge, Strategy
+
+__all__ = ["merge", "Strategy"]
diff --git a/contrib/python/mergedeep/mergedeep/mergedeep.py b/contrib/python/mergedeep/mergedeep/mergedeep.py
new file mode 100644
index 0000000000..6dda8e82f3
--- /dev/null
+++ b/contrib/python/mergedeep/mergedeep/mergedeep.py
@@ -0,0 +1,100 @@
+from collections import Counter
+from collections.abc import Mapping
+from copy import deepcopy
+from enum import Enum
+from functools import reduce, partial
+from typing import MutableMapping
+
+
+class Strategy(Enum):
+ # Replace `destination` item with one from `source` (default).
+ REPLACE = 0
+ # Combine `list`, `tuple`, `set`, or `Counter` types into one collection.
+ ADDITIVE = 1
+ # Alias to: `TYPESAFE_REPLACE`
+ TYPESAFE = 2
+ # Raise `TypeError` when `destination` and `source` types differ. Otherwise, perform a `REPLACE` merge.
+ TYPESAFE_REPLACE = 3
+ # Raise `TypeError` when `destination` and `source` types differ. Otherwise, perform a `ADDITIVE` merge.
+ TYPESAFE_ADDITIVE = 4
+
+
+def _handle_merge_replace(destination, source, key):
+ if isinstance(destination[key], Counter) and isinstance(source[key], Counter):
+ # Merge both destination and source `Counter` as if they were a standard dict.
+ _deepmerge(destination[key], source[key])
+ else:
+ # If a key exists in both objects and the values are `different`, the value from the `source` object will be used.
+ destination[key] = deepcopy(source[key])
+
+
+def _handle_merge_additive(destination, source, key):
+ # Values are combined into one long collection.
+ if isinstance(destination[key], list) and isinstance(source[key], list):
+ # Extend destination if both destination and source are `list` type.
+ destination[key].extend(deepcopy(source[key]))
+ elif isinstance(destination[key], set) and isinstance(source[key], set):
+ # Update destination if both destination and source are `set` type.
+ destination[key].update(deepcopy(source[key]))
+ elif isinstance(destination[key], tuple) and isinstance(source[key], tuple):
+ # Update destination if both destination and source are `tuple` type.
+ destination[key] = destination[key] + deepcopy(source[key])
+ elif isinstance(destination[key], Counter) and isinstance(source[key], Counter):
+ # Update destination if both destination and source are `Counter` type.
+ destination[key].update(deepcopy(source[key]))
+ else:
+ _handle_merge[Strategy.REPLACE](destination, source, key)
+
+
+def _handle_merge_typesafe(destination, source, key, strategy):
+ # Raise a TypeError if the destination and source types differ.
+ if type(destination[key]) is not type(source[key]):
+ raise TypeError(
+ f'destination type: {type(destination[key])} differs from source type: {type(source[key])} for key: "{key}"'
+ )
+ else:
+ _handle_merge[strategy](destination, source, key)
+
+
+_handle_merge = {
+ Strategy.REPLACE: _handle_merge_replace,
+ Strategy.ADDITIVE: _handle_merge_additive,
+ Strategy.TYPESAFE: partial(_handle_merge_typesafe, strategy=Strategy.REPLACE),
+ Strategy.TYPESAFE_REPLACE: partial(_handle_merge_typesafe, strategy=Strategy.REPLACE),
+ Strategy.TYPESAFE_ADDITIVE: partial(_handle_merge_typesafe, strategy=Strategy.ADDITIVE),
+}
+
+
+def _is_recursive_merge(a, b):
+ both_mapping = isinstance(a, Mapping) and isinstance(b, Mapping)
+ both_counter = isinstance(a, Counter) and isinstance(b, Counter)
+ return both_mapping and not both_counter
+
+
+def _deepmerge(dst, src, strategy=Strategy.REPLACE):
+ for key in src:
+ if key in dst:
+ if _is_recursive_merge(dst[key], src[key]):
+ # If the key for both `dst` and `src` are both Mapping types (e.g. dict), then recurse.
+ _deepmerge(dst[key], src[key], strategy)
+ elif dst[key] is src[key]:
+ # If a key exists in both objects and the values are `same`, the value from the `dst` object will be used.
+ pass
+ else:
+ _handle_merge.get(strategy)(dst, src, key)
+ else:
+ # If the key exists only in `src`, the value from the `src` object will be used.
+ dst[key] = deepcopy(src[key])
+ return dst
+
+
+def merge(destination: MutableMapping, *sources: Mapping, strategy: Strategy = Strategy.REPLACE) -> MutableMapping:
+ """
+ A deep merge function for 🐍.
+
+ :param destination: The destination mapping.
+ :param sources: The source mappings.
+ :param strategy: The merge strategy.
+ :return:
+ """
+ return reduce(partial(_deepmerge, strategy=strategy), sources, destination)
diff --git a/contrib/python/mergedeep/mergedeep/test_mergedeep.py b/contrib/python/mergedeep/mergedeep/test_mergedeep.py
new file mode 100644
index 0000000000..ef39728835
--- /dev/null
+++ b/contrib/python/mergedeep/mergedeep/test_mergedeep.py
@@ -0,0 +1,397 @@
+"""mergedeep test module"""
+import inspect
+import unittest
+from collections import Counter
+from copy import deepcopy
+
+from mergedeep import merge, Strategy
+
+
+class test_mergedeep(unittest.TestCase):
+ """mergedeep function tests."""
+
+ ##############################################################################################################################
+ # REPLACE
+ ##############################################################################################################################
+
+ def test_should_merge_3_dicts_into_new_dict_using_replace_strategy_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 5, "b": 1, "c": 1}),
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 2}),
+ }
+
+ a = {
+ "a": {"b": {"c": 5}},
+ "d": 1,
+ "e": {2: 3},
+ "f": [1, 2, 3],
+ "g": (2, 4, 6),
+ "h": Counter({"a": 1, "b": 1}),
+ "j": 1,
+ }
+ a_copy = deepcopy(a)
+
+ b = {
+ "a": {"B": {"C": 10}},
+ "d": 2,
+ "e": 2,
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 5, "c": 1}),
+ "i": Counter({"a": 1}),
+ "z": Counter({"a": 2}),
+ }
+ b_copy = deepcopy(b)
+
+ c = {
+ "a": {"b": {"_c": 15}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 2}),
+ }
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.REPLACE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_2_dicts_into_existing_dict_using_replace_strategy_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 1, "b": 1, "c": 1}),
+ "i": 2,
+ "j": Counter({"z": 2}),
+ }
+
+ a = {
+ "a": {"b": {"c": 5}},
+ "d": 1,
+ "e": {2: 3},
+ "f": [1, 2, 3],
+ "g": (2, 4, 6),
+ "h": Counter({"a": 1, "b": 1}),
+ "j": 1,
+ }
+ a_copy = deepcopy(a)
+
+ b = {
+ "a": {"B": {"C": 10}},
+ "d": 2,
+ "e": 2,
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 1, "c": 1}),
+ "i": Counter({"a": 1}),
+ }
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}, "i": 2, "j": Counter({"z": 2})}
+ c_copy = deepcopy(c)
+
+ actual = merge(a, b, c, strategy=Strategy.REPLACE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(actual, a)
+ self.assertNotEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_have_default_strategy_of_replace(self):
+ func_spec = inspect.getfullargspec(merge)
+ default_strategy = Strategy.REPLACE
+
+ self.assertEqual(func_spec.kwonlydefaults.get("strategy"), default_strategy)
+
+ # mock_merge.method.assert_called_with(target, source, strategy=Strategy.REPLACE)
+
+ ##############################################################################################################################
+ # ADDITIVE
+ ##############################################################################################################################
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_lists_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": [1, 2, 3, 4, 5, 6],
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_sets_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": {1, 2, 3, 4, 5, 6},
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": {1, 2, 3}}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": {4, 5, 6}}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_tuples_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": (1, 2, 3, 4, 5, 6),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": (1, 2, 3)}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": (4, 5, 6)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_counters_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": Counter({"a": 2, "c": 1, "b": 1}),
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 4}),
+ }
+
+ a = {
+ "a": {"b": {"c": 5}},
+ "d": 1,
+ "e": {2: 3},
+ "f": Counter({"a": 1, "c": 1}),
+ "i": Counter({"f": 9}),
+ "j": Counter({"a": 1, "z": 4}),
+ }
+ a_copy = deepcopy(a)
+
+ b = {
+ "a": {"B": {"C": 10}},
+ "d": 2,
+ "e": 2,
+ "f": Counter({"a": 1, "b": 1}),
+ "j": [1, 2, 3],
+ "z": Counter({"a": 2}),
+ }
+ b_copy = deepcopy(b)
+
+ c = {
+ "a": {"b": {"_c": 15}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 2}),
+ }
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_not_copy_references(self):
+ before = 1
+ after = 99
+
+ o1 = {"key1": before}
+ o2 = {"key2": before}
+
+ expected = {"list": deepcopy([o1, o2]), "tuple": deepcopy((o1, o2))}
+
+ a = {"list": [o1], "tuple": (o1,)}
+ b = {"list": [o2], "tuple": (o2,)}
+
+ actual = merge({}, a, b, strategy=Strategy.ADDITIVE)
+
+ o1["key1"] = after
+ o2["key2"] = after
+
+ self.assertEqual(actual, expected)
+
+ # Copied dicts should `not` mutate
+ self.assertEqual(actual["list"][0]["key1"], before)
+ self.assertEqual(actual["list"][1]["key2"], before)
+ self.assertEqual(actual["tuple"][0]["key1"], before)
+ self.assertEqual(actual["tuple"][1]["key2"], before)
+
+ # Non-copied dicts should mutate
+ self.assertEqual(a["list"][0]["key1"], after)
+ self.assertEqual(b["list"][0]["key2"], after)
+ self.assertEqual(a["tuple"][0]["key1"], after)
+ self.assertEqual(b["tuple"][0]["key2"], after)
+
+ ##############################################################################################################################
+ # TYPESAFE
+ # TYPESAFE_REPLACE
+ ##############################################################################################################################
+
+ def test_should_raise_TypeError_using_typesafe_strategy_if_types_differ(self):
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+
+ with self.assertRaises(TypeError):
+ merge({}, a, b, c, strategy=Strategy.TYPESAFE)
+
+ def test_should_raise_TypeError_using_typesafe_replace_strategy_if_types_differ(self,):
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+
+ with self.assertRaises(TypeError):
+ merge({}, a, b, c, strategy=Strategy.TYPESAFE_REPLACE)
+
+ def test_should_merge_3_dicts_into_new_dict_using_typesafe_strategy_and_only_mutate_target_if_types_are_compatible(
+ self,
+ ):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "f": [4, 5, 6],
+ "g": {2, 3, 4},
+ "h": (1, 3),
+ "z": Counter({"a": 1, "b": 1, "c": 1}),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "f": [1, 2, 3], "g": {1, 2, 3}, "z": Counter({"a": 1, "b": 1})}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "f": [4, 5, 6], "g": {2, 3, 4}, "h": (1,)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "h": (1, 3), "z": Counter({"a": 1, "c": 1})}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.TYPESAFE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_typesafe_replace_strategy_and_only_mutate_target_if_types_are_compatible(
+ self,
+ ):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "f": [4, 5, 6],
+ "g": {2, 3, 4},
+ "h": (1, 3),
+ "z": Counter({"a": 1, "b": 1, "c": 1}),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "f": [1, 2, 3], "g": {1, 2, 3}, "z": Counter({"a": 1, "b": 1})}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "f": [4, 5, 6], "g": {2, 3, 4}, "h": (1,)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "h": (1, 3), "z": Counter({"a": 1, "c": 1})}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.TYPESAFE_REPLACE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ ##############################################################################################################################
+ # TYPESAFE_ADDITIVE
+ ##############################################################################################################################
+
+ def test_should_raise_TypeError_using_typesafe_additive_strategy_if_types_differ(self,):
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+
+ with self.assertRaises(TypeError):
+ merge({}, a, b, c, strategy=Strategy.TYPESAFE_ADDITIVE)
+
+ def test_should_merge_3_dicts_into_new_dict_using_typesafe_additive_strategy_and_only_mutate_target_if_types_are_compatible(
+ self,
+ ):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "f": [1, 2, 3, 4, 5, 6],
+ "g": {1, 2, 3, 4},
+ "h": (1, 1, 3),
+ "z": Counter({"a": 2, "b": 1, "c": 1}),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "f": [1, 2, 3], "g": {1, 2, 3}, "z": Counter({"a": 1, "b": 1})}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "f": [4, 5, 6], "g": {2, 3, 4}, "h": (1,)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "h": (1, 3), "z": Counter({"a": 1, "c": 1})}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.TYPESAFE_ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/contrib/python/mergedeep/tests/ya.make b/contrib/python/mergedeep/tests/ya.make
new file mode 100644
index 0000000000..a0a227f788
--- /dev/null
+++ b/contrib/python/mergedeep/tests/ya.make
@@ -0,0 +1,17 @@
+PY3TEST()
+
+SUBSCRIBER(g:python-contrib)
+
+PEERDIR(
+ contrib/python/mergedeep
+)
+
+SRCDIR(contrib/python/mergedeep/mergedeep)
+
+TEST_SRCS(
+ test_mergedeep.py
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/mergedeep/ya.make b/contrib/python/mergedeep/ya.make
new file mode 100644
index 0000000000..ec7f03aaf4
--- /dev/null
+++ b/contrib/python/mergedeep/ya.make
@@ -0,0 +1,29 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+SUBSCRIBER(g:python-contrib)
+
+VERSION(1.3.4)
+
+LICENSE(MIT)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ mergedeep/__init__.py
+ mergedeep/mergedeep.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/mergedeep/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)