framework for $[] export parameters
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index 1aea3b3..c6c3b28 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -17,13 +17,15 @@
uri of the Entity that is being merged.
'''
def __init__(self, classes=None, applications=None, parameters=None,
- uri=None, name=None, environment=None):
+ exports=None, uri=None, name=None, environment=None):
if classes is None: classes = Classes()
self._set_classes(classes)
if applications is None: applications = Applications()
self._set_applications(applications)
if parameters is None: parameters = Parameters()
+ if exports is None: exports = Parameters()
self._set_parameters(parameters)
+ self._set_exports(exports)
self._uri = uri or ''
self._name = name or ''
self._environment = environment or ''
@@ -35,6 +37,7 @@
classes = property(lambda s: s._classes)
applications = property(lambda s: s._applications)
parameters = property(lambda s: s._parameters)
+ exports = property(lambda s: s._exports)
def _set_classes(self, classes):
if not isinstance(classes, Classes):
@@ -54,10 +57,17 @@
'instance of type %s' % type(parameters))
self._parameters = parameters
+ def _set_exports(self, exports):
+ if not isinstance(exports, Parameters):
+ raise TypeError('Entity.exports cannot be set to '\
+ 'instance of type %s' % type(exports))
+ self._exports = exports
+
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._name = other.name
self._uri = other.uri
self._environment = other.environment
@@ -66,13 +76,15 @@
self._parameters.merge(params)
def interpolate(self):
- self._parameters.interpolate()
+ self._exports.interpolate_from_external(self._parameters)
+ self._parameters.interpolate(exports={ self._name: self._exports.as_dict() })
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
@@ -84,6 +96,7 @@
self.classes,
self.applications,
self.parameters,
+ self.exports,
self.uri,
self.name)
@@ -91,5 +104,6 @@
return {'classes': self._classes.as_list(),
'applications': self._applications.as_list(),
'parameters': self._parameters.as_dict(),
+ 'exports': self._exports.as_dict(),
'environment': self._environment
}
diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py
index 91016d2..3af80ff 100644
--- a/reclass/datatypes/parameters.py
+++ b/reclass/datatypes/parameters.py
@@ -49,7 +49,7 @@
delimiter = Parameters.DEFAULT_PATH_DELIMITER
self._delimiter = delimiter
self._base = {}
- self._unrendered = {}
+ self._unrendered = None
self._escapes_handled = {}
if mapping is not None:
# we initialise by merging, otherwise the list of references might
@@ -199,6 +199,7 @@
"""
+ self._unrendered = None
if isinstance(other, dict):
wrapped = copy.deepcopy(other)
self._wrap_dict(wrapped)
@@ -213,17 +214,15 @@
raise TypeError('Cannot merge %s objects into %s' % (type(other),
self.__class__.__name__))
- def has_unresolved_refs(self):
- return len(self._unrendered) > 0
-
def render_simple(self, options=None):
if options is None:
options = MergeOptions()
+ self._unrendered = {}
self._render_simple_dict(self._base, DictPath(self.delimiter), options)
def _render_simple_container(self, container, key, value, path, options):
if isinstance(value, ValueList):
- if value.has_references():
+ if value.has_references() or value.has_exports():
self._unrendered[path.new_subpath(key)] = True
return
else:
@@ -237,10 +236,10 @@
self._render_simple_list(value, path.new_subpath(key), options)
container[key] = value
elif isinstance(value, Value):
- if value.has_references():
+ if value.has_references() or value.has_exports():
self._unrendered[path.new_subpath(key)] = True
else:
- container[key] = value.render({}, options)
+ container[key] = value.render(None, None, options)
def _render_simple_dict(self, dictionary, path, options):
for key, value in dictionary.iteritems():
@@ -250,19 +249,35 @@
for n, value in enumerate(item_list):
self._render_simple_container(item_list, n, value, path, options)
- def interpolate(self, options=None):
+ def _ensure_render_simple(self, options):
+ if self._unrendered is None:
+ self.render_simple(options)
+
+ def interpolate_from_external(self, external, options=None):
+ if self._unrendered is None:
+ options = MergeOptions()
+ self._ensure_render_simple(options)
+ external._ensure_render_simple(options)
+ while len(self._unrendered) > 0:
+ path, v = self._unrendered.iteritems().next()
+ value = path.get_value(self._base)
+ external._interpolate_references(path, value, None, options)
+ new = value.render(external._base, options)
+ path.set_value(self._base, new)
+ del self._unrendered[path]
+
+ def interpolate(self, exports=None, options=None):
if options is None:
options = MergeOptions()
- self._unrendered = {}
- self.render_simple(options)
- while self.has_unresolved_refs():
+ self._ensure_render_simple(options)
+ while len(self._unrendered) > 0:
# 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, value = self._unrendered.iteritems().next()
- self._interpolate_inner(path, options)
+ path, v = self._unrendered.iteritems().next()
+ self._interpolate_inner(path, exports, options)
- def _interpolate_inner(self, path, options):
+ def _interpolate_inner(self, path, exports, options):
value = path.get_value(self._base)
if not isinstance(value, (Value, ValueList)):
# references to lists and dicts are only deepcopied when merged
@@ -270,50 +285,55 @@
# list or dict has already been visited by _interpolate_inner
del self._unrendered[path]
return
- self._unrendered[path] = False # mark as seen
- for ref in value.get_references():
- path_from_ref = DictPath(self.delimiter, ref)
+ self._unrendered[path] = False
+ self._interpolate_references(path, value, exports, options)
+ new = self._interpolate_render_value(path, value, exports, options)
+ path.set_value(self._base, new)
+ del self._unrendered[path]
- if path_from_ref in self._unrendered:
- if self._unrendered[path_from_ref] is False:
- # every call to _interpolate_inner replaces the value of
- # self._unrendered[path] with False
- # Therefore, if we encounter False instead of True,
- # it means that we have already processed it and are now
- # faced with a cyclical reference.
- raise InfiniteRecursionError(path, ref)
+ def _interpolate_render_value(self, path, value, exports, options):
+ try:
+ new = value.render(self._base, exports, options)
+ except UndefinedVariableError as e:
+ raise UndefinedVariableError(e.var, path)
+
+ if isinstance(new, dict):
+ self._render_simple_dict(new, path, options)
+ elif isinstance(new, list):
+ self._render_simple_list(new, path, options)
+ return new
+
+ def _interpolate_references(self, path, value, exports, options):
+ all_refs = False
+ while not all_refs:
+ for ref in value.get_references():
+ path_from_ref = DictPath(self.delimiter, ref)
+
+ if path_from_ref in self._unrendered:
+ if self._unrendered[path_from_ref] is False:
+ # every call to _interpolate_inner replaces the value of
+ # self._unrendered[path] with False
+ # Therefore, if we encounter False instead of True,
+ # it means that we have already processed it and are now
+ # faced with a cyclical reference.
+ raise InfiniteRecursionError(path, ref)
+ else:
+ self._interpolate_inner(path_from_ref, exports, options)
else:
- self._interpolate_inner(path_from_ref, options)
+ # ensure ancestor keys are already dereferenced
+ ancestor = DictPath(self.delimiter)
+ for k in path_from_ref.key_parts():
+ ancestor = ancestor.new_subpath(k)
+ if ancestor in self._unrendered:
+ self._interpolate_inner(ancestor, exports, options)
+ if value.allRefs():
+ all_refs = True
else:
- # ensure ancestor keys are already dereferenced
- ancestor = DictPath(self.delimiter)
- for k in path_from_ref.key_parts():
- ancestor = ancestor.new_subpath(k)
- if ancestor in self._unrendered:
- self._interpolate_inner(ancestor, options)
-
- if value.allRefs():
- # all references have been deferenced so render value
- try:
- new = value.render(self._base, options)
- if isinstance(new, dict):
- self._render_simple_dict(new, path, options)
- elif isinstance(new, list):
- self._render_simple_list(new, path, options)
- path.set_value(self._base, new)
-
- # remove the reference from the unrendered list
- del self._unrendered[path]
- except UndefinedVariableError as e:
- raise UndefinedVariableError(e.var, path)
- else:
- # not all references in the value could be calculated previously so
- # try recalculating references with current context and recursively
- # call _interpolate_inner if the number of references has increased
- # Otherwise raise an error
- old = len(value.get_references())
- value.assembleRefs(self._base)
- if old != len(value.get_references()):
- self._interpolate_inner(path, options)
- else:
- raise InterpolationError('Bad reference count, path:' + repr(path))
+ # not all references in the value could be calculated previously so
+ # try recalculating references with current context and recursively
+ # call _interpolate_inner if the number of references has increased
+ # Otherwise raise an error
+ old = len(value.get_references())
+ value.assembleRefs(self._base)
+ if old == len(value.get_references()):
+ raise InterpolationError('Bad reference count, path:' + repr(path))
diff --git a/reclass/datatypes/tests/test_entity.py b/reclass/datatypes/tests/test_entity.py
index 17ec9e8..4976dd3 100644
--- a/reclass/datatypes/tests/test_entity.py
+++ b/reclass/datatypes/tests/test_entity.py
@@ -14,12 +14,11 @@
import mock
@mock.patch.multiple('reclass.datatypes', autospec=True, Classes=mock.DEFAULT,
- Applications=mock.DEFAULT,
- Parameters=mock.DEFAULT)
+ Applications=mock.DEFAULT, Parameters=mock.DEFAULT)
class TestEntity(unittest.TestCase):
def _make_instances(self, Classes, Applications, Parameters):
- return Classes(), Applications(), Parameters()
+ return Classes(), Applications(), Parameters(), mock.MagicMock(spec=Parameters)
def test_constructor_default(self, **mocks):
# Actually test the real objects by calling the default constructor,
@@ -30,19 +29,22 @@
self.assertIsInstance(e.classes, Classes)
self.assertIsInstance(e.applications, Applications)
self.assertIsInstance(e.parameters, Parameters)
+ self.assertIsInstance(e.exports, Parameters)
def test_constructor_empty(self, **types):
instances = self._make_instances(**types)
e = Entity(*instances)
self.assertEqual(e.name, '')
self.assertEqual(e.uri, '')
- cl, al, pl = [getattr(i, '__len__') for i in instances]
+ cl, al, pl, ex = [getattr(i, '__len__') for i in instances]
self.assertEqual(len(e.classes), cl.return_value)
cl.assert_called_once_with()
self.assertEqual(len(e.applications), al.return_value)
al.assert_called_once_with()
self.assertEqual(len(e.parameters), pl.return_value)
pl.assert_called_once_with()
+ self.assertEqual(len(e.exports), pl.return_value)
+ ex.assert_called_once_with()
def test_constructor_empty_named(self, **types):
name = 'empty'
@@ -147,6 +149,7 @@
comp['classes'] = instances[0].as_list()
comp['applications'] = instances[1].as_list()
comp['parameters'] = instances[2].as_dict()
+ comp['exports'] = instances[3].as_dict()
comp['environment'] = 'test'
d = entity.as_dict()
self.assertDictEqual(d, comp)
diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py
index e33f3b8..7dc2d7c 100644
--- a/reclass/datatypes/tests/test_parameters.py
+++ b/reclass/datatypes/tests/test_parameters.py
@@ -7,7 +7,7 @@
# Released under the terms of the Artistic Licence 2.0
#
from reclass.datatypes import Parameters
-from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, ESCAPE_CHARACTER
+from reclass.defaults import REFERENCE_SENTINELS, ESCAPE_CHARACTER
from reclass.errors import InfiniteRecursionError
from reclass.utils.mergeoptions import MergeOptions
import unittest
@@ -234,7 +234,7 @@
def test_interpolate_single(self):
v = 42
- d = {'foo': 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ d = {'foo': 'bar'.join(REFERENCE_SENTINELS),
'bar': v}
p = Parameters(d)
p.interpolate()
@@ -242,7 +242,7 @@
def test_interpolate_multiple(self):
v = '42'
- d = {'foo': 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS) + 'meep'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ d = {'foo': 'bar'.join(REFERENCE_SENTINELS) + 'meep'.join(REFERENCE_SENTINELS),
'bar': v[0],
'meep': v[1]}
p = Parameters(d)
@@ -251,8 +251,8 @@
def test_interpolate_multilevel(self):
v = 42
- d = {'foo': 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
- 'bar': 'meep'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ d = {'foo': 'bar'.join(REFERENCE_SENTINELS),
+ 'bar': 'meep'.join(REFERENCE_SENTINELS),
'meep': v}
p = Parameters(d)
p.interpolate()
@@ -260,7 +260,7 @@
def test_interpolate_list(self):
l = [41,42,43]
- d = {'foo': 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ d = {'foo': 'bar'.join(REFERENCE_SENTINELS),
'bar': l}
p = Parameters(d)
p.interpolate()
@@ -268,8 +268,8 @@
def test_interpolate_infrecursion(self):
v = 42
- d = {'foo': 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
- 'bar': 'foo'.join(PARAMETER_INTERPOLATION_SENTINELS)}
+ d = {'foo': 'bar'.join(REFERENCE_SENTINELS),
+ 'bar': 'foo'.join(REFERENCE_SENTINELS)}
p = Parameters(d)
with self.assertRaises(InfiniteRecursionError):
p.interpolate()
@@ -355,8 +355,8 @@
self.assertEqual(p1.as_dict(), r)
def test_interpolate_escaping(self):
- v = 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS)
- d = {'foo': ESCAPE_CHARACTER + 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ v = 'bar'.join(REFERENCE_SENTINELS)
+ d = {'foo': ESCAPE_CHARACTER + 'bar'.join(REFERENCE_SENTINELS),
'bar': 'unused'}
p = Parameters(d)
p.render_simple()
@@ -364,7 +364,7 @@
def test_interpolate_double_escaping(self):
v = ESCAPE_CHARACTER + 'meep'
- d = {'foo': ESCAPE_CHARACTER + ESCAPE_CHARACTER + 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ d = {'foo': ESCAPE_CHARACTER + ESCAPE_CHARACTER + 'bar'.join(REFERENCE_SENTINELS),
'bar': 'meep'}
p = Parameters(d)
p.interpolate()
@@ -379,7 +379,7 @@
# Escape character followed by escape character
'2', ESCAPE_CHARACTER + ESCAPE_CHARACTER,
# Escape character followed by interpolation end sentinel
- '3', ESCAPE_CHARACTER + PARAMETER_INTERPOLATION_SENTINELS[1],
+ '3', ESCAPE_CHARACTER + REFERENCE_SENTINELS[1],
# Escape character at the end of the string
'4', ESCAPE_CHARACTER
])
diff --git a/reclass/defaults.py b/reclass/defaults.py
index 2bc27a4..557d511 100644
--- a/reclass/defaults.py
+++ b/reclass/defaults.py
@@ -25,7 +25,8 @@
]
CONFIG_FILE_NAME = RECLASS_NAME + '-config.yml'
-PARAMETER_INTERPOLATION_SENTINELS = ('${', '}')
+REFERENCE_SENTINELS = ('${', '}')
+EXPORT_SENTINELS = ('$[', ']')
PARAMETER_INTERPOLATION_DELIMITER = ':'
PARAMETER_DICT_KEY_OVERRIDE_PREFIX = '~'
ESCAPE_CHARACTER = '\\'
diff --git a/reclass/errors.py b/reclass/errors.py
index d6936d3..b1504b8 100644
--- a/reclass/errors.py
+++ b/reclass/errors.py
@@ -10,7 +10,7 @@
import posix, sys
import traceback
-from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS
+from reclass.defaults import REFERENCE_SENTINELS
class ReclassException(Exception):
@@ -135,7 +135,7 @@
context = property(lambda self: self._context)
def _get_message(self):
- msg = "Cannot resolve " + self._var.join(PARAMETER_INTERPOLATION_SENTINELS)
+ msg = "Cannot resolve " + self._var.join(REFERENCE_SENTINELS)
if self._context:
msg += ' in the context of %s' % self._context
return msg
@@ -148,7 +148,7 @@
def __init__(self, string, end_sentinel):
super(IncompleteInterpolationError, self).__init__(msg=None)
- self._ref = string.join(PARAMETER_INTERPOLATION_SENTINELS)
+ self._ref = string.join(REFERENCE_SENTINELS)
self._end_sentinel = end_sentinel
def _get_message(self):
@@ -161,7 +161,7 @@
def __init__(self, path, ref):
super(InfiniteRecursionError, self).__init__(msg=None)
self._path = path
- self._ref = ref.join(PARAMETER_INTERPOLATION_SENTINELS)
+ self._ref = ref.join(REFERENCE_SENTINELS)
def _get_message(self):
msg = "Infinite recursion while resolving {0} at {1}"
diff --git a/reclass/storage/yaml_fs/yamlfile.py b/reclass/storage/yaml_fs/yamlfile.py
index 717a911..03a5c11 100644
--- a/reclass/storage/yaml_fs/yamlfile.py
+++ b/reclass/storage/yaml_fs/yamlfile.py
@@ -47,12 +47,17 @@
parameters = {}
parameters = datatypes.Parameters(parameters)
+ exports = self._data.get('exports')
+ if exports is None:
+ exports = {}
+ exports = datatypes.Parameters(exports)
+
env = self._data.get('environment', default_environment)
if name is None:
name = self._path
- return datatypes.Entity(classes, applications, parameters,
+ return datatypes.Entity(classes, applications, parameters, exports,
name=name, environment=env,
uri='yaml_fs://{0}'.format(self._path))
diff --git a/reclass/utils/compitem.py b/reclass/utils/compitem.py
index 2e41f92..12845f7 100644
--- a/reclass/utils/compitem.py
+++ b/reclass/utils/compitem.py
@@ -35,14 +35,17 @@
def get_references(self):
return self._refs
- def render(self, context):
+ def has_exports(self):
+ return False
+
+ def render(self, context, exports):
# Preserve type if only one item
if len(self._items) == 1:
- return self._items[0].render(context)
+ return self._items[0].render(context, exports)
# Multiple items
string = ''
for item in self._items:
- string += str(item.render(context))
+ string += str(item.render(context, exports))
return string
def __repr__(self):
diff --git a/reclass/utils/dictitem.py b/reclass/utils/dictitem.py
index d612480..1782e70 100644
--- a/reclass/utils/dictitem.py
+++ b/reclass/utils/dictitem.py
@@ -40,6 +40,9 @@
def has_references(self):
return len(self._refs) > 0
+ def has_exports(self):
+ return False
+
def get_references(self):
return self._refs
@@ -53,7 +56,7 @@
raise TypeError('allow dict over scalar = False: cannot merge %s onto %s' % (repr(self), repr(item)))
raise TypeError('Cannot merge %s over %s' % (repr(self), repr(item)))
- def render(self, context):
+ def render(self, context, exports):
return self._dict
def __repr__(self):
diff --git a/reclass/utils/dictpath.py b/reclass/utils/dictpath.py
index 7395679..a7353b1 100644
--- a/reclass/utils/dictpath.py
+++ b/reclass/utils/dictpath.py
@@ -132,3 +132,20 @@
def set_value(self, base, value):
self._get_innermost_container(base)[self._get_key()] = value
+
+ def drop_first(self):
+ del self._parts[0]
+
+ def exists_in(self, base):
+ container = base
+ for i in self._parts:
+ if isinstance(container, (dict, list)) and i in container:
+ if isinstance(container, dict):
+ container = container[i]
+ elif isinstance(container, list):
+ container = container[int(i)]
+ elif i == self._parts[-1]:
+ return True
+ else:
+ return False
+ return True
diff --git a/reclass/utils/expitem.py b/reclass/utils/expitem.py
new file mode 100644
index 0000000..fe2bf31
--- /dev/null
+++ b/reclass/utils/expitem.py
@@ -0,0 +1,50 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass
+#
+
+from reclass.utils.dictpath import DictPath
+from reclass.errors import UndefinedVariableError
+
+class ExpItem(object):
+
+ def __init__(self, items, delimiter):
+ self._delimiter = delimiter
+ self._items = items
+
+ def contents(self):
+ return self._items
+
+ def has_references(self):
+ return False
+
+ def has_exports(self):
+ return True
+
+ def _resolve(self, path, key, dictionary):
+ try:
+ return path.get_value(dictionary)
+ except KeyError as e:
+ raise UndefinedVariableError(key)
+
+ def _key(self):
+ if len(self._items) == 1:
+ return self._items[0].contents()
+ string = ''
+ for item in self._items:
+ string += item.contents()
+ return string
+
+ def render(self, context, exports):
+ result = []
+ exp_key = self._key()
+ exp_path = DictPath(self._delimiter, exp_key)
+ exp_path.drop_first()
+ for node, items in exports.iteritems():
+ if exp_path.exists_in(items):
+ result.append(self._resolve(exp_path, exp_key, items))
+ return result
+
+ def __repr__(self):
+ return 'ExpItem(%r)' % self._items
diff --git a/reclass/utils/listitem.py b/reclass/utils/listitem.py
index fa17491..d80ebc9 100644
--- a/reclass/utils/listitem.py
+++ b/reclass/utils/listitem.py
@@ -34,10 +34,13 @@
def has_references(self):
return len(self._refs) > 0
+ def has_exports(self):
+ return False
+
def get_references(self):
return self._refs
- def render(self, context):
+ def render(self, context, exports):
return self._list
def merge_over(self, item, options):
diff --git a/reclass/utils/refitem.py b/reclass/utils/refitem.py
index b14514d..7f1f759 100644
--- a/reclass/utils/refitem.py
+++ b/reclass/utils/refitem.py
@@ -25,7 +25,7 @@
item.assembleRefs(context)
self._refs.extend(item.get_references())
try:
- value += str(item.render(context))
+ value += str(item.render(context, None))
except UndefinedVariableError as e:
self._allRefs = False
if self._allRefs:
@@ -40,6 +40,9 @@
def has_references(self):
return len(self._refs) > 0
+ def has_exports(self):
+ return False
+
def get_references(self):
return self._refs
@@ -50,14 +53,12 @@
except KeyError as e:
raise UndefinedVariableError(ref)
- def render(self, context):
- # Preserve type if only one item
+ def render(self, context, exports):
if len(self._items) == 1:
- return self._resolve(self._items[0].render(context), context)
- # Multiple items
+ return self._resolve(self._items[0].render(context, exports), context)
string = ''
for item in self._items:
- string += str(item.render(context))
+ string += str(item.render(context, exports))
return self._resolve(string, context)
def __repr__(self):
diff --git a/reclass/utils/scaitem.py b/reclass/utils/scaitem.py
index b9e49f8..421afdc 100644
--- a/reclass/utils/scaitem.py
+++ b/reclass/utils/scaitem.py
@@ -21,6 +21,9 @@
def has_references(self):
return False
+ def has_exports(self):
+ return False
+
def contents(self):
return self._value
@@ -42,7 +45,7 @@
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):
+ def render(self, context, exports):
return self._value
def __repr__(self):
diff --git a/reclass/utils/tests/test_value.py b/reclass/utils/tests/test_value.py
index 5e11ef4..f4ac296 100644
--- a/reclass/utils/tests/test_value.py
+++ b/reclass/utils/tests/test_value.py
@@ -10,15 +10,15 @@
import pyparsing as pp
from reclass.utils.value import Value
-from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, \
+from reclass.defaults import REFERENCE_SENTINELS, \
PARAMETER_INTERPOLATION_DELIMITER
from reclass.errors import UndefinedVariableError, \
IncompleteInterpolationError, ParseError
import unittest
def _var(s):
- return '%s%s%s' % (PARAMETER_INTERPOLATION_SENTINELS[0], s,
- PARAMETER_INTERPOLATION_SENTINELS[1])
+ return '%s%s%s' % (REFERENCE_SENTINELS[0], s,
+ REFERENCE_SENTINELS[1])
CONTEXT = {'favcolour':'yellow',
'motd':{'greeting':'Servus!',
@@ -39,12 +39,12 @@
s = 'my cat likes to hide in boxes'
tv = Value(s)
self.assertFalse(tv.has_references())
- self.assertEquals(tv.render(CONTEXT), s)
+ self.assertEquals(tv.render(CONTEXT, None), s)
def _test_solo_ref(self, key):
s = _var(key)
tv = Value(s)
- res = tv.render(CONTEXT)
+ res = tv.render(CONTEXT, None)
self.assertTrue(tv.has_references())
self.assertEqual(res, CONTEXT[key])
@@ -67,7 +67,7 @@
s = 'I like ' + _var('favcolour') + ' and I like it'
tv = Value(s)
self.assertTrue(tv.has_references())
- self.assertEqual(tv.render(CONTEXT),
+ self.assertEqual(tv.render(CONTEXT, None),
_poor_mans_template(s, 'favcolour',
CONTEXT['favcolour']))
@@ -75,7 +75,7 @@
s = _var('favcolour') + ' is my favourite colour'
tv = Value(s)
self.assertTrue(tv.has_references())
- self.assertEqual(tv.render(CONTEXT),
+ self.assertEqual(tv.render(CONTEXT, None),
_poor_mans_template(s, 'favcolour',
CONTEXT['favcolour']))
@@ -83,7 +83,7 @@
s = 'I like ' + _var('favcolour')
tv = Value(s)
self.assertTrue(tv.has_references())
- self.assertEqual(tv.render(CONTEXT),
+ self.assertEqual(tv.render(CONTEXT, None),
_poor_mans_template(s, 'favcolour',
CONTEXT['favcolour']))
@@ -92,7 +92,7 @@
s = _var(var)
tv = Value(s)
self.assertTrue(tv.has_references())
- self.assertEqual(tv.render(CONTEXT),
+ self.assertEqual(tv.render(CONTEXT, None),
_poor_mans_template(s, var,
CONTEXT['motd']['greeting']))
@@ -103,7 +103,7 @@
self.assertTrue(tv.has_references())
want = _poor_mans_template(s, greet, CONTEXT['motd']['greeting'])
want = _poor_mans_template(want, 'favcolour', CONTEXT['favcolour'])
- self.assertEqual(tv.render(CONTEXT), want)
+ self.assertEqual(tv.render(CONTEXT, None), want)
def test_multiple_subst_flush(self):
greet = PARAMETER_INTERPOLATION_DELIMITER.join(('motd', 'greeting'))
@@ -112,16 +112,16 @@
self.assertTrue(tv.has_references())
want = _poor_mans_template(s, greet, CONTEXT['motd']['greeting'])
want = _poor_mans_template(want, 'favcolour', CONTEXT['favcolour'])
- self.assertEqual(tv.render(CONTEXT), want)
+ self.assertEqual(tv.render(CONTEXT, None), want)
def test_undefined_variable(self):
s = _var('no_such_variable')
tv = Value(s)
with self.assertRaises(UndefinedVariableError):
- tv.render(CONTEXT)
+ tv.render(CONTEXT, None)
def test_incomplete_variable(self):
- s = PARAMETER_INTERPOLATION_SENTINELS[0] + 'incomplete'
+ s = REFERENCE_SENTINELS[0] + 'incomplete'
with self.assertRaises(ParseError):
tv = Value(s)
diff --git a/reclass/utils/value.py b/reclass/utils/value.py
index 7af16c1..65dbe8d 100644
--- a/reclass/utils/value.py
+++ b/reclass/utils/value.py
@@ -9,25 +9,40 @@
from reclass.utils.mergeoptions import MergeOptions
from reclass.utils.compitem import CompItem
from reclass.utils.dictitem import DictItem
+from reclass.utils.expitem import ExpItem
from reclass.utils.listitem import ListItem
from reclass.utils.refitem import RefItem
from reclass.utils.scaitem import ScaItem
-from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER, PARAMETER_INTERPOLATION_SENTINELS, ESCAPE_CHARACTER
+from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER, ESCAPE_CHARACTER, REFERENCE_SENTINELS, EXPORT_SENTINELS
from reclass.errors import *
_STR = 'STR'
_REF = 'REF'
-_OPEN = PARAMETER_INTERPOLATION_SENTINELS[0]
-_CLOSE = PARAMETER_INTERPOLATION_SENTINELS[1]
-_CLOSE_FIRST = _CLOSE[0]
+_EXP = 'EXP'
+
_ESCAPE = ESCAPE_CHARACTER
_DOUBLE_ESCAPE = _ESCAPE + _ESCAPE
-_ESCAPE_OPEN = _ESCAPE + _OPEN
-_ESCAPE_CLOSE = _ESCAPE + _CLOSE
-_DOUBLE_ESCAPE_OPEN = _DOUBLE_ESCAPE + _OPEN
-_DOUBLE_ESCAPE_CLOSE = _DOUBLE_ESCAPE + _CLOSE
-_EXCLUDES = _ESCAPE + _OPEN + _CLOSE
+
+_REF_OPEN = REFERENCE_SENTINELS[0]
+_REF_CLOSE = REFERENCE_SENTINELS[1]
+_REF_CLOSE_FIRST = _REF_CLOSE[0]
+_REF_ESCAPE_OPEN = _ESCAPE + _REF_OPEN
+_REF_ESCAPE_CLOSE = _ESCAPE + _REF_CLOSE
+_REF_DOUBLE_ESCAPE_OPEN = _DOUBLE_ESCAPE + _REF_OPEN
+_REF_DOUBLE_ESCAPE_CLOSE = _DOUBLE_ESCAPE + _REF_CLOSE
+_REF_EXCLUDES = _ESCAPE + _REF_OPEN + _REF_CLOSE
+
+_EXP_OPEN = EXPORT_SENTINELS[0]
+_EXP_CLOSE = EXPORT_SENTINELS[1]
+_EXP_CLOSE_FIRST = _EXP_CLOSE[0]
+_EXP_ESCAPE_OPEN = _ESCAPE + _EXP_OPEN
+_EXP_ESCAPE_CLOSE = _ESCAPE + _EXP_CLOSE
+_EXP_DOUBLE_ESCAPE_OPEN = _DOUBLE_ESCAPE + _EXP_OPEN
+_EXP_DOUBLE_ESCAPE_CLOSE = _DOUBLE_ESCAPE + _EXP_CLOSE
+_EXP_EXCLUDES = _ESCAPE + _EXP_OPEN + _EXP_CLOSE
+
+_EXCLUDES = _ESCAPE + _REF_OPEN + _REF_CLOSE + _EXP_OPEN + _EXP_CLOSE
class Value(object):
@@ -42,28 +57,44 @@
token = list(tokens[0])
tokens[0] = (_REF, token)
- ref_open = pp.Literal(_OPEN).suppress()
- ref_close = pp.Literal(_CLOSE).suppress()
- not_open = ~pp.Literal(_OPEN) + ~pp.Literal(_ESCAPE_OPEN) + ~pp.Literal(_DOUBLE_ESCAPE_OPEN)
- not_close = ~pp.Literal(_CLOSE) + ~pp.Literal(_ESCAPE_CLOSE) + ~pp.Literal(_DOUBLE_ESCAPE_CLOSE)
- escape_open = pp.Literal(_ESCAPE_OPEN).setParseAction(pp.replaceWith(_OPEN))
- escape_close = pp.Literal(_ESCAPE_CLOSE).setParseAction(pp.replaceWith(_CLOSE))
- double_escape = pp.Combine(pp.Literal(_DOUBLE_ESCAPE) + pp.MatchFirst([pp.FollowedBy(_OPEN), pp.FollowedBy(_CLOSE)])).setParseAction(pp.replaceWith(_ESCAPE))
- text = pp.MatchFirst([pp.Word(pp.printables, excludeChars=_EXCLUDES), pp.CharsNotIn('', exact=1)])
- text_ref = pp.MatchFirst([pp.Word(pp.printables, excludeChars=_EXCLUDES), pp.CharsNotIn(_CLOSE_FIRST, exact=1)])
+ def _export(string, location, tokens):
+ token = list(tokens[0])
+ tokens[0] = (_EXP, token)
+
white_space = pp.White()
+ double_escape = pp.Combine(pp.Literal(_DOUBLE_ESCAPE) + pp.MatchFirst([pp.FollowedBy(_REF_OPEN), pp.FollowedBy(_REF_CLOSE)])).setParseAction(pp.replaceWith(_ESCAPE))
- content = pp.Combine(pp.OneOrMore(not_open + text))
- ref_content = pp.Combine(pp.OneOrMore(not_open + not_close + text_ref))
- string = pp.MatchFirst([double_escape, escape_open, content, white_space]).setParseAction(_string)
- refString = pp.MatchFirst([double_escape, escape_open, escape_close, ref_content, white_space]).setParseAction(_string)
+ ref_open = pp.Literal(_REF_OPEN).suppress()
+ ref_close = pp.Literal(_REF_CLOSE).suppress()
+ ref_not_open = ~pp.Literal(_REF_OPEN) + ~pp.Literal(_REF_ESCAPE_OPEN) + ~pp.Literal(_REF_DOUBLE_ESCAPE_OPEN)
+ ref_not_close = ~pp.Literal(_REF_CLOSE) + ~pp.Literal(_REF_ESCAPE_CLOSE) + ~pp.Literal(_REF_DOUBLE_ESCAPE_CLOSE)
+ ref_escape_open = pp.Literal(_REF_ESCAPE_OPEN).setParseAction(pp.replaceWith(_REF_OPEN))
+ ref_escape_close = pp.Literal(_REF_ESCAPE_CLOSE).setParseAction(pp.replaceWith(_REF_CLOSE))
+ ref_text = pp.MatchFirst([pp.Word(pp.printables, excludeChars=_REF_EXCLUDES), pp.CharsNotIn(_REF_CLOSE_FIRST, exact=1)])
+ ref_content = pp.Combine(pp.OneOrMore(ref_not_open + ref_not_close + ref_text))
+ ref_string = pp.MatchFirst([double_escape, ref_escape_open, ref_escape_close, ref_content, white_space]).setParseAction(_string)
+ ref_item = pp.Forward()
+ ref_items = pp.OneOrMore(ref_item)
+ reference = (ref_open + pp.Group(ref_items) + ref_close).setParseAction(_reference)
+ ref_item << (reference | ref_string)
- refItem = pp.Forward()
- refItems = pp.OneOrMore(refItem)
- reference = (ref_open + pp.Group(refItems) + ref_close).setParseAction(_reference)
- refItem << (reference | refString)
+ exp_open = pp.Literal(_EXP_OPEN).suppress()
+ exp_close = pp.Literal(_EXP_CLOSE).suppress()
+ exp_not_open = ~pp.Literal(_EXP_OPEN) + ~pp.Literal(_EXP_ESCAPE_OPEN) + ~pp.Literal(_EXP_DOUBLE_ESCAPE_OPEN)
+ exp_not_close = ~pp.Literal(_EXP_CLOSE) + ~pp.Literal(_EXP_ESCAPE_CLOSE) + ~pp.Literal(_EXP_DOUBLE_ESCAPE_CLOSE)
+ exp_escape_open = pp.Literal(_EXP_ESCAPE_OPEN).setParseAction(pp.replaceWith(_EXP_OPEN))
+ exp_escape_close = pp.Literal(_EXP_ESCAPE_CLOSE).setParseAction(pp.replaceWith(_EXP_CLOSE))
+ exp_text = pp.MatchFirst([pp.Word(pp.printables, excludeChars=_EXP_EXCLUDES), pp.CharsNotIn(_EXP_CLOSE_FIRST, exact=1)])
+ exp_content = pp.Combine(pp.OneOrMore(exp_not_open + exp_not_close + exp_text))
+ exp_string = pp.MatchFirst([double_escape, exp_escape_open, exp_escape_close, exp_content, white_space]).setParseAction(_string)
+ exp_items = pp.OneOrMore(exp_string)
+ export = (exp_open + pp.Group(exp_items) + exp_close).setParseAction(_export)
- item = reference | string
+ text = pp.MatchFirst([pp.Word(pp.printables, excludeChars=_EXCLUDES), pp.CharsNotIn('', exact=1)])
+ content = pp.Combine(pp.OneOrMore(ref_not_open + exp_not_open + text))
+ string = pp.MatchFirst([double_escape, ref_escape_open, exp_escape_open, content, white_space]).setParseAction(_string)
+
+ item = pp.MatchFirst([reference, export, string])
line = pp.OneOrMore(item) + pp.StringEnd()
return line
@@ -103,6 +134,12 @@
items.append(self._createRef(token[1]))
return RefItem(items, self._delimiter)
+ def _createExp(self, tokens):
+ items = []
+ for token in tokens:
+ items.append(ScaItem(token[1]))
+ return ExpItem(items, self._delimiter)
+
def _createItems(self, tokens):
items = []
for token in tokens:
@@ -110,6 +147,8 @@
items.append(ScaItem(token[1]))
elif token[0] == _REF:
items.append(self._createRef(token[1]))
+ elif token[0] == _EXP:
+ items.append(self._createExp(token[1]))
return items
def assembleRefs(self, context={}):
@@ -130,11 +169,14 @@
def has_references(self):
return len(self._refs) > 0
+ def has_exports(self):
+ return self._item.has_exports()
+
def get_references(self):
return self._refs
- def render(self, context, dummy=None):
- return self._item.render(context)
+ def render(self, context, exports, dummy=None):
+ return self._item.render(context, exports)
def contents(self):
return self._item.contents()
diff --git a/reclass/utils/valuelist.py b/reclass/utils/valuelist.py
index 0fa7d0a..29d6d68 100644
--- a/reclass/utils/valuelist.py
+++ b/reclass/utils/valuelist.py
@@ -17,6 +17,8 @@
if value is not None:
self._values.append(value)
self.assembleRefs()
+ self._has_exports = False
+ self._check_for_exports()
def append(self, value):
self._values.append(value)
@@ -31,12 +33,21 @@
def has_references(self):
return len(self._refs) > 0
+ def has_exports(self):
+ return self._has_exports
+
def get_references(self):
return self._refs
def allRefs(self):
return self._allRefs
+ def _check_for_exports(self):
+ self._has_exports = False
+ for value in self._values:
+ if value.has_exports():
+ self._has_exports = True
+
def assembleRefs(self, context={}):
self._refs = []
self._allRefs = True
@@ -58,7 +69,7 @@
output = value.merge_over(output, options)
return output
- def render(self, context, options=None):
+ def render(self, context, exports, options=None):
from reclass.datatypes.parameters import Parameters
if options is None:
@@ -67,10 +78,10 @@
deepCopied = False
for n, value in enumerate(self._values):
if output is None:
- output = self._values[n].render(context, options)
+ output = self._values[n].render(context, exports, options)
deepCopied = False
else:
- new = value.render(context, options)
+ new = value.render(context, exports, options)
if isinstance(output, dict) and isinstance(new, dict):
p1 = Parameters(output, value._delimiter)
p2 = Parameters(new, value._delimiter)