diff --git a/docs/source/general/basics.rst b/docs/source/general/basics.rst index 6d01e08df..502449cd8 100644 --- a/docs/source/general/basics.rst +++ b/docs/source/general/basics.rst @@ -275,11 +275,13 @@ refactor the previous code as follows: } router2 = OpenWrt(router2_config, templates=[dhcp_template]) -The function used under the hood to merge dictionaries and lists -is ``netjsonconfig.utils.merge_config``: +The functions used under the hood to merge configurations and templates +are ``netjsonconfig.utils.merge_config`` and ``netjsonconfig.utils.merge_list``: .. autofunction:: netjsonconfig.utils.merge_config +.. autofunction:: netjsonconfig.utils.merge_list + .. _multiple_templates: Multiple template inheritance diff --git a/netjsonconfig/utils.py b/netjsonconfig/utils.py index 069a47ffc..b60daa61e 100644 --- a/netjsonconfig/utils.py +++ b/netjsonconfig/utils.py @@ -13,8 +13,8 @@ def merge_config(template, config): * simple values (eg: ``str``, ``int``, ``float``, ecc) in ``config`` will overwrite the ones in ``template`` - * values of type ``list`` in both ``config`` and ``template`` will be summed - in order to create a list which contains elements of both + * values of type ``list`` in both ``config`` and ``template`` will be + merged using to the ``merge_list`` function * values of type ``dict`` will be merged recursively :param template: template ``dict`` @@ -27,12 +27,43 @@ def merge_config(template, config): node = result.get(key, {}) result[key] = merge_config(node, value) elif isinstance(value, list) and isinstance(result.get(key), list): - result[key] = deepcopy(result[key]) + deepcopy(value) + result[key] = merge_list(result[key], value) else: result[key] = value return result +def merge_list(list1, list2): + """ + Merges ``list2`` on top of ``list1``. + + If both lists contain dictionaries which have keys like + ``name`` or ``id`` of equal values, those dicts are merged + (dicts in ``list2`` will override dicts in ``list1``). + The remaining elements will be summed in order to create a list + which contains elements of both lists. + + :param list1: list from template + :param list2: list from config + :returns: merged ``list`` + """ + dict_map = {'list1': OrderedDict(), 'list2': OrderedDict()} + counter = 1 + for list_ in [list1, list2]: + container = dict_map['list{0}'.format(counter)] + for el in list_: + if isinstance(el, dict) and 'name' in el: + key = el['name'] + elif isinstance(el, dict) and 'id' in el: + key = el['id'] + else: + key = id(el) + container[key] = deepcopy(el) + counter += 1 + merged = merge_config(dict_map['list1'], dict_map['list2']) + return list(merged.values()) + + def sorted_dict(dictionary): return OrderedDict(sorted(dictionary.items())) diff --git a/tests/test_utils.py b/tests/test_utils.py index 06e9d4edc..52fab3501 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import unittest -from netjsonconfig.utils import evaluate_vars, merge_config +from netjsonconfig.utils import evaluate_vars, merge_config, merge_list class TestUtils(unittest.TestCase): @@ -168,3 +168,21 @@ def test_evaluate_vars_immersed(self): def test_evaluate_vars_one_char(self): self.assertEqual(evaluate_vars('{{ a }}', {'a': 'letter-A'}), 'letter-A') + + def test_merge_list_override(self): + template = [{"name": "test1", "tx": 1}] + config = [{"name": "test1", "tx": 2}] + result = merge_list(template, config) + self.assertEqual(result, config) + + def test_merge_list_union_and_override(self): + template = [{"id": "test1", "a": "a"}] + config = [ + {"id": "test1", "a": "0", "b": "b"}, + {"id": "test2", "c": "c"} + ] + result = merge_list(template, config) + self.assertEqual(result, [ + {"id": "test1", "a": "0", "b": "b"}, + {"id": "test2", "c": "c"} + ])