blob: b8e730beb3220f95a7152498e65466a1dded2c30 [file] [log] [blame]
#
# -*- 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 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]
_RE = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS)
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.
'''
INTERPOLATION_RE = re.compile(_RE)
def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER):
self._strings = []
self._refs = []
self._delim = delim
self._parse(string)
def _parse(self, string):
parts = RefValue.INTERPOLATION_RE.split(string)
self._refs = parts[1:][::2]
self._strings = parts[0:][::2]
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 _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):
resolver = lambda s: self._resolve(s, context)
return self._assemble(resolver)
def __repr__(self):
do_not_resolve = lambda s: s.join(PARAMETER_INTERPOLATION_SENTINELS)
return 'RefValue(%r, %r)' % (self._assemble(do_not_resolve),
self._delim)