[Checkins] SVN: zope.component/trunk/ merged the conditional-zope.security branch to the trunk

Fabio Tranchitella kobold at kobold.it
Sun Nov 8 06:05:28 EST 2009


Log message for revision 105528:
  merged the conditional-zope.security branch to the trunk

Changed:
  U   zope.component/trunk/CHANGES.txt
  U   zope.component/trunk/setup.py
  A   zope.component/trunk/src/zope/component/hooks.py
  A   zope.component/trunk/src/zope/component/hooks.txt
  U   zope.component/trunk/src/zope/component/interfaces.py
  A   zope.component/trunk/src/zope/component/security.py
  U   zope.component/trunk/src/zope/component/tests.py
  U   zope.component/trunk/src/zope/component/zcml.py
  A   zope.component/trunk/src/zope/component/zcml_conditional.txt

-=-
Modified: zope.component/trunk/CHANGES.txt
===================================================================
--- zope.component/trunk/CHANGES.txt	2009-11-08 11:03:47 UTC (rev 105527)
+++ zope.component/trunk/CHANGES.txt	2009-11-08 11:05:28 UTC (rev 105528)
@@ -4,8 +4,17 @@
 3.8.0 (unreleased)
 ==================
 
-- ...
+- Removed the dependencies on zope.proxy and zope.security from the zcml extra:
+  zope.component does not hard depend on them anymore; the support for security
+  proxied components ZCML registrations is enabled only if zope.security and
+  zope.proxy are available.
 
+- Moved the IPossibleSite and ISite interfaces here from zope.location as they
+  are dealing with zope.component's concept of a site, but not with location.
+
+- Moved the zope.site.hooks functionality to zope.component.hooks as it isn't
+  actually dealing with zope.site's concept of a site.
+
 3.7.1 (2009-07-24)
 ==================
 

Modified: zope.component/trunk/setup.py
===================================================================
--- zope.component/trunk/setup.py	2009-11-08 11:03:47 UTC (rev 105527)
+++ zope.component/trunk/setup.py	2009-11-08 11:05:28 UTC (rev 105528)
@@ -63,7 +63,13 @@
     package_dir = {'': 'src'},
 
     namespace_packages=['zope',],
-    tests_require = ['zope.testing'],
+    tests_require = [
+        'zope.testing'
+        'zope.hookable',
+        'zope.location',
+        'zope.proxy',
+        'zope.security',
+        ],
     install_requires=['setuptools',
                       'zope.interface',
                       'zope.event',
@@ -74,14 +80,14 @@
         hook = ['zope.hookable'],
         persistentregistry = ['ZODB3'],
         zcml = ['zope.configuration',
-                'zope.security',
-                'zope.proxy',
                 'zope.i18nmessageid',
                 ],
         test = ['ZODB3',
                 'zope.testing',
                 'zope.hookable',
                 'zope.location',
+                'zope.proxy',
+                'zope.security',
                 ],
         docs = ['z3c.recipe.sphinxdoc'],
         ),

Added: zope.component/trunk/src/zope/component/hooks.py
===================================================================
--- zope.component/trunk/src/zope/component/hooks.py	                        (rev 0)
+++ zope.component/trunk/src/zope/component/hooks.py	2009-11-08 11:05:28 UTC (rev 105528)
@@ -0,0 +1,127 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Hooks for getting and setting a site in the thread global namespace.
+
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+import threading
+import zope.component
+
+try:
+    import zope.security.proxy
+except ImportError:
+    SECURITY_SUPPORT = False
+else:
+    SECURITY_SUPPORT = True
+
+
+class read_property(object):
+    def __init__(self, func):
+        self.func = func
+
+    def __get__(self, inst, cls):
+        if inst is None:
+            return self
+
+        return self.func(inst)
+
+class SiteInfo(threading.local):
+    site = None
+    sm = zope.component.getGlobalSiteManager()
+
+    def adapter_hook(self):
+        adapter_hook = self.sm.adapters.adapter_hook
+        self.adapter_hook = adapter_hook
+        return adapter_hook
+
+    adapter_hook = read_property(adapter_hook)
+
+siteinfo = SiteInfo()
+
+def setSite(site=None):
+    if site is None:
+        sm = zope.component.getGlobalSiteManager()
+    else:
+
+        # We remove the security proxy because there's no way for
+        # untrusted code to get at it without it being proxied again.
+
+        # We should really look look at this again though, especially
+        # once site managers do less.  There's probably no good reason why
+        # they can't be proxied.  Well, except maybe for performance.
+
+        if SECURITY_SUPPORT:
+            site = zope.security.proxy.removeSecurityProxy(site)
+        # The getSiteManager method is defined by IPossibleSite.
+        sm = site.getSiteManager()
+
+    siteinfo.site = site
+    siteinfo.sm = sm
+    try:
+        del siteinfo.adapter_hook
+    except AttributeError:
+        pass
+
+def getSite():
+    return siteinfo.site
+
+
+def getSiteManager(context=None):
+    """A special hook for getting the site manager.
+
+    Here we take the currently set site into account to find the appropriate
+    site manager.
+    """
+    if context is None:
+        return siteinfo.sm
+
+    # We remove the security proxy because there's no way for
+    # untrusted code to get at it without it being proxied again.
+
+    # We should really look look at this again though, especially
+    # once site managers do less.  There's probably no good reason why
+    # they can't be proxied.  Well, except maybe for performance.
+    sm = zope.component.interfaces.IComponentLookup(
+        context, zope.component.getGlobalSiteManager())
+    if SECURITY_SUPPORT:
+        sm = zope.security.proxy.removeSecurityProxy(sm)
+    return sm
+
+
+def adapter_hook(interface, object, name='', default=None):
+    try:
+        return siteinfo.adapter_hook(interface, object, name, default)
+    except zope.component.interfaces.ComponentLookupError:
+        return default
+
+
+def setHooks():
+    zope.component.adapter_hook.sethook(adapter_hook)
+    zope.component.getSiteManager.sethook(getSiteManager)
+
+def resetHooks():
+    # Reset hookable functions to original implementation.
+    zope.component.adapter_hook.reset()
+    zope.component.getSiteManager.reset()
+
+# Clear the site thread global
+clearSite = setSite
+try:
+    from zope.testing.cleanup import addCleanUp
+except ImportError:
+    pass
+else:
+    addCleanUp(resetHooks)

Added: zope.component/trunk/src/zope/component/hooks.txt
===================================================================
--- zope.component/trunk/src/zope/component/hooks.txt	                        (rev 0)
+++ zope.component/trunk/src/zope/component/hooks.txt	2009-11-08 11:05:28 UTC (rev 105528)
@@ -0,0 +1,65 @@
+==============================
+The current component registry
+==============================
+
+There can be any number of component registries in an application. One of them
+is the global component registry, and there is also the concept of a currently
+used component registry. Component registries other than the global one are
+associated with objects called sites. The ``zope.component.hooks`` module
+provides an API to set and access the current site as well as manipulate the
+adapter hook associated with it.
+
+As long as we haven't set a site, none is being considered current:
+
+>>> from zope.component.hooks import getSite
+>>> print getSite()
+None
+
+We can also ask for the current component registry (aka site manager
+historically); it will return the global one if no current site is set:
+
+>>> from zope.component.hooks import getSiteManager
+>>> getSiteManager()
+<BaseGlobalComponents base>
+
+Let's set a site now. A site has to be an object that provides the
+``getSiteManager`` method, which is specified by
+``zope.component.interfaces.IPossibleSite``:
+
+>>> from zope.component.registry import Components
+>>> class Site(object):
+...     def __init__(self):
+...         self.registry = Components('components')
+...     def getSiteManager(self):
+...         return self.registry
+
+>>> from zope.component.hooks import setSite
+>>> site1 = Site()
+>>> setSite(site1)
+
+After this, the newly set site is considered the currently active one:
+
+>>> getSite() is site1
+True
+>>> getSiteManager() is site1.registry
+True
+
+If we set another site, that one will be considered current:
+
+>>> site2 = Site()
+>>> site2.registry is not site1.registry
+True
+>>> setSite(site2)
+
+>>> getSite() is site2
+True
+>>> getSiteManager() is site2.registry
+True
+
+Finally we can unset the site and the global component registry is used again:
+
+>>> setSite()
+>>> print getSite()
+None
+>>> getSiteManager()
+<BaseGlobalComponents base>

Modified: zope.component/trunk/src/zope/component/interfaces.py
===================================================================
--- zope.component/trunk/src/zope/component/interfaces.py	2009-11-08 11:03:47 UTC (rev 105527)
+++ zope.component/trunk/src/zope/component/interfaces.py	2009-11-08 11:05:28 UTC (rev 105528)
@@ -923,3 +923,22 @@
 class IComponents(IComponentLookup, IComponentRegistry):
     """Component registration and access
     """
+
+
+class IPossibleSite(Interface):
+    """An object that could be a site.
+    """
+
+    def setSiteManager(sitemanager):
+        """Sets the site manager for this object.
+        """
+
+    def getSiteManager():
+        """Returns the site manager contained in this object.
+
+        If there isn't a site manager, raise a component lookup.
+        """
+
+
+class ISite(IPossibleSite):
+    """Marker interface to indicate that we have a site"""

Added: zope.component/trunk/src/zope/component/security.py
===================================================================
--- zope.component/trunk/src/zope/component/security.py	                        (rev 0)
+++ zope.component/trunk/src/zope/component/security.py	2009-11-08 11:05:28 UTC (rev 105528)
@@ -0,0 +1,97 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""zope.security support for the configuration handlers
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+from zope.interface import providedBy
+from zope.proxy import ProxyBase, getProxiedObject
+from zope.security.adapter import LocatingTrustedAdapterFactory, \
+    LocatingUntrustedAdapterFactory, TrustedAdapterFactory
+from zope.security.checker import Checker, CheckerPublic, InterfaceChecker
+from zope.security.proxy import Proxy
+
+
+PublicPermission = 'zope.Public'
+
+class PermissionProxy(ProxyBase):
+
+    __slots__ = ('__Security_checker__', )
+
+    def __providedBy__(self):
+        return providedBy(getProxiedObject(self))
+    __providedBy__ = property(__providedBy__)
+
+def _checker(_context, permission, allowed_interface, allowed_attributes):
+    if (not allowed_attributes) and (not allowed_interface):
+        allowed_attributes = ["__call__"]
+
+    if permission == PublicPermission:
+        permission = CheckerPublic
+
+    require={}
+    if allowed_attributes:
+        for name in allowed_attributes:
+            require[name] = permission
+    if allowed_interface:
+        for i in allowed_interface:
+            for name in i.names(all=True):
+                require[name] = permission
+
+    checker = Checker(require)
+    return checker
+
+def proxify(ob, checker=None, provides=None, permission=None):
+    """Try to get the object proxied with the `checker`, but not too soon
+
+    We really don't want to proxy the object unless we need to.
+    """
+
+    if checker is None:
+        if provides is None or permission is None:
+            raise ValueError, 'Required arguments: checker or both provides and permissions'
+        if permission == PublicPermission:
+            permission = CheckerPublic
+        checker = InterfaceChecker(provides, permission)
+    ob = PermissionProxy(ob)
+    ob.__Security_checker__ = checker
+    return ob
+
+def protectedFactory(original_factory, provides, permission):
+    if permission == PublicPermission:
+        permission = CheckerPublic
+    checker = InterfaceChecker(provides, permission)
+    # This has to be named 'factory', aparently, so as not to confuse apidoc :(
+    def factory(*args):
+        ob = original_factory(*args)
+        try:
+            ob.__Security_checker__ = checker
+        except AttributeError:
+            ob = Proxy(ob, checker)
+        return ob
+    factory.factory = original_factory
+    return factory
+
+def securityAdapterFactory(factory, permission, locate, trusted):
+    if locate or (permission is not None and permission != PublicPermission):
+        if trusted:
+            return LocatingTrustedAdapterFactory(factory)
+        else:
+            return LocatingUntrustedAdapterFactory(factory)
+    elif trusted:
+        return TrustedAdapterFactory(factory)
+    else:
+        return factory

Modified: zope.component/trunk/src/zope/component/tests.py
===================================================================
--- zope.component/trunk/src/zope/component/tests.py	2009-11-08 11:03:47 UTC (rev 105527)
+++ zope.component/trunk/src/zope/component/tests.py	2009-11-08 11:05:28 UTC (rev 105528)
@@ -15,16 +15,18 @@
 
 $Id$
 """
+import persistent
 import re
+import sys
 import unittest
 import transaction
-import persistent
 from cStringIO import StringIO
 
 from zope import interface, component
 from zope.interface.verify import verifyObject
 from zope.interface.interfaces import IInterface
 from zope.testing import doctest, renormalizing
+from zope.testing.testrunner.layer import UnitTests
 
 from zope.component.interfaces import ComponentLookupError
 from zope.component.interfaces import IComponentArchitecture
@@ -1635,6 +1637,28 @@
         self.assertRaises(ValueError, xmlconfig, config, testing=1)
 
 
+class ConditionalSecurityLayer(UnitTests):
+
+    __name__ = 'ConditionalSecurity'
+    __bases__ = ()
+
+    def setUp(self):
+        setUp()
+        self.modules = {}
+        for m in ('zope.security', 'zope.proxy'):
+            self.modules[m] = sys.modules[m]
+            sys.modules[m] = None
+        import zope.component.zcml
+        reload(zope.component.zcml)
+
+    def tearDown(self):
+        tearDown()
+        for m in ('zope.security', 'zope.proxy'):
+            sys.modules[m] = self.modules[m]
+        import zope.component.zcml
+        reload(zope.component.zcml)
+
+
 def setUpRegistryTests(tests):
     setUp()
 
@@ -1655,6 +1679,12 @@
                     r'exceptions.\1Error:'),
         ])
 
+    zcml_conditional = doctest.DocFileSuite('zcml_conditional.txt', checker=checker)
+    zcml_conditional.layer = ConditionalSecurityLayer()
+
+    hooks_conditional = doctest.DocFileSuite('hooks.txt', checker=checker)
+    hooks_conditional.layer = ConditionalSecurityLayer()
+
     return unittest.TestSuite((
         doctest.DocTestSuite(setUp=setUp, tearDown=tearDown),
         unittest.makeSuite(HookableTests),
@@ -1670,10 +1700,14 @@
         doctest.DocFileSuite('registry.txt', checker=checker,
                              setUp=setUpRegistryTests,
                              tearDown=tearDownRegistryTests),
+        doctest.DocFileSuite('hooks.txt',checker=checker,
+                             setUp=setUp, tearDown=tearDown),
         doctest.DocFileSuite('event.txt',
                              setUp=setUp, tearDown=tearDown),
-        doctest.DocFileSuite('zcml.txt',checker=checker,
+        doctest.DocFileSuite('zcml.txt', checker=checker,
                              setUp=setUp, tearDown=tearDown),
+        zcml_conditional,
+        hooks_conditional,
         unittest.makeSuite(StandaloneTests),
         unittest.makeSuite(ResourceViewTests),
         ))

Modified: zope.component/trunk/src/zope/component/zcml.py
===================================================================
--- zope.component/trunk/src/zope/component/zcml.py	2009-11-08 11:03:47 UTC (rev 105527)
+++ zope.component/trunk/src/zope/component/zcml.py	2009-11-08 11:05:28 UTC (rev 105528)
@@ -19,22 +19,30 @@
 
 import warnings
 import zope.component
+import zope.configuration.fields
 import zope.interface
 import zope.schema
-import zope.configuration.fields
-from zope.configuration.exceptions import ConfigurationError
-import zope.security.zcml
+
 from zope.component.interface import provideInterface
-from zope.proxy import ProxyBase, getProxiedObject
-from zope.security.proxy import Proxy
-from zope.security.checker import InterfaceChecker, CheckerPublic, Checker
-from zope.security.adapter import LocatingTrustedAdapterFactory
-from zope.security.adapter import LocatingUntrustedAdapterFactory
-from zope.security.adapter import TrustedAdapterFactory
+from zope.configuration.exceptions import ConfigurationError
 from zope.i18nmessageid import MessageFactory
+
+try:
+    from zope.component.security import _checker, proxify, protectedFactory, \
+        securityAdapterFactory
+    from zope.security.zcml import Permission
+except ImportError:
+    SECURITY_SUPPORT = False
+    from zope.schema import TextLine as Permission
+else:
+    SECURITY_SUPPORT = True
+
 _ = MessageFactory('zope')
 
-PublicPermission = 'zope.Public'
+def check_security_support():
+    if not SECURITY_SUPPORT:
+        raise ConfigurationError("security proxied components are not "
+            "supported because zope.security is not available")
 
 def handler(methodName, *args, **kwargs):
     method = getattr(zope.component.getGlobalSiteManager(), methodName)
@@ -51,7 +59,7 @@
         required=False,
         )
 
-    permission = zope.security.zcml.Permission(
+    permission = Permission(
         title=_("Permission"),
         description=_("Permission required to use this component."),
         required=False,
@@ -96,7 +104,7 @@
           ),
         )
 
-    permission = zope.security.zcml.Permission(
+    permission = Permission(
         title=_("Permission"),
         description=_("This adapter is only available, if the principal"
                       " has this permission."),
@@ -147,20 +155,6 @@
     factory.factory = factories[0]
     return factory
 
-def _protectedFactory(original_factory, checker):
-    # This has to be named 'factory', aparently, so as not to confuse
-    # apidoc :(
-    def factory(*args):
-        ob = original_factory(*args)
-        try:
-            ob.__Security_checker__ = checker
-        except AttributeError:
-            ob = Proxy(ob, checker)
-
-        return ob
-    factory.factory = original_factory
-    return factory
-
 def adapter(_context, factory, provides=None, for_=None, permission=None,
             name='', trusted=False, locate=False):
 
@@ -195,20 +189,13 @@
         factory = _rolledUpFactory(factories)
 
     if permission is not None:
-        if permission == PublicPermission:
-            permission = CheckerPublic
-        checker = InterfaceChecker(provides, permission)
-        factory = _protectedFactory(factory, checker)
+        check_security_support()
+        factory = protectedFactory(factory, provides, permission)
 
     # invoke custom adapter factories
-    if locate or (permission is not None and permission is not CheckerPublic):
-        if trusted:
-            factory = LocatingTrustedAdapterFactory(factory)
-        else:
-            factory = LocatingUntrustedAdapterFactory(factory)
-    else:
-        if trusted:
-            factory = TrustedAdapterFactory(factory)
+    if locate or permission is not None or trusted:
+        check_security_support()
+        factory = securityAdapterFactory(factory, permission, locate, trusted)
 
     _context.action(
         discriminator = ('adapter', for_, provides, name),
@@ -263,7 +250,7 @@
           ),
         )
 
-    permission = zope.security.zcml.Permission(
+    permission = Permission(
         title=_("Permission"),
         description=_("This subscriber is only available, if the"
                       " principal has this permission."),
@@ -319,22 +306,15 @@
                             "determine what the factory (or handler) adapts.")
 
     if permission is not None:
-        if permission == PublicPermission:
-            permission = CheckerPublic
-        checker = InterfaceChecker(provides, permission)
-        factory = _protectedFactory(factory, checker)
+        check_security_support()
+        factory = protectedFactory(factory, provides, permission)
 
     for_ = tuple(for_)
 
     # invoke custom adapter factories
-    if locate or (permission is not None and permission is not CheckerPublic):
-        if trusted:
-            factory = LocatingTrustedAdapterFactory(factory)
-        else:
-            factory = LocatingUntrustedAdapterFactory(factory)
-    else:
-        if trusted:
-            factory = TrustedAdapterFactory(factory)
+    if locate or permission is not None or trusted:
+        check_security_support()
+        factory = securityAdapterFactory(factory, permission, locate, trusted)
 
     if handler is not None:
         _context.action(
@@ -383,24 +363,6 @@
         required=False,
         )
 
-class PermissionProxy(ProxyBase):
-
-    __slots__ = ('__Security_checker__', )
-
-    def __providedBy__(self):
-        return zope.interface.providedBy(getProxiedObject(self))
-    __providedBy__ = property(__providedBy__)
-
-def proxify(ob, checker):
-    """Try to get the object proxied with the `checker`, but not too soon
-
-    We really don't want to proxy the object unless we need to.
-    """
-
-    ob = PermissionProxy(ob)
-    ob.__Security_checker__ = checker
-    return ob
-
 def utility(_context, provides=None, component=None, factory=None,
             permission=None, name=''):
     if factory and component:
@@ -417,12 +379,9 @@
             raise TypeError("Missing 'provides' attribute")
 
     if permission is not None:
-        if permission == PublicPermission:
-            permission = CheckerPublic
-        checker = InterfaceChecker(provides, permission)
+        check_security_support()
+        component = proxify(component, provides=provides, permission=permission)
 
-        component = proxify(component, checker)
-
     _context.action(
         discriminator = ('utility', provides, name),
         callable = handler,
@@ -475,7 +434,7 @@
           ),
         )
 
-    permission = zope.security.zcml.Permission(
+    permission = Permission(
         title=_("Permission"),
         description=_("The permission needed to use the view."),
         required=False,
@@ -572,7 +531,8 @@
     if not factory:
         raise ConfigurationError("No view factory specified.")
 
-    if permission:
+    if permission is not None:
+        check_security_support()
 
         checker = _checker(_context, permission,
                            allowed_interface, allowed_attributes)
@@ -684,7 +644,9 @@
             "allowed_attributes"
             )
 
-    if permission:
+    if permission is not None:
+        check_security_support()
+
         checker = _checker(_context, permission,
                            allowed_interface, allowed_attributes)
 
@@ -716,22 +678,3 @@
         callable = provideInterface,
         args = (provides.__module__ + '.' + provides.__name__, type)
                )
-
-def _checker(_context, permission, allowed_interface, allowed_attributes):
-    if (not allowed_attributes) and (not allowed_interface):
-        allowed_attributes = ["__call__"]
-
-    if permission == PublicPermission:
-        permission = CheckerPublic
-
-    require={}
-    if allowed_attributes:
-        for name in allowed_attributes:
-            require[name] = permission
-    if allowed_interface:
-        for i in allowed_interface:
-            for name in i.names(all=True):
-                require[name] = permission
-
-    checker = Checker(require)
-    return checker

Added: zope.component/trunk/src/zope/component/zcml_conditional.txt
===================================================================
--- zope.component/trunk/src/zope/component/zcml_conditional.txt	                        (rev 0)
+++ zope.component/trunk/src/zope/component/zcml_conditional.txt	2009-11-08 11:05:28 UTC (rev 105528)
@@ -0,0 +1,612 @@
+ZCML directives without zope.security support
+=============================================
+
+This tests run without zope.security available:
+
+  >>> from zope.component.zcml import check_security_support
+  >>> check_security_support()
+  Traceback (most recent call last):
+  ...
+  ConfigurationError: security proxied components are not supported because zope.security is not available
+
+Components may be registered using the registration API exposed by
+``zope.component`` (provideAdapter, provideUtility, etc.).  They may
+also be registered using configuration files.  The common way to do
+that is by using ZCML (Zope Configuration Markup Language), an XML
+spelling of component registration.
+
+In ZCML, each XML element is a *directive*.  There are different
+top-level directives that let us register components.  We will
+introduce them one by one here.
+
+This helper will let us easily execute ZCML snippets:
+
+  >>> from cStringIO import StringIO
+  >>> from zope.configuration.xmlconfig import xmlconfig
+  >>> def runSnippet(snippet):
+  ...     template = """\
+  ...     <configure xmlns='http://namespaces.zope.org/zope'
+  ...                i18n_domain="zope">
+  ...     %s
+  ...     </configure>"""
+  ...     xmlconfig(StringIO(template % snippet))
+
+adapter
+-------
+
+Adapters play a key role in the Component Architecture.  In ZCML, they
+are registered with the <adapter /> directive.
+
+  >>> from zope.component.testfiles.adapter import A1, A2, A3, Handler
+  >>> from zope.component.testfiles.adapter import I1, I2, I3, IS
+  >>> from zope.component.testfiles.components import IContent, Content, Comp, comp
+
+Before we register the first test adapter, we can verify that adapter
+lookup doesn't work yet:
+
+  >>> from zope.component.tests import clearZCML
+  >>> clearZCML()
+  >>> from zope.component.testfiles.components import IApp
+  >>> IApp(Content(), None) is None
+  True
+
+Then we register the adapter and see that the lookup works:
+
+  >>> runSnippet('''
+  ...   <adapter
+  ...       factory="zope.component.testfiles.components.Comp"
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       for="zope.component.testfiles.components.IContent"
+  ...       />''')
+
+  >>> IApp(Content()).__class__
+  <class 'zope.component.testfiles.components.Comp'>
+
+It is also possible to give adapters names.  Then the combination of
+required interface, provided interface and name makes the adapter
+lookup unique.  The name is supplied using the ``name`` argument to
+the <adapter /> directive:
+
+  >>> from zope.component.tests import clearZCML
+  >>> clearZCML()
+  >>> import zope.component
+  >>> zope.component.queryAdapter(Content(), IApp, 'test') is None
+  True
+
+  >>> runSnippet('''
+  ...   <adapter
+  ...       factory="zope.component.testfiles.components.Comp"
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       for="zope.component.testfiles.components.IContent"
+  ...       name="test"
+  ...       />''')
+
+  >>> zope.component.getAdapter(Content(), IApp, 'test').__class__
+  <class 'zope.component.testfiles.components.Comp'>
+
+Adapter factories
+~~~~~~~~~~~~~~~~~
+
+It is possible to supply more than one adapter factory.  In this case,
+during adapter lookup each factory will be called and the return value
+will be given to the next factory.  The return value of the last
+factory is returned as the result of the adapter lookup.  For examle:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <adapter
+  ...       factory="zope.component.testfiles.adapter.A1
+  ...                zope.component.testfiles.adapter.A2
+  ...                zope.component.testfiles.adapter.A3"
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       for="zope.component.testfiles.components.IContent"
+  ...       />''')
+
+The resulting adapter is an A3, around an A2, around an A1, around the
+adapted object:
+
+  >>> content = Content()
+  >>> a3 = IApp(content)
+  >>> a3.__class__ is A3
+  True
+
+  >>> a2 = a3.context[0]
+  >>> a2.__class__ is A2
+  True
+
+  >>> a1 = a2.context[0]
+  >>> a1.__class__ is A1
+  True
+
+  >>> a1.context[0] is content
+  True
+
+Of course, if no factory is provided at all, we will get an error:
+
+  >>> runSnippet('''
+  ...   <adapter
+  ...       factory=""
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       for="zope.component.testfiles.components.IContent"
+  ...       />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-8.8
+      ValueError: No factory specified
+
+Declaring ``for`` and ``provides`` in Python
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The <adapter /> directive can figure out from the in-line Python
+declaration (using ``zope.component.adapts()`` or
+``zope.component.adapter()`` as well as ``zope.interface.implements``)
+what the adapter should be registered for and what it provides::
+
+  >>> clearZCML()
+  >>> IApp(Content(), None) is None
+  True
+
+  >>> runSnippet('''
+  ...   <adapter factory="zope.component.testfiles.components.Comp" />''')
+
+  >>> IApp(Content()).__class__
+  <class 'zope.component.testfiles.components.Comp'>
+
+Of course, if the adapter has no ``implements()`` declaration, ZCML
+can't figure out what it provides:
+
+  >>> runSnippet('''
+  ...   <adapter
+  ...       factory="zope.component.testfiles.adapter.A4"
+  ...       for="zope.component.testfiles.components.IContent"
+  ...       />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-7.8
+      TypeError: Missing 'provides' attribute
+
+On the other hand, if the factory implements more than one interface,
+ZCML can't figure out what it should provide either:
+
+  >>> runSnippet('''
+  ...   <adapter
+  ...       factory="zope.component.testfiles.adapter.A5"
+  ...       for="zope.component.testfiles.components.IContent"
+  ...       />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-7.8
+      TypeError: Missing 'provides' attribute
+
+A not so common edge case is registering adapters directly for
+classes, not for interfaces.  For example:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <adapter
+  ...       for="zope.component.testfiles.components.Content"
+  ...       provides="zope.component.testfiles.adapter.I1"
+  ...       factory="zope.component.testfiles.adapter.A1"
+  ...       />''')
+
+  >>> content = Content()
+  >>> a1 = zope.component.getAdapter(content, I1, '')
+  >>> isinstance(a1, A1)
+  True
+
+This time, any object providing ``IContent`` won't work if it's not an
+instance of the ``Content`` class:
+
+  >>> import zope.interface
+  >>> class MyContent:
+  ...     zope.interface.implements(IContent)
+  >>> zope.component.getAdapter(MyContent(), I1, '')  # doctest: +ELLIPSIS
+  Traceback (most recent call last):
+    ...
+  ComponentLookupError: ...
+
+Multi-adapters
+~~~~~~~~~~~~~~
+
+Conventional adapters adapt one object to provide another interface.
+Multi-adapters adapt several objects at once:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <adapter
+  ...       for="zope.component.testfiles.components.IContent
+  ...            zope.component.testfiles.adapter.I1
+  ...            zope.component.testfiles.adapter.I2"
+  ...       provides="zope.component.testfiles.adapter.I3"
+  ...       factory="zope.component.testfiles.adapter.A3"
+  ...       />''')
+
+  >>> content = Content()
+  >>> a1 = A1()
+  >>> a2 = A2()
+  >>> a3 = zope.component.queryMultiAdapter((content, a1, a2), I3)
+  >>> a3.__class__ is A3
+  True
+  >>> a3.context == (content, a1, a2)
+  True
+
+You can even adapt an empty list of objects (we call this a
+null-adapter):
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <adapter
+  ...       for=""
+  ...       provides="zope.component.testfiles.adapter.I3"
+  ...       factory="zope.component.testfiles.adapter.A3"
+  ...       />''')
+
+  >>> a3 = zope.component.queryMultiAdapter((), I3)
+  >>> a3.__class__ is A3
+  True
+  >>> a3.context == ()
+  True
+
+Even with multi-adapters, ZCML can figure out the ``for`` and
+``provides`` parameters from the Python declarations:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <adapter factory="zope.component.testfiles.adapter.A3" />''')
+
+  >>> a3 = zope.component.queryMultiAdapter((content, a1, a2), I3)
+  >>> a3.__class__ is A3
+  True
+  >>> a3.context == (content, a1, a2)
+  True
+
+Chained factories are not supported for multi-adapters, though:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <adapter
+  ...       for="zope.component.testfiles.components.IContent
+  ...            zope.component.testfiles.adapter.I1
+  ...            zope.component.testfiles.adapter.I2"
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       factory="zope.component.testfiles.adapter.A1
+  ...                zope.component.testfiles.adapter.A2"
+  ...       />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-11.8
+      ValueError: Can't use multiple factories and multiple for
+
+And neither for null-adapters:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <adapter
+  ...       for=""
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       factory="zope.component.testfiles.adapter.A1
+  ...                zope.component.testfiles.adapter.A2"
+  ...       />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-9.8
+      ValueError: Can't use multiple factories and multiple for
+
+subscriber
+----------
+
+With the <subscriber /> directive you can register subscription
+adapters or event subscribers with the adapter registry.  Consider
+this very typical example of a <subscriber /> directive:
+ 
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <subscriber
+  ...       provides="zope.component.testfiles.adapter.IS"
+  ...       factory="zope.component.testfiles.adapter.A3"
+  ...       for="zope.component.testfiles.components.IContent
+  ...            zope.component.testfiles.adapter.I1"
+  ...       />''')
+
+  >>> content = Content()
+  >>> a1 = A1()
+
+  >>> subscribers = zope.component.subscribers((content, a1), IS)
+  >>> a3 = subscribers[0]
+  >>> a3.__class__ is A3
+  True
+  >>> a3.context == (content, a1)
+  True
+
+Note how ZCML provides some additional information when registering
+components, such as the ZCML filename and line numbers:
+
+  >>> gsm = zope.component.getGlobalSiteManager()
+  >>> doc = [reg.info for reg in gsm.registeredSubscriptionAdapters()
+  ...        if reg.provided is IS][0]
+  >>> print doc
+  File "<string>", line 4.2-9.8
+    Could not read source.
+
+The "fun" behind subscription adapters/subscribers is that when
+several ones are declared for the same for/provides, they are all
+found.  With regular adapters, the most specific one (and in doubt the
+one registered last) wins.  Consider these two subscribers:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <subscriber
+  ...       provides="zope.component.testfiles.adapter.IS"
+  ...       factory="zope.component.testfiles.adapter.A3"
+  ...       for="zope.component.testfiles.components.IContent
+  ...            zope.component.testfiles.adapter.I1"
+  ...       />
+  ...   <subscriber
+  ...       provides="zope.component.testfiles.adapter.IS"
+  ...       factory="zope.component.testfiles.adapter.A2"
+  ...       for="zope.component.testfiles.components.IContent
+  ...            zope.component.testfiles.adapter.I1"
+  ...       />''')
+
+  >>> subscribers = zope.component.subscribers((content, a1), IS)
+  >>> len(subscribers)
+  2
+  >>> sorted([a.__class__.__name__ for a in subscribers])
+  ['A2', 'A3']
+
+Declaring ``for`` and ``provides`` in Python
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Like the <adapter /> directive, the <subscriber /> directive can
+figure out from the in-line Python declaration (using
+``zope.component.adapts()`` or ``zope.component.adapter()``) what the
+subscriber should be registered for:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <subscriber
+  ...       provides="zope.component.testfiles.adapter.IS"
+  ...       factory="zope.component.testfiles.adapter.A3"
+  ...       />''')
+
+  >>> content = Content()
+  >>> a2 = A2()
+  >>> subscribers = zope.component.subscribers((content, a1, a2), IS)
+
+  >>> a3 = subscribers[0]
+  >>> a3.__class__ is A3
+  True
+  >>> a3.context == (content, a1, a2)
+  True
+
+In the same way the directive can figure out what a subscriber
+provides:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <subscriber handler="zope.component.testfiles.adapter.A3" />''')
+
+  >>> sm = zope.component.getSiteManager()
+  >>> a3 = sm.adapters.subscriptions((IContent, I1, I2), None)[0]
+  >>> a3 is A3
+  True
+
+A not so common edge case is declaring subscribers directly for
+classes, not for interfaces.  For example:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <subscriber
+  ...       for="zope.component.testfiles.components.Content"
+  ...       provides="zope.component.testfiles.adapter.I1"
+  ...       factory="zope.component.testfiles.adapter.A1"
+  ...       />''')
+
+  >>> subs = list(zope.component.subscribers((Content(),), I1))
+  >>> isinstance(subs[0], A1)
+  True
+
+This time, any object providing ``IContent`` won't work if it's not an
+instance of the ``Content`` class:
+
+  >>> list(zope.component.subscribers((MyContent(),), I1))
+  []
+
+Event subscriber (handlers)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes, subscribers don't need to be adapters that actually provide
+anything.  It's enough that a callable is called for a certain event.
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <subscriber
+  ...       for="zope.component.testfiles.components.IContent
+  ...            zope.component.testfiles.adapter.I1"
+  ...       handler="zope.component.testfiles.adapter.Handler"
+  ...       />''')
+
+In this case, simply getting the subscribers is enough to invoke them:
+
+  >>> list(zope.component.subscribers((content, a1), None))
+  []
+  >>> content.args == ((a1,),)
+  True
+
+
+utility
+-------
+
+Apart from adapters (and subscription adapters), the Component
+Architecture knows a second kind of component: utilities.  They are
+registered using the <utility /> directive.
+
+Before we register the first test utility, we can verify that utility
+lookup doesn't work yet:
+
+  >>> clearZCML()
+  >>> zope.component.queryUtility(IApp) is None
+  True
+
+Then we register the utility:
+
+  >>> runSnippet('''
+  ...   <utility
+  ...       component="zope.component.testfiles.components.comp"
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       />''')
+  >>> zope.component.getUtility(IApp) is comp
+  True
+
+Like adapters, utilities can also have names.  There can be more than
+one utility registered for a certain interface, as long as they each
+have a different name.
+
+First, we make sure that there's no utility yet:
+
+  >>> clearZCML()
+  >>> zope.component.queryUtility(IApp, 'test') is None
+  True
+
+Then we register it:
+
+  >>> runSnippet('''
+  ...   <utility
+  ...       component="zope.component.testfiles.components.comp"
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       name="test"
+  ...       />''')
+  >>> zope.component.getUtility(IApp, 'test') is comp
+  True
+
+Utilities can also be registered from a factory.  In this case, the
+ZCML handler calls the factory (without any arguments) and registers
+the returned value as a utility.  Typically, you'd pass a class for
+the factory:
+
+  >>> clearZCML()
+  >>> zope.component.queryUtility(IApp) is None
+  True
+
+  >>> runSnippet('''
+  ...   <utility
+  ...       factory="zope.component.testfiles.components.Comp"
+  ...       provides="zope.component.testfiles.components.IApp"
+  ...       />''')
+  >>> zope.component.getUtility(IApp).__class__ is Comp
+  True
+
+Declaring ``provides`` in Python
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Like other directives, <utility /> can also figure out which interface
+a utility provides from the Python declaration:
+
+  >>> clearZCML()
+  >>> zope.component.queryUtility(IApp) is None
+  True
+
+  >>> runSnippet('''
+  ...   <utility component="zope.component.testfiles.components.comp" />''')
+  >>> zope.component.getUtility(IApp) is comp
+  True
+
+It won't work if the component that is to be registered doesn't
+provide anything:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <utility component="zope.component.testfiles.adapter.a4" />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-4.61
+      TypeError: Missing 'provides' attribute
+
+Or if more than one interface is provided (then the ZCML directive
+handler doesn't know under which the utility should be registered):
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <utility component="zope.component.testfiles.adapter.a5" />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-4.61
+      TypeError: Missing 'provides' attribute
+
+We can repeat the same drill for utility factories:
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <utility factory="zope.component.testfiles.components.Comp" />''')
+  >>> zope.component.getUtility(IApp).__class__ is Comp
+  True
+
+  >>> runSnippet('''
+  ...   <utility factory="zope.component.testfiles.adapter.A4" />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-4.59
+      TypeError: Missing 'provides' attribute
+
+  >>> clearZCML()
+  >>> runSnippet('''
+  ...   <utility factory="zope.component.testfiles.adapter.A5" />''')
+  Traceback (most recent call last):
+    ...
+  ZopeXMLConfigurationError: File "<string>", line 4.2-4.59
+      TypeError: Missing 'provides' attribute
+
+interface
+---------
+
+The <interface /> directive lets us register an interface.  Interfaces
+are registered as named utilities.  We therefore needn't go though all
+the lookup details again, it is sufficient to see whether the
+directive handler emits the right actions.
+
+First we provide a stub configuration context:
+
+  >>> import re, pprint
+  >>> atre = re.compile(' at [0-9a-fA-Fx]+')
+  >>> class Context(object):
+  ...    actions = ()
+  ...    def action(self, discriminator, callable, args):
+  ...        self.actions += ((discriminator, callable, args), )
+  ...    def __repr__(self):
+  ...        stream = StringIO()
+  ...        pprinter = pprint.PrettyPrinter(stream=stream, width=60)
+  ...        pprinter.pprint(self.actions)
+  ...        r = stream.getvalue()
+  ...        return (''.join(atre.split(r))).strip()
+  >>> context = Context()
+
+Then we provide a test interface that we'd like to register:
+
+  >>> from zope.interface import Interface
+  >>> class I(Interface):
+  ...     pass
+
+It doesn't yet provide ``ITestType``:
+
+  >>> from zope.component.tests import ITestType
+  >>> ITestType.providedBy(I)
+  False
+
+However, after calling the directive handler...
+
+  >>> from zope.component.zcml import interface
+  >>> interface(context, I, ITestType)
+  >>> context
+  ((None,
+    <function provideInterface>,
+    ('',
+     <InterfaceClass __builtin__.I>,
+     <InterfaceClass zope.component.tests.ITestType>)),)
+
+...it does provide ``ITestType``:
+
+  >>> from zope.interface.interfaces import IInterface
+  >>> ITestType.extends(IInterface)
+  True
+  >>> IInterface.providedBy(I)
+  True



More information about the checkins mailing list