20.12.05. The Zope Component Architecture - One Way To Do It All

My previous post showed how Interfaces and Adapters provide the heart of the Zope Component Architecture, which beats at the center of the Zope 3 framework and application server, and is also pumping new life into Zope 2. The other chief piece of the Zope Component Architecture are Utilities. Interfaces allow coding to a specification, which can be as detailed or as general as one chooses. Adapters allow for other objects to fit that specification. Utilities are more general purpose tools that match a specification. The rest of the core 'zope' packages enhance or build on these facets, and the 'zope.app' package provides the Zope application server which is the most akin to the Zope that people know and love or hate.

Yesterday I showed how a very simple web publishing system could be made using just the basic pieces of the component architecture. Today I want to augment yesterday's example by showing how the component architecture provides "the one way to do it".

Kevin Dangoor, the primary developer behind the Turbogears project, wrote a great post today about What Turbogears Is Not. In it he discusses how choices are made about what goes into Turbogears, which prides itself on not being new technology but a combination of already offered Python toolkits. Kevin goes into coverage "one way to do it" by mentioning how the Turbogears widgets system uses Kid, the primary template system used for Turbogears. It's the default and chosen template system, so why not take advantage and say "this is how we do widgets. period," right?

Anyways, it got me thinking about what I said yesterday about the Zope Component Architecture and how everything flows through it at some point. 'Multi-adapters', especially, have become powerful. Most commonly, they're used to provide views for objects. Yesterday showed a couple of custom views built to display simple and detailed information about racing greyhounds. Today I want to show how widgets work.

First off - I did a little bit of code cleanup of the module and use zope.publisher.interfaces.IRequest and zope.publisher.browser.TestRequest instead of defining my own dummy request.

import zope.component
import zope.interface
import zope.publisher.browser
import zope.publisher.interfaces
import zope.schema

I'm not going to show the entire module again, but I want to bring back IGreyhoundData. This interface specifies what we expect any greyhound data providing object to give us:

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"Is the greyhound still racing?")
    adopted = zope.schema.Bool(title=u"Has the greyhound been adopted?")

Lastly, I created a 'simpleConfigure' function in the top level of the module that registers the main views and utilities with the component architecture. This configure option can be run without any requirements aside from the 'zope' Python packages being in the Python path.

def basicConfigure():
    """ 
    Register the views and utilities with the component architecture. This
    could have been done in the top-level of the module, but deferring until
    now is a little bit cleaner and allows other configuration options to be
    made.
    """
    greyhounds = GreyhoundFinder()
    zope.component.provideUtility(greyhounds, name='greyhound')
    zope.component.provideAdapter(RacerView, name='view')
    zope.component.provideAdapter(DetailsView, name='detail')

All of the provided components in this setup also provide their own information about what interfaces they in turn provide and adapt. As a refresher, 'greyhounds' is an IDataFinder utility, used by the publishing system to look up the "model" object. The two adapters are multi adapters for IGreyhoundData and zope.publisher.interfaces.IRequest. They provide 'ISimpleView', which has a 'render()' method to render the view to the response stream. I want to reiterate that this has no dependencies on anything that the world thinks of as Zope. This is all in a single module that could very easily be published via CGI without ZODB, transactions, long-running-anything, security restrictions, through the web programming... None of it.. This just uses the heart.

However, showcasing the widgets used by Zope 3 does require a full Zope setup. Technically, I could configure the needed widgets myself and show how that's done, but that's beyond the point of this exercise. I chiefly want to show how the Widgets flow through the same heart. There's just one way to do it. To make a rudimentary edit form for a Greyhound in the little system I have set up, we create another class to provide the edit form view. I try to document everything that's going on. Take note of the use of zope.component.getMultiAdapter() to get the widget.

class EditForm(object):
    """ Requires zope.app.form widgets to be configured. """
    zope.interface.implements(ISimpleView)
    zope.component.adapts(IGreyhoundData, zope.publisher.interfaces.IRequest)

    def __init__(self, context, request):
        self.context = context   # aka, the racer
        self.request = request

    def render(self):
        """ 
        Goes through the schema fields and looks up the IInputWidget adapter
        configured for the field and request type. 'zope.app.form' is imported
        here to avoid placing burden on the rest of this module, since this
        code requires the widget configuration to have occurred. Just importing
        'zope' does not provide this, but running a Zope application server
        does.
        """
        from zope.component import getMultiAdapter
        from zope.app.form.interfaces import IInputWidget
        racer = self.context
        request = self.request
        out = []
        for name, field in zope.schema.getFieldsInOrder(IGreyhoundData):
            value = field.get(racer)

            # Just like getting a full 'view' to render, widgets are also
            # provided as multi adapters, adapting an object (in this case, a
            # field) and the request to an IInputWidget implementation. A
            # display-only widget would use IDisplayWidget.
            widget = getMultiAdapter((field, request), IInputWidget)
            widget.setRenderedValue(value)

            # A widget has a label, which it often gets from the field's title.
            # To render the widget, just call it.
            out.append('%s: %s' % (widget.label, widget()))

        return '\n'.join(out)

The first few lines of the render() method just help establish some shorter names, and also defer loading zope.app.form so it's not required by this whole module - although in theory it wouldn't harm anything if it were imported at the top. The zope.schema introspection machinery is used again to walk through the fields of an Interface object and get the fields. We get the object's current value by going through the field. Then we get the widget. The Zope Component Architecture comes into play once again as we try to adapt both the field and request object to an IInputWidget. The value is set on the widget based on the object's value, and we add it to the output.

How is that widget made? Does it use templates? Just write out strings in Python directly? It doesn't matter. The IInputWidget interface is all we care about here. Form submission and validation goes through the widgets and fields too, but that's not important for right now. What is important and interesting is how adapters are used here to deal with widgets, and they're completely independent from the schema. Using the 'schema' objects, one could write completely different adapters to do object-relational mapping and provide converters to read/write SQL data based on the object specification, while still using that same specification to render web forms or write out dynamic XML-RPC or JSON request responses. sqlformatter = getMultiAdapter((field, databaseConnection), ISQLFormatter) could easily be used. As a technical aside - with how multi-adapters and interfaces work, there could be generic formatters for all data types but a specific datetime formatter could be provided for a SQLite or Oracle connection. Multi-adapter lookup will match the most specific adapter based on the specifications of the objects passed in. See what I mean about everything flowing through the same heart?

It's time to put our new view into play. I added an extra configuration method that would set up the basic components and add in the edit form:

def configureWithWidgets():
    """ 
    Establishes basicConfigure and also enables the edit form which uses the
    zope.app.form system for widgets. Requires a configured Zope app server
    setup - at least with the zope.app.form widgets configured to provide
    adapters.
    """
    basicConfigure()
    zope.component.provideAdapter(EditForm, name='edit')

And with how our 'simplePublish' system is set up (the only change since yesterdays code is the use of zope.publisher.browser.TestRequest instead of the self-defined simple request), we now have edit actions:

>>> simple.configureWithWidgets()
>>> print simple.simplePublish('greyhound/edit/1')
The Greyhound's Name: <input class="textType" id="field.name" name="field.name" size="20" type="text" value="Betty Joan" />
The name on the race card.: <input class="textType" id="field.raceName" name="field.raceName" size="20" type="text" value="Beauty" />
Fastest race time (in seconds).: <input class="textType" id="field.fastestTime" name="field.fastestTime" size="10" type="text" value="31.22" />
The length of the track, in yards, of the fastest time.: <input class="textType" id="field.trackLength" name="field.trackLength" size="10" type="text" value="503" />
Is the greyhound still racing?: <input class="hiddenType" id="field.retired.used" name="field.retired.used" type="hidden" value="" /> <input class="checkboxType" checked="checked" id="field.retired" name="field.retired" type="checkbox" value="on" />
Has the greyhound been adopted?: <input class="hiddenType" id="field.adopted.used" name="field.adopted.used" type="hidden" value="" /> <input class="checkboxType" checked="checked" id="field.adopted" name="field.adopted" type="checkbox" value="on" />