More refactoring

Added unit tests, removed some redundant code,
removed parser from settings -- it was hardcoded, so
no real reason to keep it there, amended logic for
parser application: previosly only default sentinels were
used in parser selection optimization, now a sentinel is
picked from settings.
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..5760239 100644
--- a/reclass/settings.py
+++ b/reclass/settings.py
@@ -6,7 +6,6 @@
 from __future__ import unicode_literals
 
 import copy
-import reclass.values.parser_funcs
 from reclass.defaults import *
 
 from six import string_types
@@ -43,8 +42,6 @@
 
         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)
 
     def __eq__(self, other):
         return isinstance(other, type(self)) \
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..4d4e12c 100644
--- a/reclass/values/parser.py
+++ b/reclass/values/parser.py
@@ -17,37 +17,41 @@
 
 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 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 ref_parser.parseString(value).asList()
             except pp.ParseException as e:
                 raise ParseError(e.msg, e.line, e.col, e.lineno)
 
+        self._settings = settings
+        parser_settings = (settings.escape_character,
+                           settings.reference_sentinels,
+                           settings.export_sentinels)
+        ref_parser = parsers.get_ref_parser(*parser_settings)
+        simple_ref_parser = parsers.get_simple_ref_parser(*parser_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 = 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)),
diff --git a/reclass/values/parser_funcs.py b/reclass/values/parser_funcs.py
index 50babd0..3c24b40 100644
--- a/reclass/values/parser_funcs.py
+++ b/reclass/values/parser_funcs.py
@@ -143,7 +143,7 @@
 
     item = reference | export | string
     line = pp.OneOrMore(item) + pp.StringEnd()
-    return line
+    return line.leaveWhitespace()
 
 def get_simple_ref_parser(escape_character, reference_sentinels, export_sentinels):
     _ESCAPE = escape_character
@@ -158,4 +158,4 @@
     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
+    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..736e01a 100644
--- a/reclass/values/value.py
+++ b/reclass/values/value.py
@@ -22,7 +22,7 @@
 
     def __init__(self, value, settings, uri, parse_string=True):
         self._settings = settings
-        self._uri = uri
+        self.uri = uri
         self.overwrite = False
         self._constant = False
         if isinstance(value, string_types):
@@ -30,7 +30,7 @@
                 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)
@@ -42,10 +42,6 @@
             self._item = ScaItem(value, self._settings)
 
     @property
-    def uri(self):
-        return self._uri
-
-    @property
     def constant(self):
         return self._constant
 
@@ -102,7 +98,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={}):