Adapters¶
Introduction¶
Adapters make it possible to extend the behavior of a class without modifying the class itself. This allows more modular, readable code in complex systems where there might be hundreds of methods per class. Some more advantages of this concept are:
- The 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 extensively, because adapters provide easy integration points. External code can override adapters to retrofit/modify functionality. For example: a theme product might want to override a searchbox viewlet to have a search box with slightly different functionality and theme-specific goodies.
The downside is that adapters cannot be found by "exploring" classes or source code. They must be well documented in order to be discoverable.
Read more about adapters in the zope.component README.
Adapters are matched by:
- Provider interface (what functionality adapter provides).
- Parameter interfaces.
There are two kinds of adapters:
- Normal adapters that take only one parameter.
- Multi-adapters take many parameters in the form of a tuple.
Registering an adapter¶
Registering using ZCML¶
An adapter provides functionality to a class. This functionality becomes available when the interface is queried from the instance of class.
Below is an example how to make a custom "image provider". The image provider provides a list of images for arbitrary 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 a 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 construction
# parameter and possibly provides other parameters for the
# interface adaption
self.context = context
def getImages(self):
""" Return a sequence of images.
Perform folder listing and filter image content from it.
"""
images = self.context.listFolderContents(
contentFilter={"portal_type" : "Image"})
return images
Register the adapter for your custom content type
MyShoppableItemType
in the
configure.zcml
file of your product:
<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)
Registering using Python¶
Register to Global Site Manager using
registerAdapter()
.
Example:
from zope.component import getGlobalSiteManager
layer = klass.layer
gsm = getGlobalSiteManager()
gsm.registerAdapter(factory=MyClass, required=(layer,),
name=klass.__name__, provided=IWidgetDemo)
return klass
More info
Generic adapter contexts¶
The 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 interfaces in the
<adapter
for=""
/>
attribute. Separate them with spaces or newlines.
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 that may be used to get an adapter:
-
zope.component.getAdapter
will raise an exception if the adapter is not found. -
zope.component.queryAdapter
will returnNone
if the adapter is not found.
getAdapter
/queryAdapter
arguments:
- # Tuple consisting of: (Object implementing the first interface,
-
object implementing the second interface, ...)
The interfaces are in the order in which they were
declared in the
<adapter for="">
attribute.
# 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, to look up an adapter implementing the interfaces:
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-level code during import, as the Zope Component Architecture is not yet initialized.
Listing adapter registers¶
The following code checks whether the
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):
from zope.component import getAdapters
adapters = getAdapters((context, request), provided=Interface)
Warning
This does not list locally-registered adapters such as Zope views.
Local adapters¶
Local adapters are effective only inside a certain
container, such as a folder. They use
five.localsitemanager
to register themselves.