From 5b0fe812706e2bd23d0ec7836f8e120a25f2489f Mon Sep 17 00:00:00 2001 From: rabi Date: Thu, 4 Aug 2016 10:56:41 +0530 Subject: [PATCH] Improve deep merge for parameters This allows for merging lists and deep merging maps. This does not support comma_delimited_lists and is implemented by using parameter schema in the subsequent patches. Change-Id: I89df95eca713a7125cc53ed8bf352274f28bf6d9 Blueprint: environment_merging --- heat/common/environment_util.py | 57 +++++++++++++++++++++++------- heat/tests/test_common_env_util.py | 39 ++++++++++++++++++++ 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/heat/common/environment_util.py b/heat/common/environment_util.py index b19a4606d4..833ee0c816 100644 --- a/heat/common/environment_util.py +++ b/heat/common/environment_util.py @@ -12,7 +12,9 @@ # under the License. import collections -from heat.common import environment_format +import six + +from heat.common import environment_format as env_fmt ALLOWED_PARAM_MERGE_STRATEGIES = (OVERWRITE, MERGE, DEEP_MERGE) = ( 'overwrite', 'merge', 'deep_merge') @@ -32,14 +34,37 @@ def get_param_merge_strategy(merge_strategies, param_key): return env_default -def deep_update(old, new): - '''Merge nested dictionaries.''' +def merge_list(old, new): + """merges lists and comma delimited lists.""" + if not old: + return new + + if isinstance(new, list): + old.extend(new) + return old + else: + return ','.join([old, new]) + + +def merge_map(old, new, deep_merge=False): + """Merge nested dictionaries.""" + if not old: + return new + for k, v in new.items(): - if isinstance(v, collections.Mapping): - r = deep_update(old.get(k, {}), v) - old[k] = r - else: - old[k] = new[k] + if v: + if not deep_merge: + old[k] = v + elif isinstance(v, collections.Mapping): + old_v = old.get(k) + old[k] = merge_map(old_v, v, deep_merge) if old_v else v + elif (isinstance(v, collections.Sequence) and + not isinstance(v, six.string_types)): + old_v = old.get(k) + old[k] = merge_list(old_v, v) if old_v else v + else: + old[k] = v + return old @@ -60,8 +85,14 @@ def merge_environments(environment_files, files, params): :param params: parameters describing the stack :type dict: """ - if environment_files: - for filename in environment_files: - raw_env = files[filename] - parsed_env = environment_format.parse(raw_env) - deep_update(params, parsed_env) + if not environment_files: + return + + for filename in environment_files: + raw_env = files[filename] + parsed_env = env_fmt.parse(raw_env) + for section_key, section_value in parsed_env.items(): + if section_value: + params[section_key] = merge_map(params[section_key], + section_value, + deep_merge=True) diff --git a/heat/tests/test_common_env_util.py b/heat/tests/test_common_env_util.py index b8975462d5..a29928f978 100644 --- a/heat/tests/test_common_env_util.py +++ b/heat/tests/test_common_env_util.py @@ -83,6 +83,45 @@ class TestMergeEnvironments(common.HeatTestCase): }} self.assertEqual(expected, params) + def test_merge_environments_deep_merge(self): + # Setup + params = {'parameters': { + 'p0': 'CORRECT', + 'p1': ['CORRECT1'], + 'p2': {'A': ['CORRECT1', 'CORRECT2']}, + 'p3': 'CORRECT1,CORRECT2'} + } + env_1 = ''' + {'parameters' : { + 'p1': ['CORRECT2', 'CORRECT3'], + 'p2': {'B': ['CORRECT3', 'CORRECT4'], + 'C': [CORRECT5]}, + 'p3': 'CORRECT3,CORRECT4' + }}''' + env_2 = ''' + {'parameters': { + 'p2': {'C': ['CORRECT6']}, + 'p3': 'CORRECT5,CORRECT6' + }}''' + + files = {'env_1': env_1, 'env_2': env_2} + environment_files = ['env_1', 'env_2'] + + # Test + env_util.merge_environments(environment_files, files, params) + + # Verify + # Does not work for comma_delimited_list. + expected = {'parameters': { + 'p0': 'CORRECT', + 'p1': ['CORRECT1', 'CORRECT2', 'CORRECT3'], + 'p2': {'A': ['CORRECT1', 'CORRECT2'], + 'B': ['CORRECT3', 'CORRECT4'], + 'C': ['CORRECT5', 'CORRECT6']}, + 'p3': 'CORRECT5,CORRECT6' + }} + self.assertEqual(expected, params) + def test_merge_environments_no_env_files(self): params = {'parameters': {'p0': 'CORRECT'}} env_1 = '''