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={}):