diff options
author | rekby <rekby@ydb.tech> | 2024-08-06 16:45:38 +0300 |
---|---|---|
committer | rekby <rekby@ydb.tech> | 2024-08-06 17:08:00 +0300 |
commit | 4f495b080a6a0f72c77fa503b9f4a1b357969037 (patch) | |
tree | 8e1b1094ac2d92937715c3a5cd04a2cc89f276ed | |
parent | 0322f8aa6f5794b8dca79988173ee93dd29c49f6 (diff) | |
download | ydb-4f495b080a6a0f72c77fa503b9f4a1b357969037.tar.gz |
Export python mergedeep library to in github.com/ydb-platform/ydb
d57ca70b751f8907c3621fa2935e0a9fcdba7972
-rw-r--r-- | contrib/python/mergedeep/.dist-info/METADATA | 154 | ||||
-rw-r--r-- | contrib/python/mergedeep/.dist-info/top_level.txt | 1 | ||||
-rw-r--r-- | contrib/python/mergedeep/.yandex_meta/yamaker.yaml | 2 | ||||
-rw-r--r-- | contrib/python/mergedeep/LICENSE | 21 | ||||
-rw-r--r-- | contrib/python/mergedeep/README.md | 133 | ||||
-rw-r--r-- | contrib/python/mergedeep/mergedeep/__init__.py | 5 | ||||
-rw-r--r-- | contrib/python/mergedeep/mergedeep/mergedeep.py | 100 | ||||
-rw-r--r-- | contrib/python/mergedeep/mergedeep/test_mergedeep.py | 397 | ||||
-rw-r--r-- | contrib/python/mergedeep/tests/ya.make | 17 | ||||
-rw-r--r-- | contrib/python/mergedeep/ya.make | 29 |
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 © [**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 © [**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 +) |