Merge pull request #61 from Rtzq0/issue_60

Add functionality for overriding dictionary merges.
diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py
index 37419fc..a39324e 100644
--- a/reclass/datatypes/parameters.py
+++ b/reclass/datatypes/parameters.py
@@ -7,8 +7,8 @@
 # Released under the terms of the Artistic Licence 2.0
 #
 import types
-
-from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER
+from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER,\
+                             PARAMETER_DICT_KEY_OVERRIDE_PREFIX
 from reclass.utils.dictpath import DictPath
 from reclass.utils.refvalue import RefValue
 from reclass.errors import InfiniteRecursionError, UndefinedVariableError
@@ -37,6 +37,7 @@
     functionality and does not try to be a really mapping object.
     '''
     DEFAULT_PATH_DELIMITER = PARAMETER_INTERPOLATION_DELIMITER
+    DICT_KEY_OVERRIDE_PREFIX = PARAMETER_DICT_KEY_OVERRIDE_PREFIX
 
     def __init__(self, mapping=None, delimiter=None):
         if delimiter is None:
@@ -47,7 +48,7 @@
         if mapping is not None:
             # we initialise by merging, otherwise the list of references might
             # not be updated
-            self.merge(mapping)
+            self.merge(mapping, initmerge=True)
 
     delimiter = property(lambda self: self._delimiter)
 
@@ -119,7 +120,25 @@
             ret.append(self._merge_recurse(None, new[i], path.new_subpath(offset + i)))
         return ret
 
-    def _merge_dict(self, cur, new, path):
+    def _merge_dict(self, cur, new, path, initmerge):
+        """Merge a dictionary with another dictionary.
+
+        Iterate over keys in new. If this is not an initialization merge and
+        the key begins with PARAMETER_DICT_KEY_OVERRIDE_PREFIX, override the
+        value of the key in cur. Otherwise deeply merge the contents of the key
+        in cur with the contents of the key in _merge_recurse over the item.
+
+        Args:
+            cur (dict): Current dictionary
+            new (dict): Dictionary to be merged
+            path (string): Merging path from recursion
+            initmerge (bool): True if called as part of entity init
+
+        Returns:
+            dict: a merged dictionary
+
+        """
+
         if isinstance(cur, dict):
             ret = cur
         else:
@@ -134,19 +153,42 @@
             ret.update(new)
             return ret
 
+        ovrprfx = Parameters.DICT_KEY_OVERRIDE_PREFIX
+
         for key, newvalue in new.iteritems():
-            ret[key] = self._merge_recurse(ret.get(key), newvalue,
-                                           path.new_subpath(key))
+            if key.startswith(ovrprfx) and not initmerge:
+                ret[key.lstrip(ovrprfx)] = newvalue
+            else:
+                ret[key] = self._merge_recurse(ret.get(key), newvalue,
+                                            path.new_subpath(key), initmerge)
         return ret
 
-    def _merge_recurse(self, cur, new, path=None):
+    def _merge_recurse(self, cur, new, path=None, initmerge=False):
+        """Merge a parameter with another parameter.
+
+        Iterate over keys in new. Call _merge_dict, _extend_list, or
+        _update_scalar depending on type. Pass along whether this is an
+        initialization merge.
+
+        Args:
+            cur (dict): Current dictionary
+            new (dict): Dictionary to be merged
+            path (string): Merging path from recursion
+            initmerge (bool): True if called as part of entity init, defaults
+                to False
+
+        Returns:
+            dict: a merged dictionary
+
+        """
+
         if path is None:
             path = DictPath(self.delimiter)
 
         if isinstance(new, dict):
             if cur is None:
                 cur = {}
-            return self._merge_dict(cur, new, path)
+            return self._merge_dict(cur, new, path, initmerge)
 
         elif isinstance(new, list):
             if cur is None:
@@ -156,13 +198,27 @@
         else:
             return self._update_scalar(cur, new, path)
 
-    def merge(self, other):
+    def merge(self, other, initmerge=False):
+        """Merge function (public edition).
+
+        Call _merge_recurse on self with either another Parameter object or a
+        dict (for initialization). Set initmerge if it's a dict.
+
+        Args:
+            other (dict or Parameter): Thing to merge with self._base
+
+        Returns:
+            None: Nothing
+
+        """
+
         if isinstance(other, dict):
-            self._base = self._merge_recurse(self._base, other, None)
+            self._base = self._merge_recurse(self._base, other,
+                                             None, initmerge)
 
         elif isinstance(other, self.__class__):
             self._base = self._merge_recurse(self._base, other._base,
-                                             None)
+                                             None, initmerge)
 
         else:
             raise TypeError('Cannot merge %s objects into %s' % (type(other),
diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py
index f58056d..5100639 100644
--- a/reclass/datatypes/tests/test_parameters.py
+++ b/reclass/datatypes/tests/test_parameters.py
@@ -188,6 +188,18 @@
         goal.update(mergee)
         self.assertDictEqual(p.as_dict(), dict(dict=goal))
 
+    def test_merge_dicts_override(self):
+        """Validate that tilde merge overrides function properly."""
+        mergee = {'~one': {'a': 'alpha'},
+                  '~two': ['gamma']}
+        base = {'one': {'b': 'beta'},
+                'two': ['delta']}
+        goal = {'one': {'a': 'alpha'},
+                'two': ['gamma']}
+        p = Parameters(dict(dict=base))
+        p.merge(Parameters(dict(dict=mergee)))
+        self.assertDictEqual(p.as_dict(), dict(dict=goal))
+
     def test_merge_dict_into_scalar(self):
         p = Parameters(dict(base='foo'))
         with self.assertRaises(TypeError):
diff --git a/reclass/defaults.py b/reclass/defaults.py
index d066290..fb04c83 100644
--- a/reclass/defaults.py
+++ b/reclass/defaults.py
@@ -26,3 +26,4 @@
 
 PARAMETER_INTERPOLATION_SENTINELS = ('${', '}')
 PARAMETER_INTERPOLATION_DELIMITER = ':'
+PARAMETER_DICT_KEY_OVERRIDE_PREFIX = '~'