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 atraverse()
method. This means that other code using the container can find objects inside of it using thecontainer[key]
Python dictionary syntax. - A Python
__getitem__
method is required to raise theKeyError
exception when someone tries to look up a key that does not exist in the container. It is not sufficient to merely returnNone
, like it was in ourTraverser
above, because, without the exception, Python will assume that the key lookup was successful and thatNone
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 GrokTraverser
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.