20.12.05. The Zope Component Architecture - Interfaces, Adaptation, and Duck Typing

We've done amazing work in the past few months with Zope 3. Compared to the black magic of Zope 2 and so many other frameworks I look at, it's quite refreshing. It's not always easy, but it's been fun while also being quite trustworthy. Even without a deep understanding of all that's available, my very small company and our even smaller team of developers have achieved incredible results: a from scratch rewrite of one of our oldest and most heavily used customer's web sites, three other customers with solutions built on top of the same stack written and extracted for that earlier customer; a one week turnaround (including graphic design) of one of those other customer's web sites; sharing of benefits of refactoring of code from the customer specific layers of the stack to more general layers; an 'admin' user interface far more capable and complete than just about anything we've delivered in the past; all developed faster than ever before; and with a clean architecture despite only the lightest of planning.

What makes all this possible? The Zope Component Architecture. The Zope Component Architecture is a pretty lightweight system, especially since Zope 3.1 which simplified the component architecture that shipped with Zope 3.0 into a simple vocabulary. There are only a couple of core Python packages that provide the heart of the component architecture. And everything essentially flow through the heart.

The core packages that provide the Zope Component Architecture are zope.interface and zope.component. zope.interface provides a way of describing and querying specifications. Similar in some ways to Interface objects in Java or in systems like CORBA and ILU, zope.interface Interfaces can be used to describe objects, policies, and APIs separate from their implementation. By writing against the interface, Zope 3 takes advantage of one of the key advantages of dynamic languages like Python - Duck Typing. Duck Typing is a way of saying "if it quacks like a duck, then that's good enough for me!" Just putting the zope libraries on the Python path - configuring and setting up nothing else - here's how this works:

>>> from zope.interface import implements,  providedBy, Interface
>>> class IQuacksLikeADuck(Interface):
...     def quack():
...         """ Returns a quacking sound. """
... 
>>> class Platypus(object):
...     implements(IQuacksLikeADuck)
...  
...     def quack(self):
...         return "In my world, a platypus goes 'quack!'"
... 
>>> ben = Platypus()
>>> IQuacksLikeADuck.providedBy(ben)
True
>>> print ben.quack()
In my world, a platypus goes 'quack!'

Now, the normal and generally allowable Python way of Duck Typing is to do something like if hasattr(ben, 'quack'): ben.quack(). But that sort of 'hasattr' code only goes so far. Zope 2 is littered with skeletons like if getattr(obj, '_isPrincipiaFolderish', 0):.... As systems grow, developers forget those old names. Or objects just grow heavy with odd little attributes that say they have not only one thing about them that makes them folderish, but many. And if its folderish, then maybe we'll draw a tree for it. But if '_isPrincipiaFolderish' is really an important attribute - where does that get documented? Who knows it's there? Who knows what its values may be. Can it be callable? Zope 2 actually has attributes like this - elements that started out as simple attributes, and then grew to be callable for situations that needed to do computation. And then grew to be callable with parameters for really specific situations. But finding out about all of these combinations and their meaning was not always easy to document or even infer. Recognizing this, an Interfaces package was made for Zope 2 but was initially used only for documentation. But Interfaces were not used as core elements, and there was little impetus to keep an interface spec up to date when it wasn't actually used in code. But in Zope 3 and in new Zope 2 work, that changes. Interfaces are core members. It means that not only are most important things documented, but that it's really more important for an object providing a certain interface to just provide that interface - and not how its implemented. I don't care how it quacks, as long as it quacks. Quacking is all that I require. Interfaces allow one to formalize duck typing, without really adding too much weight to their program. In fact, with interfaces being a sort of public interface, I find it easiest now to look at a packages 'interfaces.py' module first when looking at new code. How a specific implementation of a security policy doesn't really matter to me at first - I want to know how I interact with the security policy. So interfaces become useful for documentation without having to generate or wade through documentation generated for every single function and class and method in a particular module.

Of course, interfaces on their own are still not all that great. The second part offered by zope.interface is adaptation. Sticking with the quacking example, let's add a hunter into the mix. He wants to go duck hunting. He's going to need to attract ducks by quacking like them.

>>> class Hunter(object):
...     def __init__(self, name):
...             self.name = name
... 
>>> tom = Hunter('Tom')
>>> IQuacksLikeADuck.providedBy(tom)
False

What we need here is an adapter. This is where zope.component and zope.interface really collaborate. By default, most adapters in Zope adapt from one interface to another. But you can also adapt a class to an interface, which is what we'll do here since we didn't make an 'IHunter' or 'IPerson' or 'IQuackless' interface.

>>> from zope.component import adapts
>>> class DuckCall(object):
...     implements(IQuacksLikeADuck)
...     adapts(Hunter)
...  
...     def __init__(self, hunter):
...         # Adapters are passed in their adaptee as first argument
...         self.hunter = hunter                                 
...  
...     def quack(self):
...         return self.hunter.name + ' quacks with a duck call'
... 
>>> zope.component.provideAdapter(DuckCall)

So here, we say that instances of DuckCall implement the IQuackLikeADuck interface, and that this particular class adapts Hunter objects. The next line registers the adapter with the global component architecture adapter registry. This is the easiest version of the call - a factory (in this case a class) is passed in which declares what it adapts and provides directly. Now there's a duck call available for hunters. How do we use it? There are a couple of ways. One is to use zope.component.queryAdapter or getAdapter - the only difference being that queryAdapter returns 'None' or a user supplied default if the adaptation can't happen, while getAdapter raises an exception. But the interesting way is to use the interface directly, passing in the object:

>>> IQuacksLikeADuck(tom)
<__main__.DuckCall object at 0x67cad0>

Like with 'queryAdapter', a second argument can be passed in as a default if the object cannot be adapted, otherwise an exception is raised. What this form allows for is code that works with objects by interface, and it allows for other code to provide expected interfaces for existing objects. Lets add to the wildlife. First, there's the hunters dog:

>>> class IrishWolfhound(object):
...     size = 'HUGE'
...     def __init__(self, name):
...             self.name = name
... 
>>> squeekers = IrishWolfhound('Squeekers')

And a duck. Since the duck provides 'quack' directly, we can separately say that instances of the class provide the IQuacksLikeADuck interface, or we could even say that a particular instance provides it. This shows how we can separately say that the class implements it.

>>> class Duck(object):
...     def quack(self):
...             return "The best quack is a duck quack."
... 
>>> hunted = Duck()
>>> IQuacksLikeADuck.providedBy(hunted)
False
>>> from zope.interface import classImplements
>>> classImplements(Duck, IQuacksLikeADuck)
>>> IQuacksLikeADuck.providedBy(hunted)
True

And it should be noted that if an object provides an interface itself, then the adapter lookup will return the object itself. So now that we have quite a little menagerie. Lets see what some simple tickler code might look like that would take a list of objects and try to make them quack.

>>> def tickler(*args):
...     " Goes through the provided arguments and tries to make them quack "   
...     for potentialQuacker in args:
...         quacker = IQuacksLikeADuck(potentialQuacker, None)
...         if quacker is None:
...             print "Could not quack: %r" % potentialQuacker
...         else:
...             print quacker.quack()
... 
>>> tickler(ben, tom, squeekers, hunted)
In my world, a platypus goes 'quack!'
Tom quacks with a duck call
Could not quack: <__main__.IrishWolfhound object at 0x698bd0>
The best quack is a duck quack.

Of course, this is a fiendishly simple showcase. But it's a powerful building block, and is one of the core concepts of Zope 3. Just about everything flows through interfaces and adapter registries. A powerful extension of the 'adapter' concept is the 'multi-adapter'. The multi-adapter situation most commonly used is the Browser View. A Browser View is made by adapting an object, like the Duck, and another object, like an HTTP Request, to something that gets rendered to a web browser via HTTP. Fundamentally, the Component Architecture merely provides the registries for loosely-coupled objects to work together, and it removes a lot of the magic.

In something purely theoretical, lets remove all of Zope as people know it (the object database, the publisher that traverses URLs to return objects, and so on and so on. Using the component architecture, lets do an extremely lightweight 'rails'-ish thing. This code makes a fake little database with just tuples of data, as might be returned by a basic dbapi adapter. It defines a Utility to find the 'model' from this database. A utility is a new concept. In quick, utilities are other abstract notions, used loosely. One can define and install different utilities, and the code that uses the utility has no hard dependencies on the utility object. Examples include RDB connections - 'get me an IRDBConnection with the name "racers"'; caches; and more. Sometimes all utilities for a particular interface are queried, sometimes you just want an individual one. The zope.component.provideUtility call establishes the utility. In this code we have only one and we name it 'greyhound'. That name is used later in the 'simplePublish()' function to find the utility by combination of its name and published interface. Again - this code was tested by ensuring that my Zope 3.1 installations lib/python directory was in the Python path and that's it.

import zope.component
import zope.interface
import zope.schema
class IGreyhoundData(zope.interface.Interface):
    """
    zope.schema provides a way of defining the properties and their bounderies
    as they make up a component. It integrates with the interface system and
    can be used for documentation, validation, data retrieval and setting, 
    and more. The fields define 'get' and 'set' methods, among others, useful
    for querying and setting data on an object without having to have intimate
    knowledge of the object's structure. Useful for web forms, but also for
    database storage and retrieval.
    """
    name = zope.schema.TextLine(title=u"The Greyhound's Name")
    raceName = zope.schema.TextLine(title=u"The name on the race card.")
    fastestTime = zope.schema.Float(title=u"Fastest race time (in seconds).")
    trackLength = zope.schema.Int(title=u"The length of the track, in yards, of the fastest time.")
    retired = zope.schema.Bool(title=u"False if the greyhound is still racing.")
    adopted = zope.schema.Bool(title=u"True if the greyhound has a home after the track.")

class IDataFinder(zope.interface.Interface):
    def find(oid):
        """ Returns an object that matches this object id. Raises KeyError if not found. oid should be an integer. """

# Define a fake database
greyhounddb = {
  1: ('Betty Joan', 'Beauty', 31.22, 503, True, True),
  2: ('Dazzling', 'Dazzling Pet', 32.00, 503, True, False),
}

class Phantom(object): 
    """ Phantoms can be anything... """
    pass
 
class DataFinder(object):
    zope.interface.implements(IDataFinder)
    schema = None
    factory = None
    db = None
    
    def find(self, oid):
        """ 
        Calls the fake database and uses the schema system to map fields
        from the tuple returned to a new Phantom object, and then ensures that 
        the phantom is marked to provide the schema in 'self.schema'
        """
        raw = self.db.get(oid)
        newobj = self.factory()
        for idx, name in enumerate(zope.schema.getFieldNamesInOrder(self.schema)):
            field = self.schema[name]
            value = raw[idx]
            field.set(newobj, value)
        zope.interface.directlyProvides(newobj, self.schema)
        return newobj

# Define a basic data finder
class GreyhoundFinder(DataFinder):
    schema = IGreyhoundData
    factory = Phantom
    db = greyhounddb

greyhounds = GreyhoundFinder()
zope.component.provideUtility(greyhounds, name='greyhound')

class ISimpleRequest(zope.interface.Interface):
    headers = zope.interface.Attribute('Mapping of request headers')

class SimpleRequest(object):
    zope.interface.implements(ISimpleRequest)
    def __init__(self, **headers):
        self.headers = {}
        self.headers.update(headers)

class IView(zope.interface.Interface):
    """ Simple view interface. """
    def render():
        """ Returns a rendered string of this view. """

class RacerView(object):
    zope.interface.implements(IView)
    
    def __init__(self, context, request):
        self.context = context   # aka, the racer
        self.request = request
        
    def render(self):
        return "Name: %s; Fastest Time: %0.2f" % (self.context.name, self.context.fastestTime)
zope.component.provideAdapter(RacerView, (IGreyhoundData, ISimpleRequest), name='view')

detail_template = """Name: %(name)s
Racing Name: %(raceName)s
Fastest Time: %(fastestTime)0.2f for length %(trackLength)d
Retired: %(retired)s
Adopted: %(adopted)s
"""

class DetailsView(object):
    zope.interface.implements(IView)
    
    def __init__(self, context, request):
        self.context = context   # aka, the racer
        self.request = request

    def render(self):
        racer = self.context
        mapping = dict([ 
            (name, field.get(racer)) 
            for name, field in zope.schema.getFieldsInOrder(IGreyhoundData) 
            ])
        return detail_template % mapping
zope.component.provideAdapter(DetailsView, (IGreyhoundData, ISimpleRequest), name='detail')
        
def simplePublish(path):
    """ 
    Breaks 'path' up into three components:
    
    - ``name``: used to look up a data finder utility
    - ``viewname``: used to query a multi-adapter for the found object and simple
      http request
    - ``oid``: used to look up an object out of the finder
    
    from ``name/viewname/oid``
    
    returns the rendered view.
    
    A very very simple showcase of how the zope component architecture might
    be used to register and find model accessors and views in a web publishing
    system besides the traditional Zope and 'zope.app' setup.
    """
    request = SimpleRequest(ignored='hi')
    getter, viewname, oid = path.split('/')
    oid = int(oid)
    
    # This gets an 'IDataFinder' utility with the name in the 'getter' value.
    # A greyhound getter that queries a real database could have been installed
    # instead - it doesn't impact this code.
    model = zope.component.getUtility(IDataFinder, name=getter).find(oid)
    
    # Now that we have the model object and the fake request, find something
    # that adapts both of them to a single object. Since the request is fake
    # and the views are simple, we don't really show how the request object
    # is used here. But this is basically where the web environment and model
    # environment would actually meet - only in the multi-adapter. Other than
    # that, both environments are oblivious to each other. It's only the zope
    # component architecture registries that know what's in them, and they're
    # pretty agnostic.
    view = zope.component.getMultiAdapter((model, request), name=viewname)
    return view.render()

def test():
    print "Publishing ``greyhound/view/1``"
    print simplePublish('greyhound/view/1')
    print
    print "Publishing ``greyhound/view/2``"
    print simplePublish('greyhound/view/2')
    print
    print "Publishing ``greyhound/detail/1``"
    print simplePublish('greyhound/detail/1')
    print
    print "Publishing ``greyhound/detail/2``"
    print simplePublish('greyhound/detail/2')
    
if __name__ == '__main__':
    test()

And when run:

Desktop> python zopetest.py
Publishing ``greyhound/view/1``
Name: Betty Joan; Fastest Time: 31.22

Publishing ``greyhound/view/2``
Name: Dazzling; Fastest Time: 32.00

Publishing ``greyhound/detail/1``
Name: Betty Joan
Racing Name: Beauty
Fastest Time: 31.22 for length 503
Retired: True
Adopted: True


Publishing ``greyhound/detail/2``
Name: Dazzling
Racing Name: Dazzling Pet
Fastest Time: 32.00 for length 503
Retired: True
Adopted: False