|  | # | 
|  | # -*- coding: utf-8 -*- | 
|  | # | 
|  | # This file is part of reclass (http://github.com/madduck/reclass) | 
|  | # | 
|  | # Copyright © 2007–14 martin f. krafft <madduck@madduck.net> | 
|  | # Released under the terms of the Artistic Licence 2.0 | 
|  | # | 
|  |  | 
|  | import pyparsing as pp | 
|  | from lxml import etree | 
|  |  | 
|  | import re | 
|  |  | 
|  | from reclass.utils.dictpath import DictPath | 
|  | from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, \ | 
|  | PARAMETER_INTERPOLATION_DELIMITER | 
|  | from reclass.errors import IncompleteInterpolationError, \ | 
|  | UndefinedVariableError | 
|  |  | 
|  | _SENTINELS = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS] | 
|  |  | 
|  | _STR = 'STR' | 
|  | _REF = 'REF' | 
|  |  | 
|  | class RefValue(object): | 
|  | ''' | 
|  | Isolates references in string values | 
|  |  | 
|  | 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. | 
|  |  | 
|  | 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 RefValue instance | 
|  | will also be a string. | 
|  |  | 
|  | 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]} | 
|  | RefValue('My favourite colour is ${favolour}').render(mydict) | 
|  | → 'My favourite colour is yellow'      # a string | 
|  |  | 
|  | RefValue('The answer is ${answer}').render(mydict) | 
|  | → 'The answer is 42'                   # a string | 
|  |  | 
|  | RefValue('${answer}').render(mydict) | 
|  | → 42                                   # an int | 
|  |  | 
|  | 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. | 
|  | ''' | 
|  |  | 
|  | def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER): | 
|  | self._delim = delim | 
|  | self._tokens = [] | 
|  | self._refs = [] | 
|  | self._allRefs = False | 
|  | self._parse(string) | 
|  |  | 
|  | def _getParser(): | 
|  |  | 
|  | def _string(string, location, tokens): | 
|  | token = tokens[0] | 
|  | tokens[0] = (_STR, token) | 
|  |  | 
|  | def _reference(string, location, tokens): | 
|  | token = list(tokens[0]) | 
|  | tokens[0] = (_REF, token) | 
|  |  | 
|  | string = (pp.Literal('\\\\').setParseAction(pp.replaceWith('\\')) | | 
|  | pp.Literal('\\$').setParseAction(pp.replaceWith('$')) | | 
|  | pp.White() | | 
|  | pp.Word(pp.printables, excludeChars='\\$')).setParseAction(_string) | 
|  |  | 
|  | refString = (pp.Literal('\\\\').setParseAction(pp.replaceWith('\\')) | | 
|  | pp.Literal('\\$').setParseAction(pp.replaceWith('$')) | | 
|  | pp.Literal('\\{').setParseAction(pp.replaceWith('{')) | | 
|  | pp.Literal('\\}').setParseAction(pp.replaceWith('}')) | | 
|  | pp.White() | | 
|  | pp.Word(pp.printables, excludeChars='\\${}')).setParseAction(_string) | 
|  |  | 
|  | refItem = pp.Forward() | 
|  | refItems = pp.OneOrMore(refItem) | 
|  | reference = (pp.Literal(PARAMETER_INTERPOLATION_SENTINELS[0]).suppress() + | 
|  | pp.Group(refItems) + | 
|  | pp.Literal(PARAMETER_INTERPOLATION_SENTINELS[1]).suppress()).setParseAction(_reference) | 
|  | refItem << (reference | refString) | 
|  |  | 
|  | item = reference | string | 
|  | line = pp.OneOrMore(item) + pp.StringEnd() | 
|  | return line | 
|  |  | 
|  | _parser = _getParser() | 
|  |  | 
|  | def _parse(self, string): | 
|  | result = RefValue._parser.leaveWhitespace().parseString(string) | 
|  | self._tokens = result.asList() | 
|  | self.assembleRefs() | 
|  |  | 
|  | def _assembleRefs(self, tokens, resolver, first=True): | 
|  | for token in tokens: | 
|  | if token[0] == _REF: | 
|  | self._assembleRefs(token[1], resolver, False) | 
|  | try: | 
|  | s = self._assemble(token[1], resolver) | 
|  | self._refs.append(s) | 
|  | except UndefinedVariableError as e: | 
|  | self._allRefs = False | 
|  | pass | 
|  |  | 
|  | def assembleRefs(self, context={}): | 
|  | resolver = lambda s: self._resolve(s, context) | 
|  | self._refs = [] | 
|  | self._allRefs = True | 
|  | self._assembleRefs(self._tokens, resolver, True) | 
|  |  | 
|  | def assembledAllRefs(self): | 
|  | return self._allRefs | 
|  |  | 
|  | 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 _assemble(self, tokens, resolver): | 
|  | # Preserve type if only one token | 
|  | if len(tokens) == 1: | 
|  | if tokens[0][0] == _STR: | 
|  | return tokens[0][1] | 
|  | elif tokens[0][0] == _REF: | 
|  | return resolver(self._assemble(tokens[0][1], resolver)) | 
|  | # Multiple tokens | 
|  | string = '' | 
|  | for token in tokens: | 
|  | if token[0] == _STR: | 
|  | string += token[1] | 
|  | elif token[0] == _REF: | 
|  | string += str(resolver(self._assemble(token[1], resolver))) | 
|  | return string | 
|  |  | 
|  | def render(self, context): | 
|  | resolver = lambda s: self._resolve(s, context) | 
|  | return self._assemble(self._tokens, resolver) | 
|  |  | 
|  | def __repr__(self): | 
|  | do_not_resolve = lambda s: s.join(PARAMETER_INTERPOLATION_SENTINELS) | 
|  | return 'RefValue(%r, %r)' % (self._assemble(self._tokens, do_not_resolve), | 
|  | self._delim) |