Writing Your Own Container

The above approach, using Traversers, gives Grok just enough information to let users visit your objects, and for you to assign URLs to them.

But there are several features of a normal grok.Container that are missing — there is no way for Grok to list or iterate over the objects, for example, nor can it ask whether a particular object lives in the container or not.

While taking full advantage of containers is beyond the scope of this tutorial, I ought to show you how the above would be accomplished:

import grok
from transient.natural import Natural
from zope.app.container.interfaces import IItemContainer
from zope.app.container.contained import Contained
import zope.location.location

class TransientApp(grok.Application, grok.Container):
    pass

class BaseTraverser(grok.Traverser):
    grok.context(TransientApp)
    def traverse(self, name):
        if name == 'natural':
            return NaturalBox()

class NaturalBox(Contained):
    grok.implements(IItemContainer)
    def __getitem__(self, key):
        if key.isdigit() and int(key) > 0:
            n = Natural(int(key))
            return zope.location.location.located(n, self, key)
        else:
            raise KeyError

class NaturalIndex(grok.View):
    grok.context(Natural)
    grok.name('index.html')

    @property
    def previous(self):
        if getattr(self.context, 'previous'):
            n = self.context.previous
            parent = self.context.__parent__
            return zope.location.location.located(n, parent, str(n))

    @property
    def next(self):
        n = self.context.next
        parent = self.context.__parent__
        return zope.location.location.located(n, parent, str(n))

Note, first, that this is almost identical to the application we built in the last section; the grok.Application, its Traverser, and the NaturalIndex are all the same — and you can leave alone the naturalindex.pt you wrote as well.

But instead of placing a Traverser between our Application and the actual objects we are delivering, we have created an actual “container” that follows a more fundamental protocol. There are a few differences in even this simple example.

  • A container is supposed to act like a Python dictionary, so we have overriden the Python operation __getitem__ instead of providing a traverse() method. This means that other code using the container can find objects inside of it using the container[key] Python dictionary syntax.
  • A Python __getitem__ method is required to raise the KeyError exception when someone tries to look up a key that does not exist in the container. It is not sufficient to merely return None, like it was in our Traverser above, because, without the exception, Python will assume that the key lookup was successful and that None is the value that was found!
  • Finally, before returning an object from your container, you need to call the Zope located() function to make sure the object gets marked up with information about where it lives on your site. A Grok Traverser does this for you.

Again, in most circumstances I can imagine, you will be happier just using a Traverser like the third example shows, and not incurring the slight bit of extra work necessary to offer a full-fledged container. But, in case you ever find yourself wanting to use a widget or utility that needs an actual container to process, I wanted you to have this example available.