Using the Adapter Registry
This is a small demonstration of the zope.interface
package including its
adapter registry. It is intended to provide a concrete but narrow example on
how to use interfaces and adapters outside of Zope 3.
First we have to import the interface package:
>>> import zope.interface
We now develop an interface for our object, which is a simple file in this case. For now we simply support one attribute, the body, which contains the actual file contents:
>>> class IFile(zope.interface.Interface):
...
... body = zope.interface.Attribute('Contents of the file.')
...
For statistical reasons we often want to know the size of a file. However, it would be clumsy to implement the size directly in the file object, since the size really represents meta-data. Thus we create another interface that provides the size of something:
>>> class ISize(zope.interface.Interface):
...
... def getSize():
... 'Return the size of an object.'
...
Now we need to implement the file interface. It is essential that the object states
that it implements the IFile
interface. We also provide a default body
value (just to make things simpler for this example):
>>> @zope.interface.implementer(IFile)
... class File(object):
...
... body = 'foo bar'
...
Next we implement an adapter that can provide the ISize
interface given any
object providing IFile
. By convention we use __used_for__
to specify the
interface that we expect the adapted object to provide, in our case
IFile
. However, this attribute is not used for anything. If you have
multiple interfaces for which an adapter is used, just specify the interfaces
via a tuple.
Again by convention, the constructor of an adapter takes one argument, the
context. The context in this case is an instance of File
(providing IFile
)
that is used to extract the size from. Also by convention the context is
stored in an attribute named context
on the adapter. The twisted community
refers to the context as the original
object. However, you may feel free to
use a specific argument name, such as file
:
>>> @zope.interface.implementer(ISize)
... class FileSize(object):
...
... __used_for__ = IFile
...
... def __init__(self, context):
... self.context = context
...
... def getSize(self):
... return len(self.context.body)
...
Now that we have written our adapter, we have to register it with an adapter registry, so that it can be looked up when needed. There is no such thing as a global registry; thus we have to instantiate one for our example manually:
>>> from zope.interface.adapter import AdapterRegistry
>>> registry = AdapterRegistry()
The registry keeps a map of what adapters implement based on another
interface the object already provides. Therefore, we next have to register an
adapter that adapts from IFile
to ISize
. The first argument to
the registry’s register()
method is a list of original interfaces.In our
cause we have only one original interface, IFile
. A list makes sense, since
the interface package has the concept of multi-adapters, which are adapters
that require multiple objects to adapt to a new interface. In these
situations, your adapter constructor will require an argument for each
specified interface.
The second argument is the interface the adapter provides, in our case
ISize
. The third argument is the name of the adapter. Since we do not care
about names, we simply leave it as an empty string. Names are commonly useful,
if you have adapters for the same set of interfaces, but they are useful in
different situations. The last argument is simply the adapter class:
>>> registry.register([IFile], ISize, '', FileSize)
You can now use the the registry to lookup the adapter:
>>> registry.lookup1(IFile, ISize, '')
<class 'FileSize'>
Let’s get a little bit more practical. Let’s create a File
instance and
create the adapter using a registry lookup. Then we see whether the adapter
returns the correct size by calling getSize()
:
>>> file = File()
>>> size = registry.lookup1(IFile, ISize, '')(file)
>>> size.getSize()
7
However, this is not very practical, since I have to manually pass in the
arguments to the lookup method. There is some syntactic candy that will allow
us to get an adapter instance by simply calling ISize(file)
. To make use of
this functionality, we need to add our registry to the adapter_hooks
list,
which is a member of the adapters module. This list stores a collection of
callables that are automatically invoked when IFoo(obj)
is called; their
purpose is to locate adapters that implement an interface for a certain
context instance.
You are required to implement your own adapter hook; this example covers one
of the simplest hooks that use the registry, but you could implement one that
used an adapter cache or persistent adapters, for instance. The helper hook is
required to expect as first argument the desired output interface (for us
ISize
) and as the second argument the context of the adapter (here
file
). The function returns an adapter, i.e. a FileSize
instance:
>>> def hook(provided, object):
... adapter = registry.lookup1(zope.interface.providedBy(object),
... provided, '')
... return adapter(object)
...
We now just add the hook to an adapter_hooks
list:
>>> from zope.interface.interface import adapter_hooks
>>> adapter_hooks.append(hook)
Once the hook is registered, you can use the desired syntax:
>>> size = ISize(file)
>>> size.getSize()
7
Now we have to clean up after ourselves, so that others after us have a clean
adapter_hooks
list:
>>> adapter_hooks.remove(hook)
That’s it. I have intentionally left out a discussion of named adapters and
multi-adapters, since this text is intended as a practical and simple
introduction to Zope 3 interfaces and adapters. You might want to read the
adapter.txt
in the zope.interface
package for a more formal, referential
and complete treatment of the package. Warning: People have reported that
adapter.txt
makes their brain feel soft!