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 )