From 57cd5a93feb158c8dd13d0f8ba5548d8e42c8c45 Mon Sep 17 00:00:00 2001
From: Sebastian Goscik <sebastian.goscik@live.co.uk>
Date: Fri, 12 Aug 2016 15:26:46 +0100
Subject: [PATCH] Added merge_using_priority_specificity

This complex function handles merging of config with two priorities in mind.
The specificity of the config (`device_config` vs `nexus10`) and the priorty
of the source.
---
 wlauto/core/configuration/configuration.py | 87 +++++++++++++++++++++-
 1 file changed, 85 insertions(+), 2 deletions(-)

diff --git a/wlauto/core/configuration/configuration.py b/wlauto/core/configuration/configuration.py
index 9d614b6e..ba4fb67c 100644
--- a/wlauto/core/configuration/configuration.py
+++ b/wlauto/core/configuration/configuration.py
@@ -14,12 +14,13 @@
 
 import os
 from copy import copy
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
 
 from wlauto.exceptions import ConfigError
 from wlauto.utils.misc import (get_article, merge_config_values)
 from wlauto.utils.types import (identifier, integer, boolean,
-                                list_of_strings, toggle_set)
+                                list_of_strings, toggle_set,
+                                obj_dict)
 from wlauto.core.configuration.tree import SectionNode
 
 ##########################
@@ -306,6 +307,88 @@ class ConfigurationPoint(object):
 ### Configuration ###
 #####################
 
+# pylint: disable=too-many-nested-blocks, too-many-branches
+def merge_using_priority_specificity(generic_name, specific_name, plugin_cache):
+    """
+    WA configuration can come from various sources of increasing priority, as well
+    as being specified in a generic and specific manner (e.g. ``device_config``
+    and ``nexus10`` respectivly). WA has two rules for the priority of configuration:
+
+        - Configuration from higher priority sources overrides configuration from
+          lower priority sources.
+        - More specific configuration overrides less specific configuration.
+
+    There is a situation where these two rules come into conflict. When a generic
+    configuration is given in config source of high priority and a specific
+    configuration is given in a config source of lower priority. In this situation
+    it is not possible to know the end users intention and WA will error.
+
+    :param generic_name: The name of the generic configuration e.g ``device_config``
+    :param specific_name: The name of the specific configuration used, e.g ``nexus10``
+    :param cfg_point: A dict of ``ConfigurationPoint``s to be used when merging configuration.
+                      keys=config point name, values=config point
+
+    :rtype: A fully merged and validated configuration in the form of a obj_dict.
+    """
+    generic_config = plugin_cache.get_plugin_config(generic_name)
+    specific_config = plugin_cache.get_plugin_config(specific_name)
+    cfg_points = plugin_cache.get_plugin_config_points(specific_name)
+    sources = plugin_cache.sources
+    final_config = obj_dict(not_in_dict=['name'])
+    seen_specific_config = defaultdict(list)
+
+    # set_value uses the 'name' attribute of the passed object in it error
+    # messages, to ensure these messages make sense the name will have to be
+    # changed several times during this function.
+    final_config.name = specific_name
+
+    # Load default config
+    for cfg_point in cfg_points.itervalues():
+        cfg_point.set_value(final_config, check_mandatory=False)
+
+    # pylint: disable=too-many-nested-blocks
+    for source in sources:
+        try:
+            if source in generic_config:
+                for name, cfg_point in cfg_points.iteritems():
+                    final_config.name = generic_name
+                    if name in generic_config[source]:
+                        if name in seen_specific_config:
+                            msg = ('"{generic_name}" configuration "{config_name}" has already been '
+                                   'specified more specifically for {specific_name} in:\n\t\t{sources}')
+                            msg = msg.format(generic_name=generic_name,
+                                             config_name=name,
+                                             specific_name=specific_name,
+                                             sources="\n\t\t".join(seen_specific_config[name]))
+                            raise ConfigError(msg)
+                        value = generic_config[source].pop(name)
+                        cfg_point.set_value(final_config, value, check_mandatory=False)
+                if generic_config[source]:
+                    msg = 'Invalid entry(ies) for "{}" in "{}": "{}"'
+                    msg = msg.format(specific_name, generic_name, '", "'.join(generic_config[source]))
+                    raise ConfigError(msg)
+
+            if source in specific_config:
+                final_config.name = specific_name
+                for name, cfg_point in cfg_points.iteritems():
+                    if name in specific_config[source]:
+                        seen_specific_config[name].append(source)
+                        value = specific_config[source].pop(name)
+                        cfg_point.set_value(final_config, value, check_mandatory=False)
+                if specific_config[source]:
+                    msg = 'Invalid entry(ies) for "{}": "{}"'
+                    raise ConfigError(msg.format(specific_name, '", "'.join(specific_config[source])))
+
+        except ConfigError as e:
+            raise ConfigError('Error in "{}":\n\t{}'.format(source, str(e)))
+
+    # Validate final configuration
+    final_config.name = specific_name
+    for cfg_point in cfg_points.itervalues():
+        cfg_point.validate(final_config)
+
+    return final_config
+
 
 class Configuration(object):