We often have the need to use the same configuration value
both in a ZConfig configuration as well as in other non-ZConfig
based "components".

Prominent examples are the "INSTANCE_HOME" in the Zope/ZEO startup
scripts and the "INSTANCE" definition in the corresponding
configuration files.

Moreover, configuration values are sometimes arithmethically
related, but ZConfig lacks arithmetic capabilities.
An example is the ZEO port which should (under some conditions)
be derived from "PORT_BASE".

Giving "ZConfig" access to the environment, allows configuration
setup to use the arithmetic capabilities of the shell
and communicate configuation values via the environment
between "ZConfig" based components and components that use different
configuration schemes.


The attached patch gives "ZConfig" an elementary function framework.
An application can register functions with "ZConfig".
These functions can be called through "substitution".
Its syntax is similar to "gmakes" function call:

    $(f)   calls function "f" with no arguments
    $(f a1,a2,...) calls function "f" with arguments "a1", "a2", ...

  Arguments are "," separated, stripped and substitution expanded
  before they are passed to the function.
  "," can be escaped through duplication, "(" is not allowed
  in the "ai" (although they can come up after substitution).

  Functions are called with "mapping" as additional first argument
  (such that they can do fancy things; e.g. implement
  Bash's "default" interpolation ("${name:-default}").

The patch uses the framework to implement a single function
"env":

        env(name, default=None)

        returns the value of "name" in the environment
        or "default".
        If the result it "None", a "KeyError" is raised.

-- 
Dieter

--- :functions.py	1970-01-01 01:00:00.000000000 +0100
+++ functions.py	2004-01-16 16:02:01.000000000 +0100
@@ -0,0 +1,53 @@
+#       $Id: functions.py,v 1.1 2004/01/16 15:02:01 dieter Exp $
+
+"""Substitution function support for ZConfig values.
+
+Functions are called with an additional (first) mapping argument.
+It contains the currently known definitions.
+"""
+
+# registry
+class Registry:
+    '''a function registry.'''
+
+    def __init__(self):
+        self._functions = {}
+
+    def _normalize(self, name):
+        return name.lower() # ZConfig seems to strive for case insensitiveness
+
+    def register(self, name, function, overwrite=False):
+        '''register *function* under *name*.'''
+        name = self._normalize(name)
+        known = self._functions
+        if not overwrite:
+            # check, we do not yet know this function
+            f = known.get(name)
+            if f is not None and f != function:
+                raise ValueError("function '%s' already registered" % name)
+        known[name] = function
+
+    def resolve(self, name):
+        '''return function registered for *name* or 'None'.'''
+        name = self._normalize(name)
+        return self._functions.get(name)
+
+_registry = Registry()
+
+registerFunction = _registry.register
+resolveFunction = _registry.resolve
+
+def _env(mapping, name, default=None):
+    '''look up *name* in environment.
+
+    return *default* if unsuccessful;
+    raise 'KeyError' if *default is 'None'
+    '''
+    from os import environ
+    v = environ.get(name, default)
+    if v is not None: return v
+    raise KeyError("environment does not define name", name)
+
+registerFunction('env', _env)
+
+
--- :substitution.py	2003-12-22 07:27:36.000000000 +0100
+++ substitution.py	2004-01-16 13:38:07.000000000 +0100
@@ -15,6 +15,8 @@
 
 import ZConfig
 
+from functions import resolveFunction
+
 try:
     True
 except NameError:
@@ -31,9 +33,12 @@
             p, name, namecase, rest = _split(rest)
             result += p
             if name:
-                v = mapping.get(name)
-                if v is None:
-                    raise ZConfig.SubstitutionReplacementError(s, namecase)
+                if isinstance(name, _Function):
+                    v, rest = name(mapping)
+                else:
+                    v = mapping.get(name)
+                    if v is None:
+                        raise ZConfig.SubstitutionReplacementError(s, namecase)
                 result += v
         return result
     else:
@@ -75,6 +80,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:
@@ -90,3 +103,62 @@
 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
--- tests/:test_subst.py	2003-12-22 07:27:36.000000000 +0100
+++ tests/test_subst.py	2004-01-16 13:44:28.000000000 +0100
@@ -18,7 +18,8 @@
 
 import unittest
 
-from ZConfig import SubstitutionReplacementError, SubstitutionSyntaxError
+from ZConfig import SubstitutionReplacementError, SubstitutionSyntaxError, \
+     SubstitutionUnknownFunctionError
 from ZConfig.substitution import isname, substitute
 
 
@@ -89,6 +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)
--- :__init__.py	2003-12-22 07:27:36.000000000 +0100
+++ __init__.py	2004-01-16 11:37:29.000000000 +0100
@@ -118,8 +118,14 @@
 class SubstitutionReplacementError(ConfigurationSyntaxError, LookupError):
     """Raised when no replacement is available for a reference."""
 
+    _messagePrefix = "no replacement for "
+
     def __init__(self, source, name, url=None, lineno=None):
         self.source = source
         self.name = name
         ConfigurationSyntaxError.__init__(
-            self, "no replacement for " + `name`, url, lineno)
+            self, self._messagePrefix + `name`, url, lineno)
+
+class SubstitutionUnknownFunctionError(SubstitutionReplacementError):
+    _messagePrefix = "no definition for function "
+
_______________________________________________
Zope-Dev maillist  -  [EMAIL PROTECTED]
http://mail.zope.org/mailman/listinfo/zope-dev
**  No cross posts or HTML encoding!  **
(Related lists - 
 http://mail.zope.org/mailman/listinfo/zope-announce
 http://mail.zope.org/mailman/listinfo/zope )

Reply via email to