Safer, more stable and faster replaceTag interpolation (switched from cycle over all tags to re.sub with callable)

pull/1698/head
sebres 2017-02-16 18:25:34 +01:00
parent a6318b159b
commit 9ebf70cd6a
2 changed files with 65 additions and 27 deletions

View File

@ -34,7 +34,7 @@ from collections import MutableMapping
from .ipdns import asip from .ipdns import asip
from .mytime import MyTime from .mytime import MyTime
from .utils import Utils from .utils import Utils
from ..helpers import getLogger, substituteRecursiveTags from ..helpers import getLogger, substituteRecursiveTags, TAG_CRE, MAX_TAG_REPLACE_COUNT
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
@ -399,33 +399,71 @@ class CommandAction(ActionBase):
str str
`query` string with tags replaced. `query` string with tags replaced.
""" """
if '<' not in query: return query
# use cache if allowed: # use cache if allowed:
if cache is not None: if cache is not None:
ckey = (query, conditional) ckey = (query, conditional)
string = cache.get(ckey) value = cache.get(ckey)
if string is not None: if value is not None:
return string return value
# replace:
string = query # first try get cached tags dictionary:
aInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags) subInfo = csubkey = None
for tag in aInfo:
if "<%s>" % tag in query:
value = aInfo.get(tag + '?' + conditional)
if value is None:
value = aInfo.get(tag)
value = str(value) # assure string
if tag in cls._escapedTags:
# That one needs to be escaped since its content is
# out of our control
value = cls.escapeTag(value)
string = string.replace('<' + tag + '>', value)
# New line, space
string = reduce(lambda s, kv: s.replace(*kv), (("<br>", '\n'), ("<sp>", " ")), string)
# cache if properties:
if cache is not None: if cache is not None:
cache[ckey] = string csubkey = ('subst-tags', id(aInfo), conditional)
subInfo = cache.get(csubkey)
# interpolation of dictionary:
if subInfo is None:
subInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags)
# New line, space
for (tag, value) in (("br", '\n'), ("sp", " ")):
if subInfo.get(tag) is None: subInfo[tag] = value
# cache if possible:
if csubkey is not None:
cache[csubkey] = subInfo
# substitution callable, used by interpolation of each tag
repeatSubst = {}
def substVal(m):
tag = m.group(1) # tagname from match
value = None
if conditional:
value = subInfo.get(tag + '?' + conditional)
if value is None:
value = subInfo.get(tag)
if value is None:
return m.group() # fallback (no replacement)
value = str(value) # assure string
if tag in cls._escapedTags:
# That one needs to be escaped since its content is
# out of our control
value = cls.escapeTag(value)
# possible contains tags:
if '<' in value:
repeatSubst[1] = True
return value
# interpolation of query:
count = MAX_TAG_REPLACE_COUNT + 1
while True:
repeatSubst = {}
value = TAG_CRE.sub(substVal, query)
# possible recursion ?
if not repeatSubst or value == query: break
query = value
count -= 1
if count <= 0: # pragma: no cover - almost impossible (because resolved above)
raise ValueError(
"unexpected too long replacement interpolation, "
"possible self referencing definitions in query: %s" % (query,))
# cache if possible:
if cache is not None:
cache[ckey] = value
# #
return string return value
def _processCmd(self, cmd, aInfo=None, conditional=''): def _processCmd(self, cmd, aInfo=None, conditional=''):
"""Executes a command with preliminary checks and substitutions. """Executes a command with preliminary checks and substitutions.
@ -491,7 +529,7 @@ class CommandAction(ActionBase):
realCmd = self.replaceTag(cmd, self._properties, realCmd = self.replaceTag(cmd, self._properties,
conditional=conditional, cache=self.__substCache) conditional=conditional, cache=self.__substCache)
# Replace tags # Replace dynamical tags (don't use cache here)
if aInfo is not None: if aInfo is not None:
realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional) realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional)
else: else:

View File

@ -217,10 +217,10 @@ class CommandActionTest(LogCaptureTestCase):
self.__action.replaceTag("<banaction> '<abc>'", self.__action._properties, self.__action.replaceTag("<banaction> '<abc>'", self.__action._properties,
conditional="family=inet6", cache=cache), conditional="family=inet6", cache=cache),
"Text 890-567 text 567 '567'") "Text 890-567 text 567 '567'")
self.assertEqual(len(cache) if cache is not None else -1, 3) self.assertTrue(len(cache) >= 3)
# set one parameter - internal properties and cache should be reseted: # set one parameter - internal properties and cache should be reseted:
setattr(self.__action, 'xyz', "000-<abc>") setattr(self.__action, 'xyz', "000-<abc>")
self.assertEqual(len(cache) if cache is not None else -1, 0) self.assertEqual(len(cache), 0)
# test againg, should have 000 instead of 890: # test againg, should have 000 instead of 890:
for i in range(2): for i in range(2):
self.assertEqual( self.assertEqual(
@ -235,7 +235,7 @@ class CommandActionTest(LogCaptureTestCase):
self.__action.replaceTag("<banaction> '<abc>'", self.__action._properties, self.__action.replaceTag("<banaction> '<abc>'", self.__action._properties,
conditional="family=inet6", cache=cache), conditional="family=inet6", cache=cache),
"Text 000-567 text 567 '567'") "Text 000-567 text 567 '567'")
self.assertEqual(len(cache), 3) self.assertTrue(len(cache) >= 3)
def testExecuteActionBan(self): def testExecuteActionBan(self):