blob: a6ae7ecbc1a1cf866ca79f9f17494c61de1cf9b3 [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 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)