Adapters

Introduction

Adapters allow extending the class behavior outside the class itself. This allows more modular, nice looking code in complex systems where there are hundreds of applicable methods per class.

  • Class interface itself is more readable (less visible clutter)
  • Class functionality can be extended outside the class source code
  • Add-on products may extend or override parts of the class functionality. Frameworks use adapters excessively, because adapters provide easy joint points. External code can override adapters to retrofit/modify functiolity. An example is that a theme product might want to override a searchbox viewlet to have little different search box functionality with theme specific goodies.

The downside is that adapters cannot be found by “exploring” classes or source code. They must be well documented to be discoverable.

Read more about adapters in zope.component README.

Adapter ZCML.

Adapters are matched by

  • Provider interface (what functionality adapter provides)
  • Parameter interfaces

There are two kinds of adapters

  • Normal adapters take in only one parameter
  • Multi-adapters take many parameters as tuple

Registering an adapter

Adapter provides functionality for an class and this functionality becomes available when interface is queried from the instance of class.

Below is an example how to make a custom “image provider”. Image provider provides a list of images for arbitary content.

This is the image provider interface:

from zope.interface import Interface

class IProductImageProvider(Interface):

    def getImages(self):
        """ Get Images associated with the product.

        @return: iterable of Image objects
        """

This is our content class:

class MyShoppableItemType(folder.ATFolder):
    """ Buyable physical good with variants of title and price and multiple images """
    implements(IVariantProduct)

    meta_type = "VariantProduct"
    schema = VariantProductSchema

This is the adapter for the content class:

import zope.interface

from getpaid.variantsproduct.interfaces.multiimageproduct import IProductImageProvider

class FolderishProductImageProvider(object):
    """ Mix-in class which provide product image management functions.

    Assume the content itself is folderish archetype content type and
    all contained image objects are product images.
    """

    zope.interface.implements(IProductImageProvider)

    def __init__(self, context):
        # Each adapter takes the object itself as the contruction parameter
        # and possibly provided other parameters for the interface adaption
        self.context = context

    def getImages(self):
        """ Return sequence of images.

        Perform folder listing and filter image content from it.
        """

        images = self.context.listFolderContents(contentFilter={"portal_type" : "Image"})
        return images

configure.zcml registers the adapter for my custom content type MyShoppableItemType:

<adapter for=".shop.MyShoppableItemType"
         provides=".interfaces.IProductImageProvider"
         factory=".images.FolderishProductImageProvider" />

Then we can query the adapter and use it. Unit testing example:

def test_get_images(self):

    self.loginAsPortalOwner()

    self.portal.invokeFactory("MyShoppableItemType", "product")

    product = self.portal.product

    image_provider = IProductImageProvider(product)

    images = image_provider.getImages()

    # Not yet any uploaded images
    self.assertEqual(len(images), 0)

Generic adapter contexts

Following interfaces are useful when registering adapters

  • zope.interface.Interface: Adapts to any object
  • Products.CMFCore.interfaces.IContentish: Adapts to any Plone content object
  • zope.publisher.interfaces.IBrowserView: Adapts to any BrowserView(context, request) object

Multi-adapter registration

You can specify any number of interface in <adapter for=”” /> . Separate them with space or newline.

Below is a view like example which registers against

  • any context (zope.interface.Interace)
  • HTTP request objects (zope.publisher.interfaces.browser.IBrowserRequest)

Emulate view registration (context, request):

<adapter for="zope.interface.Interface zope.publisher.interfaces.browser.IBrowserRequest"
       provides="gomobile.mobile.interfaces.IMobileTracker"
       factory=".bango.BangoTracker" />

Getting the adapter

There are two functions

  • zope.component.getAdapter will raise exception if adapter is not found
  • zope.component.queryAdapter will return None if adapter is not found

getAdapter/queryAdapter arguments

# Tuple consisting of (Object implementing interface in <adapter for=”“> first declaration, object implementing interface in <adapter for=”“> second declaration, ...)

# Adapter marker interface

Example registration:

<!-- Register header animation picking logic - override this for your custom logic -->
<adapter
 provides="plone.app.headeranimation.interfaces.IHeaderAnimationPicker"
 for="plone.app.headeranimation.behaviors.IHeaderBehavior
      Products.CMFCore.interfaces.IContentish
      zope.publisher.interfaces.browser.IBrowserRequest
      "
 factory=".picker.RandomHeaderAnimationPicker" />

Corresponding query code:

from zope.component import getUtility, getAdapter, getMultiAdapter

# header implements IHeaderBehavior
# doc implements Products.CMFCore.interfaces.IContentish
# request implements zope.publisher.interfaces.browser.IBrowserRequest

from Products.CMFCore.interfaces import IContentish
from zope.publisher.interfaces.browser import IBrowserRequest

self.assertTrue(IHeaderBehavior.providedBy(header))
self.assertTrue(IContentish.providedBy(doc))
self.assertTrue(IBrowserRequest.providedBy(self.portal.REQUEST))

# Throws exception if not found
picker = getMultiAdapter((header, doc, self.portal.REQUEST), IHeaderAnimationPicker)

Note

You cannot get adapters on module body level code during import, as Zope Component Architecture is not yet initialized.

Listing adapter registers

The following code checks if IHeaderBehavior adapter is registered correctly:

from zope.component import getGlobalSiteManager
sm = getGlobalSiteManager()

registrations = [a for a in sm.registeredAdapters() if a.provided == IHeaderBehavior ]
self.assertEqual(len(registrations), 1)

Alternative listing adapters

Getting all multi-adapters (context, request)

Example:

from zope.component import getAdapters
adapters = getAdapters((context, request), provided=Interface)

Warning

This does not list locally registered adapters like Zope views.

Local adapters

Local adapters are effective only inside a certain container, like a folder. They use five.localsitemanager to register themselves.