[Checkins] SVN: ZConfig/branches/haufe-legacy-integration/ - Launchpad #373609: added support for function interpolation

Andreas Jung andreas at andreas-jung.com
Sun May 10 12:14:42 EDT 2009


Log message for revision 99828:
  
  - Launchpad #373609: added support for function interpolation
  
  

Changed:
  U   ZConfig/branches/haufe-legacy-integration/NEWS.txt
  U   ZConfig/branches/haufe-legacy-integration/ZConfig/__init__.py
  U   ZConfig/branches/haufe-legacy-integration/ZConfig/substitution.py
  U   ZConfig/branches/haufe-legacy-integration/ZConfig/tests/test_subst.py

-=-
Modified: ZConfig/branches/haufe-legacy-integration/NEWS.txt
===================================================================
--- ZConfig/branches/haufe-legacy-integration/NEWS.txt	2009-05-10 16:09:52 UTC (rev 99827)
+++ ZConfig/branches/haufe-legacy-integration/NEWS.txt	2009-05-10 16:14:42 UTC (rev 99828)
@@ -7,6 +7,8 @@
 
 - Launchpad #373614: new ``includeGlobPattern`` directive
 
+- Launchpad #373609: added support for function interpolation
+
 ZConfig 2.6.1 (2008/12/05)
 --------------------------
 

Modified: ZConfig/branches/haufe-legacy-integration/ZConfig/__init__.py
===================================================================
--- ZConfig/branches/haufe-legacy-integration/ZConfig/__init__.py	2009-05-10 16:09:52 UTC (rev 99827)
+++ ZConfig/branches/haufe-legacy-integration/ZConfig/__init__.py	2009-05-10 16:14:42 UTC (rev 99828)
@@ -159,3 +159,6 @@
         self.name = name
         ConfigurationSyntaxError.__init__(
             self, "no replacement for " + `name`, url, lineno)
+
+class SubstitutionUnknownFunctionError(SubstitutionReplacementError):
+    _messagePrefix = "no definition for function "

Modified: ZConfig/branches/haufe-legacy-integration/ZConfig/substitution.py
===================================================================
--- ZConfig/branches/haufe-legacy-integration/ZConfig/substitution.py	2009-05-10 16:09:52 UTC (rev 99827)
+++ ZConfig/branches/haufe-legacy-integration/ZConfig/substitution.py	2009-05-10 16:14:42 UTC (rev 99828)
@@ -15,7 +15,9 @@
 
 import ZConfig
 
+from functions import resolveFunction
 
+
 def substitute(s, mapping):
     """Interpolate variables from `mapping` into `s`."""
     if "$" in s:
@@ -25,7 +27,10 @@
             p, name, namecase, rest = _split(rest)
             result += p
             if name:
-                v = mapping.get(name)
+                if isinstance(name, _Function):
+                    v, rest = name(mapping)
+                else:
+                    v = mapping.get(name)
                 if v is None:
                     raise ZConfig.SubstitutionReplacementError(s, namecase)
                 result += v
@@ -69,6 +74,14 @@
             if not s.startswith("}", i - 1):
                 raise ZConfig.SubstitutionSyntaxError(
                     "'${%s' not followed by '}'" % name)
+        elif c == "(":
+            m = _name_match(s, i + 2)
+            if not m:
+                raise ZConfig.SubstitutionSyntaxError(
+                    "'$(' not followed by name")
+            name = m.group(0)
+            i = m.end() + 1
+            return prefix, _Function(s, m), None, None
         else:
             m = _name_match(s, i+1)
             if not m:
@@ -84,3 +97,64 @@
 import re
 _name_match = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*").match
 del re
+
+
+class _Function:
+    '''encapsulates a function call substitution.
+
+    A function call has the syntax '$(name args)'
+    where *args* is an optionally empty sequence of arguments.
+    Argumens in *args* are comma separated.
+    Comma can be escaped by duplication.
+    Arguments are 'stripped' and then substitution is applied
+    to them.
+
+    Note: We currently do not allow parenthesis (neither open nor closed)
+      in arguments. Use a definition, should you need such characters
+      in your arguments.
+    '''
+    def __init__(self, s, match):
+        '''function instance for function identified by *match* object in string *s*.'''
+        name = s[match.start():match.end()]
+        f = resolveFunction(name)
+        if f is None:
+            raise ZConfig.SubstitutionUnknownFunctionError(s, name)
+        self._function = f
+        # parse arguments
+        i = match.end()
+        self._args = args = []
+        if i >= len(s):
+            raise  ZConfig.SubstitutionSyntaxError("'$(%s' is not closed" % name)
+        if s[i] == ')':
+            self._rest = s[i+1:]
+            return
+        if not s[i].isspace():
+            raise ZConfig.SubstitutionSyntaxError("'$(%s' not followed by either ')' or whitespace" % name)
+
+        i += 1; arg = ''
+        while i < len(s):
+            c = s[i]; i += 1
+            if c in '(': # forbidden characters
+                raise  ZConfig.SubstitutionSyntaxError("'$(%s' contains forbidden character '%c'" % (name, c))
+            if c not in ',)':
+                arg += c; continue
+            if c == ',':
+                if i < len(s) and s[i] == c: # excaped
+                    arg += c; i += 1
+                    continue
+            args.append(arg.strip()); arg = ''
+            if c == ')': # end of function call
+                self._rest = s[i:]
+                return
+        raise  ZConfig.SubstitutionSyntaxError("'$(%s' is not closed" % name)
+
+    def __call__(self, mapping):
+        '''call the function.
+
+        Arguments are substitution expanded via *mapping*.
+
+        Returns text for function call and remaining text.
+        '''
+        args = [substitute(arg, mapping) for arg in self._args]
+        v = self._function(mapping, *args)
+        return v, self._rest

Modified: ZConfig/branches/haufe-legacy-integration/ZConfig/tests/test_subst.py
===================================================================
--- ZConfig/branches/haufe-legacy-integration/ZConfig/tests/test_subst.py	2009-05-10 16:09:52 UTC (rev 99827)
+++ ZConfig/branches/haufe-legacy-integration/ZConfig/tests/test_subst.py	2009-05-10 16:14:42 UTC (rev 99828)
@@ -18,7 +18,8 @@
 
 import unittest
 
-from ZConfig import SubstitutionReplacementError, SubstitutionSyntaxError
+from ZConfig import SubstitutionReplacementError, SubstitutionSyntaxError, \
+     SubstitutionUnknownFunctionError
 from ZConfig.substitution import isname, substitute
 
 
@@ -89,7 +90,51 @@
         self.assert_(not isname("abc-"))
         self.assert_(not isname(""))
 
+    def test_functions(self):
+        from ZConfig.functions import registerFunction
+        registerFunction('f', lambda mapping, *args: str(args), True)
+        # check syntax
+        def check(s):
+            self.assertRaises(SubstitutionSyntaxError,
+                              substitute, s, {})
+        check('$(')
+        check('$(f')
+        check('$(f,')
+        check('$(f a,b,c')
+        check('$(f a,(b),c')
 
+        # check arguments
+        def check(*args):
+            argstring = ', '.join(args)
+            if args: argstring = ' ' + argstring
+            v = substitute('$(f%s)' % argstring, {})
+            self.assertEqual(v, str(tuple([arg.strip() for arg in args])))
+        check()
+        check('')
+        check('','','')
+        check(' a ', 'bc ', ' xy')
+        # check correct left and right boundaries
+        self.assertEqual(substitute('a $(f) b',{}), 'a () b')
+        self.assertEqual(substitute('a$(f)b',{}), 'a()b')
+        self.assertEqual(substitute('a$(f)b$(f)',{}), 'a()b()')
+        # check interpolation in arguments
+        self.assertEqual(substitute('$(F $a)',{'a' : 'A'}), str(('A',)))
+        # check unknown function
+        self.assertRaises(SubstitutionUnknownFunctionError,
+                          substitute,'$(g)',{}
+                          )
+
+    def test_function_env(self):
+        from os import environ
+        key = 'ZCONFIG_TEST_ENV_FUNCTION'; key2 = key + '_none'
+        environ[key] = '1'
+        self.assertEqual(substitute('$(env %s)' % key, {}), '1')
+        self.assertEqual(substitute('$(env %s, 0)' % key2, {}), '0')
+        self.assertRaises(KeyError,
+                          substitute,'$(env %s)' % key2, {}
+                          )
+
+
 def test_suite():
     return unittest.makeSuite(SubstitutionTestCase)
 



More information about the Checkins mailing list