[Grok-dev] Re: implements() works, alsoProvides() breaks

Brandon Craig Rhodes brandon at rhodesmill.org
Thu Aug 30 23:26:27 EDT 2007


Philipp von Weitershausen <philipp at weitershausen.de> writes:

> Brandon Craig Rhodes wrote:
>
>>     zope.component.provideAdapter('index',
>>                                   adapts=(Plain, IBrowserRequest),
>>                                   provides=IDefaultViewName)
>>
>> I swiped this code from the bootstrap() code of grok._grok, where it
>> is run twice to provide a default View name of 'index' to grok.Model
>> and grok.Container.
>
> I do recommend doing this declaration in ZCML:
>
>   <browser:defaultView for="Plain" name="index" />
>
> Doing setup at import time isn't such a good idea. Alternatively, grok
> could grow a directive that would allow you to set the default view
> name for objects.

I would actually prefer that 'index' become the universal default,
instead of having to configure separate defaults for each class.
There is no use case I can think of at the moment for some objects
having something other than 'index' as their default view.

But if the use case exists, and a universal default of 'index' is
therefore a bad idea, then I would love to see a Grok directive for
this - that would be wonderful!  Grok the caveman must, after all,
smash ZCML. :-)

> Please be more specific. What's the actual error?

Your answers have helped me realize that I should throw out that whole
example!  It looked so clear last night, but now it just looks like a
mess.  I have new test cases below showing how, I think, I have solved
the problems that were preventing my tutorial examples from working
(and I will quote the actual errors, too!).

> If this is about __parent__, grok should take care of it.  If the
> object returned by the traverser provides ILocation ... __parent__
> and __name__ will automatically be assigned to the object.  If not,
> grok will wrap it in a LocationProxy that adds __parent__ and
> __name__ without altering the object itself.

The LocationProxy pattern works well when using cascaded Traversers;
creating an instance named "a" of the following "Example" application
displays this correct output when you visit "/a/foo/bar/index":

   The URL of this page is http://localhost:8080/a/foo/bar/index

------------------------------------------------------------------------
import grok

class Example(grok.Application, grok.Container):
    def traverse(self, name):
        if name == 'foo':
            return Foo()

class Foo(object):
    pass

class FooTraverser(grok.Traverser):
    grok.context(Foo)
    def traverse(self, name):
        if name == 'bar':
            return Bar()

class Bar(object):
    pass

class BarIndex(grok.View):
    grok.context(Bar)
    grok.name('index')
    def render(self):
        return 'The URL of this page is %s' % self.url()
------------------------------------------------------------------------

And, just as you have described, both the Foo and Bar objects above
get wrapped in LocationProxy objects that allow the framework to
travel back up the hierarchy so the View can determine its URL.

But what happens when the user wants to create a virual Container,
rather than simply a Traverser (maybe because then he can support
things like iteration that go beyond traversal)?

The answer is, unfortunately, that the Proxy pattern breaks.

Consider the following application:

------------------------------------------------------------------------
import grok
import zope.location
from zope.app.container.interfaces import IItemContainer

class Example(grok.Application, grok.Container):
    def traverse(self, name):
        if name == 'foo':
            return Foo()

class Foo(object):
    grok.implements(IItemContainer)
    def __getitem__(self, key):
        if key == 'bar':
            obj = Bar()
            return zope.location.LocationProxy(obj, self, key)
        else:
            raise KeyError

class Bar(object):
    pass

class BarIndex(grok.View):
    grok.context(Bar)
    grok.name('index')
    def render(self):
        return 'The URL of this page is %s' % self.url()
------------------------------------------------------------------------

Here the URL "a/foo" selects a simple container that wants to act as
though there are Bar objects (well, one Bar object) inside of it.
Since the plain Bar object does not support ILocation, I have used the
LocationProxy pattern, exactly as you suggested, to provide it with
such support (and that part works).

But the result, when one tries to access the same "/a/foo/bar/index"
URL, is the error (as displayed by "bin/zopectl fg"):

   TypeError: There isn't enough context to get URL information. This
   is probably due to a bug in setting up location information.

I think the problem is that, in the first example application, the
object connections that result look something like this (if I may
indulge in a bit of ASCII art):


                          Foo                     Bar
                           ^                       ^
                           | proxies               | proxies
           __parent__      |       __parent__      |       context
 Example <------------ Foo Proxy <------------ Bar Proxy <--------- View


But in the second instance, when the container offers "self" as the
parent of the new Bar object, it is, I think, the unadorned "self",
without the Proxy around it!  Which means the object relationships
wind up looking like:

                                               Bar
                                                ^
                                                | proxies
                                __parent__      |       context
                          Foo <------------ Bar Proxy <--------- View
                           ^
                           | proxies
           __parent__      |
 Example <------------ Foo Proxy

So, because the Container has only its "real self" available to offer
as the parent of the Bar LocationProxy, the chain of __parent__
relationships winds up broken: the process of following __parent__
references backwards from the View stops dead when it reaches the Foo
object, and so "There isn't enough context" is returned.

The solution is to abandon the Proxy pattern for the Container (it's
still a fine pattern for the final object of the traversal), and make
the on-demand Container itself capable of being contained.  The
following application succeeds in creating a dynamic, on-demand,
virtual Container, which can be both traversed in both directions:

------------------------------------------------------------------------
import grok
import zope.location
from zope.app.container.interfaces import IItemContainer
from zope.app.container.contained import Contained

class Example(grok.Application, grok.Container):
    def traverse(self, name):
        if name == 'foo':
            return Foo()

class Foo(Contained):
    grok.implements(IItemContainer)
    def __getitem__(self, key):
        if key == 'bar':
            obj = Bar()
            return zope.location.LocationProxy(obj, self, key)
        else:
            raise KeyError

class Bar(object):
    pass

class BarIndex(grok.View):
    grok.context(Bar)
    grok.name('index')
    def render(self):
        return 'The URL of this page is %s' % self.url()
------------------------------------------------------------------------

With this change - I guess the only difference between this third
application and my second example above is that the Foo container now
mixes in "Contained" - one gets this correct output when you visit
"/a/foo/bar/index":

   The URL of this page is http://localhost:8080/a/foo/bar/index

And so this last example, I believe, is the magic combination of
elements for which I have been searching for my tutorial: a Container
that itself implements IContained, but constructs a proxy for the
objects "inside" of it.

(Actually, I will probably use the line

        return zope.location.location.located(obj, self, key)

instead of manually creating the LocationProxy, on the off chance that
one of my containers winds up having objects in it that provide
IContained natively.)

Should there be a class with a name like "grok.VirtualContainer" that
brings these elements together and only requires the user to provide a
get() method or something?  I guess the above code is pretty dratted
simple already, so further convenience is really not necessary as long
as we have documentation showing the users the pattern.

> From your code:
>
>>   zope.interface.alsoProvides(WorkingObject2, IContained)
>
> alsoProvides takes an *object* as a first parameter. Not a class. This
> is where your problem is.

Actually, the code you are quoting was the code that actually worked!
The code that broke was, in fact, the code that passed an instance:

            b = BrokenObject()
            zope.interface.alsoProvides(b, IContained)

I have no idea why; as I said, the whole example turns out to have
been a mess.  But thanks for pointing out that I was doing the wrong
thing here; I will stop using alsoProvides with classes.

-- 
Brandon Craig Rhodes   brandon at rhodesmill.org   http://rhodesmill.org/brandon


More information about the Grok-dev mailing list