From fb124751eba367423c52fb843ffb17e2eed089e1 Mon Sep 17 00:00:00 2001 From: James Lott Date: Sun, 7 Aug 2016 09:47:45 -0400 Subject: [PATCH] Implement mutators Mutators allow the definition of raw python functions to manipulate entities before returning them to the caller. The Mutators implementation obsoletes function interpolation --- doc/source/operations.rst | 61 -------- reclass/cli.py | 2 + reclass/core.py | 32 +++-- reclass/datatypes/__init__.py | 1 + reclass/datatypes/entity.py | 14 +- reclass/datatypes/mutators.py | 62 ++++++++ reclass/datatypes/parameters.py | 50 +++---- reclass/defaults.py | 2 - reclass/errors.py | 13 +- reclass/storage/yaml_fs/yamlfile.py | 8 +- reclass/utils/function.py | 82 ----------- reclass/utils/refvalue.py | 203 ++++++++------------------- reclass/utils/tests/test_refvalue.py | 24 ++-- 13 files changed, 192 insertions(+), 362 deletions(-) create mode 100644 reclass/datatypes/mutators.py delete mode 100644 reclass/utils/function.py diff --git a/doc/source/operations.rst b/doc/source/operations.rst index 45072364..f744148a 100644 --- a/doc/source/operations.rst +++ b/doc/source/operations.rst @@ -149,64 +149,3 @@ 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: $ - -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. diff --git a/reclass/cli.py b/reclass/cli.py index 5666e165..9d702a7b 100644 --- a/reclass/cli.py +++ b/reclass/cli.py @@ -31,6 +31,8 @@ def main(): class_mappings = defaults.get('class_mappings') reclass = Core(storage, class_mappings) + sys.path.append(options.inventory_base_uri) + if options.mode == MODE_NODEINFO: data = reclass.nodeinfo(options.nodename) diff --git a/reclass/core.py b/reclass/core.py index e32013e7..517ac41f 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -13,7 +13,8 @@ #import sys import fnmatch import shlex -from reclass.datatypes import Entity, Classes, Parameters +import inspect +from reclass.datatypes import Entity, Classes, Parameters, Mutators from reclass.errors import MappingFormatError, ClassNotFound class Core(object): @@ -22,6 +23,7 @@ def __init__(self, storage, class_mappings, input_data=None): self._storage = storage self._class_mappings = class_mappings self._input_data = input_data + self._mutators = Mutators() @staticmethod def _get_timestamp(): @@ -120,6 +122,7 @@ def _nodeinfo(self, nodename): ret = self._recurse_entity(node_entity, merge_base, seen=seen, nodename=node_entity.name) ret.interpolate() + self._mutators.push(ret.mutators) return ret def _nodeinfo_as_dict(self, nodename, entity): @@ -132,23 +135,29 @@ def _nodeinfo_as_dict(self, nodename, entity): ret.update(entity.as_dict()) return ret + def _mutate(self, **kwargs): + params = set(kwargs.keys()) + mutators = self._mutators.as_deque() + while len(mutators): + mutator = mutators.popleft() + args, varargs, keywords, defaults = inspect.getargspec(mutator) + required = set(args) + # If the caller passed us all parameters that this mutator requires, + # call the mutator with the arguments it wants + if (required & params) == required: + args = { arg: value for arg, value in kwargs.items() if arg in required } + mutator(**args) + def nodeinfo(self, nodename): - return self.inventory()['nodes'][nodename] + inventory = self.inventory() + self._mutate(inventory=inventory, nodename=nodename) + return inventory['nodes'][nodename] 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, node in entities.items(): - node.expand_functions(inventory=entities) - nodes = {} applications = {} classes = {} @@ -164,6 +173,7 @@ def inventory(self): classes[c].append(f) else: classes[c] = [f] + self._mutate(nodes=nodes, classes=classes, applications=applications) return {'__reclass__' : {'timestamp': Core._get_timestamp()}, 'nodes': nodes, diff --git a/reclass/datatypes/__init__.py b/reclass/datatypes/__init__.py index 20f7551f..7519e441 100644 --- a/reclass/datatypes/__init__.py +++ b/reclass/datatypes/__init__.py @@ -10,3 +10,4 @@ from classes import Classes from entity import Entity from parameters import Parameters +from mutators import Mutators diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py index 48a1e68a..94b61194 100644 --- a/reclass/datatypes/entity.py +++ b/reclass/datatypes/entity.py @@ -9,6 +9,7 @@ from classes import Classes from applications import Applications from parameters import Parameters +from mutators import Mutators class Entity(object): ''' @@ -17,13 +18,15 @@ class Entity(object): uri of the Entity that is being merged. ''' def __init__(self, classes=None, applications=None, parameters=None, - uri=None, name=None, environment=None): + uri=None, name=None, environment=None, mutators=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() self._set_parameters(parameters) + if mutators is None: mutators = Mutators() + self._set_mutators(mutators) self._uri = uri or '' self._name = name or '' self._environment = environment or '' @@ -34,6 +37,7 @@ def __init__(self, classes=None, applications=None, parameters=None, classes = property(lambda s: s._classes) applications = property(lambda s: s._applications) parameters = property(lambda s: s._parameters) + mutators = property(lambda s: s._mutators) def _set_classes(self, classes): if not isinstance(classes, Classes): @@ -53,13 +57,17 @@ 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 _set_mutators(self, mutators): + if not isinstance(mutators, Mutators): + raise TypeError('Entity.mutators cannot be set to '\ + 'instance of type %s' % type(mutators)) + self._mutators = mutators def merge(self, other): self._classes.merge_unique(other._classes) self._applications.merge_unique(other._applications) self._parameters.merge(other._parameters) + self._mutators.push(other._mutators) self._name = other.name self._uri = other.uri self._environment = other.environment diff --git a/reclass/datatypes/mutators.py b/reclass/datatypes/mutators.py new file mode 100644 index 00000000..321f6725 --- /dev/null +++ b/reclass/datatypes/mutators.py @@ -0,0 +1,62 @@ +# +# -*- 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 collections import deque + +class Mutators(object): + ''' + A class to maintain a push-only stack of python callables, with the following specialty: + + + If a callable is being pushed onto the stack which is already there, + it is removed from its previous position and placed back on top + + Class also provides a static method for building lists of callables from iterables + of module paths + ''' + @staticmethod + def load_callables(iterable): + callables = [] + for path in iterable: + modname, callname = path.rsplit('.', 1) + module = __import__(modname, globals(), locals(), [callname], -1) + call = getattr(module, callname) + callables.append(call) + return callables + def __init__(self, iterable=None): + self._stack = deque() + if iterable is not None: + self.push(iterable) + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, + self.as_list()) + def __len__(self): + return len(self._stack) + def __eq__(self, rhs): + if isinstance(rhs, deque): + return self._stack == rhs + if isinstance(rhs, list): + return list(self._stack) == rhs + else: + try: + return self._stack == rhs._stack + except AttributeError: + return False + def __ne__(self, rhs): + return not self.__eq__(rhs) + def push(self, iterable): + if isinstance(iterable, self.__class__): + iterable = iterable.as_list() + for item in iterable: + if item in self._stack: + self._stack.remove(item) + self._stack.appendleft(item) + def as_list(self): + return list(self._stack) + def as_deque(self): + return deque(self._stack) \ No newline at end of file diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py index 4aee5453..37419fc6 100644 --- a/reclass/datatypes/parameters.py +++ b/reclass/datatypes/parameters.py @@ -10,10 +10,8 @@ from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER from reclass.utils.dictpath import DictPath -from reclass.utils.refvalue import (ReferenceStringParameter, ReferenceParameter, - ReferenceFunction, ReferenceStringFunction) -from reclass.errors import (InfiniteRecursionError, UndefinedVariableError, - UndefinedFunctionError) +from reclass.utils.refvalue import RefValue +from reclass.errors import InfiniteRecursionError, UndefinedVariableError class Parameters(object): ''' @@ -72,37 +70,37 @@ def as_dict(self): return self._base.copy() def _update_scalar(self, cur, new, path): - if isinstance(cur, ReferenceStringParameter) and path in self._occurrences: - # If the current value already holds a ReferenceStringParameter, we better forget + if isinstance(cur, RefValue) and path in self._occurrences: + # If the current value already holds a RefValue, we better forget # the occurrence, or else interpolate() will later overwrite - # unconditionally. If the new value is a ReferenceStringParameter, the occurrence + # unconditionally. If the new value is a RefValue, the occurrence # will be added again further on del self._occurrences[path] if self.delimiter is None or not isinstance(new, (types.StringTypes, - ReferenceStringParameter)): + RefValue)): # 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 ReferenceStringParameter), and not a ReferenceStringParameter. We can + # cannot be turned into a RefValue), and not a RefValue. We can # shortcut and just return the new scalar return new - elif isinstance(new, ReferenceStringParameter): - # the new value is (already) a ReferenceStringParameter, so we need not touch it + elif isinstance(new, RefValue): + # the new value is (already) a RefValue, 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 ReferenceStringParameter and querying the result - ret = ReferenceStringParameter(new, self.delimiter) + # by way of wrapping it in a RefValue and querying the result + ret = RefValue(new, self.delimiter) if not ret.has_references(): - # do not replace with ReferenceStringParameter instance if there are no - # references, i.e. discard the ReferenceStringParameter in ret, just return + # do not replace with RefValue instance if there are no + # references, i.e. discard the RefValue in ret, just return # the new value return new - # So we now have a ReferenceStringParameter. Let's, keep a reference to the instance + # So we now have a RefValue. 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 @@ -173,21 +171,6 @@ 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 = ReferenceStringFunction(v) - if refval.has_references(): - try: - node[k] = refval.render(inventory) - except UndefinedFunctionError as e: - raise UndefinedFunctionError(e.var) - def interpolate(self): while self.has_unresolved_refs(): # we could use a view here, but this is simple enough: @@ -199,7 +182,7 @@ 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.string) + path_from_ref = DictPath(self.delimiter, ref) try: refvalue_inner = self._occurrences[path_from_ref] @@ -218,13 +201,14 @@ def _interpolate_inner(self, path, refvalue): # 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) + 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 + try: new = refvalue.render(self._base) path.set_value(self._base, new) diff --git a/reclass/defaults.py b/reclass/defaults.py index 24be49f2..d0662908 100644 --- a/reclass/defaults.py +++ b/reclass/defaults.py @@ -26,5 +26,3 @@ PARAMETER_INTERPOLATION_SENTINELS = ('${', '}') PARAMETER_INTERPOLATION_DELIMITER = ':' - -FUNCTION_INTERPOLATION_SENTINELS = ('$<', '>') diff --git a/reclass/errors.py b/reclass/errors.py index 479bda27..ddb95fdb 100644 --- a/reclass/errors.py +++ b/reclass/errors.py @@ -10,8 +10,7 @@ import posix, sys import traceback -from reclass.defaults import (PARAMETER_INTERPOLATION_SENTINELS, - FUNCTION_INTERPOLATION_SENTINELS) +from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS class ReclassException(Exception): @@ -145,16 +144,6 @@ 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/storage/yaml_fs/yamlfile.py b/reclass/storage/yaml_fs/yamlfile.py index 717a9117..f50af337 100644 --- a/reclass/storage/yaml_fs/yamlfile.py +++ b/reclass/storage/yaml_fs/yamlfile.py @@ -47,6 +47,11 @@ def get_entity(self, name=None, default_environment=None): parameters = {} parameters = datatypes.Parameters(parameters) + mutators = self._data.get('mutators') + if mutators is None: + mutators = [] + mutators = datatypes.Mutators(datatypes.Mutators.load_callables(mutators)) + env = self._data.get('environment', default_environment) if name is None: @@ -54,7 +59,8 @@ def get_entity(self, name=None, default_environment=None): return datatypes.Entity(classes, applications, parameters, name=name, environment=env, - uri='yaml_fs://{0}'.format(self._path)) + uri='yaml_fs://{0}'.format(self._path), + mutators=mutators) def __repr__(self): return '<{0} {1}, {2}>'.format(self.__class__.__name__, self._path, diff --git a/reclass/utils/function.py b/reclass/utils/function.py deleted file mode 100644 index 1016f5a4..00000000 --- a/reclass/utils/function.py +++ /dev/null @@ -1,82 +0,0 @@ -# -# -*- 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() - if name == 'get': - return FunctionGet() - if name == 'list': - return FunctionList() - 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, inventory, *args): - return " ".join(args) - - -class FunctionAggregate(Function): - def __init__(self): - super(FunctionAggregate, self).__init__() - - def execute(self, inventory, *args): - func_filter, func_extract = args[0:2] - result = {} - matching_hosts = {} - for hostname, hostinfo in inventory.items(): - node = hostinfo.parameters.as_dict() - try: - 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 - -class FunctionGet(Function): - def __init__(self): - super(FunctionGet, self).__init__() - - def execute(self, inventory, *args): - nodename, func_extract = args[0:2] - node = inventory[nodename].parameters.as_dict() - return eval(func_extract) - -class FunctionList(Function): - def __init__(self): - super(FunctionList, self).__init__() - - def execute(self, inventory, *args): - func_filter, func_extract = args[0:2] - result = [] - for hostname, hostinfo in inventory.items(): - node = hostinfo.parameters.as_dict() - if eval(func_filter): - result.append(eval(func_extract)) - return result diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py index 9f1e9155..b8e730be 100644 --- a/reclass/utils/refvalue.py +++ b/reclass/utils/refvalue.py @@ -11,192 +11,105 @@ from reclass.utils.dictpath import DictPath from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, \ - PARAMETER_INTERPOLATION_DELIMITER, \ - FUNCTION_INTERPOLATION_SENTINELS + PARAMETER_INTERPOLATION_DELIMITER from reclass.errors import IncompleteInterpolationError, \ - UndefinedVariableError, \ - UndefinedFunctionError -from reclass.utils.function import get_function + UndefinedVariableError -_SENTINELS_PARAMETER = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] -_SENTINELS_FUNCTIONS = [re.escape(s) for s in FUNCTION_INTERPOLATION_SENTINELS] +_SENTINELS = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] +_RE = '{0}\s*(.+?)\s*{1}'.format(*_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): - def __init__(self, string): - self.string = string - - -class ReferenceFunction(Reference): - def __init__(self, string): - super(ReferenceFunction, self).__init__(string) - - def resolve(self, inventory, *args, **kwargs): - return self._execute(inventory) - - def _execute(self, inventory): - 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] - - try: - func = get_function(func_name) - ret = func.execute(inventory, *func_args) - return ret - except UndefinedFunctionError: - raise UndefinedFunctionError(self.string) - - -class ReferenceParameter(Reference): - def __init__(self, string): - super(ReferenceParameter, self).__init__(string) - - def resolve(self, context, *args, **kwargs): - path = DictPath(kwargs['delim'], self.string) - try: - return path.get_value(context) - except KeyError as e: - raise UndefinedVariableError(self.string) - - -class ReferenceString(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 ReferenceStringFunction(ReferenceString): - - INTERPOLATION_RE_FUNCTIONS = re.compile(_RE_FUNCTIONS) - - def __init__(self, string): - super(ReferenceStringFunction,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 = self.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 ReferenceStringParameter(ReferenceString): +class RefValue(object): ''' Isolates references in string values - ReferenceStringParameter can be used to isolate and eventually expand references to other + RefValue 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. - ReferenceStringParameter always gets constructed from a string, because templating + RefValue 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 ReferenceStringParameter instance + strings. Therefore, generally, the rendered value of a RefValue instance will also be a string. - Nevertheless, as this might not be desirable, ReferenceStringParameter will return the + Nevertheless, as this might not be desirable, RefValue 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]} - ReferenceStringParameter('My favourite colour is ${favolour}').render(mydict) + RefValue('My favourite colour is ${favolour}').render(mydict) → 'My favourite colour is yellow' # a string - ReferenceStringParameter('The answer is ${answer}').render(mydict) + RefValue('The answer is ${answer}').render(mydict) → 'The answer is 42' # a string - ReferenceStringParameter('${answer}').render(mydict) + RefValue('${answer}').render(mydict) → 42 # an int - ReferenceStringParameter('${list}').render(mydict) + RefValue('${list}').render(mydict) → [1,2,3] # an list The markers used to identify references are set in reclass.defaults, as is the default delimiter. ''' - INTERPOLATION_RE_PARAMETER = re.compile(_RE_PARAMETER) + INTERPOLATION_RE = re.compile(_RE) def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER): + self._strings = [] + self._refs = [] self._delim = delim - super(ReferenceStringParameter,self).__init__(string) - + self._parse(string) def _parse(self, string): - parts = ReferenceStringParameter.INTERPOLATION_RE_PARAMETER.split(string) + parts = RefValue.INTERPOLATION_RE.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._strings, PARAMETER_INTERPOLATION_SENTINELS) + self._check_strings(string) + + def _check_strings(self, orig): + for s in self._strings: + pos = s.find(PARAMETER_INTERPOLATION_SENTINELS[0]) + if pos >= 0: + raise IncompleteInterpolationError(orig, + PARAMETER_INTERPOLATION_SENTINELS[1]) + + def _resolve(self, ref, context): + path = DictPath(self._delim, ref) + try: + return path.get_value(context) + except KeyError as e: + raise UndefinedVariableError(ref) + + def has_references(self): + return len(self._refs) > 0 + def get_references(self): + return self._refs - def _resolve(self, ref, context, additional_info): - return ref.resolve(context, additional_info, delim=self._delim) + def _assemble(self, resolver): + if not self.has_references(): + return self._strings[0] - def render(self, context, additional_info=None): - resolver = lambda s: self._resolve(s, context, additional_info) - ret = self._assemble(resolver) + 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): + resolver = lambda s: self._resolve(s, context) + return self._assemble(resolver) + def __repr__(self): - do_not_resolve = lambda s: s.string.join(PARAMETER_INTERPOLATION_SENTINELS) - return 'ReferenceStringParameter(%r, %r)' % (self._assemble(do_not_resolve), + do_not_resolve = lambda s: s.join(PARAMETER_INTERPOLATION_SENTINELS) + return 'RefValue(%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 a99784d8..23d7e7b0 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 ReferenceStringParameter +from reclass.utils.refvalue import RefValue 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 TestReferenceStringParameter(unittest.TestCase): +class TestRefValue(unittest.TestCase): def test_simple_string(self): s = 'my cat likes to hide in boxes' - tv = ReferenceStringParameter(s) + tv = RefValue(s) self.assertFalse(tv.has_references()) self.assertEquals(tv.render(CONTEXT), s) def _test_solo_ref(self, key): s = _var(key) - tv = ReferenceStringParameter(s) + tv = RefValue(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 = ReferenceStringParameter(s) + tv = RefValue(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 = ReferenceStringParameter(s) + tv = RefValue(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 = ReferenceStringParameter(s) + tv = RefValue(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 = ReferenceStringParameter(s) + tv = RefValue(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 = ReferenceStringParameter(s) + tv = RefValue(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 = ReferenceStringParameter(s) + tv = RefValue(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 = ReferenceStringParameter(s) + tv = RefValue(s) with self.assertRaises(UndefinedVariableError): tv.render(CONTEXT) def test_incomplete_variable(self): s = PARAMETER_INTERPOLATION_SENTINELS[0] + 'incomplete' with self.assertRaises(IncompleteInterpolationError): - tv = ReferenceStringParameter(s) + tv = RefValue(s) if __name__ == '__main__': unittest.main()