Source code for config_patterns.patterns.merge_key_value.impl

# -*- coding: utf-8 -*-

"""
In configuration management, it is common practice to divide the configuration
into multiple files. From a machine's perspective, merging multiple dictionary data
into one can be more convenient to process it later. This module is designed to
recursively merge two dictionaries or a list of dictionaries.
"""

import typing as T
import copy


[docs]def merge_key_value( data1: dict, data2: dict, _fullpath: T.Optional[str] = None, ) -> dict: """ Merge two dict recursively. Both dict are equally important. Note that the original data will NOT be modified, it copy the data and return a new dict (the merged one). :param data1: dict data 1. :param data2: dict data 2. Example:: >>> data1 = { ... "key1": "value1", ... "credentials": [ ... {"username": "alice"}, ... {"username": "bob"}, ... ] ... } >>> data2 = { ... "key2": "value2", ... "credentials": [ ... {"password": "alice.pwd"}, ... {"password": "bob.pwd"}, ... ] ... } >>> merge_key_value(data1, data2) { "key1": "value1", "key2": "value2", "credentials": [ { "username": "alice", "password": "alice.pwd", }, { "username": "bob", "password": "bob.pwd", }, ], } """ data1 = copy.deepcopy(data1) data2 = copy.deepcopy(data2) if _fullpath is None: _fullpath = "" difference = data2.keys() - data1.keys() # extra keys intersection = data1.keys() & data2.keys() # common keys # for extra keys, just add them to data1 for key in difference: data1[key] = data2[key] for key in intersection: value1, value2 = data1[key], data2[key] # if both values are dict, merge them recursively if isinstance(value1, dict) and isinstance(value2, dict): data1[key] = merge_key_value(value1, value2, f"{_fullpath}.{key}") # if both values are list of dict, and has the same size, merge them recursively elif isinstance(value1, list) and isinstance(value2, list): if len(value1) != len(value2): raise ValueError(f"list length mismatch: path = '{_fullpath}.{key}'") value = list() for item1, item2 in zip(value1, value2): if isinstance(item1, dict) and isinstance(item2, dict): value.append(merge_key_value(item1, item2, f"{_fullpath}.{key}")) else: raise TypeError( f"items in '{_fullpath}.{key}' are not dict, so you cannot merge them!" ) data1[key] = value else: raise TypeError( f"type of value at '{_fullpath}.{key}' in data1 and data2 " f"has to be both dict or list of dict to merge! " f"they are {type(value1)} and {type(value2)}." ) return data1