From 347b99c497566b687acdf8c2b783fa20bc2f0a5b Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 13:47:52 +0200 Subject: [PATCH 01/14] Move parameter interpolation into own class. This makes it easier to add other kinds of interpolation later on. --- reclass/datatypes/parameters.py | 57 +++++++++++++++++---------------- reclass/utils/refvalue.py | 54 +++++++++++++++++++++---------- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py index 37419fc6..f669973f 100644 --- a/reclass/datatypes/parameters.py +++ b/reclass/datatypes/parameters.py @@ -10,7 +10,7 @@ from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER from reclass.utils.dictpath import DictPath -from reclass.utils.refvalue import RefValue +from reclass.utils.refvalue import RefValue, ReferenceParameter from reclass.errors import InfiniteRecursionError, UndefinedVariableError class Parameters(object): @@ -182,33 +182,34 @@ def interpolate(self): def _interpolate_inner(self, path, refvalue): self._occurrences[path] = True # mark as seen for ref in refvalue.get_references(): - path_from_ref = DictPath(self.delimiter, ref) - try: - refvalue_inner = self._occurrences[path_from_ref] - - # If there is no reference, then this will throw a KeyError, - # look further down where this is caught and execution passed - # to the next iteration of the loop - # - # If we get here, then the ref references another parameter, - # requiring us to recurse, dereferencing first those refs that - # are most used and are thus at the leaves of the dependency - # tree. - - if refvalue_inner is True: - # every call to _interpolate_inner replaces the value of - # the saved occurrences of a reference with True. - # Therefore, if we encounter True instead of a refvalue, - # it means that we have already processed it and are now - # faced with a cyclical reference. - raise InfiniteRecursionError(path, ref) - self._interpolate_inner(path_from_ref, refvalue_inner) - - except KeyError as e: - # not actually an error, but we are done resolving all - # dependencies of the current ref, so move on - continue - + if isinstance(ref, ReferenceParameter): + paths_from_ref = ref.get_dependencies(delim=self.delimiter) + for path_from_ref in paths_from_ref: + try: + refvalue_inner = self._occurrences[path_from_ref] + + # If there is no reference, then this will throw a KeyError, + # look further down where this is caught and execution passed + # to the next iteration of the loop + # + # If we get here, then the ref references another parameter, + # requiring us to recurse, dereferencing first those refs that + # are most used and are thus at the leaves of the dependency + # tree. + + if refvalue_inner is True: + # every call to _interpolate_inner replaces the value of + # the saved occurrences of a reference with True. + # Therefore, if we encounter True instead of a refvalue, + # it means that we have already processed it and are now + # faced with a cyclical reference. + raise InfiniteRecursionError(path, ref.string) + self._interpolate_inner(path_from_ref, refvalue_inner) + + except KeyError as e: + # not actually an error, but we are done resolving all + # dependencies of the current ref, so move on + continue try: new = refvalue.render(self._base) path.set_value(self._base, new) diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index b8e730be..92957eae 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -15,8 +15,30 @@ from reclass.errors import IncompleteInterpolationError, \ UndefinedVariableError -_SENTINELS = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] -_RE = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS) +_SENTINELS_PARAMETER = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] + +_RE_PARAMETER = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS_PARAMETER) + + +class Reference(object): + def __init__(self, string): + self.string = string + + +class ReferenceParameter(Reference): + def __init__(self, string): + super(ReferenceParameter, self).__init__(string) + + def resolve(self, context, **kwargs): + path = DictPath(kwargs['delim'], self.string) + try: + return path.get_value(context) + except KeyError as e: + raise UndefinedVariableError(self.string) + + def get_dependencies(self, **kwargs): + return [DictPath(kwargs['delim'], self.string)] + class RefValue(object): ''' @@ -54,7 +76,7 @@ class RefValue(object): the default delimiter. ''' - INTERPOLATION_RE = re.compile(_RE) + INTERPOLATION_RE_PARAMETER = re.compile(_RE_PARAMETER) def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER): self._strings = [] @@ -63,24 +85,21 @@ def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER): self._parse(string) def _parse(self, string): - parts = RefValue.INTERPOLATION_RE.split(string) + parts = RefValue.INTERPOLATION_RE_PARAMETER.split(string) self._refs = parts[1:][::2] + self._refs = [ReferenceParameter(ref) for ref in self._refs] self._strings = parts[0:][::2] - self._check_strings(string) + self._check_strings(string, self._strings, PARAMETER_INTERPOLATION_SENTINELS) + - def _check_strings(self, orig): - for s in self._strings: - pos = s.find(PARAMETER_INTERPOLATION_SENTINELS[0]) + def _check_strings(self, orig, strings, sentinel): + for s in strings: + pos = s.find(sentinel[0]) if pos >= 0: - raise IncompleteInterpolationError(orig, - PARAMETER_INTERPOLATION_SENTINELS[1]) + raise IncompleteInterpolationError(orig, sentinel[1]) def _resolve(self, ref, context): - path = DictPath(self._delim, ref) - try: - return path.get_value(context) - except KeyError as e: - raise UndefinedVariableError(ref) + return ref.resolve(context, delim=self._delim) def has_references(self): return len(self._refs) > 0 @@ -107,9 +126,10 @@ def _assemble(self, resolver): def render(self, context): resolver = lambda s: self._resolve(s, context) - return self._assemble(resolver) + ret = self._assemble(resolver) + return ret def __repr__(self): - do_not_resolve = lambda s: s.join(PARAMETER_INTERPOLATION_SENTINELS) + do_not_resolve = lambda s: s.string.join(PARAMETER_INTERPOLATION_SENTINELS) return 'RefValue(%r, %r)' % (self._assemble(do_not_resolve), self._delim) From 0996ee160c89c83150daaae00ce7864f927b575a Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 13:52:32 +0200 Subject: [PATCH 02/14] Add function interpolation. Everything inside $[...] blocks is treated as a function. Right now, only print() is supported, which just concatenates and all its arguments to a string. So for example, key: $[print(a, b, c)] would resolve to key: "a b c" --- reclass/defaults.py | 2 ++ reclass/utils/refvalue.py | 74 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/reclass/defaults.py b/reclass/defaults.py index d0662908..490c9ced 100644 --- a/reclass/defaults.py +++ b/reclass/defaults.py @@ -26,3 +26,5 @@ PARAMETER_INTERPOLATION_SENTINELS = ('${', '}') PARAMETER_INTERPOLATION_DELIMITER = ':' + +FUNCTION_INTERPOLATION_SENTINELS = ('$[', ']') diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 92957eae..fee4b21a 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -11,13 +11,20 @@ from reclass.utils.dictpath import DictPath from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, \ - PARAMETER_INTERPOLATION_DELIMITER + PARAMETER_INTERPOLATION_DELIMITER, \ + FUNCTION_INTERPOLATION_SENTINELS from reclass.errors import IncompleteInterpolationError, \ UndefinedVariableError _SENTINELS_PARAMETER = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] +_SENTINELS_FUNCTIONS = [re.escape(s) for s in FUNCTION_INTERPOLATION_SENTINELS] _RE_PARAMETER = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS_PARAMETER) +_RE_FUNCTIONS = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS_FUNCTIONS) + +# matches a string like 'function, args)' +_RE_FUNC = '([^(]+)\(([^)]+)\)' +_RE_FUNC = re.compile(_RE_FUNC) class Reference(object): @@ -25,6 +32,42 @@ def __init__(self, string): self.string = string +class ReferenceFunction(Reference): + def __init__(self, string): + super(ReferenceFunction, self).__init__(string) + + def resolve(self, context, **kwargs): + return self._execute(context) + + def _execute(self, context): + match = _RE_FUNC.match(self.string) + func_name = match.group(1) + func_args = match.groups()[1].split(',') + + func_args = [f.strip(' ') for f in func_args] + + print("name: " + str(func_name)) + print("args: " + str(func_args)) + + #if func_name == 'aggregate': + # result = [] + # matching_hosts = [] + # for host in hosts: + # if func_args[0](host) is True: + # matching_hosts.append(host) + # result = [] + # for host in matching_hosts: + # result.append(func_args[1](host)) + # return result + + if func_name == 'print': + return ' '.join(func_args) + + def get_dependences(self, **kwargs): + return [] + + + class ReferenceParameter(Reference): def __init__(self, string): super(ReferenceParameter, self).__init__(string) @@ -77,6 +120,7 @@ class RefValue(object): ''' INTERPOLATION_RE_PARAMETER = re.compile(_RE_PARAMETER) + INTERPOLATION_RE_FUNCTIONS = re.compile(_RE_FUNCTIONS) def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER): self._strings = [] @@ -91,6 +135,34 @@ def _parse(self, string): self._strings = parts[0:][::2] self._check_strings(string, self._strings, PARAMETER_INTERPOLATION_SENTINELS) + # each string could contain a function + for i in range(len(self._strings)): + strings, refs = self._parse_functions(self._strings[i]) + refs = [ReferenceFunction(ref) for ref in refs] + if len(refs) == 0: + continue + del self._strings[i] + self._strings.insert(i, strings) + self._refs.insert(i, refs) + + self._refs = self._flatten(self._refs) + self._strings = self._flatten(self._strings) + + def _flatten(self, l): + ret = [] + for element in l: + if isinstance(element, list): + ret.extend(element) + else: + ret.append(element) + return ret + + def _parse_functions(self, string): + parts = RefValue.INTERPOLATION_RE_FUNCTIONS.split(string) + strings = parts[0:][::2] + functions = parts[1:][::2] + self._check_strings(string, strings, FUNCTION_INTERPOLATION_SENTINELS) + return (strings, functions) def _check_strings(self, orig, strings, sentinel): for s in strings: From 3e2aa320117fdb5f54c785d37d8da3b35d48bb83 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 14:11:17 +0200 Subject: [PATCH 03/14] Add proper error handling if function is not known. --- reclass/datatypes/parameters.py | 5 ++++- reclass/errors.py | 13 ++++++++++++- reclass/utils/refvalue.py | 6 +++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py index f669973f..69c2cc0e 100644 --- a/reclass/datatypes/parameters.py +++ b/reclass/datatypes/parameters.py @@ -11,7 +11,8 @@ from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER from reclass.utils.dictpath import DictPath from reclass.utils.refvalue import RefValue, ReferenceParameter -from reclass.errors import InfiniteRecursionError, UndefinedVariableError +from reclass.errors import InfiniteRecursionError, UndefinedVariableError, \ + UndefinedFunctionError class Parameters(object): ''' @@ -218,4 +219,6 @@ def _interpolate_inner(self, path, refvalue): del self._occurrences[path] except UndefinedVariableError as e: raise UndefinedVariableError(e.var, path) + except UndefinedFunctionError as e: + raise UndefinedFunctionError(e.var) diff --git a/reclass/errors.py b/reclass/errors.py index ddb95fdb..33780531 100644 --- a/reclass/errors.py +++ b/reclass/errors.py @@ -10,7 +10,8 @@ import posix, sys import traceback -from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS +from reclass.defaults import (PARAMETER_INTERPOLATION_SENTINELS, +FUNCTION_INTERPOLATION_SENTINELS) class ReclassException(Exception): @@ -144,6 +145,16 @@ def set_context(self, context): self._context = context +class UndefinedFunctionError(InterpolationError): + def __init__(self, var): + super(UndefinedFunctionError, self).__init__(msg=None) + self._var = var + var = property(lambda self: self._var) + + def _get_message(self): + return "Unknown function in " + self._var.join(FUNCTION_INTERPOLATION_SENTINELS) + + class IncompleteInterpolationError(InterpolationError): def __init__(self, string, end_sentinel): diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index fee4b21a..7a417723 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -14,7 +14,8 @@ PARAMETER_INTERPOLATION_DELIMITER, \ FUNCTION_INTERPOLATION_SENTINELS from reclass.errors import IncompleteInterpolationError, \ - UndefinedVariableError + UndefinedVariableError, \ + UndefinedFunctionError _SENTINELS_PARAMETER = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] _SENTINELS_FUNCTIONS = [re.escape(s) for s in FUNCTION_INTERPOLATION_SENTINELS] @@ -62,6 +63,9 @@ def _execute(self, context): if func_name == 'print': return ' '.join(func_args) + else: + raise UndefinedFunctionError(self.string) + def get_dependences(self, **kwargs): return [] From 98e93a5e054bbfee5d7abb473d57aacda5d803a5 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 14:50:54 +0200 Subject: [PATCH 04/14] Add function module. --- reclass/utils/function.py | 54 +++++++++++++++++++++++++++++++++++++++ reclass/utils/refvalue.py | 24 ++++------------- 2 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 reclass/utils/function.py diff --git a/reclass/utils/function.py b/reclass/utils/function.py new file mode 100644 index 00000000..a25edbd0 --- /dev/null +++ b/reclass/utils/function.py @@ -0,0 +1,54 @@ +# +# -*- coding: utf-8 -*- +# +# This file is part of reclass (http://github.com/madduck/reclass) +# +# Copyright © 2007–14 martin f. krafft +# Released under the terms of the Artistic Licence 2.0 +# + +from reclass.errors import UndefinedFunctionError + + +def get_function(name): + if name == 'print': + return FunctionPrint() + if name == 'aggregate': + return FunctionAggregate() + else: + raise UndefinedFunctionError(name) + + +class Function(object): + def __init__(self): + pass + + def execute(self, *args, **kwargs): + pass + + +class FunctionPrint(Function): + def __init__(self): + super(FunctionPrint, self).__init__() + + def execute(self, *args): + return " ".join(*args) + + +class FunctionAggregate(Function): + def __init__(self): + super(FunctionAggregate, self).__init__() + + def execute(self, *args): + return "{aggregate()}" + #result = [] + #matching_hosts = [] + #for host in hosts: + # if func_args[0](host) is True: + # matching_hosts.append(host) + #result = [] + #for host in matching_hosts: + # result.append(func_args[1](host)) + #return result + + diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 7a417723..8f5d5d9e 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -16,6 +16,7 @@ from reclass.errors import IncompleteInterpolationError, \ UndefinedVariableError, \ UndefinedFunctionError +from reclass.utils.function import get_function _SENTINELS_PARAMETER = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] _SENTINELS_FUNCTIONS = [re.escape(s) for s in FUNCTION_INTERPOLATION_SENTINELS] @@ -47,31 +48,16 @@ def _execute(self, context): func_args = [f.strip(' ') for f in func_args] - print("name: " + str(func_name)) - print("args: " + str(func_args)) - - #if func_name == 'aggregate': - # result = [] - # matching_hosts = [] - # for host in hosts: - # if func_args[0](host) is True: - # matching_hosts.append(host) - # result = [] - # for host in matching_hosts: - # result.append(func_args[1](host)) - # return result - - if func_name == 'print': - return ' '.join(func_args) - else: + try: + func = get_function(func_name) + return func.execute(func_args) + except UndefinedFunctionError: raise UndefinedFunctionError(self.string) - def get_dependences(self, **kwargs): return [] - class ReferenceParameter(Reference): def __init__(self, string): super(ReferenceParameter, self).__init__(string) From c422686c1b4f3bdd1a3b2a98e911f2f84e57d29c Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 14:51:23 +0200 Subject: [PATCH 05/14] Fix function regex to allow no arguments. --- reclass/utils/refvalue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 8f5d5d9e..2cca4d1a 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -25,7 +25,7 @@ _RE_FUNCTIONS = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS_FUNCTIONS) # matches a string like 'function, args)' -_RE_FUNC = '([^(]+)\(([^)]+)\)' +_RE_FUNC = '([^(]+)\(([^)]*)\)' _RE_FUNC = re.compile(_RE_FUNC) From 03f2dd3460583c0150f0ccf3f8f1ac332b6690e9 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 16:51:35 +0200 Subject: [PATCH 06/14] Change function sentinels to $<...>. This is to allow python dict access in the function, like node['ip']. --- reclass/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reclass/defaults.py b/reclass/defaults.py index 490c9ced..24be49f2 100644 --- a/reclass/defaults.py +++ b/reclass/defaults.py @@ -27,4 +27,4 @@ PARAMETER_INTERPOLATION_SENTINELS = ('${', '}') PARAMETER_INTERPOLATION_DELIMITER = ':' -FUNCTION_INTERPOLATION_SENTINELS = ('$[', ']') +FUNCTION_INTERPOLATION_SENTINELS = ('$<', '>') From 7a36b48a595662725b0d0b9dbf5c985fa1fb2358 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 16:52:59 +0200 Subject: [PATCH 07/14] Derive nodeinfo from inventory. This is needed because functions in a node definition might access other nodes. --- reclass/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reclass/core.py b/reclass/core.py index 76bd0a8e..f9e1aa41 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -133,7 +133,7 @@ def _nodeinfo_as_dict(self, nodename, entity): return ret def nodeinfo(self, nodename): - return self._nodeinfo_as_dict(nodename, self._nodeinfo(nodename)) + return self.inventory()['nodes'][nodename] def inventory(self): entities = {} From bf80b6c2cb2e83d7d0fb226bbb6dd66f52ca9c8e Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Sat, 25 Jul 2015 16:57:07 +0200 Subject: [PATCH 08/14] Implement function parsing at the end of an inventory. This is a bit hacky. Parameter expansion inside function interpolation does not work, so $ fails. --- reclass/core.py | 19 +++++++++++++++++++ reclass/datatypes/parameters.py | 11 ++++++++--- reclass/utils/function.py | 24 +++++++++++++++++------- reclass/utils/refvalue.py | 19 ++++++++++--------- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/reclass/core.py b/reclass/core.py index f9e1aa41..479c67f1 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -15,6 +15,7 @@ import shlex from reclass.datatypes import Entity, Classes, Parameters from reclass.errors import MappingFormatError, ClassNotFound +from reclass.utils.refvalue import RefValue class Core(object): @@ -135,11 +136,29 @@ def _nodeinfo_as_dict(self, nodename, entity): def nodeinfo(self, nodename): return self.inventory()['nodes'][nodename] + def _expand_all_functions(self, nodename, parameters, all_parameters): + for k, v in parameters.items(): + if isinstance(v, RefValue): + parameters[k] = v.render(all_parameters[nodename]._base, + all_parameters) + elif isinstance(v, dict): + v = self._expand_all_functions(nodename, v, all_parameters) + def inventory(self): entities = {} for n in self._storage.enumerate_nodes(): entities[n] = self._nodeinfo(n) + all_parameters = {} + for nodename, info in entities.items(): + all_parameters.update({nodename: info.parameters}) + for nodename, info in entities.items(): + params = info.parameters._base + info._set_parameters = self._expand_all_functions( + nodename, + params, + all_parameters) + nodes = {} applications = {} classes = {} diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py index 69c2cc0e..f65dfe50 100644 --- a/reclass/datatypes/parameters.py +++ b/reclass/datatypes/parameters.py @@ -10,7 +10,7 @@ from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER from reclass.utils.dictpath import DictPath -from reclass.utils.refvalue import RefValue, ReferenceParameter +from reclass.utils.refvalue import RefValue, ReferenceParameter, ReferenceFunction from reclass.errors import InfiniteRecursionError, UndefinedVariableError, \ UndefinedFunctionError @@ -212,8 +212,13 @@ def _interpolate_inner(self, path, refvalue): # dependencies of the current ref, so move on continue try: - new = refvalue.render(self._base) - path.set_value(self._base, new) + if isinstance(ref, ReferenceFunction): + # we cannot render functions yet, because they need access to the + # complete inventory + pass + else: + new = refvalue.render(self._base) + path.set_value(self._base, new) # finally, remove the reference from the occurrences cache del self._occurrences[path] diff --git a/reclass/utils/function.py b/reclass/utils/function.py index a25edbd0..13967ead 100644 --- a/reclass/utils/function.py +++ b/reclass/utils/function.py @@ -31,24 +31,34 @@ class FunctionPrint(Function): def __init__(self): super(FunctionPrint, self).__init__() - def execute(self, *args): - return " ".join(*args) + def execute(self, additional_info, *args): + return " ".join(args) class FunctionAggregate(Function): def __init__(self): super(FunctionAggregate, self).__init__() - def execute(self, *args): - return "{aggregate()}" - #result = [] - #matching_hosts = [] - #for host in hosts: + def execute(self, additional_info, *args): + func_filter, func_extract = args[0:2] + hosts = additional_info + result = [] + matching_hosts = {} + for hostname, hostinfo in additional_info.items(): + hostinfo = hostinfo._base + expr = func_filter.replace("node", "hostinfo") + if eval(expr): + matching_hosts.update({hostname: hostinfo}) + for hostname, hostinfo in matching_hosts.items(): + expr = func_extract.replace("node", "hostinfo") + result.append(eval(expr)) + # if func_args[0](host) is True: # matching_hosts.append(host) #result = [] #for host in matching_hosts: # result.append(func_args[1](host)) #return result + return result diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 2cca4d1a..5b7b0fa9 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -34,14 +34,15 @@ def __init__(self, string): self.string = string + class ReferenceFunction(Reference): def __init__(self, string): super(ReferenceFunction, self).__init__(string) - def resolve(self, context, **kwargs): - return self._execute(context) + def resolve(self, context, additional_info, *args, **kwargs): + return self._execute(context, additional_info) - def _execute(self, context): + def _execute(self, context, additional_info): match = _RE_FUNC.match(self.string) func_name = match.group(1) func_args = match.groups()[1].split(',') @@ -50,7 +51,7 @@ def _execute(self, context): try: func = get_function(func_name) - return func.execute(func_args) + return func.execute(additional_info, *func_args) except UndefinedFunctionError: raise UndefinedFunctionError(self.string) @@ -62,7 +63,7 @@ class ReferenceParameter(Reference): def __init__(self, string): super(ReferenceParameter, self).__init__(string) - def resolve(self, context, **kwargs): + def resolve(self, context, *args, **kwargs): path = DictPath(kwargs['delim'], self.string) try: return path.get_value(context) @@ -160,8 +161,8 @@ def _check_strings(self, orig, strings, sentinel): if pos >= 0: raise IncompleteInterpolationError(orig, sentinel[1]) - def _resolve(self, ref, context): - return ref.resolve(context, delim=self._delim) + def _resolve(self, ref, context, additional_info): + return ref.resolve(context, additional_info, delim=self._delim) def has_references(self): return len(self._refs) > 0 @@ -186,8 +187,8 @@ def _assemble(self, resolver): ret += self._strings[-1] return ret - def render(self, context): - resolver = lambda s: self._resolve(s, context) + def render(self, context, additional_info=None): + resolver = lambda s: self._resolve(s, context, additional_info) ret = self._assemble(resolver) return ret From 07e0a8ce4ec009cbf85fff2346694d918eedf3ce Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Tue, 28 Jul 2015 03:34:02 +0200 Subject: [PATCH 09/14] Make function regex greedy to get arguments with brackets. --- reclass/utils/refvalue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 5b7b0fa9..3537f249 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -25,7 +25,7 @@ _RE_FUNCTIONS = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS_FUNCTIONS) # matches a string like 'function, args)' -_RE_FUNC = '([^(]+)\(([^)]*)\)' +_RE_FUNC = '([^(]+)\((.*)\)' _RE_FUNC = re.compile(_RE_FUNC) From ccfbccccc3a137ae9d04a7e5807b4ac84c6ff526 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Tue, 28 Jul 2015 03:35:31 +0200 Subject: [PATCH 10/14] Change behaviour of aggregate(). Instead of returning a list of all extracted values, it now returns a dict mapping the nodename to the extracted values. --- reclass/utils/function.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/reclass/utils/function.py b/reclass/utils/function.py index 13967ead..5ff528eb 100644 --- a/reclass/utils/function.py +++ b/reclass/utils/function.py @@ -42,7 +42,7 @@ def __init__(self): def execute(self, additional_info, *args): func_filter, func_extract = args[0:2] hosts = additional_info - result = [] + result = {} matching_hosts = {} for hostname, hostinfo in additional_info.items(): hostinfo = hostinfo._base @@ -51,14 +51,5 @@ def execute(self, additional_info, *args): matching_hosts.update({hostname: hostinfo}) for hostname, hostinfo in matching_hosts.items(): expr = func_extract.replace("node", "hostinfo") - result.append(eval(expr)) - - # if func_args[0](host) is True: - # matching_hosts.append(host) - #result = [] - #for host in matching_hosts: - # result.append(func_args[1](host)) - #return result + result[hostname] = eval(expr) return result - - From 903078d74ff1532b993698fa4a93b7d7dfd093b9 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Tue, 28 Jul 2015 20:10:27 +0200 Subject: [PATCH 11/14] Allow parameter references in functions This did not work beforehand because functions and references were parsed at the same time, but evaluated later. This parsing failed when a reference was nested in a function. To solve this, function evaluation *and parsing* are now done after the whole inventory is available. This means that all references are already expanded, so neither the function nor the reference parsing need to be altered. --- reclass/core.py | 20 ++--- reclass/datatypes/entity.py | 3 + reclass/datatypes/parameters.py | 16 +++- reclass/utils/function.py | 14 +-- reclass/utils/refvalue.py | 146 +++++++++++++++++--------------- 5 files changed, 109 insertions(+), 90 deletions(-) diff --git a/reclass/core.py b/reclass/core.py index 479c67f1..287846b3 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -136,29 +136,19 @@ def _nodeinfo_as_dict(self, nodename, entity): def nodeinfo(self, nodename): return self.inventory()['nodes'][nodename] - def _expand_all_functions(self, nodename, parameters, all_parameters): - for k, v in parameters.items(): - if isinstance(v, RefValue): - parameters[k] = v.render(all_parameters[nodename]._base, - all_parameters) - elif isinstance(v, dict): - v = self._expand_all_functions(nodename, v, all_parameters) - def inventory(self): entities = {} + + # first run, reference parameters are expanded for n in self._storage.enumerate_nodes(): entities[n] = self._nodeinfo(n) + # second run, function are executed all_parameters = {} for nodename, info in entities.items(): all_parameters.update({nodename: info.parameters}) - for nodename, info in entities.items(): - params = info.parameters._base - info._set_parameters = self._expand_all_functions( - nodename, - params, - all_parameters) - + for nodename, node in entities.items(): + node.expand_functions(inventory=all_parameters) nodes = {} applications = {} classes = {} diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py index 573a28c9..48a1e68a 100644 --- a/reclass/datatypes/entity.py +++ b/reclass/datatypes/entity.py @@ -53,6 +53,9 @@ def _set_parameters(self, parameters): 'instance of type %s' % type(parameters)) self._parameters = parameters + def expand_functions(self, inventory): + self.parameters.interpolate_functions(inventory) + def merge(self, other): self._classes.merge_unique(other._classes) self._applications.merge_unique(other._applications) diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py index f65dfe50..a29145ae 100644 --- a/reclass/datatypes/parameters.py +++ b/reclass/datatypes/parameters.py @@ -10,7 +10,8 @@ from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER from reclass.utils.dictpath import DictPath -from reclass.utils.refvalue import RefValue, ReferenceParameter, ReferenceFunction +from reclass.utils.refvalue import (RefValue, ReferenceParameter, + ReferenceFunction, RefFunction) from reclass.errors import InfiniteRecursionError, UndefinedVariableError, \ UndefinedFunctionError @@ -172,6 +173,19 @@ def merge(self, other): def has_unresolved_refs(self): return len(self._occurrences) > 0 + def interpolate_functions(self, inventory): + self._interpolate_functions_inner(self._base, inventory) + + def _interpolate_functions_inner(self, node, inventory): + for k, v in node.items(): + if isinstance(v, dict): + self._interpolate_functions_inner(node[k], inventory) + elif isinstance(v, types.StringTypes): + refval = RefFunction(v) + if refval.has_references(): + ret = refval.render(inventory) + node[k] = ret + def interpolate(self): while self.has_unresolved_refs(): # we could use a view here, but this is simple enough: diff --git a/reclass/utils/function.py b/reclass/utils/function.py index 5ff528eb..e90b35c8 100644 --- a/reclass/utils/function.py +++ b/reclass/utils/function.py @@ -47,9 +47,13 @@ def execute(self, additional_info, *args): for hostname, hostinfo in additional_info.items(): hostinfo = hostinfo._base expr = func_filter.replace("node", "hostinfo") - if eval(expr): - matching_hosts.update({hostname: hostinfo}) - for hostname, hostinfo in matching_hosts.items(): - expr = func_extract.replace("node", "hostinfo") - result[hostname] = eval(expr) + try: + if eval(expr): + matching_hosts.update({hostname: hostinfo}) + for hostname, hostinfo in matching_hosts.items(): + expr = func_extract.replace("node", "hostinfo") + result[hostname] = eval(expr) + except KeyError: + raise + return result diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 3537f249..1b1280ed 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -39,10 +39,10 @@ class ReferenceFunction(Reference): def __init__(self, string): super(ReferenceFunction, self).__init__(string) - def resolve(self, context, additional_info, *args, **kwargs): - return self._execute(context, additional_info) + def resolve(self, inventory, *args, **kwargs): + return self._execute(inventory) - def _execute(self, context, additional_info): + def _execute(self, inventory): match = _RE_FUNC.match(self.string) func_name = match.group(1) func_args = match.groups()[1].split(',') @@ -51,13 +51,11 @@ def _execute(self, context, additional_info): try: func = get_function(func_name) - return func.execute(additional_info, *func_args) + ret = func.execute(inventory, *func_args) + return ret except UndefinedFunctionError: raise UndefinedFunctionError(self.string) - def get_dependences(self, **kwargs): - return [] - class ReferenceParameter(Reference): def __init__(self, string): @@ -74,7 +72,76 @@ def get_dependencies(self, **kwargs): return [DictPath(kwargs['delim'], self.string)] -class RefValue(object): +class Ref(object): + def __init__(self, string): + self._strings = [] + self._refs = [] + self._parse(string) + + def has_references(self): + return len(self._refs) > 0 + + def get_references(self): + return self._refs + + def render(self, inventory): + pass + + def _check_strings(self, orig, strings, sentinel): + for s in strings: + pos = s.find(sentinel[0]) + if pos >= 0: + raise IncompleteInterpolationError(orig, sentinel[1]) + + def _assemble(self, resolver): + if not self.has_references(): + return self._strings[0] + + if self._strings == ['', '']: + # preserve the type of the referenced variable + ret = resolver(self._refs[0]) + else: + + # reassemble the string by taking a string and str(ref) pairwise + ret = '' + for i in range(0, len(self._refs)): + ret += self._strings[i] + str(resolver(self._refs[i])) + if len(self._strings) > len(self._refs): + # and finally append a trailing string, if any + ret += self._strings[-1] + return ret + + +class RefFunction(Ref): + + INTERPOLATION_RE_FUNCTIONS = re.compile(_RE_FUNCTIONS) + + def __init__(self, string): + super(RefFunction,self).__init__(string) + + def _parse(self, string): + strings, refs = self._parse_functions(string) + self._strings = strings + self._refs = [ReferenceFunction(ref) for ref in refs] + + def _parse_functions(self, string): + parts = RefFunction.INTERPOLATION_RE_FUNCTIONS.split(string) + strings = parts[0:][::2] + functions = parts[1:][::2] + self._check_strings(string, strings, FUNCTION_INTERPOLATION_SENTINELS) + return (strings, functions) + + + def _resolve(self, ref, inventory): + return ref.resolve(inventory) + + def render(self, inventory): + resolver = lambda s: self._resolve(s, inventory) + ret = self._assemble(resolver) + return ret + + +class RefValue(Ref): ''' Isolates references in string values @@ -111,13 +178,11 @@ class RefValue(object): ''' INTERPOLATION_RE_PARAMETER = re.compile(_RE_PARAMETER) - INTERPOLATION_RE_FUNCTIONS = re.compile(_RE_FUNCTIONS) def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER): - self._strings = [] - self._refs = [] self._delim = delim - self._parse(string) + super(RefValue,self).__init__(string) + def _parse(self, string): parts = RefValue.INTERPOLATION_RE_PARAMETER.split(string) @@ -126,67 +191,10 @@ def _parse(self, string): self._strings = parts[0:][::2] self._check_strings(string, self._strings, PARAMETER_INTERPOLATION_SENTINELS) - # each string could contain a function - for i in range(len(self._strings)): - strings, refs = self._parse_functions(self._strings[i]) - refs = [ReferenceFunction(ref) for ref in refs] - if len(refs) == 0: - continue - del self._strings[i] - self._strings.insert(i, strings) - self._refs.insert(i, refs) - - self._refs = self._flatten(self._refs) - self._strings = self._flatten(self._strings) - - def _flatten(self, l): - ret = [] - for element in l: - if isinstance(element, list): - ret.extend(element) - else: - ret.append(element) - return ret - - def _parse_functions(self, string): - parts = RefValue.INTERPOLATION_RE_FUNCTIONS.split(string) - strings = parts[0:][::2] - functions = parts[1:][::2] - self._check_strings(string, strings, FUNCTION_INTERPOLATION_SENTINELS) - return (strings, functions) - - def _check_strings(self, orig, strings, sentinel): - for s in strings: - pos = s.find(sentinel[0]) - if pos >= 0: - raise IncompleteInterpolationError(orig, sentinel[1]) def _resolve(self, ref, context, additional_info): return ref.resolve(context, additional_info, delim=self._delim) - def has_references(self): - return len(self._refs) > 0 - - def get_references(self): - return self._refs - - def _assemble(self, resolver): - if not self.has_references(): - return self._strings[0] - - if self._strings == ['', '']: - # preserve the type of the referenced variable - return resolver(self._refs[0]) - - # reassemble the string by taking a string and str(ref) pairwise - ret = '' - for i in range(0, len(self._refs)): - ret += self._strings[i] + str(resolver(self._refs[i])) - if len(self._strings) > len(self._refs): - # and finally append a trailing string, if any - ret += self._strings[-1] - return ret - def render(self, context, additional_info=None): resolver = lambda s: self._resolve(s, context, additional_info) ret = self._assemble(resolver) From 601402a04f4fc802f15346eede3f5a36d33d1abd Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Tue, 28 Jul 2015 20:41:14 +0200 Subject: [PATCH 12/14] Add documentation for function interpolation. --- doc/source/operations.rst | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/doc/source/operations.rst b/doc/source/operations.rst index f744148a..5795fb2e 100644 --- a/doc/source/operations.rst +++ b/doc/source/operations.rst @@ -149,3 +149,60 @@ value of ``dict_reference`` will actually be a dictionary. You should now be ready to :doc:`use reclass `! .. include:: substs.inc + +Function interpolation +---------------------- + +Certain functions can be used to dynamically generate values. They are specified +like this:: + + parameters: + key: $ + +The following functions are supported: + +print +***** + +This function simply concatenates all its parameters and returns a string. For +example, take this:: + + test: $ + +which results in this:: + + test: "first second third" + +Yeah, it's quite useless. Now to something a bit more useful: + +aggregate +********* + +This can be used to extract values from hosts that satisfy certain conditions. +The syntax looks like this:: + + aggregate(filter, extractor) + +The ``filter`` parameter specifies the condition the host has to fulfil, and +the ``extractor`` determines what will be taken from that host. Inside the +function, ``node`` refers to all parameters of a node, and regular python +dictionary functions can be used to access it. + +The return value is always a dictionary that maps the nodename to the extracted +values. + +Here is an example that extracts the IP address of every host in our domain +``example.com``:: + + hosts: $ + +which might result in something like this:: + + hosts: + first.example.com: 192.168.1.1 + second.example.com: 192.168.1.2 + +Note that parameter interpolation can be used inside functions, but one has to +pay attention to proper quoting:: + + hosts: $ From 3d4f2b2cb459bb4a43906545e0ceb10aea7de1e3 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Tue, 28 Jul 2015 21:48:35 +0200 Subject: [PATCH 13/14] Cleanup. - Rename RefValue classes to something more specific: - ReferenceStringParameter and ReferenceStringFunction represent strings that contain references (parameters and functions respectively) - ReferenceParameter and ReferenceFunction represent references in the classes above, and can be interpolates/executed - Revert the behaviour that functions and parameters were parsed at the same time, but functions "saved" until the whole inventory was available. Now, functions will be parsed and executed after a complete inventory run. --- reclass/core.py | 10 +-- reclass/datatypes/parameters.py | 105 +++++++++++++-------------- reclass/errors.py | 2 +- reclass/utils/function.py | 15 ++-- reclass/utils/refvalue.py | 38 +++++----- reclass/utils/tests/test_refvalue.py | 24 +++--- 6 files changed, 90 insertions(+), 104 deletions(-) diff --git a/reclass/core.py b/reclass/core.py index 287846b3..e32013e7 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -15,7 +15,6 @@ import shlex from reclass.datatypes import Entity, Classes, Parameters from reclass.errors import MappingFormatError, ClassNotFound -from reclass.utils.refvalue import RefValue class Core(object): @@ -144,11 +143,12 @@ def inventory(self): entities[n] = self._nodeinfo(n) # second run, function are executed - all_parameters = {} - for nodename, info in entities.items(): - all_parameters.update({nodename: info.parameters}) + #all_parameters = {} + #for nodename, info in entities.items(): + # all_parameters.update({nodename: info.parameters}) for nodename, node in entities.items(): - node.expand_functions(inventory=all_parameters) + node.expand_functions(inventory=entities) + nodes = {} applications = {} classes = {} diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py index a29145ae..4aee5453 100644 --- a/reclass/datatypes/parameters.py +++ b/reclass/datatypes/parameters.py @@ -10,10 +10,10 @@ from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER from reclass.utils.dictpath import DictPath -from reclass.utils.refvalue import (RefValue, ReferenceParameter, - ReferenceFunction, RefFunction) -from reclass.errors import InfiniteRecursionError, UndefinedVariableError, \ - UndefinedFunctionError +from reclass.utils.refvalue import (ReferenceStringParameter, ReferenceParameter, + ReferenceFunction, ReferenceStringFunction) +from reclass.errors import (InfiniteRecursionError, UndefinedVariableError, + UndefinedFunctionError) class Parameters(object): ''' @@ -72,37 +72,37 @@ def as_dict(self): return self._base.copy() def _update_scalar(self, cur, new, path): - if isinstance(cur, RefValue) and path in self._occurrences: - # If the current value already holds a RefValue, we better forget + if isinstance(cur, ReferenceStringParameter) and path in self._occurrences: + # If the current value already holds a ReferenceStringParameter, we better forget # the occurrence, or else interpolate() will later overwrite - # unconditionally. If the new value is a RefValue, the occurrence + # unconditionally. If the new value is a ReferenceStringParameter, the occurrence # will be added again further on del self._occurrences[path] if self.delimiter is None or not isinstance(new, (types.StringTypes, - RefValue)): + ReferenceStringParameter)): # either there is no delimiter defined (and hence no references # are being used), or the new value is not a string (and hence - # cannot be turned into a RefValue), and not a RefValue. We can + # cannot be turned into a ReferenceStringParameter), and not a ReferenceStringParameter. We can # shortcut and just return the new scalar return new - elif isinstance(new, RefValue): - # the new value is (already) a RefValue, so we need not touch it + elif isinstance(new, ReferenceStringParameter): + # the new value is (already) a ReferenceStringParameter, so we need not touch it # at all ret = new else: # the new value is a string, let's see if it contains references, - # by way of wrapping it in a RefValue and querying the result - ret = RefValue(new, self.delimiter) + # by way of wrapping it in a ReferenceStringParameter and querying the result + ret = ReferenceStringParameter(new, self.delimiter) if not ret.has_references(): - # do not replace with RefValue instance if there are no - # references, i.e. discard the RefValue in ret, just return + # do not replace with ReferenceStringParameter instance if there are no + # references, i.e. discard the ReferenceStringParameter in ret, just return # the new value return new - # So we now have a RefValue. Let's, keep a reference to the instance + # So we now have a ReferenceStringParameter. Let's, keep a reference to the instance # we just created, in a dict indexed by the dictionary path, instead # of just a list. The keys are required to resolve dependencies during # interpolation @@ -181,10 +181,12 @@ def _interpolate_functions_inner(self, node, inventory): if isinstance(v, dict): self._interpolate_functions_inner(node[k], inventory) elif isinstance(v, types.StringTypes): - refval = RefFunction(v) + refval = ReferenceStringFunction(v) if refval.has_references(): - ret = refval.render(inventory) - node[k] = ret + try: + node[k] = refval.render(inventory) + except UndefinedFunctionError as e: + raise UndefinedFunctionError(e.var) def interpolate(self): while self.has_unresolved_refs(): @@ -197,47 +199,38 @@ def interpolate(self): def _interpolate_inner(self, path, refvalue): self._occurrences[path] = True # mark as seen for ref in refvalue.get_references(): - if isinstance(ref, ReferenceParameter): - paths_from_ref = ref.get_dependencies(delim=self.delimiter) - for path_from_ref in paths_from_ref: - try: - refvalue_inner = self._occurrences[path_from_ref] - - # If there is no reference, then this will throw a KeyError, - # look further down where this is caught and execution passed - # to the next iteration of the loop - # - # If we get here, then the ref references another parameter, - # requiring us to recurse, dereferencing first those refs that - # are most used and are thus at the leaves of the dependency - # tree. - - if refvalue_inner is True: - # every call to _interpolate_inner replaces the value of - # the saved occurrences of a reference with True. - # Therefore, if we encounter True instead of a refvalue, - # it means that we have already processed it and are now - # faced with a cyclical reference. - raise InfiniteRecursionError(path, ref.string) - self._interpolate_inner(path_from_ref, refvalue_inner) - - except KeyError as e: - # not actually an error, but we are done resolving all - # dependencies of the current ref, so move on - continue + path_from_ref = DictPath(self.delimiter, ref.string) + try: + refvalue_inner = self._occurrences[path_from_ref] + + # If there is no reference, then this will throw a KeyError, + # look further down where this is caught and execution passed + # to the next iteration of the loop + # + # If we get here, then the ref references another parameter, + # requiring us to recurse, dereferencing first those refs that + # are most used and are thus at the leaves of the dependency + # tree. + + if refvalue_inner is True: + # every call to _interpolate_inner replaces the value of + # the saved occurrences of a reference with True. + # Therefore, if we encounter True instead of a refvalue, + # it means that we have already processed it and are now + # faced with a cyclical reference. + raise InfiniteRecursionError(path, ref.string) + self._interpolate_inner(path_from_ref, refvalue_inner) + + except KeyError as e: + # not actually an error, but we are done resolving all + # dependencies of the current ref, so move on + continue try: - if isinstance(ref, ReferenceFunction): - # we cannot render functions yet, because they need access to the - # complete inventory - pass - else: - new = refvalue.render(self._base) - path.set_value(self._base, new) + new = refvalue.render(self._base) + path.set_value(self._base, new) # finally, remove the reference from the occurrences cache del self._occurrences[path] except UndefinedVariableError as e: raise UndefinedVariableError(e.var, path) - except UndefinedFunctionError as e: - raise UndefinedFunctionError(e.var) diff --git a/reclass/errors.py b/reclass/errors.py index 33780531..479bda27 100644 --- a/reclass/errors.py +++ b/reclass/errors.py @@ -11,7 +11,7 @@ import traceback from reclass.defaults import (PARAMETER_INTERPOLATION_SENTINELS, -FUNCTION_INTERPOLATION_SENTINELS) + FUNCTION_INTERPOLATION_SENTINELS) class ReclassException(Exception): diff --git a/reclass/utils/function.py b/reclass/utils/function.py index e90b35c8..3f032980 100644 --- a/reclass/utils/function.py +++ b/reclass/utils/function.py @@ -31,7 +31,7 @@ class FunctionPrint(Function): def __init__(self): super(FunctionPrint, self).__init__() - def execute(self, additional_info, *args): + def execute(self, inventory, *args): return " ".join(args) @@ -39,21 +39,18 @@ class FunctionAggregate(Function): def __init__(self): super(FunctionAggregate, self).__init__() - def execute(self, additional_info, *args): + def execute(self, inventory, *args): func_filter, func_extract = args[0:2] - hosts = additional_info result = {} matching_hosts = {} - for hostname, hostinfo in additional_info.items(): - hostinfo = hostinfo._base - expr = func_filter.replace("node", "hostinfo") + for hostname, hostinfo in inventory.items(): + node = hostinfo.parameters.as_dict() try: - if eval(expr): - matching_hosts.update({hostname: hostinfo}) + if eval(func_filter): + matching_hosts.update({hostname: node}) for hostname, hostinfo in matching_hosts.items(): expr = func_extract.replace("node", "hostinfo") result[hostname] = eval(expr) except KeyError: raise - return result diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 1b1280ed..9f1e9155 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -34,7 +34,6 @@ def __init__(self, string): self.string = string - class ReferenceFunction(Reference): def __init__(self, string): super(ReferenceFunction, self).__init__(string) @@ -68,11 +67,8 @@ def resolve(self, context, *args, **kwargs): except KeyError as e: raise UndefinedVariableError(self.string) - def get_dependencies(self, **kwargs): - return [DictPath(kwargs['delim'], self.string)] - -class Ref(object): +class ReferenceString(object): def __init__(self, string): self._strings = [] self._refs = [] @@ -99,7 +95,7 @@ def _assemble(self, resolver): if self._strings == ['', '']: # preserve the type of the referenced variable - ret = resolver(self._refs[0]) + ret = resolver(self._refs[0]) else: # reassemble the string by taking a string and str(ref) pairwise @@ -112,12 +108,12 @@ def _assemble(self, resolver): return ret -class RefFunction(Ref): +class ReferenceStringFunction(ReferenceString): INTERPOLATION_RE_FUNCTIONS = re.compile(_RE_FUNCTIONS) def __init__(self, string): - super(RefFunction,self).__init__(string) + super(ReferenceStringFunction,self).__init__(string) def _parse(self, string): strings, refs = self._parse_functions(string) @@ -125,7 +121,7 @@ def _parse(self, string): self._refs = [ReferenceFunction(ref) for ref in refs] def _parse_functions(self, string): - parts = RefFunction.INTERPOLATION_RE_FUNCTIONS.split(string) + parts = self.INTERPOLATION_RE_FUNCTIONS.split(string) strings = parts[0:][::2] functions = parts[1:][::2] self._check_strings(string, strings, FUNCTION_INTERPOLATION_SENTINELS) @@ -141,36 +137,36 @@ def render(self, inventory): return ret -class RefValue(Ref): +class ReferenceStringParameter(ReferenceString): ''' Isolates references in string values - RefValue can be used to isolate and eventually expand references to other + ReferenceStringParameter can be used to isolate and eventually expand references to other parameters in strings. Those references can then be iterated and rendered in the context of a dictionary to resolve those references. - RefValue always gets constructed from a string, because templating + ReferenceStringParameter always gets constructed from a string, because templating — essentially this is what's going on — is necessarily always about - strings. Therefore, generally, the rendered value of a RefValue instance + strings. Therefore, generally, the rendered value of a ReferenceStringParameter instance will also be a string. - Nevertheless, as this might not be desirable, RefValue will return the + Nevertheless, as this might not be desirable, ReferenceStringParameter will return the referenced variable without casting it to a string, if the templated string contains nothing but the reference itself. For instance: mydict = {'favcolour': 'yellow', 'answer': 42, 'list': [1,2,3]} - RefValue('My favourite colour is ${favolour}').render(mydict) + ReferenceStringParameter('My favourite colour is ${favolour}').render(mydict) → 'My favourite colour is yellow' # a string - RefValue('The answer is ${answer}').render(mydict) + ReferenceStringParameter('The answer is ${answer}').render(mydict) → 'The answer is 42' # a string - RefValue('${answer}').render(mydict) + ReferenceStringParameter('${answer}').render(mydict) → 42 # an int - RefValue('${list}').render(mydict) + ReferenceStringParameter('${list}').render(mydict) → [1,2,3] # an list The markers used to identify references are set in reclass.defaults, as is @@ -181,11 +177,11 @@ class RefValue(Ref): def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER): self._delim = delim - super(RefValue,self).__init__(string) + super(ReferenceStringParameter,self).__init__(string) def _parse(self, string): - parts = RefValue.INTERPOLATION_RE_PARAMETER.split(string) + parts = ReferenceStringParameter.INTERPOLATION_RE_PARAMETER.split(string) self._refs = parts[1:][::2] self._refs = [ReferenceParameter(ref) for ref in self._refs] self._strings = parts[0:][::2] @@ -202,5 +198,5 @@ def render(self, context, additional_info=None): def __repr__(self): do_not_resolve = lambda s: s.string.join(PARAMETER_INTERPOLATION_SENTINELS) - return 'RefValue(%r, %r)' % (self._assemble(do_not_resolve), + return 'ReferenceStringParameter(%r, %r)' % (self._assemble(do_not_resolve), self._delim) diff --git a/reclass/utils/tests/test_refvalue.py b/reclass/utils/tests/test_refvalue.py index 23d7e7b0..a99784d8 100644 --- a/reclass/utils/tests/test_refvalue.py +++ b/reclass/utils/tests/test_refvalue.py @@ -7,7 +7,7 @@ # Released under the terms of the Artistic Licence 2.0 # -from reclass.utils.refvalue import RefValue +from reclass.utils.refvalue import ReferenceStringParameter from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, \ PARAMETER_INTERPOLATION_DELIMITER from reclass.errors import UndefinedVariableError, \ @@ -31,17 +31,17 @@ def _var(s): def _poor_mans_template(s, var, value): return s.replace(_var(var), value) -class TestRefValue(unittest.TestCase): +class TestReferenceStringParameter(unittest.TestCase): def test_simple_string(self): s = 'my cat likes to hide in boxes' - tv = RefValue(s) + tv = ReferenceStringParameter(s) self.assertFalse(tv.has_references()) self.assertEquals(tv.render(CONTEXT), s) def _test_solo_ref(self, key): s = _var(key) - tv = RefValue(s) + tv = ReferenceStringParameter(s) res = tv.render(CONTEXT) self.assertTrue(tv.has_references()) self.assertEqual(res, CONTEXT[key]) @@ -63,7 +63,7 @@ def test_solo_ref_bool(self): def test_single_subst_bothends(self): s = 'I like ' + _var('favcolour') + ' and I like it' - tv = RefValue(s) + tv = ReferenceStringParameter(s) self.assertTrue(tv.has_references()) self.assertEqual(tv.render(CONTEXT), _poor_mans_template(s, 'favcolour', @@ -71,7 +71,7 @@ def test_single_subst_bothends(self): def test_single_subst_start(self): s = _var('favcolour') + ' is my favourite colour' - tv = RefValue(s) + tv = ReferenceStringParameter(s) self.assertTrue(tv.has_references()) self.assertEqual(tv.render(CONTEXT), _poor_mans_template(s, 'favcolour', @@ -79,7 +79,7 @@ def test_single_subst_start(self): def test_single_subst_end(self): s = 'I like ' + _var('favcolour') - tv = RefValue(s) + tv = ReferenceStringParameter(s) self.assertTrue(tv.has_references()) self.assertEqual(tv.render(CONTEXT), _poor_mans_template(s, 'favcolour', @@ -88,7 +88,7 @@ def test_single_subst_end(self): def test_deep_subst_solo(self): var = PARAMETER_INTERPOLATION_DELIMITER.join(('motd', 'greeting')) s = _var(var) - tv = RefValue(s) + tv = ReferenceStringParameter(s) self.assertTrue(tv.has_references()) self.assertEqual(tv.render(CONTEXT), _poor_mans_template(s, var, @@ -97,7 +97,7 @@ def test_deep_subst_solo(self): def test_multiple_subst(self): greet = PARAMETER_INTERPOLATION_DELIMITER.join(('motd', 'greeting')) s = _var(greet) + ' I like ' + _var('favcolour') + '!' - tv = RefValue(s) + tv = ReferenceStringParameter(s) self.assertTrue(tv.has_references()) want = _poor_mans_template(s, greet, CONTEXT['motd']['greeting']) want = _poor_mans_template(want, 'favcolour', CONTEXT['favcolour']) @@ -106,7 +106,7 @@ def test_multiple_subst(self): def test_multiple_subst_flush(self): greet = PARAMETER_INTERPOLATION_DELIMITER.join(('motd', 'greeting')) s = _var(greet) + ' I like ' + _var('favcolour') - tv = RefValue(s) + tv = ReferenceStringParameter(s) self.assertTrue(tv.has_references()) want = _poor_mans_template(s, greet, CONTEXT['motd']['greeting']) want = _poor_mans_template(want, 'favcolour', CONTEXT['favcolour']) @@ -114,14 +114,14 @@ def test_multiple_subst_flush(self): def test_undefined_variable(self): s = _var('no_such_variable') - tv = RefValue(s) + tv = ReferenceStringParameter(s) with self.assertRaises(UndefinedVariableError): tv.render(CONTEXT) def test_incomplete_variable(self): s = PARAMETER_INTERPOLATION_SENTINELS[0] + 'incomplete' with self.assertRaises(IncompleteInterpolationError): - tv = RefValue(s) + tv = ReferenceStringParameter(s) if __name__ == '__main__': unittest.main() From 96ec768d254363d661146a8ec6a81e68b1db8f48 Mon Sep 17 00:00:00 2001 From: Hannes Koerber Date: Tue, 28 Jul 2015 22:18:49 +0200 Subject: [PATCH 14/14] Add explanation about chained references. --- doc/source/operations.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/operations.rst b/doc/source/operations.rst index 5795fb2e..45072364 100644 --- a/doc/source/operations.rst +++ b/doc/source/operations.rst @@ -206,3 +206,7 @@ Note that parameter interpolation can be used inside functions, but one has to pay attention to proper quoting:: hosts: $ + +Note that chained references are not supported: If the result of a function +contains a function itself, this function will not be interpolated, but taken +verbatim. This also prevents circular references.