allow nested parameter substitutions
diff --git a/reclass/utils/refvalue.py b/reclass/utils/refvalue.py
index b8e730b..e02f195 100644
--- a/reclass/utils/refvalue.py
+++ b/reclass/utils/refvalue.py
@@ -7,6 +7,9 @@
# 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
@@ -16,7 +19,10 @@
UndefinedVariableError
_SENTINELS = [re.escape(s) for s in PARAMETER_INTERPOLATION_SENTINELS]
-_RE = '{0}\s*(.+?)\s*{1}'.format(*_SENTINELS)
+_RE = '(.+?)(?={0}|$)'.format(_SENTINELS[0])
+
+_STR = 'STR'
+_REF = 'REF'
class RefValue(object):
'''
@@ -54,26 +60,57 @@
the default delimiter.
'''
- INTERPOLATION_RE = re.compile(_RE)
-
def __init__(self, string, delim=PARAMETER_INTERPOLATION_DELIMITER):
- self._strings = []
- self._refs = []
self._delim = delim
+ self._tokens = []
+ self._refs = []
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)
+ # order of checking is important here, the nested ${} check then the regex
+ scanner = pp.ZeroOrMore(pp.MatchFirst([
+ pp.nestedExpr(opener=PARAMETER_INTERPOLATION_SENTINELS[0], closer=PARAMETER_INTERPOLATION_SENTINELS[1]).setResultsName(_REF),
+ pp.Regex(_RE).setResultsName(_STR, listAllMatches=True)
+ ]))
+ result = scanner.leaveWhitespace().parseString(string)
+ xml = etree.fromstring(result.asXML())
+ self._parseXML(xml)
+ self._assembleRefs(self._tokens)
- 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 _parseXML(self, elements):
+ self._tokens = []
+ for el in elements:
+ if (el.tag == _STR):
+ self._tokens.append((_STR, el.text))
+ elif (el.tag == _REF):
+ self._tokens.append((_REF, self._parseRefXML(el)))
+ else:
+ self._tokens.append(('???', '???'))
+
+ def _parseRefXML(self, elements):
+ result = []
+ for el in elements:
+ if (len(el) == 0):
+ result.append((_STR, el.text))
+ else:
+ result.append((_REF, self._parseRefXML(el)))
+ return result
+
+ def _assembleRefs(self, tokens, first=True):
+ string = ''
+ retNone = False
+ for token in tokens:
+ if token[0] == _STR:
+ string += token[1]
+ elif token[0] == _REF:
+ s = self._assembleRefs(token[1], first=False)
+ if s != None:
+ self._refs.append(s)
+ if not first:
+ retNone = True
+ if retNone:
+ string = None
+ return string
def _resolve(self, ref, context):
path = DictPath(self._delim, ref)
@@ -88,28 +125,27 @@
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 _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(resolver)
+ 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(do_not_resolve),
+ return 'RefValue(%r, %r)' % (self._assemble(self._tokens, do_not_resolve),
self._delim)