diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 8402f2745c..0fecfaf2f8 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -260,10 +260,12 @@ for the ``heat_template_version`` key: The key with value ``2017-02-24`` or ``ocata`` indicates that the YAML document is a HOT template and it may contain features added and/or removed up until the Ocata release. This version adds the ``str_replace_strict`` - function which raises errors for missing params. The complete list of - supported functions is:: + function which raises errors for missing params and the ``filter`` function + which filters out values from lists. The complete list of supported + functions is:: digest + filter get_attr get_file get_param @@ -1782,3 +1784,33 @@ Another example reference other conditions This function returns true if any one of other_condition_1 or other_condition_2 evaluate to true, otherwise returns false. + +filter +------ +The ``filter`` function removes values from lists. + +The syntax of the ``filter`` function is + +.. code-block:: yaml + + filter: + - + - + +For example + +.. code-block:: yaml + + parameters: + list_param: + type: comma_delimited_list + default: [1, 2, 3] + + outputs: + output_list: + value: + filter: + - [3] + - {get_param: list_param} + +output_list will be evaluated to [1, 2]. diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index 3f81b7d743..f7af96c5f3 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -1271,3 +1271,49 @@ class Or(ConditionBoolean): def result(self): return any(self._get_condition(cd) for cd in function.resolve(self.args)) + + +class Filter(function.Function): + """A function for filtering out values from lists. + + Takes the form:: + + filter: + - + - + + Returns a new list without the values. + """ + def __init__(self, stack, fn_name, args): + super(Filter, self).__init__(stack, fn_name, args) + + self._values, self._sequence = self._parse_args() + + def _parse_args(self): + if (not isinstance(self.args, collections.Sequence) or + isinstance(self.args, six.string_types)): + raise TypeError(_('Argument to "%s" must be a list') % + self.fn_name) + + if len(self.args) != 2: + raise ValueError(_('"%(fn)s" expected 2 arguments of the form ' + '[values, sequence] but got %(len)d arguments ' + 'instead') % + {'fn': self.fn_name, 'len': len(self.args)}) + + return self.args[0], self.args[1] + + def result(self): + sequence = function.resolve(self._sequence) + if not sequence: + return sequence + if not isinstance(sequence, list): + raise TypeError(_('"%s" only works with lists') % self.fn_name) + + values = function.resolve(self._values) + if not values: + return sequence + if not isinstance(values, list): + raise TypeError( + _('"%(fn)s" filters a list of values') % self.fn_name) + return [i for i in sequence if i not in values] diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 17f36388f9..15ce69d443 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -539,6 +539,7 @@ class HOTemplate20170224(HOTemplate20161014): 'if': hot_funcs.If, # functions added in 2017-02-24 + 'filter': hot_funcs.Filter, 'str_replace_strict': hot_funcs.ReplaceJsonStrict, # functions removed from 2015-10-15 diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index 9f201148b6..643884166b 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -1757,6 +1757,46 @@ conditions: self.assertEqual(hot_tpl['resources'], empty.t['resources']) + def test_filter(self): + snippet = {'filter': [[None], [1, None, 4, 2, None]]} + tmpl = template.Template(hot_ocata_tpl_empty) + stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl) + resolved = self.resolve(snippet, tmpl, stack=stack) + + self.assertEqual([1, 4, 2], resolved) + + def test_filter_wrong_args_type(self): + snippet = {'filter': 'foo'} + tmpl = template.Template(hot_ocata_tpl_empty) + stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl) + self.assertRaises(exception.StackValidationFailed, self.resolve, + snippet, tmpl, stack=stack) + + def test_filter_wrong_args_number(self): + snippet = {'filter': [[None], [1, 2], 'foo']} + tmpl = template.Template(hot_ocata_tpl_empty) + stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl) + self.assertRaises(exception.StackValidationFailed, self.resolve, + snippet, tmpl, stack=stack) + + def test_filter_dict(self): + snippet = {'filter': [[None], {'a': 1}]} + tmpl = template.Template(hot_ocata_tpl_empty) + stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl) + self.assertRaises(TypeError, self.resolve, snippet, tmpl, stack=stack) + + def test_filter_str(self): + snippet = {'filter': [['a'], 'abcd']} + tmpl = template.Template(hot_ocata_tpl_empty) + stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl) + self.assertRaises(TypeError, self.resolve, snippet, tmpl, stack=stack) + + def test_filter_str_values(self): + snippet = {'filter': ['abcd', ['a', 'b', 'c', 'd']]} + tmpl = template.Template(hot_ocata_tpl_empty) + stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl) + self.assertRaises(TypeError, self.resolve, snippet, tmpl, stack=stack) + class HotStackTest(common.HeatTestCase): """Test stack function when stack was created from HOT template."""