Merge pull request #60 from a-ovchinnikov/develop

More refactoring
diff --git a/reclass/__init__.py b/reclass/__init__.py
index 2167a30..d5f3410 100644
--- a/reclass/__init__.py
+++ b/reclass/__init__.py
@@ -19,7 +19,7 @@
     storage_class = StorageBackendLoader(storage_type).load()
     return MemcacheProxy(storage_class(nodes_uri, classes_uri, compose_node_name, **kwargs))
 
-def get_path_mangler(storage_type,**kwargs):
+def get_path_mangler(storage_type, **kwargs):
     return StorageBackendLoader(storage_type).path_mangler()
 
 def output(data, fmt, pretty_print=False, no_refs=False):
diff --git a/reclass/core.py b/reclass/core.py
index 6dac5c3..3e0ab34 100644
--- a/reclass/core.py
+++ b/reclass/core.py
@@ -23,7 +23,6 @@
 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, ClassNameResolveError, ClassNotFound, InvQueryClassNameResolveError, InvQueryClassNotFound, InvQueryError, InterpolationError, ResolveError
 from reclass.values.parser import Parser
@@ -39,7 +38,8 @@
         self._settings = settings
         self._input_data = input_data
         if self._settings.ignore_class_notfound:
-            self._cnf_r = re.compile('|'.join([x for x in self._settings.ignore_class_notfound_regexp]))
+            self._cnf_r = re.compile(
+                '|'.join(self._settings.ignore_class_notfound_regexp))
 
     @staticmethod
     def _get_timestamp():
@@ -114,7 +114,9 @@
 
         for klass in entity.classes.as_list():
             # class name contain reference
-            if klass.count('$') > 0:
+            num_references = klass.count(self._settings.reference_sentinels[0]) +\
+                             klass.count(self._settings.export_sentinels[0])
+            if num_references > 0:
                 try:
                     klass = str(self._parser.parse(klass, self._settings).render(merge_base.parameters.as_dict(), {}))
                 except ResolveError as e:
@@ -152,8 +154,16 @@
 
     def _get_automatic_parameters(self, nodename, environment):
         if self._settings.automatic_parameters:
-            return Parameters({ '_reclass_': { 'name': { 'full': nodename, 'short': nodename.split('.')[0] },
-                                               'environment': environment } }, self._settings, '__auto__')
+            pars = {
+                '_reclass_': {
+                    'name': {
+                        'full': nodename,
+                        'short': nodename.split('.')[0]
+                    },
+                'environment': environment
+                }
+            }
+            return Parameters(pars, self._settings, '__auto__')
         else:
             return Parameters({}, self._settings, '')
 
@@ -162,13 +172,12 @@
         for nodename in self._storage.enumerate_nodes():
             try:
                 node_base = self._storage.get_node(nodename, self._settings)
-                if node_base.environment == None:
+                if node_base.environment is None:
                     node_base.environment = self._settings.default_environment
             except yaml.scanner.ScannerError as e:
                 if self._settings.inventory_ignore_failed_node:
                     continue
-                else:
-                    raise
+                raise
 
             if all_envs or node_base.environment == environment:
                 try:
@@ -220,7 +229,8 @@
             raise
 
     def _nodeinfo_as_dict(self, nodename, entity):
-        ret = {'__reclass__' : {'node': entity.name, 'name': nodename,
+        ret = {'__reclass__' : {'node': entity.name,
+                                'name': nodename,
                                 'uri': entity.uri,
                                 'environment': entity.environment,
                                 'timestamp': Core._get_timestamp()
diff --git a/reclass/datatypes/applications.py b/reclass/datatypes/applications.py
index 90ae54c..4f6ee10 100644
--- a/reclass/datatypes/applications.py
+++ b/reclass/datatypes/applications.py
@@ -28,18 +28,14 @@
 
     def __init__(self, iterable=None,
                  negation_prefix=DEFAULT_NEGATION_PREFIX):
-        self._negation_prefix = negation_prefix
+        self.negation_prefix = negation_prefix
         self._offset = len(negation_prefix)
         self._negations = []
         super(Applications, self).__init__(iterable)
 
-    @property
-    def negation_prefix(self):
-        return self._negation_prefix
-
     def append_if_new(self, item):
         self._assert_is_string(item)
-        if item.startswith(self._negation_prefix):
+        if item.startswith(self.negation_prefix):
             item = item[self._offset:]
             self._negations.append(item)
             try:
@@ -64,6 +60,6 @@
 
     def __repr__(self):
         contents = self._items + \
-                ['%s%s' % (self._negation_prefix, i) for i in self._negations]
+                ['%s%s' % (self.negation_prefix, i) for i in self._negations]
         return "%s(%r, %r)" % (self.__class__.__name__, contents,
-                               str(self._negation_prefix))
+                               str(self.negation_prefix))
diff --git a/reclass/datatypes/classes.py b/reclass/datatypes/classes.py
index 5270e28..fa9cbcf 100644
--- a/reclass/datatypes/classes.py
+++ b/reclass/datatypes/classes.py
@@ -57,7 +57,7 @@
 
     def _assert_is_string(self, item):
         if not isinstance(item, six.string_types):
-            raise TypeError('%s instances can only contain strings, '\
+            raise TypeError('%s instances can only contain strings, '
                             'not %s' % (self.__class__.__name__, type(item)))
 
     def _assert_valid_characters(self, item):
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index 8133de5..2e0e1e4 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -35,7 +35,6 @@
         self._environment = environment
 
     name = property(lambda s: s._name)
-    short_name = property(lambda s: s._short_name)
     uri = property(lambda s: s._uri)
     classes = property(lambda s: s._classes)
     applications = property(lambda s: s._applications)
@@ -61,10 +60,10 @@
         return received_value
 
     def merge(self, other):
-        self._classes.merge_unique(other._classes)
-        self._applications.merge_unique(other._applications)
-        self._parameters.merge(other._parameters)
-        self._exports.merge(other._exports)
+        self._classes.merge_unique(other.classes)
+        self._applications.merge_unique(other.applications)
+        self._parameters.merge(other.parameters)
+        self._exports.merge(other.exports)
         self._name = other.name
         self._uri = other.uri
         self._parameters._uri = other.uri
@@ -91,12 +90,12 @@
 
     def __eq__(self, other):
         return isinstance(other, type(self)) \
-                and self._applications == other._applications \
-                and self._classes == other._classes \
-                and self._parameters == other._parameters \
-                and self._exports == other._exports \
-                and self._name == other._name \
-                and self._uri == other._uri
+                and self._applications == other.applications \
+                and self._classes == other.classes \
+                and self._parameters == other.parameters \
+                and self._exports == other.exports \
+                and self._name == other.name \
+                and self._uri == other.uri
 
     def __ne__(self, other):
         return not self.__eq__(other)
diff --git a/reclass/datatypes/exports.py b/reclass/datatypes/exports.py
index 04ab200..984a15a 100644
--- a/reclass/datatypes/exports.py
+++ b/reclass/datatypes/exports.py
@@ -23,9 +23,6 @@
     def __init__(self, mapping, settings, uri):
         super(Exports, self).__init__(mapping, settings, uri)
 
-    def __repr__(self):
-        return '%s(%r)' % (self.__class__.__name__, self._base)
-
     def delete_key(self, key):
         self._base.pop(key, None)
         self._unrendered.pop(key, None)
diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py
index ee404ce..bab2a28 100644
--- a/reclass/datatypes/parameters.py
+++ b/reclass/datatypes/parameters.py
@@ -105,24 +105,23 @@
                 e.context = DictPath(self._settings.delimiter)
                 raise
 
+    def _get_wrapped(self, position, value):
+        try:
+            return self._wrap_value(value)
+        except InterpolationError as e:
+            e.context.add_ancestor(str(position))
+            raise
+
     def _wrap_list(self, source):
         l = ParameterList(uri=self._uri)
         for (k, v) in enumerate(source):
-            try:
-                l.append(self._wrap_value(v))
-            except InterpolationError as e:
-                e.context.add_ancestor(str(k))
-                raise
+            l.append(self._get_wrapped(k, v))
         return l
 
     def _wrap_dict(self, source):
         d = ParameterDict(uri=self._uri)
         for (k, v) in iteritems(source):
-            try:
-                d[k] = self._wrap_value(v)
-            except InterpolationError as e:
-                e.context.add_ancestor(str(k))
-                raise
+            d[k] = self._get_wrapped(k, v)
         return d
 
     def _update_value(self, cur, new):
diff --git a/reclass/settings.py b/reclass/settings.py
index 3e223cc..b7f5252 100644
--- a/reclass/settings.py
+++ b/reclass/settings.py
@@ -5,69 +5,61 @@
 from __future__ import print_function
 from __future__ import unicode_literals
 
-import copy
-import reclass.values.parser_funcs
-from reclass.defaults import *
+import reclass.defaults as defaults
 
-from six import string_types
+from six import string_types, iteritems
+
 
 class Settings(object):
 
+    known_opts = {
+        'allow_scalar_over_dict': defaults.OPT_ALLOW_SCALAR_OVER_DICT,
+        'allow_scalar_over_list': defaults.OPT_ALLOW_SCALAR_OVER_LIST,
+        'allow_list_over_scalar': defaults.OPT_ALLOW_LIST_OVER_SCALAR,
+        'allow_dict_over_scalar': defaults.OPT_ALLOW_DICT_OVER_SCALAR,
+        'allow_none_override': defaults.OPT_ALLOW_NONE_OVERRIDE,
+        'automatic_parameters': defaults.AUTOMATIC_RECLASS_PARAMETERS,
+        'default_environment': defaults.DEFAULT_ENVIRONMENT,
+        'delimiter': defaults.PARAMETER_INTERPOLATION_DELIMITER,
+        'dict_key_override_prefix':
+            defaults.PARAMETER_DICT_KEY_OVERRIDE_PREFIX,
+        'dict_key_constant_prefix':
+            defaults.PARAMETER_DICT_KEY_CONSTANT_PREFIX,
+        'escape_character': defaults.ESCAPE_CHARACTER,
+        'export_sentinels': defaults.EXPORT_SENTINELS,
+        'inventory_ignore_failed_node':
+            defaults.OPT_INVENTORY_IGNORE_FAILED_NODE,
+        'inventory_ignore_failed_render':
+            defaults.OPT_INVENTORY_IGNORE_FAILED_RENDER,
+        'reference_sentinels': defaults.REFERENCE_SENTINELS,
+        'ignore_class_notfound': defaults.OPT_IGNORE_CLASS_NOTFOUND,
+        'strict_constant_parameters':
+            defaults.OPT_STRICT_CONSTANT_PARAMETERS,
+        'ignore_class_notfound_regexp':
+            defaults.OPT_IGNORE_CLASS_NOTFOUND_REGEXP,
+        'ignore_class_notfound_warning':
+            defaults.OPT_IGNORE_CLASS_NOTFOUND_WARNING,
+        'ignore_overwritten_missing_referencesdefaults.':
+            defaults.OPT_IGNORE_OVERWRITTEN_MISSING_REFERENCES,
+        'group_errors': defaults.OPT_GROUP_ERRORS,
+        'compose_node_name': defaults.OPT_COMPOSE_NODE_NAME,
+    }
+
     def __init__(self, options={}):
-        self.allow_scalar_over_dict = options.get('allow_scalar_over_dict', OPT_ALLOW_SCALAR_OVER_DICT)
-        self.allow_scalar_over_list = options.get('allow_scalar_over_list', OPT_ALLOW_SCALAR_OVER_LIST)
-        self.allow_list_over_scalar = options.get('allow_list_over_scalar', OPT_ALLOW_LIST_OVER_SCALAR)
-        self.allow_dict_over_scalar = options.get('allow_dict_over_scalar', OPT_ALLOW_DICT_OVER_SCALAR)
-        self.allow_none_override = options.get('allow_none_override', OPT_ALLOW_NONE_OVERRIDE)
-        self.automatic_parameters = options.get('automatic_parameters', AUTOMATIC_RECLASS_PARAMETERS)
-        self.default_environment = options.get('default_environment', DEFAULT_ENVIRONMENT)
-        self.delimiter = options.get('delimiter', PARAMETER_INTERPOLATION_DELIMITER)
-        self.dict_key_override_prefix = options.get('dict_key_override_prefix', PARAMETER_DICT_KEY_OVERRIDE_PREFIX)
-        self.dict_key_constant_prefix = options.get('dict_key_constant_prefix', PARAMETER_DICT_KEY_CONSTANT_PREFIX)
-        self.dict_key_prefixes = [ str(self.dict_key_override_prefix), str(self.dict_key_constant_prefix) ]
-        self.escape_character = options.get('escape_character', ESCAPE_CHARACTER)
-        self.export_sentinels = options.get('export_sentinels', EXPORT_SENTINELS)
-        self.inventory_ignore_failed_node = options.get('inventory_ignore_failed_node', OPT_INVENTORY_IGNORE_FAILED_NODE)
-        self.inventory_ignore_failed_render = options.get('inventory_ignore_failed_render', OPT_INVENTORY_IGNORE_FAILED_RENDER)
-        self.reference_sentinels = options.get('reference_sentinels', REFERENCE_SENTINELS)
-        self.ignore_class_notfound = options.get('ignore_class_notfound', OPT_IGNORE_CLASS_NOTFOUND)
-        self.strict_constant_parameters = options.get('strict_constant_parameters', OPT_STRICT_CONSTANT_PARAMETERS)
-        self.compose_node_name = options.get('compose_node_name', OPT_COMPOSE_NODE_NAME)
+        for opt_name, opt_value in iteritems(self.known_opts):
+            setattr(self, opt_name, options.get(opt_name, opt_value))
 
-        self.ignore_class_notfound_regexp = options.get('ignore_class_notfound_regexp', OPT_IGNORE_CLASS_NOTFOUND_REGEXP)
+        self.dict_key_prefixes = [str(self.dict_key_override_prefix),
+                                  str(self.dict_key_constant_prefix)]
         if isinstance(self.ignore_class_notfound_regexp, string_types):
-            self.ignore_class_notfound_regexp = [ self.ignore_class_notfound_regexp ]
-
-        self.ignore_class_notfound_warning = options.get('ignore_class_notfound_warning', OPT_IGNORE_CLASS_NOTFOUND_WARNING)
-        self.ignore_overwritten_missing_references = options.get('ignore_overwritten_missing_references', OPT_IGNORE_OVERWRITTEN_MISSING_REFERENCES)
-
-        self.group_errors = options.get('group_errors', OPT_GROUP_ERRORS)
-
-        self.ref_parser = reclass.values.parser_funcs.get_ref_parser(self.escape_character, self.reference_sentinels, self.export_sentinels)
-        self.simple_ref_parser = reclass.values.parser_funcs.get_simple_ref_parser(self.escape_character, self.reference_sentinels, self.export_sentinels)
+            self.ignore_class_notfound_regexp = [
+                    self.ignore_class_notfound_regexp]
 
     def __eq__(self, other):
-        return isinstance(other, type(self)) \
-               and self.allow_scalar_over_dict == other.allow_scalar_over_dict \
-               and self.allow_scalar_over_list == other.allow_scalar_over_list \
-               and self.allow_list_over_scalar == other.allow_list_over_scalar \
-               and self.allow_dict_over_scalar == other.allow_dict_over_scalar \
-               and self.allow_none_override == other.allow_none_override \
-               and self.automatic_parameters == other.automatic_parameters \
-               and self.default_environment == other.default_environment \
-               and self.delimiter == other.delimiter \
-               and self.dict_key_override_prefix == other.dict_key_override_prefix \
-               and self.dict_key_constant_prefix == other.dict_key_constant_prefix \
-               and self.escape_character == other.escape_character \
-               and self.export_sentinels == other.export_sentinels \
-               and self.inventory_ignore_failed_node == other.inventory_ignore_failed_node \
-               and self.inventory_ignore_failed_render == other.inventory_ignore_failed_render \
-               and self.reference_sentinels == other.reference_sentinels \
-               and self.ignore_class_notfound == other.ignore_class_notfound \
-               and self.ignore_class_notfound_regexp == other.ignore_class_notfound_regexp \
-               and self.ignore_class_notfound_warning == other.ignore_class_notfound_warning \
-               and self.strict_constant_parameters == other.strict_constant_parameters \
-               and self.compose_node_name == other.compose_node_name
+        if isinstance(other, type(self)):
+            return all(getattr(self, opt) == getattr(other, opt)
+                       for opt in self.known_opts)
+        return False
 
     def __copy__(self):
         cls = self.__class__
diff --git a/reclass/storage/loader.py b/reclass/storage/loader.py
index aab554a..0a66a66 100644
--- a/reclass/storage/loader.py
+++ b/reclass/storage/loader.py
@@ -32,5 +32,6 @@
     def path_mangler(self, name='path_mangler'):
         function = getattr(self._module, name, None)
         if function is None:
-            raise AttributeError('Storage backend class {0} does not export "{1}"'.format(self._name, name))
+            raise AttributeError('Storage backend class {0} does not export '
+                                 '"{1}"'.format(self._name, name))
         return function
diff --git a/reclass/values/invitem.py b/reclass/values/invitem.py
index 5461612..adb1cb6 100644
--- a/reclass/values/invitem.py
+++ b/reclass/values/invitem.py
@@ -135,6 +135,7 @@
     def __init__(self, newitem, settings):
         super(InvItem, self).__init__(newitem.render(None, None), settings)
         self.needs_all_envs = False
+        self.has_inv_query = True
         self.ignore_failed_render = (
                 self._settings.inventory_ignore_failed_render)
         self._parse_expression(self.contents)
@@ -179,10 +180,6 @@
             raise ExpressionError(msg % self._expr_type, tbFlag=False)
 
     @property
-    def has_inv_query(self):
-        return True
-
-    @property
     def has_references(self):
         return len(self._question.refs) > 0
 
diff --git a/reclass/values/item.py b/reclass/values/item.py
index ee46995..45aeb77 100644
--- a/reclass/values/item.py
+++ b/reclass/values/item.py
@@ -22,6 +22,7 @@
     def __init__(self, item, settings):
         self._settings = settings
         self.contents = item
+        self.has_inv_query = False
 
     def allRefs(self):
         return True
@@ -30,10 +31,6 @@
     def has_references(self):
         return False
 
-    @property
-    def has_inv_query(self):
-        return False
-
     def is_container(self):
         return False
 
@@ -60,6 +57,10 @@
 
     def __init__(self, items, settings):
         super(ItemWithReferences, self).__init__(items, settings)
+        try:
+            iter(self.contents)
+        except TypeError:
+            self.contents = [self.contents]
         self.assembleRefs()
 
     @property
@@ -81,6 +82,7 @@
                 if item.allRefs is False:
                     self.allRefs = False
 
+
 class ContainerItem(Item):
 
     def is_container(self):
diff --git a/reclass/values/parser.py b/reclass/values/parser.py
index 914e825..27e6d2d 100644
--- a/reclass/values/parser.py
+++ b/reclass/values/parser.py
@@ -17,37 +17,54 @@
 
 from reclass.errors import ParseError
 from reclass.values.parser_funcs import STR, REF, INV
+import reclass.values.parser_funcs as parsers
 
 class Parser(object):
 
+    def __init__(self):
+        self._ref_parser = None
+        self._simple_parser = None
+        self._old_settings = None
+
+    @property
+    def ref_parser(self):
+        if self._ref_parser is None or self._settings != self._old_settings:
+            self._ref_parser = parsers.get_ref_parser(self._settings)
+            self._old_settings = self._settings
+        return self._ref_parser
+
+    @property
+    def simple_ref_parser(self):
+        if self._simple_parser is None or self._settings != self._old_settings:
+            self._simple_parser = parsers.get_simple_ref_parser(self._settings)
+            self._old_settings = self._settings
+        return self._simple_parser
+
     def parse(self, value, settings):
-        self._settings = settings
-        dollars = value.count('$')
-        if dollars == 0:
-            # speed up: only use pyparsing if there is a $ in the string
-            return ScaItem(value, self._settings)
-        elif dollars == 1:
-            # speed up: try a simple reference
+        def full_parse():
             try:
-                tokens = self._settings.simple_ref_parser.leaveWhitespace().parseString(value).asList()
-            except pp.ParseException:
-                # fall back on the full parser
-                try:
-                    tokens = self._settings.ref_parser.leaveWhitespace().parseString(value).asList()
-                except pp.ParseException as e:
-                    raise ParseError(e.msg, e.line, e.col, e.lineno)
-        else:
-            # use the full parser
-            try:
-                tokens = self._settings.ref_parser.leaveWhitespace().parseString(value).asList()
+                return self.ref_parser.parseString(value).asList()
             except pp.ParseException as e:
                 raise ParseError(e.msg, e.line, e.col, e.lineno)
 
+        self._settings = settings
+        sentinel_count = (value.count(settings.reference_sentinels[0]) +
+                          value.count(settings.export_sentinels[0]))
+        if sentinel_count == 0:
+            # speed up: only use pyparsing if there are sentinels in the value
+            return ScaItem(value, self._settings)
+        elif sentinel_count == 1:  # speed up: try a simple reference
+            try:
+                tokens = self.simple_ref_parser.parseString(value).asList()
+            except pp.ParseException:
+                tokens = full_parse()  # fall back on the full parser
+        else:
+            tokens = full_parse()  # use the full parser
+
         items = self._create_items(tokens)
         if len(items) == 1:
             return items[0]
-        else:
-            return CompItem(items, self._settings)
+        return CompItem(items, self._settings)
 
     _create_dict = { STR: (lambda s, v: ScaItem(v, s._settings)),
                      REF: (lambda s, v: s._create_ref(v)),
@@ -64,5 +81,4 @@
         items = [ ScaItem(v, self._settings) for t, v in tokens ]
         if len(items) == 1:
             return InvItem(items[0], self._settings)
-        else:
-            return InvItem(CompItem(items), self._settings)
+        return InvItem(CompItem(items), self._settings)
diff --git a/reclass/values/parser_funcs.py b/reclass/values/parser_funcs.py
index 50babd0..f702910 100644
--- a/reclass/values/parser_funcs.py
+++ b/reclass/values/parser_funcs.py
@@ -34,6 +34,8 @@
 ALL_ENVS = '+AllEnvs'
 
 
+s_end = pp.StringEnd()
+
 def _tag_with(tag, transform=lambda x:x):
     def inner(tag, string, location, tokens):
         token = transform(tokens[0])
@@ -41,8 +43,6 @@
     return functools.partial(inner, tag)
 
 def get_expression_parser():
-
-    s_end = pp.StringEnd()
     sign = pp.Optional(pp.Literal('-'))
     number = pp.Word(pp.nums)
     dpoint = pp.Literal('.')
@@ -80,12 +80,11 @@
     line = options + expr + s_end
     return line
 
-def get_ref_parser(escape_character, reference_sentinels, export_sentinels):
-    _ESCAPE = escape_character
+def get_ref_parser(settings):
+    _ESCAPE = settings.escape_character
     _DOUBLE_ESCAPE = _ESCAPE + _ESCAPE
 
-    _REF_OPEN = reference_sentinels[0]
-    _REF_CLOSE = reference_sentinels[1]
+    _REF_OPEN, _REF_CLOSE = settings.reference_sentinels
     _REF_CLOSE_FIRST = _REF_CLOSE[0]
     _REF_ESCAPE_OPEN = _ESCAPE + _REF_OPEN
     _REF_ESCAPE_CLOSE = _ESCAPE + _REF_CLOSE
@@ -93,8 +92,7 @@
     _REF_DOUBLE_ESCAPE_CLOSE = _DOUBLE_ESCAPE + _REF_CLOSE
     _REF_EXCLUDES = _ESCAPE + _REF_OPEN + _REF_CLOSE
 
-    _INV_OPEN = export_sentinels[0]
-    _INV_CLOSE = export_sentinels[1]
+    _INV_OPEN, _INV_CLOSE = settings.export_sentinels
     _INV_CLOSE_FIRST = _INV_CLOSE[0]
     _INV_ESCAPE_OPEN = _ESCAPE + _INV_OPEN
     _INV_ESCAPE_CLOSE = _ESCAPE + _INV_CLOSE
@@ -142,20 +140,20 @@
     string = pp.MatchFirst([double_escape, ref_escape_open, inv_escape_open, content]).setParseAction(_tag_with(STR))
 
     item = reference | export | string
-    line = pp.OneOrMore(item) + pp.StringEnd()
-    return line
+    line = pp.OneOrMore(item) + s_end
+    return line.leaveWhitespace()
 
-def get_simple_ref_parser(escape_character, reference_sentinels, export_sentinels):
-    _ESCAPE = escape_character
-    _REF_OPEN = reference_sentinels[0]
-    _REF_CLOSE = reference_sentinels[1]
-    _INV_OPEN = export_sentinels[0]
-    _INV_CLOSE = export_sentinels[1]
-    _EXCLUDES = _ESCAPE + _REF_OPEN + _REF_CLOSE + _INV_OPEN + _INV_CLOSE
 
-    string = pp.CharsNotIn(_EXCLUDES).setParseAction(_tag_with(STR))
-    ref_open = pp.Literal(_REF_OPEN).suppress()
-    ref_close = pp.Literal(_REF_CLOSE).suppress()
+def get_simple_ref_parser(settings):
+
+    ESCAPE = settings.escape_character
+    REF_OPEN, REF_CLOSE = settings.reference_sentinels
+    INV_OPEN, INV_CLOSE = settings.export_sentinels
+    EXCLUDES = ESCAPE + REF_OPEN + REF_CLOSE + INV_OPEN + INV_CLOSE
+
+    string = pp.CharsNotIn(EXCLUDES).setParseAction(_tag_with(STR))
+    ref_open = pp.Literal(REF_OPEN).suppress()
+    ref_close = pp.Literal(REF_CLOSE).suppress()
     reference = (ref_open + pp.Group(string) + ref_close).setParseAction(_tag_with(REF))
-    line = pp.StringStart() + pp.Optional(string) + reference + pp.Optional(string) + pp.StringEnd()
-    return line
+    line = pp.StringStart() + pp.Optional(string) + reference + pp.Optional(string) + s_end
+    return line.leaveWhitespace()
diff --git a/reclass/values/refitem.py b/reclass/values/refitem.py
index df713e1..64bf450 100644
--- a/reclass/values/refitem.py
+++ b/reclass/values/refitem.py
@@ -16,12 +16,14 @@
     def assembleRefs(self, context={}):
         super(RefItem, self).assembleRefs(context)
         try:
-            strings = [str(i.render(context, None)) for i in self.contents]
-            value = "".join(strings)
-            self._refs.append(value)
+            self._refs.append(self._flatten_contents(context))
         except ResolveError as e:
             self.allRefs = False
 
+    def _flatten_contents(self, context, inventory=None):
+        result = [str(i.render(context, inventory)) for i in self.contents]
+        return "".join(result)
+
     def _resolve(self, ref, context):
         path = DictPath(self._settings.delimiter, ref)
         try:
@@ -30,11 +32,10 @@
             raise ResolveError(ref)
 
     def render(self, context, inventory):
-        if len(self.contents) == 1:
-            return self._resolve(self.contents[0].render(context, inventory),
-                                 context)
-        strings = [str(i.render(context, inventory)) for i in self.contents]
-        return self._resolve("".join(strings), context)
+        #strings = [str(i.render(context, inventory)) for i in self.contents]
+        #return self._resolve("".join(strings), context)
+        return self._resolve(self._flatten_contents(context, inventory),
+                             context)
 
     def __str__(self):
         strings = [str(i) for i in self.contents]
diff --git a/reclass/values/tests/test_compitem.py b/reclass/values/tests/test_compitem.py
index 71a6f0e..c3ee690 100644
--- a/reclass/values/tests/test_compitem.py
+++ b/reclass/values/tests/test_compitem.py
@@ -71,6 +71,14 @@
         self.assertTrue(composite.has_references)
         self.assertEquals(composite.get_references(), expected_refs)
 
+    def test_string_representation(self):
+        composite = CompItem(Value(1, SETTINGS, ''), SETTINGS)
+        expected = '1'
+
+        result = str(composite)
+
+        self.assertEquals(result, expected)
+
     def test_render_single_item(self):
         val1 = Value('${foo}',  SETTINGS, '')
 
@@ -106,20 +114,6 @@
 
         self.assertEquals(result, composite2)
 
-    def test_merge_over_merge_list_not_allowed(self):
-        val1 = Value(None, SETTINGS, '')
-        listitem = ListItem([1], SETTINGS)
-        composite = CompItem([val1], SETTINGS)
-
-        self.assertRaises(RuntimeError, composite.merge_over, listitem)
-
-    def test_merge_dict_dict_not_allowed(self):
-        val1 = Value(None, SETTINGS, '')
-        dictitem = DictItem({'foo': 'bar'}, SETTINGS)
-        composite = CompItem([val1], SETTINGS)
-
-        self.assertRaises(RuntimeError, composite.merge_over, dictitem)
-
     def test_merge_other_types_not_allowed(self):
         other = type('Other', (object,), {'type': 34})
         val1 = Value(None, SETTINGS, '')
diff --git a/reclass/values/tests/test_item.py b/reclass/values/tests/test_item.py
new file mode 100644
index 0000000..4b91f6e
--- /dev/null
+++ b/reclass/values/tests/test_item.py
@@ -0,0 +1,48 @@
+from reclass.settings import Settings
+from reclass.values.value import Value
+from reclass.values.compitem import CompItem
+from reclass.values.scaitem import ScaItem
+from reclass.values.valuelist import ValueList
+from reclass.values.listitem import ListItem
+from reclass.values.dictitem import DictItem
+from reclass.values.item import ContainerItem
+from reclass.values.item import ItemWithReferences
+import unittest
+from mock import MagicMock
+
+SETTINGS = Settings()
+
+
+class TestItemWithReferences(unittest.TestCase):
+
+    def test_assembleRef_allrefs(self):
+        phonyitem = MagicMock()
+        phonyitem.has_references = True
+        phonyitem.get_references = lambda *x: [1]
+
+        iwr = ItemWithReferences([phonyitem], {})
+
+        self.assertEquals(iwr.get_references(), [1])
+        self.assertTrue(iwr.allRefs)
+
+    def test_assembleRef_partial(self):
+        phonyitem = MagicMock()
+        phonyitem.has_references = True
+        phonyitem.allRefs = False
+        phonyitem.get_references = lambda *x: [1]
+
+        iwr = ItemWithReferences([phonyitem], {})
+
+        self.assertEquals(iwr.get_references(), [1])
+        self.assertFalse(iwr.allRefs)
+
+
+class TestContainerItem(unittest.TestCase):
+
+    def test_render(self):
+        container = ContainerItem('foo', SETTINGS)
+
+        self.assertEquals(container.render(None, None), 'foo')
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/reclass/values/tests/test_listitem.py b/reclass/values/tests/test_listitem.py
new file mode 100644
index 0000000..618b779
--- /dev/null
+++ b/reclass/values/tests/test_listitem.py
@@ -0,0 +1,31 @@
+from reclass.settings import Settings
+from reclass.values.value import Value
+from reclass.values.compitem import CompItem
+from reclass.values.scaitem import ScaItem
+from reclass.values.valuelist import ValueList
+from reclass.values.listitem import ListItem
+from reclass.values.dictitem import DictItem
+import unittest
+
+SETTINGS = Settings()
+
+class TestListItem(unittest.TestCase):
+
+    def test_merge_over_merge_list(self):
+        listitem1 = ListItem([1], SETTINGS)
+        listitem2 = ListItem([2], SETTINGS)
+        expected = ListItem([1, 2], SETTINGS)
+
+        result = listitem2.merge_over(listitem1)
+
+        self.assertEquals(result.contents, expected.contents)
+
+    def test_merge_other_types_not_allowed(self):
+        other = type('Other', (object,), {'type': 34})
+        val1 = Value(None, SETTINGS, '')
+        listitem = ListItem(val1, SETTINGS)
+
+        self.assertRaises(RuntimeError, listitem.merge_over, other)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/reclass/values/tests/test_refitem.py b/reclass/values/tests/test_refitem.py
new file mode 100644
index 0000000..6581478
--- /dev/null
+++ b/reclass/values/tests/test_refitem.py
@@ -0,0 +1,57 @@
+from reclass import errors
+
+from reclass.settings import Settings
+from reclass.values.value import Value
+from reclass.values.compitem import CompItem
+from reclass.values.scaitem import ScaItem
+from reclass.values.valuelist import ValueList
+from reclass.values.listitem import ListItem
+from reclass.values.dictitem import DictItem
+from reclass.values.refitem import RefItem
+import unittest
+from mock import MagicMock
+
+SETTINGS = Settings()
+
+class TestRefItem(unittest.TestCase):
+
+    def test_assembleRefs_ok(self):
+        phonyitem = MagicMock()
+        phonyitem.render = lambda x, k: 'bar'
+        phonyitem.has_references = True
+        phonyitem.get_references = lambda *x: ['foo']
+
+        iwr = RefItem([phonyitem], {})
+
+        self.assertEquals(iwr.get_references(), ['foo', 'bar'])
+        self.assertTrue(iwr.allRefs)
+
+    def test_assembleRefs_failedrefs(self):
+        phonyitem = MagicMock()
+        phonyitem.render.side_effect = errors.ResolveError('foo')
+        phonyitem.has_references = True
+        phonyitem.get_references = lambda *x: ['foo']
+
+        iwr = RefItem([phonyitem], {})
+
+        self.assertEquals(iwr.get_references(), ['foo'])
+        self.assertFalse(iwr.allRefs)
+
+    def test__resolve_ok(self):
+        reference = RefItem('', Settings({'delimiter': ':'}))
+
+        result = reference._resolve('foo:bar', {'foo':{'bar': 1}})
+
+        self.assertEquals(result, 1)
+
+    def test__resolve_fails(self):
+        refitem = RefItem('', Settings({'delimiter': ':'}))
+        context = {'foo':{'bar': 1}}
+        reference = 'foo:baz'
+
+        self.assertRaises(errors.ResolveError, refitem._resolve, reference,
+                          context)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/reclass/values/tests/test_scaitem.py b/reclass/values/tests/test_scaitem.py
new file mode 100644
index 0000000..b6d038d
--- /dev/null
+++ b/reclass/values/tests/test_scaitem.py
@@ -0,0 +1,38 @@
+from reclass.settings import Settings
+from reclass.values.value import Value
+from reclass.values.compitem import CompItem
+from reclass.values.scaitem import ScaItem
+from reclass.values.valuelist import ValueList
+from reclass.values.listitem import ListItem
+from reclass.values.dictitem import DictItem
+import unittest
+
+SETTINGS = Settings()
+
+class TestScaItem(unittest.TestCase):
+
+    def test_merge_over_merge_scalar(self):
+        scalar1 = ScaItem([1], SETTINGS)
+        scalar2 = ScaItem([2], SETTINGS)
+
+        result = scalar2.merge_over(scalar1)
+
+        self.assertEquals(result.contents, scalar2.contents)
+
+    def test_merge_over_merge_composite(self):
+        scalar1 = CompItem(Value(1, SETTINGS, ''), SETTINGS)
+        scalar2 = ScaItem([2], SETTINGS)
+
+        result = scalar2.merge_over(scalar1)
+
+        self.assertEquals(result.contents, scalar2.contents)
+
+    def test_merge_other_types_not_allowed(self):
+        other = type('Other', (object,), {'type': 34})
+        val1 = Value(None, SETTINGS, '')
+        scalar = ScaItem(val1, SETTINGS)
+
+        self.assertRaises(RuntimeError, scalar.merge_over, other)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/reclass/values/value.py b/reclass/values/value.py
index affd944..451617e 100644
--- a/reclass/values/value.py
+++ b/reclass/values/value.py
@@ -22,15 +22,15 @@
 
     def __init__(self, value, settings, uri, parse_string=True):
         self._settings = settings
-        self._uri = uri
+        self.uri = uri
         self.overwrite = False
-        self._constant = False
+        self.constant = False
         if isinstance(value, string_types):
             if parse_string:
                 try:
                     self._item = self._parser.parse(value, self._settings)
                 except InterpolationError as e:
-                    e.uri = self._uri
+                    e.uri = self.uri
                     raise
             else:
                 self._item = ScaItem(value, self._settings)
@@ -41,18 +41,6 @@
         else:
             self._item = ScaItem(value, self._settings)
 
-    @property
-    def uri(self):
-        return self._uri
-
-    @property
-    def constant(self):
-        return self._constant
-
-    @constant.setter
-    def constant(self, constant):
-        self._constant = constant
-
     def item_type(self):
         return self._item.type
 
@@ -78,8 +66,7 @@
     def needs_all_envs(self):
         if self._item.has_inv_query:
             return self._item.needs_all_envs
-        else:
-            return False
+        return False
 
     def ignore_failed_render(self):
         return self._item.ignore_failed_render
@@ -102,7 +89,7 @@
         try:
             return self._item.render(context, inventory)
         except InterpolationError as e:
-            e.uri = self._uri
+            e.uri = self.uri
             raise
 
     @property
diff --git a/reclass/values/valuelist.py b/reclass/values/valuelist.py
index 86563fa..e8c3a0c 100644
--- a/reclass/values/valuelist.py
+++ b/reclass/values/valuelist.py
@@ -22,9 +22,9 @@
         self.allRefs = True
         self._values = [value]
         self._inv_refs = []
-        self._has_inv_query = False
+        self.has_inv_query = False
         self.ignore_failed_render = False
-        self._is_complex = False
+        self.is_complex = False
         self._update()
 
     @property
@@ -42,40 +42,32 @@
     def _update(self):
         self.assembleRefs()
         self._check_for_inv_query()
-        self._is_complex = False
+        self.is_complex = False
         item_type = self._values[0].item_type()
         for v in self._values:
             if v.is_complex or v.constant or v.overwrite or v.item_type() != item_type:
-                self._is_complex = True
+                self.is_complex = True
 
     @property
     def has_references(self):
         return len(self._refs) > 0
 
-    @property
-    def has_inv_query(self):
-        return self._has_inv_query
-
     def get_inv_references(self):
         return self._inv_refs
 
-    @property
-    def is_complex(self):
-        return self._is_complex
-
     def get_references(self):
         return self._refs
 
     def _check_for_inv_query(self):
-        self._has_inv_query = False
+        self.has_inv_query = False
         self.ignore_failed_render = True
         for value in self._values:
             if value.has_inv_query:
                 self._inv_refs.extend(value.get_inv_references())
-                self._has_inv_query = True
+                self.has_inv_query = True
                 if value.ignore_failed_render() is False:
                     self.ignore_failed_render = False
-        if self._has_inv_query is False:
+        if self.has_inv_query is False:
             self.ignore_failed_render = False
 
     def assembleRefs(self, context={}):
diff --git a/reclass/version.py b/reclass/version.py
index 6d7d7eb..63fda26 100644
--- a/reclass/version.py
+++ b/reclass/version.py
@@ -12,12 +12,14 @@
 from __future__ import unicode_literals
 
 RECLASS_NAME = 'reclass'
-DESCRIPTION = 'merge data by recursive descent down an ancestry hierarchy (forked extended version)'
+DESCRIPTION = ('merge data by recursive descent down an ancestry hierarchy '
+               '(forked extended version)')
 VERSION = '1.5.6'
 AUTHOR = 'martin f. krafft / Andrew Pickford / salt-formulas community'
 AUTHOR_EMAIL = 'salt-formulas@freelists.org'
 MAINTAINER = 'salt-formulas community'
 MAINTAINER_EMAIL = 'salt-formulas@freelists.org'
-COPYRIGHT = 'Copyright © 2007–14 martin f. krafft, extensions © 2017 Andrew Pickford, extensions © salt-formulas community'
+COPYRIGHT = ('Copyright © 2007–14 martin f. krafft, extensions © 2017 Andrew'
+             ' Pickford, extensions © salt-formulas community')
 LICENCE = 'Artistic Licence 2.0'
 URL = 'https://github.com/salt-formulas/reclass'