[Zope-Checkins] CVS: Zope3/lib/python/Zope/Configuration - ConfigurationDirectiveInterfaces.py:1.1.2.1 meta.py:1.1.2.6 name.py:1.1.2.4 xmlconfig.py:1.1.2.4

Jim Fulton jim@zope.com
Thu, 3 Jan 2002 14:29:55 -0500


Update of /cvs-repository/Zope3/lib/python/Zope/Configuration
In directory cvs.zope.org:/tmp/cvs-serv16747/Configuration

Modified Files:
      Tag: Zope-3x-branch
	meta.py name.py xmlconfig.py 
Added Files:
      Tag: Zope-3x-branch
	ConfigurationDirectiveInterfaces.py 
Log Message:
Refactored configuration framework:

- Configuration directives must be written to a 
  a different framework. See
  ConfigurationDirectiveInterfaces.

- Configuration directives now don't take actions immediately.
  Instead, they return a sequence of discriminators and callables
  objects with arguments.  This allows configuration to be defered to
  allow overriding and conflct detection.

- Can now detect conflicting directives

- Can override directives. Directives in including configuration files
  override directives in included files. Conflicting directives are
  decided based on discriminators.

- Added new directives for defining directives. All directives, except
  for a few bootstrap irectives, are now configurable in the
  configuration file. This makes directives a little more discoverable
  and facilitates extension of directives.


=== Added File Zope3/lib/python/Zope/Configuration/ConfigurationDirectiveInterfaces.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""Configuration directives

Revision information: $Id: ConfigurationDirectiveInterfaces.py,v 1.1.2.1 2002/01/03 19:29:24 jim Exp $
"""
from Interface import Interface

class IEmptyDirective(Interface):

    def __call__(**kw):
        """Compute configuration actions

        Return a sequence of configuration actions. Each action is a
        tuple with:

        - A discriminator, value used to identify conflicting
          actions. Actions cnflict of they have the same value values
          for their discriminators.

        - callable object

        - argument tuple

        - and, optionally, a keyword argument dictionary.

        The callable object will be called with the argument tuple and
        keyword arguments to perform the action.
        """

class INonEmptyDirective(Interface):

    def __call__(**kw):
        """Compute complex directive handler

        Return an IComplexDirectiveHandler
        """

class ISubdirectiveHandler(Interface):
    """Handle subdirectives

    Provide mehods for registered subdirectives.

    Also provide a call that can provide additional configuration actions.
    """

    def __call__():
        """Return a sequence of configuration actions."""


=== Zope3/lib/python/Zope/Configuration/meta.py 1.1.2.5 => 1.1.2.6 ===
 """Registration of registration directives
 
-Currently, a directive:
-
-    Is a callable object that is called with keyword
-    arguments and may do something.
-
-    If it returns None is returned, then a directive may not have
-    subdirectives.
-
-    If an object is returned, then its methods will be treated as (sub)
-    directives, and it will be called without arguments when we get
-    are done with the (outer) directive.
-
-This will all change soon. :/
+See ConfigurationDirectiveInterfaces
 
 """
 
-_directives={}
+from ConfigurationDirectiveInterfaces import INonEmptyDirective
+from ConfigurationDirectiveInterfaces import ISubdirectiveHandler
+from name import resolve
+
+_directives = {}
 
 class InvalidDirective(Exception):
     "An invalid directive was used"
 
-def register(namespace, name, callable):
-    _directives[namespace, name] = callable
+class BrokenDirective(Exception):
+    "A directive is implemented incorrectly"
 
-def execute(_namespace, _name, **kw):
-    try: callable=_directives[_namespace, _name]
+def register(name, callable):
+    subdirs = {}
+    _directives[name] = callable, subdirs
+    return subdirs
+
+def registersub(directives, name):
+    subdirs = {}
+    directives[name] = subdirs
+    return subdirs
+
+def _exe(callable, subs, kw):
+    r = callable(**kw)
+
+    if subs or INonEmptyDirective.isImplementedBy(callable):
+        return r, subs
+    else:
+        return lambda: r, subs
+
+def begin(_custom_directives, _name, **kw):
+    if _custom_directives and (_name in _custom_directives):
+        callable, subs = _custom_directives[_name]
+    else:
+        try:
+            callable, subs = _directives[_name]
+        except KeyError:
+            raise InvalidDirective(_name)
+
+    return _exe(callable, subs, kw)
+
+def sub(subs, _name, **kw):
+
+    base, subdirs = subs
+    
+    try:
+        subs = subdirs[_name]
     except KeyError:
-        raise InvalidDirective(_namespace, _name)
-    return callable(**kw)
+        raise InvalidDirective(_name)
+
+    callable = getattr(base, _name[1])
+
+    return _exe(callable, subs, kw)
+
+defaultkw = ({},)
+def end(base):
+    actions = base[0]()
+    ractions = []
+    for action in actions:
+        if len(action) < 3 or len(action) > 4:
+            raise BrokenDirective(action)
+        if len(action) == 3:
+            action += defaultkw
+        ractions.append(action)
+    return ractions
+
+class DirectiveNamespace:
+
+    def __init__(self, namespace):
+        self._namespace = namespace
+
+    def directive(self, name, handler, attributes='', namespace=None):
+        namespace = namespace or self._namespace
+        subs = register((namespace, name), resolve(handler))
+        return Subdirective(subs, namespace)
+
+    def __call__(self):
+        return ()
+
+def Directive(namespace, name, handler, attributes=''):
+    subs = register((namespace, name), resolve(handler))
+    return Subdirective(subs, namespace)
+
+Directive.__implements__ = INonEmptyDirective
+
+class InvaliDirectiveDefinition(Exception): pass
+
+class Subdirective:
+    """This is the meta-meta-directive"""
+    # 
+    # Unlike other directives, it doesn't return any actions, but
+    # takes action right away, since it's actions are needed to process other
+    # directives.
+    # 
+    # For this reason, this isn't a good directive example.
+
+    __implements__ = ISubdirectiveHandler
+
+    def __init__(self, subs, namespace=None):
+        self._subs = subs
+        self._namespace = namespace
+
+    def subdirective(self, name, attributes='', namespace=None):
+        namespace = namespace or self._namespace
+        if not namespace:
+            raise InvaliDirectiveDefinition(name)
+            
+        subs = registersub(self._subs, (namespace, name))
+        return Subdirective(subs)
+
+    def __call__(self):
+        return ()
 
 def _clear():
     "To support unit tests"
     _directives.clear()
+    _directives[('http://namespaces.zope.org/zope', 'directive')] = (
+        Directive, {
+        ('http://namespaces.zope.org/zope', 'subdirective'): {
+        ('http://namespaces.zope.org/zope', 'subdirective'): {
+        ('http://namespaces.zope.org/zope', 'subdirective'): {
+        }}}})
+    _directives[('http://namespaces.zope.org/zope', 'directives')] = (
+        DirectiveNamespace, {
+        ('http://namespaces.zope.org/zope', 'directive'): {
+        ('http://namespaces.zope.org/zope', 'subdirective'): {
+        ('http://namespaces.zope.org/zope', 'subdirective'): {
+        ('http://namespaces.zope.org/zope', 'subdirective'): {
+        }}}}})
+
+_clear()


=== Zope3/lib/python/Zope/Configuration/name.py 1.1.2.3 => 1.1.2.4 ===
 """
 
+import sys
 from types import ModuleType
 
 def resolve(name, _silly=('__doc__',), _globals={}):
     if name[:1]=='.':
         name='Zope.Products'+name
+
+    if name[-1:] == '.':
+        #XXX finish ending dots
+        name = name[:-1]
+        
     names=name.split('.')
     last=names[-1]
     mod='.'.join(names[:-1])
                  
     while 1:
-        m=__import__(mod, _globals, _globals, _silly)
+        m=sys.modules.get(mod)
+        if m is None:
+            m=__import__(mod, _globals, _globals, _silly)
         try:
             a=getattr(m, last)
         except AttributeError:


=== Zope3/lib/python/Zope/Configuration/xmlconfig.py 1.1.2.3 => 1.1.2.4 ===
 from xml.sax.xmlreader import InputSource
 from xml.sax.handler import ContentHandler, feature_namespaces
-from meta import execute
+from meta import begin, sub, end
 from keyword import iskeyword
-import sys
+import sys, os
 
 class ZopeXMLConfigurationError(Exception):
     "Zope XML Configuration error"
@@ -30,12 +30,28 @@
         return "%s at line %s column %s of %s" % (
             self.mess, self.lno, self.cno, self.sid)
 
+class ConfigurationExecutionError(ZopeXMLConfigurationError):
+    """An error occurred during execution of a configuration action
+    """
+
+    def __init__(self, locator, mess):
+        if type(mess) is not type(''):
+            try:
+                mess = "%s: %s" % (mess.__class__.__name__, mess)
+            except AttributeError:
+                mess=str(mess)
+        self.lno, self.cno, self.sid = locator
+        self.mess=mess
+
 class ConfigurationHandler(ContentHandler):
 
     __top_name = 'http://namespaces.zope.org/zope', 'zopeConfigure' 
 
-    def __init__(self):
-        self.__stack=[]
+    def __init__(self, actions, level=None, directives=None):
+        self.__stack = []
+        self.__level = level
+        self.__actions = actions
+        self.__directives = directives
 
     def setDocumentLocator(self, locator):
         self.__locator=locator
@@ -57,43 +73,167 @@
                 kw[aname]=value
 
         if len(stack) == 1:
-            try:
-                stack.append(execute(*name, **kw))
+            try:                
+                stack.append(begin(self.__directives, name, **kw))
             except Exception, v:
                 raise ZopeXMLConfigurationError, (
-                    self.__locator, str(v)), sys.exc_info()[2] 
+                    self.__locator, v), sys.exc_info()[2] 
                 
-            self.__ns = name[0]
         else:
-            if self.__ns != name[0]:
-                raise ZopeXMLConfigurationError(self.__locator,
-                                                'Namespace missmatch')
-            ob = self.__stack[-1]
-            if ob is None:
+            subs = self.__stack[-1]
+            if subs is None:
                 raise ZopeXMLConfigurationError(self.__locator,
                                                 'Invalid sub-directive')
             try:
-                stack.append(getattr(ob, name[1])(**kw))
+                stack.append(sub(subs, name, **kw))
             except Exception, v:
                 raise ZopeXMLConfigurationError, (
-                    self.__locator, str(v)), sys.exc_info()[2] 
+                    self.__locator, v), sys.exc_info()[2] 
 
     def endElementNS(self, name, qname):
-        ob = self.__stack.pop()
-        if ob is not None:
+        subs = self.__stack.pop()
+        # to fool compiler that thinks actions is used before assignment:
+        actions = ()
+
+        if subs is not None:
             try:
-                ob()
+                actions = end(subs)
             except Exception, v:
                 raise ZopeXMLConfigurationError, (
                     self.__locator, str(v)), sys.exc_info()[2] 
 
+        append = self.__actions.append
 
-def xmlconfig(file):
-    src=InputSource(getattr(file, 'name', '<string>'))
-    src.setByteStream(file)
+        try:
+            for des, callable, args, kw in actions:
+                append((self.__level,
+                        (self.__locator.getLineNumber(),
+                         self.__locator.getColumnNumber(),
+                         self.__locator.getSystemId(),
+                         ), des, callable, args, kw))
+        except:
+            print 'endElementNS', actions
+            raise
+
+class ZopeConflictingConfigurationError(ZopeXMLConfigurationError):
+    "Zope XML Configuration error"
 
+    def __init__(self, l1, l2, des):
+        self.l1=l1
+        self.l2=l2
+        self.des=des
+
+    def __str__(self):
+        return """Conflicting configuration action:
+        %s
+        at line %s column %s of %s
+        and% at line %s column %s of %s
+        """ % ((self.des,) + self.l1 + self.l2)
+
+def xmlconfig(file, actions=None, level=None, directives=None):
+    if actions is None:
+        call=actions=[]
+    else:
+        call=0
     
+    src=InputSource(getattr(file, 'name', '<string>'))
+    src.setByteStream(file)
     parser=make_parser()
-    parser.setContentHandler(ConfigurationHandler())
+    parser.setContentHandler(ConfigurationHandler(actions, level, directives))
     parser.setFeature(feature_namespaces, 1)
     parser.parse(src)
+
+    if call:
+        descriptors = {}
+        for level, loc, des, callable, args, kw in call:
+            if des in descriptors:
+                raise ZopeConflictingConfigurationError(
+                    descriptors[des], loc, des) 
+            descriptors[des]=loc
+            callable(*args, **kw)
+            
+class ZopeConfigurationConflictError(ZopeXMLConfigurationError):
+
+    def __init__(self, conflicts):
+        self._conflicts = conflicts
+
+    def __str__(self):
+        r = ["Conflicting configuration actions"]
+        for dis, locs in self._conflicts.items():
+            r.append('for: %s' % dis)
+            for loc in locs:
+                r.append("  at line %s column %s of %s" % loc)
+        
+        return "\n".join(r)
+    
+class XMLConfig:
+
+    def __init__(self, file_name):
+        self._actions = []
+        self._directives = {('http://namespaces.zope.org/zope', 'include'):
+                            (self.include, {})}
+
+        f = open(file_name)
+        self._stack=[file_name]
+        xmlconfig(f, self._actions, tuple(self._stack), self._directives)
+
+    def include(self, file):
+        file_name = os.path.join(os.path.dirname(self._stack[-1]), file)
+
+        f = open(file_name)
+        self._stack.append(file_name)
+        xmlconfig(f, self._actions, tuple(self._stack), self._directives)
+        self._stack.pop()
+        return ()
+
+    def __call__(self):
+        self.organize()
+
+    def __iter__(self): return iter(self._actions)
+
+    def organize(self):
+        actions = self._actions
+
+        # organize actions by discriminators
+        unique = {}
+        for i in range(len(actions)):
+            path, loc, des, callable, args, kw = actions[i]
+            a = unique.setdefault(des, [])
+            a.append((path, i, loc, (callable, args, kw)))
+
+        # Check for conflicts
+        conflicts = {}
+        for des, actions in unique.items():
+            path, i, loc, f = actions[0]
+            for opath, i, oloc, f in actions[1:]:
+                if opath[:len(path)] != path:
+                    if des not in conflicts:
+                        conflicts[des] = [loc]
+                    conflicts[des].append(oloc)
+
+        if conflicts:
+            raise ZopeConfigurationConflictError(conflicts)
+
+        # Now order the configuration directives
+        cactions = []
+        for des, actions in unique.items():
+            path, i, loc, f = actions.pop(0)
+            cactions.append((i, loc, f))
+
+        unique = None
+
+        cactions.sort()
+
+        # Call actions
+        for i, loc, f in cactions:
+            try:
+                callable, args, kw = f
+                callable(*args, **kw)
+            except Exception, v:
+                raise ConfigurationExecutionError(loc, v)
+        
+        
+        
+
+
+