Merge pull request #47 from salt-formulas/develop

Release 1.5.3
diff --git a/.gitignore b/.gitignore
index c5e9682..162c415 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
 *.py[co]
 .*.sw?
+.DS_Store
 /reclass-config.yml
 /reclass.egg-info
 /build
 /dist
 /.coverage
+.kitchen
diff --git a/.kitchen-verify.sh b/.kitchen-verify.sh
new file mode 100755
index 0000000..fb3adde
--- /dev/null
+++ b/.kitchen-verify.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+#set -x
+
+# setup
+source /*.env
+INVENTORY_BASE_URI=/tmp/kitchen/test/model/$MODEL
+RECLASS=/tmp/kitchen
+
+# prereq
+python -m ensurepip --default-pip
+pip install pipenv
+
+# env
+cd $RECLASS
+pipenv --venv || pipenv install --python ${PYVER}
+test -e /etc/reclsss || mkdir /etc/reclass
+cp -avf $INVENTORY_BASE_URI/reclass-config* /etc/reclass
+
+# verify
+for n in $(ls $INVENTORY_BASE_URI/nodes/*|sort); do
+  pipenv run python${PYVER} ./reclass.py --inventory-base-uri=$INVENTORY_BASE_URI --nodeinfo $(basename $n .yml)
+done
diff --git a/.kitchen.yml b/.kitchen.yml
new file mode 100644
index 0000000..45be629
--- /dev/null
+++ b/.kitchen.yml
@@ -0,0 +1,41 @@
+---
+driver:
+  name: docker
+  priviledged: false
+  use_sudo: false
+  volume:
+    - <%= ENV['PWD'] %>:/tmp/kitchen
+
+
+provisioner:
+  name: shell
+  script: .kitchen-verify.sh
+
+
+verifier:
+  name: inspec
+
+<%- pyver = ENV['PYTHON_VERSION'] || '2.7' %>
+
+platforms:
+  <% `find test/model -maxdepth 1 -mindepth 1 -type d |sort -u`.split().each do |model| %>
+  <% model=model.split('/')[2] %>
+  - name: <%= model %>
+    driver_config:
+      image: python:<%= pyver %>
+      platform: ubuntu
+      hostname: reclass
+      provision_command:
+        #FIXME, setup reclass env (prereq, configs, upload models)
+        #- apt-get install -y rsync
+        - echo "
+            export LC_ALL=C.UTF-8;\n
+            export LANG=C.UTF-8;\n
+            export PYVER=<%= pyver %>;\n
+            export MODEL=<%= model %>;\n
+          " > /kitchen.env
+  <% end %>
+
+suites:
+  - name: model
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..2a41776
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,116 @@
+sudo: required
+language: python
+dist: trusty
+cache: pip
+python:
+- '2.7'
+- '3.6'
+service:
+- docker
+
+#apt:
+  #update: true
+
+#stages:
+#- name: test
+#- name: coverage
+#- name: models
+#- name: build
+#  if: fork = false
+#- name: publish
+#  if: tag =~ ^v.* and fork = false and branch = 'master'
+
+env:
+  global:
+    - PACKAGENAME="reclass"
+
+install: &pyinst
+- pip install pyparsing
+- pip install PyYAML
+# To test example models with kitchen:
+- |
+  test -e Gemfile || cat <<EOF > Gemfile
+  source 'https://rubygems.org'
+  gem 'rake'
+  gem 'test-kitchen'
+  gem 'kitchen-docker'
+  gem 'kitchen-inspec'
+  gem 'inspec'
+- bundle install
+
+script:
+- python setup.py install
+- find . reclass -name 'test_*.py' | sort | xargs -n1 -i% bash -c "echo %; python %"
+# To test example models with kitchen:
+- export PYTHON_VERSION=$TRAVIS_PYTHON_VERSION
+- kitchen list
+- if [ "$PYTHON_VERSION" = "2.7" ]; then kitchen test; fi
+
+# NOTE: travis stage builds, below saved for future reference
+#jobs:
+#  include:
+#  - stage: test
+#    script: &unittest
+#    - python setup.py install
+#    - find . reclass -name 'test_*.py' | sort | xargs -n1 -i% bash -c "echo %; python %"
+#
+#  - stage: coverage
+#    install: *pyinst
+#    script:
+#    - python3 -m pytest --cov=. --cov-report=term-missing:skip-covered
+#    - coverage xml
+#    #- coveralls
+#    #- |
+#        #[ ! -z "${CODACY_PROJECT_TOKEN}" ] && python-codacy-coverage -r coverage.xml || echo "Codacy coverage NOT exported"
+#
+#  - stage: lint
+#    script:
+#    - python3 -m flake8
+#
+#  - stage: models
+#    install: &kitchen
+#    - pip install PyYAML
+#    - pip install virtualenv
+#    - |
+#      test -e Gemfile || cat <<EOF > Gemfile
+#      source 'https://rubygems.org'
+#      gem 'rake'
+#      gem 'test-kitchen'
+#      gem 'kitchen-docker'
+#      gem 'kitchen-inspec'
+#      gem 'inspec'
+#    - bundle install
+#    script:
+#    - export PYTHON_VERSION=$TRAVIS_PYTHON_VERSION
+#    - kitchen list
+#    #FIXME- kitchen test
+#
+#  - stage: build
+#    install: *pyinst
+#    script: []
+#
+#  - stage: publish
+#    install:
+#    - "/bin/true"
+#    script:
+#    - "/bin/true"
+#    deploy:
+#      provider: pypi
+#      user: epcim
+#      password:
+#        secure: TBD
+#      on:
+#        tags: true
+#        repo: salt-formulas/reclass
+#        branch: master
+#        #FIXME, $TRAVIS_PYTHON_VERSION == '2.7'
+
+notifications:
+  webhooks:
+    on_success: change  # options: [always|never|change] default: always
+    on_failure: never
+    on_start: never
+    on_cancel: never
+    on_error: never
+  email: true
+
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..525e7cc
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,17 @@
+[[source]]
+url = "https://pypi.python.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[dev-packages]
+
+[packages]
+pyparsing = "*"
+PyYAML = "*"
+six = "*"
+pyyaml = "*"
+# FIXME, issues with compile phase
+#"pygit2" = "*"
+
+[requires]
+python_version = "2.7"
diff --git a/README-extentions.rst b/README-extentions.rst
index 97d78af..61364a4 100644
--- a/README-extentions.rst
+++ b/README-extentions.rst
@@ -208,6 +208,82 @@
   group_errors: True
 
 
+Use references in class names
+-----------------------------
+
+Allows to use references in the class names.
+
+References pointed to in class names cannot themselves reference another key, they should be simple strings.
+
+To avoid pitfalls do not over-engineer your class references. They should be used only for core conditions and only for them.
+A short example: `- system.wrodpress.db.${_class:database_backend}`.
+
+Best practices:
+- use references in class names always load your global class specification prior the reference is used.
+- structure your class references under parameters under one key (for example `_class`).
+- use class references as a kind of "context" or "global" available options you always know what they are set.
+
+Class referencing for existing reclass users. Frequently when constructing your models you had to load or not load some
+classes based on your setup. In most cases this lead to fork of a model or introducing kind of template generator (like cookiecutter) to
+create a model based on the base "context" or "global" variables. Class referencing is a simple way how to avoid
+"pre-processors" like this and if/else conditions around class section.
+
+
+Assuming following class setup:
+
+* node is loading `third.yml` class only
+
+
+Classes:
+
+.. code-block:: yaml
+  #/etc/reclass/classes/global.yml
+  parameters:
+    _class:
+      env:
+        override: 'env.dev'
+    lab:
+      name: default
+
+  #/etc/reclass/classes/lab/env/dev.yml
+  parameters:
+    lab:
+      name: dev
+
+  #/etc/reclass/classes/second.yml
+  classes:
+    - global
+    - lab.${_class:env:override}
+
+  #/etc/reclass/classes/third.yml
+  classes:
+    - global
+    - second
+
+
+Reclass --nodeinfo then returns:
+
+.. code-block:: yaml
+
+  ...
+  ...
+  applications: []
+  environment: base
+  exports: {}
+  classes:
+  - global
+  - lab.${_class:env:override}
+  - second
+  parameters:
+    _class:
+      env:
+        override: env.dev
+    lab:
+      name: dev
+    ...
+    ...
+
+
 Inventory Queries
 -----------------
 
diff --git a/reclass/__init__.py b/reclass/__init__.py
index adb421e..a79c8e1 100644
--- a/reclass/__init__.py
+++ b/reclass/__init__.py
@@ -11,6 +11,7 @@
 from reclass.storage.loader import StorageBackendLoader
 from reclass.storage.memcache_proxy import MemcacheProxy
 
+
 def get_storage(storage_type, nodes_uri, classes_uri, **kwargs):
     storage_class = StorageBackendLoader(storage_type).load()
     return MemcacheProxy(storage_class(nodes_uri, classes_uri, **kwargs))
diff --git a/reclass/adapters/ansible.py b/reclass/adapters/ansible.py
index 1887245..f6e9af3 100755
--- a/reclass/adapters/ansible.py
+++ b/reclass/adapters/ansible.py
@@ -16,6 +16,8 @@
 
 import os, sys, posix, optparse
 
+from six import iteritems
+
 from reclass import get_storage, output
 from reclass.core import Core
 from reclass.errors import ReclassException
@@ -81,7 +83,7 @@
             apps = data['applications']
             if options.applications_postfix:
                 postfix = options.applications_postfix
-                groups.update([(k + postfix, v) for k,v in apps.iteritems()])
+                groups.update([(k + postfix, v) for (k, v) in iteritems(apps)])
             else:
                 groups.update(apps)
 
diff --git a/reclass/adapters/salt.py b/reclass/adapters/salt.py
index 54adf5a..31179ff 100755
--- a/reclass/adapters/salt.py
+++ b/reclass/adapters/salt.py
@@ -9,6 +9,8 @@
 
 import os, sys, posix
 
+from six import iteritems
+
 from reclass import get_storage, output, get_path_mangler
 from reclass.core import Core
 from reclass.errors import ReclassException
@@ -68,7 +70,7 @@
     else:
         data = reclass.inventory()
         nodes = {}
-        for node_id, node_data in data['nodes'].iteritems():
+        for (node_id, node_data) in iteritems(data['nodes']):
             env = node_data['environment']
             if env not in nodes:
                 nodes[env] = {}
diff --git a/reclass/cli.py b/reclass/cli.py
index d1b22b8..f0b6069 100644
--- a/reclass/cli.py
+++ b/reclass/cli.py
@@ -38,9 +38,9 @@
         else:
             data = reclass.inventory()
 
-        print output(data, options.output, options.pretty_print, options.no_refs)
+        print(output(data, options.output, options.pretty_print, options.no_refs))
 
-    except ReclassException, e:
+    except ReclassException as e:
         e.exit_with_message(sys.stderr)
 
     sys.exit(posix.EX_OK)
diff --git a/reclass/core.py b/reclass/core.py
index 9da0ddb..1a08db8 100644
--- a/reclass/core.py
+++ b/reclass/core.py
@@ -15,13 +15,24 @@
 import string
 import sys
 import yaml
+
+from six import iteritems
+
 from reclass.settings import Settings
 from reclass.output.yaml_outputter import ExplicitDumper
 from reclass.datatypes import Entity, Classes, Parameters, Exports
-from reclass.errors import MappingFormatError, ClassNotFound, InvQueryClassNotFound, InvQueryError, InterpolationError
+from reclass.errors import MappingFormatError, ClassNameResolveError, ClassNotFound, InvQueryClassNameResolveError, InvQueryClassNotFound, InvQueryError, InterpolationError, ResolveError
+from reclass.values.parser import Parser
+
+try:
+    basestring
+except NameError:
+    basestring = str
 
 class Core(object):
 
+    _parser = Parser()
+
     def __init__(self, storage, class_mappings, settings, input_data=None):
         self._storage = storage
         self._class_mappings = class_mappings
@@ -54,7 +65,7 @@
             regexp = True
         try:
             key = lexer.get_token()
-        except ValueError, e:
+        except ValueError as e:
             raise MappingFormatError('Error in mapping "{0}": missing closing '
                                      'quote (or slash)'.format(instr))
         if regexp:
@@ -88,7 +99,7 @@
         p = Parameters(self._input_data, self._settings)
         return Entity(self._settings, parameters=p, name='input data')
 
-    def _recurse_entity(self, entity, merge_base=None, seen=None, nodename=None, environment=None):
+    def _recurse_entity(self, entity, merge_base=None, context=None, seen=None, nodename=None, environment=None):
         if seen is None:
             seen = {}
 
@@ -98,7 +109,19 @@
         if merge_base is None:
             merge_base = Entity(self._settings, name='empty (@{0})'.format(nodename))
 
+        if context is None:
+            context = Entity(self._settings, name='empty (@{0})'.format(nodename))
+
         for klass in entity.classes.as_list():
+            if klass.count('$') > 0:
+                try:
+                    klass = str(self._parser.parse(klass, self._settings).render(merge_base.parameters.as_dict(), {}))
+                except ResolveError as e:
+                    try:
+                        klass = str(self._parser.parse(klass, self._settings).render(context.parameters.as_dict(), {}))
+                    except ResolveError as e:
+                        raise ClassNameResolveError(klass, nodename, entity.uri)
+
             if klass not in seen:
                 try:
                     class_entity = self._storage.get_class(klass, environment, self._settings)
@@ -113,7 +136,7 @@
                     e.uri = entity.uri
                     raise
 
-                descent = self._recurse_entity(class_entity, seen=seen,
+                descent = self._recurse_entity(class_entity, context=merge_base, seen=seen,
                                                nodename=nodename, environment=environment)
                 # on every iteration, we merge the result of the recursive
                 # descent into what we have so far…
@@ -128,7 +151,7 @@
 
     def _get_automatic_parameters(self, nodename, environment):
         if self._settings.automatic_parameters:
-            return Parameters({ '_reclass_': { 'name': { 'full': nodename, 'short': string.split(nodename, '.')[0] },
+            return Parameters({ '_reclass_': { 'name': { 'full': nodename, 'short': str.split(nodename, '.')[0] },
                                                'environment': environment } }, self._settings, '__auto__')
         else:
             return Parameters({}, self._settings, '')
@@ -151,6 +174,8 @@
                     node = self._node_entity(nodename)
                 except ClassNotFound as e:
                     raise InvQueryClassNotFound(e)
+                except ClassNameResolveError as e:
+                    raise InvQueryClassNameResolveError(e)
                 if queries is None:
                     try:
                         node.interpolate_exports()
@@ -178,8 +203,8 @@
         seen = {}
         merge_base = self._recurse_entity(base_entity, seen=seen, nodename=nodename,
                                           environment=node_entity.environment)
-        return self._recurse_entity(node_entity, merge_base, seen=seen, nodename=nodename,
-                                    environment=node_entity.environment)
+        return self._recurse_entity(node_entity, merge_base=merge_base, context=merge_base, seen=seen,
+                                    nodename=nodename, environment=node_entity.environment)
 
     def _nodeinfo(self, nodename, inventory):
         try:
@@ -220,7 +245,7 @@
         nodes = {}
         applications = {}
         classes = {}
-        for f, nodeinfo in entities.iteritems():
+        for (f, nodeinfo) in iteritems(entities):
             d = nodes[f] = self._nodeinfo_as_dict(f, nodeinfo)
             for a in d['applications']:
                 if a in applications:
diff --git a/reclass/datatypes/classes.py b/reclass/datatypes/classes.py
index b8793a2..090ed70 100644
--- a/reclass/datatypes/classes.py
+++ b/reclass/datatypes/classes.py
@@ -7,12 +7,13 @@
 # Released under the terms of the Artistic Licence 2.0
 #
 
-import types
+import six
 import os
 from reclass.errors import InvalidClassnameError
 
 INVALID_CHARACTERS_FOR_CLASSNAMES = ' ' + os.sep
 
+
 class Classes(object):
     '''
     A very limited ordered set of strings with O(n) uniqueness constraints. It
@@ -51,7 +52,7 @@
             self.append_if_new(i)
 
     def _assert_is_string(self, item):
-        if not isinstance(item, types.StringTypes):
+        if not isinstance(item, six.string_types):
             raise TypeError('%s instances can only contain strings, '\
                             'not %s' % (self.__class__.__name__, type(item)))
 
diff --git a/reclass/datatypes/exports.py b/reclass/datatypes/exports.py
index 62ea03f..971befa 100644
--- a/reclass/datatypes/exports.py
+++ b/reclass/datatypes/exports.py
@@ -6,6 +6,8 @@
 
 import copy
 
+from six import iteritems, next
+
 from .parameters import Parameters
 from reclass.errors import ResolveError
 from reclass.values.value import Value
@@ -25,12 +27,12 @@
         self._unrendered.pop(key, None)
 
     def overwrite(self, other):
-        overdict = {'~' + key: value for key, value in other.iteritems()}
+        overdict = {'~' + key: value for (key, value) in iteritems(other)}
         self.merge(overdict)
 
     def interpolate_from_external(self, external):
         while len(self._unrendered) > 0:
-            path, v = self._unrendered.iteritems().next()
+            path, v = next(iteritems(self._unrendered))
             value = path.get_value(self._base)
             if isinstance(value, (Value, ValueList)):
                 external._interpolate_references(path, value, None)
@@ -51,7 +53,7 @@
         required = self._get_required_paths(mainpath)
         while len(required) > 0:
             while len(required) > 0:
-                path, v = required.iteritems().next()
+                path, v = next(iteritems(required))
                 value = path.get_value(self._base)
                 if isinstance(value, (Value, ValueList)):
                     try:
diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py
index ac15925..6ad0d27 100644
--- a/reclass/datatypes/parameters.py
+++ b/reclass/datatypes/parameters.py
@@ -10,12 +10,16 @@
 import copy
 import sys
 import types
+
+from six import iteritems, next
+
 from collections import namedtuple
 from reclass.utils.dictpath import DictPath
 from reclass.values.value import Value
 from reclass.values.valuelist import ValueList
 from reclass.errors import InfiniteRecursionError, ResolveError, ResolveErrorList, InterpolationError, ParseError, BadReferencesError
 
+
 class Parameters(object):
     '''
     A class to hold nested dictionaries with the following specialities:
@@ -40,7 +44,7 @@
     functionality and does not try to be a really mapping object.
     '''
 
-    def __init__(self, mapping, settings, uri, merge_initialise = True):
+    def __init__(self, mapping, settings, uri, parse_strings=True):
         self._settings = settings
         self._base = {}
         self._uri = uri
@@ -50,14 +54,12 @@
         self._resolve_errors = ResolveErrorList()
         self._needs_all_envs = False
         self._keep_overrides = False
+        self._parse_strings = parse_strings
         if mapping is not None:
-            if merge_initialise:
-                # we initialise by merging
-                self._keep_overrides = True
-                self.merge(mapping)
-                self._keep_overrides = False
-            else:
-                self._base = copy.deepcopy(mapping)
+            # we initialise by merging
+            self._keep_overrides = True
+            self.merge(mapping)
+            self._keep_overrides = False
 
     #delimiter = property(lambda self: self._delimiter)
 
@@ -99,7 +101,7 @@
             return value
         else:
             try:
-                return Value(value, self._settings, self._uri)
+                return Value(value, self._settings, self._uri, parse_string=self._parse_strings)
             except InterpolationError as e:
                 e.context = str(path)
                 raise
@@ -108,7 +110,7 @@
         return [ self._wrap_value(v, path.new_subpath(k)) for (k, v) in enumerate(source) ]
 
     def _wrap_dict(self, source, path):
-        return { k: self._wrap_value(v, path.new_subpath(k)) for k, v in source.iteritems() }
+        return { k: self._wrap_value(v, path.new_subpath(k)) for (k, v) in iteritems(source) }
 
     def _update_value(self, cur, new):
         if isinstance(cur, Value):
@@ -123,7 +125,7 @@
         elif isinstance(new, ValueList):
             values.extend(new)
         else:
-            values.append(Value(new, self._settings, self._uri))
+            values.append(Value(new, self._settings, self._uri, parse_string=self._parse_strings))
 
         return values
 
@@ -147,8 +149,11 @@
         """
 
         ret = cur
-        for key, newvalue in new.iteritems():
+        for (key, newvalue) in iteritems(new):
             if key.startswith(self._settings.dict_key_override_prefix) and not self._keep_overrides:
+                if not isinstance(newvalue, Value):
+                    newvalue = Value(newvalue, self._settings, self._uri, parse_string=self._parse_strings)
+                newvalue.overwrite = True
                 ret[key.lstrip(self._settings.dict_key_override_prefix)] = newvalue
             else:
                 ret[key] = self._merge_recurse(ret.get(key), newvalue, path.new_subpath(key))
@@ -173,7 +178,6 @@
 
         """
 
-
         if cur is None:
             return new
         elif isinstance(new, dict) and isinstance(cur, dict):
@@ -181,7 +185,7 @@
         else:
             return self._update_value(cur, new)
 
-    def merge(self, other, wrap=True):
+    def merge(self, other):
         """Merge function (public edition).
 
         Call _merge_recurse on self with either another Parameter object or a
@@ -197,15 +201,9 @@
 
         self._unrendered = None
         if isinstance(other, dict):
-            if wrap:
-                wrapped = self._wrap_dict(other, DictPath(self._settings.delimiter))
-            else:
-                wrapped = copy.deepcopy(other)
+            wrapped = self._wrap_dict(other, DictPath(self._settings.delimiter))
         elif isinstance(other, self.__class__):
-            if wrap:
-                wrapped = self._wrap_dict(other._base, DictPath(self._settings.delimiter))
-            else:
-                wrapped = copy.deepcopy(other._base)
+            wrapped = self._wrap_dict(other._base, DictPath(self._settings.delimiter))
         else:
             raise TypeError('Cannot merge %s objects into %s' % (type(other),
                             self.__class__.__name__))
@@ -243,7 +241,7 @@
                     container[key] = value.render(None, None)
 
     def _render_simple_dict(self, dictionary, path):
-        for key, value in dictionary.iteritems():
+        for (key, value) in iteritems(dictionary):
             self._render_simple_container(dictionary, key, value, path)
 
     def _render_simple_list(self, item_list, path):
@@ -256,7 +254,7 @@
             # we could use a view here, but this is simple enough:
             # _interpolate_inner removes references from the refs hash after
             # processing them, so we cannot just iterate the dict
-            path, v = self._unrendered.iteritems().next()
+            path, v = next(iteritems(self._unrendered))
             self._interpolate_inner(path, inventory)
         if self._resolve_errors.have_errors():
             raise self._resolve_errors
diff --git a/reclass/datatypes/tests/test_applications.py b/reclass/datatypes/tests/test_applications.py
index 307a430..6ae07cc 100644
--- a/reclass/datatypes/tests/test_applications.py
+++ b/reclass/datatypes/tests/test_applications.py
@@ -63,7 +63,7 @@
         l = ['a', '~b', 'a', '~d']
         a = Applications(l)
         is_negation = lambda x: x.startswith(a.negation_prefix)
-        GOAL = filter(lambda x: not is_negation(x), set(l)) + filter(is_negation, l)
+        GOAL = list(filter(lambda x: not is_negation(x), set(l))) + list(filter(is_negation, l))
         self.assertEqual('%r' % a, "%s(%r, '~')" % (a.__class__.__name__, GOAL))
 
 if __name__ == '__main__':
diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py
index 577bdc4..9b788dc 100644
--- a/reclass/datatypes/tests/test_parameters.py
+++ b/reclass/datatypes/tests/test_parameters.py
@@ -9,6 +9,8 @@
 
 import copy
 
+from six import iteritems
+
 from reclass.settings import Settings
 from reclass.datatypes import Parameters
 from reclass.errors import InfiniteRecursionError, InterpolationError, ResolveError, ResolveErrorList
@@ -132,7 +134,7 @@
         p2, b2 = self._construct_mocked_params(mergee)
         p1.merge(p2)
         p1.initialise_interpolation()
-        for key, value in mergee.iteritems():
+        for (key, value) in iteritems(mergee):
             # check that each key, value in mergee resulted in a get call and
             # a __setitem__ call against b1 (the merge target)
             self.assertIn(mock.call(key), b1.get.call_args_list)
@@ -202,6 +204,15 @@
         p1.initialise_interpolation()
         self.assertEqual(p1.as_dict()['key'], None)
 
+    def test_merge_none_over_list_negative(self):
+        l = ['foo', 1, 2]
+        settings = Settings({'allow_none_override': False})
+        p1 = Parameters(dict(key=l[:2]), settings, '')
+        p2 = Parameters(dict(key=None), settings, '')
+        with self.assertRaises(TypeError):
+            p1.merge(p2)
+            p1.initialise_interpolation()
+
     def test_merge_none_over_dict(self):
         settings = Settings({'allow_none_override': True})
         p1 = Parameters(dict(key=SIMPLE), settings, '')
@@ -210,6 +221,14 @@
         p1.initialise_interpolation()
         self.assertEqual(p1.as_dict()['key'], None)
 
+    def test_merge_none_over_dict_negative(self):
+        settings = Settings({'allow_none_override': False})
+        p1 = Parameters(dict(key=SIMPLE), settings, '')
+        p2 = Parameters(dict(key=None), settings, '')
+        with self.assertRaises(TypeError):
+            p1.merge(p2)
+            p1.initialise_interpolation()
+
     # def test_merge_bare_dict_over_dict(self):
         # settings = Settings({'allow_bare_override': True})
         # p1 = Parameters(dict(key=SIMPLE), settings, '')
@@ -624,5 +643,38 @@
         p1.interpolate()
         self.assertEqual(p1.as_dict(), r)
 
+    def test_complex_overwrites_1(self):
+        # find a better name for this test
+        p1 = Parameters({ 'test': { 'dict': { 'a': '${values:one}', 'b': '${values:two}' } },
+                          'values': { 'one': 1, 'two': 2, 'three': { 'x': 'X', 'y': 'Y' } } }, SETTINGS, '')
+        p2 = Parameters({ 'test': { 'dict': { 'c': '${values:two}' } } }, SETTINGS, '')
+        p3 = Parameters({ 'test': { 'dict': { '~b': '${values:three}' } } }, SETTINGS, '')
+        r = {'test': {'dict': {'a': 1, 'b': {'x': 'X', 'y': 'Y'}, 'c': 2}}, 'values': {'one': 1, 'three': {'x': 'X', 'y': 'Y'}, 'two': 2} }
+        p2.merge(p3)
+        p1.merge(p2)
+        p1.interpolate()
+        self.assertEqual(p1.as_dict(), r)
+
+    def test_escaped_string_overwrites(self):
+        p1 = Parameters({ 'test': '\${not_a_ref}' }, SETTINGS, '')
+        p2 = Parameters({ 'test': '\${also_not_a_ref}' }, SETTINGS, '')
+        r = { 'test': '${also_not_a_ref}' }
+        p1.merge(p2)
+        p1.interpolate()
+        self.assertEqual(p1.as_dict(), r)
+
+    def test_escaped_string_in_ref_dict_overwrite(self):
+        p1 = Parameters({'a': { 'one': '\${not_a_ref}' }, 'b': { 'two': '\${also_not_a_ref}' }}, SETTINGS, '')
+        p2 = Parameters({'c': '${a}'}, SETTINGS, '')
+        p3 = Parameters({'c': '${b}'}, SETTINGS, '')
+        p4 = Parameters({'c': { 'one': '\${again_not_a_ref}' } }, SETTINGS, '')
+        r = {'a': {'one': '${not_a_ref}'}, 'b': {'two': '${also_not_a_ref}'}, 'c': {'one': '${again_not_a_ref}', 'two': '${also_not_a_ref}'}}
+        p1.merge(p2)
+        p1.merge(p3)
+        p1.merge(p4)
+        p1.interpolate()
+        self.assertEqual(p1.as_dict(), r)
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/reclass/defaults.py b/reclass/defaults.py
index 980bb92..408307d 100644
--- a/reclass/defaults.py
+++ b/reclass/defaults.py
@@ -7,7 +7,7 @@
 # Released under the terms of the Artistic Licence 2.0
 #
 import os, sys
-from version import RECLASS_NAME
+from .version import RECLASS_NAME
 
 # defaults for the command-line options
 OPT_STORAGE_TYPE = 'yaml_fs'
@@ -29,7 +29,7 @@
 OPT_ALLOW_SCALAR_OVER_LIST = False
 OPT_ALLOW_LIST_OVER_SCALAR = False
 OPT_ALLOW_DICT_OVER_SCALAR = False
-OPT_ALLOW_NONE_OVERRIDE = True
+OPT_ALLOW_NONE_OVERRIDE = False
 
 OPT_INVENTORY_IGNORE_FAILED_NODE = False
 OPT_INVENTORY_IGNORE_FAILED_RENDER = False
diff --git a/reclass/errors.py b/reclass/errors.py
index a96c47b..349e242 100644
--- a/reclass/errors.py
+++ b/reclass/errors.py
@@ -158,6 +158,17 @@
         return msg
 
 
+class ClassNameResolveError(InterpolationError):
+    def __init__(self, classname, nodename, uri):
+        super(ClassNameResolveError, self).__init__(msg=None, uri=uri, nodename=nodename)
+        self.name = classname
+
+    def _get_error_message(self):
+        msg = [ 'In {0}'.format(self.uri),
+                'Class name {0} not resolvable'.format(self.name) ]
+        return msg
+
+
 class InvQueryClassNotFound(InterpolationError):
 
     def __init__(self, classNotFoundError, nodename=''):
@@ -172,6 +183,19 @@
         return msg
 
 
+class InvQueryClassNameResolveError(InterpolationError):
+    def __init__(self, classNameResolveError, nodename=''):
+        super(InvQueryClassNameResolveError, self).__init__(msg=None, nodename=nodename)
+        self.classNameResolveError = classNameResolveError
+        self._traceback = self.classNameResolveError._traceback
+
+    def _get_error_message(self):
+        msg = [ 'Inventory Queries:',
+                '-> {0}'.format(self.classNameResolveError.nodename) ]
+        msg.append(self.classNameResolveError._get_error_message())
+        return msg
+
+
 class ResolveError(InterpolationError):
 
     def __init__(self, reference, uri=None, context=None):
diff --git a/reclass/output/__init__.py b/reclass/output/__init__.py
index 58cd101..42fdb0b 100644
--- a/reclass/output/__init__.py
+++ b/reclass/output/__init__.py
@@ -6,13 +6,15 @@
 # Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
 # Released under the terms of the Artistic Licence 2.0
 #
+
+
 class OutputterBase(object):
 
     def __init__(self):
         pass
 
     def dump(self, data, pretty_print=False):
-        raise NotImplementedError, "dump() method not yet implemented"
+        raise NotImplementedError('dump() method not implemented.')
 
 
 class OutputLoader(object):
@@ -27,6 +29,5 @@
     def load(self, attr='Outputter'):
         klass = getattr(self._module, attr, None)
         if klass is None:
-            raise AttributeError, \
-                'Outputter class {0} does not export "{1}"'.format(self._name, klass)
+            raise AttributeError('Outputter class {0} does not export "{1}"'.format(self._name, klass))
         return klass
diff --git a/reclass/output/yaml_outputter.py b/reclass/output/yaml_outputter.py
index 9a0d098..ef62979 100644
--- a/reclass/output/yaml_outputter.py
+++ b/reclass/output/yaml_outputter.py
@@ -9,15 +9,17 @@
 from reclass.output import OutputterBase
 import yaml
 
+_SafeDumper = yaml.CSafeDumper if yaml.__with_libyaml__ else yaml.SafeDumper
+
 class Outputter(OutputterBase):
 
     def dump(self, data, pretty_print=False, no_refs=False):
         if (no_refs):
             return yaml.dump(data, default_flow_style=not pretty_print, Dumper=ExplicitDumper)
         else:
-            return yaml.dump(data, default_flow_style=not pretty_print)
+            return yaml.dump(data, default_flow_style=not pretty_print, Dumper=_SafeDumper)
 
-class ExplicitDumper(yaml.SafeDumper):
+class ExplicitDumper(_SafeDumper):
     """
     A dumper that will never emit aliases.
     """
diff --git a/reclass/settings.py b/reclass/settings.py
index 44c58d8..e3fc26e 100644
--- a/reclass/settings.py
+++ b/reclass/settings.py
@@ -2,6 +2,11 @@
 import reclass.values.parser_funcs
 from reclass.defaults import *
 
+try:
+    basestring
+except NameError:
+    basestring = str
+
 class Settings(object):
 
     def __init__(self, options={}):
diff --git a/reclass/storage/loader.py b/reclass/storage/loader.py
index 77fdecb..10ca74c 100644
--- a/reclass/storage/loader.py
+++ b/reclass/storage/loader.py
@@ -6,13 +6,15 @@
 # Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
 # Released under the terms of the Artistic Licence 2.0
 #
+import importlib
+
 
 class StorageBackendLoader(object):
 
     def __init__(self, storage_name):
         self._name = 'reclass.storage.' + storage_name
         try:
-            self._module = __import__(self._name, globals(), locals(), self._name)
+            self._module = importlib.import_module(self._name)
         except ImportError:
             raise NotImplementedError
 
@@ -21,7 +23,6 @@
         if klass is None:
             raise AttributeError('Storage backend class {0} does not export '
                                  '"{1}"'.format(self._name, klassname))
-
         return klass
 
     def path_mangler(self, name='path_mangler'):
diff --git a/reclass/storage/memcache_proxy.py b/reclass/storage/memcache_proxy.py
index 405ea8e..8c5e441 100644
--- a/reclass/storage/memcache_proxy.py
+++ b/reclass/storage/memcache_proxy.py
@@ -35,7 +35,7 @@
             return self._real_storage.get_node(name, settings)
         try:
             return self._nodes_cache[name]
-        except KeyError, e:
+        except KeyError as e:
             ret = self._real_storage.get_node(name, settings)
             self._nodes_cache[name] = ret
         return ret
@@ -45,7 +45,7 @@
             return self._real_storage.get_class(name, environment, settings)
         try:
             return self._classes_cache[environment][name]
-        except KeyError, e:
+        except KeyError as e:
             if environment not in self._classes_cache:
                 self._classes_cache[environment] = dict()
             ret = self._real_storage.get_class(name, environment, settings)
diff --git a/reclass/storage/mixed/__init__.py b/reclass/storage/mixed/__init__.py
index 4651e00..990c931 100644
--- a/reclass/storage/mixed/__init__.py
+++ b/reclass/storage/mixed/__init__.py
@@ -6,6 +6,8 @@
 import collections
 import copy
 
+from six import iteritems
+
 import reclass.errors
 from reclass import get_storage
 from reclass.storage import NodeStorageBase
@@ -32,7 +34,7 @@
         self._classes_storage = dict()
         if 'env_overrides' in classes_uri:
             for override in classes_uri['env_overrides']:
-                for env, options in override.iteritems():
+                for (env, options) in iteritems(override):
                         uri = copy.deepcopy(classes_uri)
                         uri.update(options)
                         uri = self._uri(uri)
diff --git a/reclass/storage/yaml_fs/__init__.py b/reclass/storage/yaml_fs/__init__.py
index b92cbfe..83f3666 100644
--- a/reclass/storage/yaml_fs/__init__.py
+++ b/reclass/storage/yaml_fs/__init__.py
@@ -93,7 +93,7 @@
             relpath = self._nodes[name]
             path = os.path.join(self.nodes_uri, relpath)
             name = os.path.splitext(relpath)[0]
-        except KeyError, e:
+        except KeyError as e:
             raise reclass.errors.NodeNotFound(self.name, name, self.nodes_uri)
         entity = YamlData.from_file(path).get_entity(name, settings)
         return entity
@@ -102,7 +102,7 @@
         vvv('GET CLASS {0}'.format(name))
         try:
             path = os.path.join(self.classes_uri, self._classes[name])
-        except KeyError, e:
+        except KeyError as e:
             raise reclass.errors.ClassNotFound(self.name, name, self.classes_uri)
         entity = YamlData.from_file(path).get_entity(name, settings)
         return entity
diff --git a/reclass/storage/yaml_fs/directory.py b/reclass/storage/yaml_fs/directory.py
index 03302b7..614e1c3 100644
--- a/reclass/storage/yaml_fs/directory.py
+++ b/reclass/storage/yaml_fs/directory.py
@@ -7,16 +7,16 @@
 # Released under the terms of the Artistic Licence 2.0
 #
 import os
-import sys
 from reclass.errors import NotFoundError
 
-SKIPDIRS = ( 'CVS', 'SCCS' )
+SKIPDIRS = ('CVS', 'SCCS')
 FILE_EXTENSION = '.yml'
 
 def vvv(msg):
     #print >>sys.stderr, msg
     pass
 
+
 class Directory(object):
 
     def __init__(self, path, fileclass=None):
@@ -39,7 +39,8 @@
     files = property(lambda self: self._files)
 
     def walk(self, register_fn=None):
-        if not callable(register_fn): register_fn = self._register_files
+        if not callable(register_fn):
+            register_fn = self._register_files
 
         def _error(exc):
             raise(exc)
diff --git a/reclass/storage/yaml_git/__init__.py b/reclass/storage/yaml_git/__init__.py
index f4cb287..86c1247 100644
--- a/reclass/storage/yaml_git/__init__.py
+++ b/reclass/storage/yaml_git/__init__.py
@@ -15,6 +15,8 @@
     warnings.simplefilter('ignore')
     import pygit2
 
+from six import iteritems
+
 import reclass.errors
 from reclass.storage import NodeStorageBase
 from reclass.storage.common import NameMangler
@@ -180,7 +182,7 @@
 
     def nodes(self, branch, subdir):
         ret = {}
-        for name, file in self.files[branch].iteritems():
+        for (name, file) in iteritems(self.files[branch]):
             if subdir is None or name.startswith(subdir):
                 node_name = os.path.splitext(file.name)[0]
                 if node_name in ret:
@@ -209,7 +211,7 @@
             self._classes_uri = []
             if 'env_overrides' in classes_uri:
                 for override in classes_uri['env_overrides']:
-                    for env, options in override.iteritems():
+                    for (env, options) in iteritems(override):
                         uri = GitURI(self._classes_default_uri)
                         uri.update({ 'branch': env })
                         uri.update(options)
diff --git a/reclass/storage/yamldata.py b/reclass/storage/yamldata.py
index 0dda2b7..b11312e 100644
--- a/reclass/storage/yamldata.py
+++ b/reclass/storage/yamldata.py
@@ -11,6 +11,8 @@
 import os
 from reclass.errors import NotFoundError
 
+_SafeLoader = yaml.CSafeLoader if yaml.__with_libyaml__ else yaml.SafeLoader
+
 class YamlData(object):
 
     @classmethod
@@ -22,18 +24,17 @@
         if not os.access(abs_path, os.R_OK):
             raise NotFoundError('Cannot open: %s' % abs_path)
         y = cls('yaml_fs://{0}'.format(abs_path))
-        fp = file(abs_path)
-        data = yaml.safe_load(fp)
-        if data is not None:
-            y._data = data
-        fp.close()
+        with open(abs_path) as fp:
+            data = yaml.load(fp, Loader=_SafeLoader)
+            if data is not None:
+                y._data = data
         return y
 
     @classmethod
     def from_string(cls, string, uri):
         ''' Initialise yaml data from a string '''
         y = cls(uri)
-        data = yaml.safe_load(string)
+        data = yaml.load(string, Loader=_SafeLoader)
         if data is not None:
             y._data = data
         return y
diff --git a/reclass/utils/dictpath.py b/reclass/utils/dictpath.py
index aec6722..dfb8b32 100644
--- a/reclass/utils/dictpath.py
+++ b/reclass/utils/dictpath.py
@@ -7,7 +7,8 @@
 # Released under the terms of the Artistic Licence 2.0
 #
 
-import types, re
+import six
+import re
 
 class DictPath(object):
     '''
@@ -61,7 +62,7 @@
         else:
             if isinstance(contents, list):
                 self._parts = contents
-            elif isinstance(contents, types.StringTypes):
+            elif isinstance(contents, six.string_types):
                 self._parts = self._split_string(contents)
             elif isinstance(contents, tuple):
                 self._parts = list(contents)
@@ -76,7 +77,7 @@
         return self._delim.join(str(i) for i in self._parts)
 
     def __eq__(self, other):
-        if isinstance(other, types.StringTypes):
+        if isinstance(other, six.string_types):
             other = DictPath(self._delim, other)
 
         return self._parts == other._parts \
diff --git a/reclass/values/compitem.py b/reclass/values/compitem.py
index 2134ea8..c6e8863 100644
--- a/reclass/values/compitem.py
+++ b/reclass/values/compitem.py
@@ -5,7 +5,7 @@
 #
 
 from reclass.settings import Settings
-from item import Item
+from .item import Item
 
 class CompItem(Item):
 
@@ -39,6 +39,21 @@
     def get_references(self):
         return self._refs
 
+    def merge_over(self, item):
+        if item.type == Item.SCALAR or item.type == Item.COMPOSITE:
+            return self
+        elif item.type == Item.LIST:
+            if self._settings.allow_scalar_over_list or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+                return self
+            else:
+                raise TypeError('allow scalar over list = False: cannot merge %s over %s' % (repr(self), repr(item)))
+        elif item.type == Item.DICTIONARY:
+            if self._settings.allow_scalar_over_dict or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+                return self
+            else:
+                raise TypeError('allow scalar over dict = False: cannot merge %s over %s' % (repr(self), repr(item)))
+        raise TypeError('Cannot merge %s over %s' % (repr(self), repr(item)))
+
     def render(self, context, inventory):
         # Preserve type if only one item
         if len(self._items) == 1:
diff --git a/reclass/values/dictitem.py b/reclass/values/dictitem.py
index d778fe2..555bd8f 100644
--- a/reclass/values/dictitem.py
+++ b/reclass/values/dictitem.py
@@ -5,7 +5,7 @@
 #
 
 from reclass.settings import Settings
-from item import Item
+from .item import Item
 
 class DictItem(Item):
 
diff --git a/reclass/values/invitem.py b/reclass/values/invitem.py
index 84ea39d..970321b 100644
--- a/reclass/values/invitem.py
+++ b/reclass/values/invitem.py
@@ -7,7 +7,9 @@
 import copy
 import pyparsing as pp
 
-from item import Item
+from six import iteritems
+
+from .item import Item
 from reclass.settings import Settings
 from reclass.utils.dictpath import DictPath
 from reclass.errors import ExpressionError, ParseError, ResolveError
@@ -301,7 +303,7 @@
 
     def _value_expression(self, inventory):
         results = {}
-        for node, items in inventory.iteritems():
+        for (node, items) in iteritems(inventory):
             if self._value_path.exists_in(items):
                 results[node] = copy.deepcopy(self._resolve(self._value_path, items))
         return results
@@ -311,14 +313,14 @@
             ExpressionError('Failed to render %s' % str(self), tbFlag=False)
 
         results = {}
-        for node, items in inventory.iteritems():
+        for (node, items) in iteritems(inventory):
             if self._question.value(context, items) and self._value_path.exists_in(items):
                 results[node] = copy.deepcopy(self._resolve(self._value_path, items))
         return results
 
     def _list_test_expression(self, context, inventory):
         results = []
-        for node, items in inventory.iteritems():
+        for (node, items) in iteritems(inventory):
             if self._question.value(context, items):
                 results.append(node)
         return results
diff --git a/reclass/values/listitem.py b/reclass/values/listitem.py
index c7f29d0..1829e32 100644
--- a/reclass/values/listitem.py
+++ b/reclass/values/listitem.py
@@ -4,7 +4,7 @@
 # This file is part of reclass
 #
 
-from item import Item
+from .item import Item
 from reclass.settings import Settings
 
 class ListItem(Item):
diff --git a/reclass/values/parser.py b/reclass/values/parser.py
index bdd881d..a8adcf0 100644
--- a/reclass/values/parser.py
+++ b/reclass/values/parser.py
@@ -6,10 +6,10 @@
 
 import pyparsing as pp
 
-from compitem import CompItem
-from invitem import InvItem
-from refitem import RefItem
-from scaitem import ScaItem
+from .compitem import CompItem
+from .invitem import InvItem
+from .refitem import RefItem
+from .scaitem import ScaItem
 
 from reclass.errors import ParseError
 from reclass.values.parser_funcs import STR, REF, INV
diff --git a/reclass/values/refitem.py b/reclass/values/refitem.py
index 0ae65e6..3e3341c 100644
--- a/reclass/values/refitem.py
+++ b/reclass/values/refitem.py
@@ -4,7 +4,7 @@
 # This file is part of reclass
 #
 
-from item import Item
+from .item import Item
 from reclass.defaults import REFERENCE_SENTINELS
 from reclass.settings import Settings
 from reclass.utils.dictpath import DictPath
diff --git a/reclass/values/scaitem.py b/reclass/values/scaitem.py
index 466d3c9..f4265b5 100644
--- a/reclass/values/scaitem.py
+++ b/reclass/values/scaitem.py
@@ -5,7 +5,7 @@
 #
 
 from reclass.settings import Settings
-from item import Item
+from .item import Item
 
 class ScaItem(Item):
 
@@ -18,15 +18,15 @@
         return self._value
 
     def merge_over(self, item):
-        if item.type == Item.SCALAR:
+        if item.type == Item.SCALAR or item.type == Item.COMPOSITE:
             return self
         elif item.type == Item.LIST:
-            if self._settings.allow_scalar_over_list or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+            if self._settings.allow_scalar_over_list or (self._settings.allow_none_override and self._value is None):
                 return self
             else:
                 raise TypeError('allow scalar over list = False: cannot merge %s over %s' % (repr(self), repr(item)))
         elif item.type == Item.DICTIONARY:
-            if self._settings.allow_scalar_over_dict or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+            if self._settings.allow_scalar_over_dict or (self._settings.allow_none_override and self._value is None):
                 return self
             else:
                 raise TypeError('allow scalar over dict = False: cannot merge %s over %s' % (repr(self), repr(item)))
diff --git a/reclass/values/value.py b/reclass/values/value.py
index 4ec6051..1a5b450 100644
--- a/reclass/values/value.py
+++ b/reclass/values/value.py
@@ -4,25 +4,29 @@
 # This file is part of reclass
 #
 
-from parser import Parser
-from dictitem import DictItem
-from listitem import ListItem
-from scaitem import ScaItem
+from .parser import Parser
+from .dictitem import DictItem
+from .listitem import ListItem
+from .scaitem import ScaItem
 from reclass.errors import InterpolationError
 
 class Value(object):
 
     _parser = Parser()
 
-    def __init__(self, value, settings, uri):
+    def __init__(self, value, settings, uri, parse_string=True):
         self._settings = settings
         self._uri = uri
+        self._overwrite = False
         if isinstance(value, str):
-            try:
-                self._item = self._parser.parse(value, self._settings)
-            except InterpolationError as e:
-                e.uri = self._uri
-                raise
+            if parse_string:
+                try:
+                    self._item = self._parser.parse(value, self._settings)
+                except InterpolationError as e:
+                    e.uri = self._uri
+                    raise
+            else:
+                self._item = ScaItem(value, self._settings)
         elif isinstance(value, list):
             self._item = ListItem(value, self._settings)
         elif isinstance(value, dict):
@@ -30,6 +34,14 @@
         else:
             self._item = ScaItem(value, self._settings)
 
+    @property
+    def overwrite(self):
+        return self._overwrite
+
+    @overwrite.setter
+    def overwrite(self, overwrite):
+        self._overwrite = overwrite
+
     def uri(self):
         return self._uri
 
diff --git a/reclass/values/valuelist.py b/reclass/values/valuelist.py
index 46d8ec7..bdfa01a 100644
--- a/reclass/values/valuelist.py
+++ b/reclass/values/valuelist.py
@@ -4,6 +4,8 @@
 # This file is part of reclass
 #
 
+from __future__ import print_function
+
 import copy
 import sys
 
@@ -98,18 +100,18 @@
                 if self._settings.ignore_overwritten_missing_references and not isinstance(output, (dict, list)) and n != (len(self._values)-1):
                     new = None
                     last_error = e
-                    print >>sys.stderr, "[WARNING] Reference '%s' undefined" % (str(value))
+                    print("[WARNING] Reference '%s' undefined" % str(value), file=sys.stderr)
                 else:
                     raise e
 
-            if output is None:
+            if output is None or value.overwrite:
                 output = new
                 deepCopied = False
             else:
                 if isinstance(output, dict) and isinstance(new, dict):
-                    p1 = Parameters(output, self._settings, None, merge_initialise = False)
-                    p2 = Parameters(new, self._settings, None, merge_initialise = False)
-                    p1.merge(p2, wrap=False)
+                    p1 = Parameters(output, self._settings, None, parse_strings=False)
+                    p2 = Parameters(new, self._settings, None, parse_strings=False)
+                    p1.merge(p2)
                     output = p1.as_dict()
                     continue
                 elif isinstance(output, list) and isinstance(new, list):
@@ -122,6 +124,7 @@
                     raise TypeError('Cannot merge %s over %s' % (repr(self._values[n]), repr(self._values[n-1])))
                 else:
                     output = new
+                    deepCopied = False
 
         if isinstance(output, (dict, list)) and last_error is not None:
             raise last_error
diff --git a/reclass/version.py b/reclass/version.py
index 90c2cb7..664fd6f 100644
--- a/reclass/version.py
+++ b/reclass/version.py
@@ -8,7 +8,7 @@
 #
 RECLASS_NAME = 'reclass'
 DESCRIPTION = 'merge data by recursive descent down an ancestry hierarchy (forked extended version)'
-VERSION = '1.5.2'
+VERSION = '1.5.3'
 AUTHOR = 'martin f. krafft / Andrew Pickford / salt-formulas community'
 AUTHOR_EMAIL = 'salt-formulas@freelists.org'
 MAINTAINER = 'salt-formulas community'
diff --git a/requirements.txt b/requirements.txt
index ea72e95..66f0f4b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 pyparsing
 pyyaml
 pygit2
+six
diff --git a/setup.cfg b/setup.cfg
index 2f5e543..c5cfe6b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,5 +4,3 @@
 # will need to generate wheels for each Python version that you support.
 universal=0
 
-[install]
-prefix: /usr
diff --git a/setup.py b/setup.py
index 2fb77ae..5b4b8b6 100644
--- a/setup.py
+++ b/setup.py
@@ -37,8 +37,8 @@
     license = LICENCE,
     url = URL,
     packages = find_packages(exclude=['*tests']), #FIXME validate this
-    entry_point = { 'console_scripts': console_scripts },
-    install_requires = ['pyparsing', 'pyyaml'],   #FIXME pygit2 (require libffi-dev, libgit2-dev 0.26.x )
+    entry_points = { 'console_scripts': console_scripts },
+    install_requires = ['pyparsing', 'pyyaml', 'six'], #FIXME pygit2 (require libffi-dev, libgit2-dev 0.26.x )
 
     classifiers=[
         'Development Status :: 4 - Beta',
diff --git a/test/model/default/classes/first.yml b/test/model/default/classes/first.yml
new file mode 100644
index 0000000..9b72a26
--- /dev/null
+++ b/test/model/default/classes/first.yml
@@ -0,0 +1,32 @@
+parameters:
+  _param:
+    some: param
+    colour: red
+  lab:
+    name: test
+    label: first
+  colour:
+    escaped: \${_param:colour}
+    doubleescaped: \\${_param:colour}
+    unescaped: ${_param:colour}
+  colours:
+    red:
+      name: red
+    blue:
+      name: blue
+  one:
+    a: 1
+    b: 2
+  two:
+    c: 3
+    d: 4
+  three:
+    e: 5
+  list_to_override:
+  - one
+  - two
+  dict_to_override:
+   one: 1
+   two: 2
+
+
diff --git a/test/model/default/classes/lab/env/dev.yml b/test/model/default/classes/lab/env/dev.yml
new file mode 100644
index 0000000..0cce363
--- /dev/null
+++ b/test/model/default/classes/lab/env/dev.yml
@@ -0,0 +1,4 @@
+
+parameters:
+  lab:
+    name: dev
diff --git a/test/model/default/classes/second.yml b/test/model/default/classes/second.yml
new file mode 100644
index 0000000..dab50c7
--- /dev/null
+++ b/test/model/default/classes/second.yml
@@ -0,0 +1,9 @@
+classes:
+- first
+
+parameters:
+  will:
+    warn:
+      at:
+        second: ${_param:notfound}
+  three: ${one}
diff --git a/test/model/default/classes/third.yml b/test/model/default/classes/third.yml
new file mode 100644
index 0000000..135acd4
--- /dev/null
+++ b/test/model/default/classes/third.yml
@@ -0,0 +1,18 @@
+classes:
+- second
+
+parameters:
+  _param:
+    notfound: exist
+  myparam: ${_param:some}
+  will:
+    not:
+      fail:
+        at:
+          tree: ${_param:notfound}
+  three: ${two}
+  empty:
+    list: []
+    dict: {}
+  ~list_to_override: ${empty:list}
+  ~dict_to_override: ${empty:dict}
diff --git a/test/model/default/nodes/reclass.yml b/test/model/default/nodes/reclass.yml
new file mode 100644
index 0000000..94b7519
--- /dev/null
+++ b/test/model/default/nodes/reclass.yml
@@ -0,0 +1,3 @@
+
+classes:
+- third
diff --git a/test/model/default/reclass-config.yml b/test/model/default/reclass-config.yml
new file mode 100644
index 0000000..9d8f30f
--- /dev/null
+++ b/test/model/default/reclass-config.yml
@@ -0,0 +1 @@
+storage_type: yaml_fs
diff --git a/test/model/extensions/classes/first.yml b/test/model/extensions/classes/first.yml
new file mode 100644
index 0000000..96ece27
--- /dev/null
+++ b/test/model/extensions/classes/first.yml
@@ -0,0 +1,6 @@
+parameters:
+  _param:
+    some: param
+  lab:
+    name: test
+    label: first
diff --git a/test/model/extensions/classes/lab/env/dev.yml b/test/model/extensions/classes/lab/env/dev.yml
new file mode 100644
index 0000000..0cce363
--- /dev/null
+++ b/test/model/extensions/classes/lab/env/dev.yml
@@ -0,0 +1,4 @@
+
+parameters:
+  lab:
+    name: dev
diff --git a/test/model/extensions/classes/second.yml b/test/model/extensions/classes/second.yml
new file mode 100644
index 0000000..a9babd3
--- /dev/null
+++ b/test/model/extensions/classes/second.yml
@@ -0,0 +1,8 @@
+classes:
+- first
+
+parameters:
+  will:
+    warn:
+      at:
+        second: ${_param:notfound}
diff --git a/test/model/extensions/classes/third.yml b/test/model/extensions/classes/third.yml
new file mode 100644
index 0000000..20a937c
--- /dev/null
+++ b/test/model/extensions/classes/third.yml
@@ -0,0 +1,13 @@
+classes:
+- missing.class
+- second
+
+parameters:
+  _param:
+    notfound: exist
+  myparam: ${_param:some}
+  will:
+    not:
+      fail:
+        at:
+          tree: ${_param:notfound}
diff --git a/test/model/extensions/nodes/reclass.yml b/test/model/extensions/nodes/reclass.yml
new file mode 100644
index 0000000..94b7519
--- /dev/null
+++ b/test/model/extensions/nodes/reclass.yml
@@ -0,0 +1,3 @@
+
+classes:
+- third
diff --git a/test/model/extensions/reclass-config.yml b/test/model/extensions/reclass-config.yml
new file mode 100644
index 0000000..6e2f101
--- /dev/null
+++ b/test/model/extensions/reclass-config.yml
@@ -0,0 +1,3 @@
+storage_type: yaml_fs
+ignore_class_notfound: True
+ignore_class_regexp: ['.*']