durusmail: quixote-users: Object traversal, a minefield
Object traversal, a minefield
2002-11-04
Object traversal, a minefield
Martin Maney
2002-11-05
On Mon, Nov 04, 2002 at 03:28:16PM +0100, Nicola Larosa wrote:
> I didn't expect object traversal to be so... dangerous.

Since I've had my own sometimes interesting learning experiences with
the publisher's resolution methods recently, I thought I'd offer some
comments.  Although I have read (well, skimmed, in spots) your whole
treatise, I'm going to comment on things as they come up, mostly.  I
think.  :-)  Sometimes my fingers have a different agenda; sometimes I
am forced to admit they've improved on my original plan.  Markets are
fire, but writing is learning.

Needless to say, I'll be cutting pretty vigorously.

> First try. Let's go straight to an object, and then to a method.
>
> Here is the code:
>
> _q_exports = ["Folder", "Directory"]
>
> class Folder:

First problem is right here: you're giving Quixote the class object,
not an instance of the class.  Now, an object's an object (at least
until you try to access it), but I'm pretty sure that's not what you
intended to do!

>       _q_exports = ["view"]
>
>       def __init__(self, request):
>           pass
>
>       def view(self, request):
>           return "Listing of Folder contents here"
>
> Here is the URL:
>
> http://localhost/QuixoTest/Folder/view
>
> Here is the response:
>
> error: Server Error: exceptions.TypeError, unbound method view() must be
> called with Folder instance as first argument (got HTTPRequest instance
> instead): file: /opt/python/lib/python2.2/site-packages/quixote/publish.py
> line: 572
>
> Well, yes, of course, but how do I do that? :^(

First you need to provide an instance of Folder to Quixote.  Since you
get around to that later on, I'll just sketch one simple hack to
provide access to an object's method function without requiring the
immediate caller to know about the instance object.  First, assume that
your view(self, request) is renamed to _view.  Then you want something
like this:

    def __init__(self, request):
        view = lambda r: self._view(r)

Or for 2.1 and earlier, without the nested scope change, the usual
hack:

    def __init__(self, request):
        view = lambda r,s=self: s._view(r)

This leaves a bound function taking only the request argument that
Quixote will find when it looks for "view" in the instance's __dict__.
The trick with the lambda curries the class method to make a new
function whose interface matches what Quixote expects in a published
function.

> Second try. This time we'll use _q_getname to get an object back into the
> traversal.
>
> Here is the code:
>
> from quixote.errors import TraversalError
>
> _q_exports = []
>
> def _q_getname(request, name):
>       print "first getname,", name
>       if name == "Folder":
>           return Folder(request, name)
>       elif name == "Directory":
>           return Directory(request, name)
>       else:
>           raise TraversalError, \
>               "Unknown path element: %s" % name
>
> class Folder:
>       _q_exports = ["view"]
>
>       def __init__(self, request, name):
>           print "Folder init,", name
>
>       def view(self, request):
>           print "Folder view"
>           return "Listing of Folder contents here"

> http://localhost/QuixoTest/Folder/view
> http://localhost/QuixoTest/Directory/view
>
> They both work. Anyway, such a usage of _q_getname looks like a kludge to me.

I have to admit that I'm puzzled, because this shouldn't work according
to the logic of the previous example.  :-(  Unless to Quixote there
*is* a difference between a class object and an instance object...
[makes note to review, see if I've forgotten something along these
lines.]

Anyway, yes, using _q_getname is a bit of a hack here, but the
alternative would be to have, say 'class Folder_implementation' and a
function like this:

    def Folder(request):
        return Folder_implementation(request)

THen you should be able to go back to using _q_exports rather than
_q_getname to create the proper instance object.

> Third try. Now we'll put that Integer demo to use.
>
> Here is the code of the new Folder class (the global _q_exports and
> _q_getname, and the Directory class, are the same as before):
>
> class Folder:
>       _q_exports = ["view", "show"]
>
>       def __init__(self, request, name):
>           print "Folder init,", name
>           try:
>               self.idx = int(name)
>           except ValueError:
>               self.idx = 0
>           pass
>
>       def _q_getname(self, request, name):
>           print "Folder getname,", name
>           return Folder(request, name)
>
>       def view(self, request):
>           print "Folder view"
>           return "Listing of Folder contents here"
>
>       def show(self, request):
>           print "Folder show,", self.idx
>           if self.idx:
>               return "Contents of File #%s here" % self.idx
>           else:
>               return "No File specified"
>
> Here is the URL:
>
> http://localhost/QuixoTest/Folder/1/show
>
> This works, too. Alas, the debug log shows this:
>
> first getname, Folder
> Folder init, Folder

This is what you asked it to do: global _q_getname creates an instance of
Folder.  The next component is processed by that instance's _q_getname:

> Folder getname, 1
> Folder init, 1

Again, it has been told to create a new Folder instance, so it does.
If it's any consolation, the first Folder instance will be discarded,
probably (I'm guessing, haven't grovelled the sources) as soon as the
new one is returned to the publisher's parser.

> Folder show, 1
>
> An unnecessary Folder object has been created, and its init code has tried
> to convert the class name to an integer. It would be nice if this could be
> avoided.

The root of the problem is that you're using the Folder object typr for
two different things, but have only defined sensible behavior for one
of them.  There are several ways to resolve this: I would be inclined
to have the global _q_getname (or the simple function Folder as
suggested previously) return a Folder_imp object that would have

    self.path = ''

in its __init__.  its _q_getname would append the current component to
this internal path (add checking for legal cases as desired), and then
when it hit a 'view' or 'show' component... well, maybe this doesn't
generalize so well that way in general!  Anyway, that would be much the
same.  If you're certain you only want to allow single-component path
names that are positive integers, then you could initialize self.idx to
zero and use a check that and the current component for legality.

> Fourth try.
>       def _q_getname(self, request, name):
>           print "Folder getname,", name
>           try:
>               self.idx = int(name)
>           except ValueError:
>               self.idx = 0
>           return self(request, name)

> error: Server Error: exceptions.AttributeError, Folder instance has no
> __call__ method: file: /home/nl/Araknos/aKab/aKab/Web/__init__.py line: 35
>
> Well, well. Python tried to call the self object.

Uhm, you *told* it to.  'self(request, name)'

> Fifth try. We want to return the self object without calling it, so let's
> take away those parameters, unnecessary anyway. We will have to touch the
> global _q_getname too, since it creates the Folder object.

>       def _q_getname(self, request, name):
>           print "Folder getname,", name
>           try:
>               self.idx = int(name)
>           except ValueError:
>               self.idx = 0
>           return self

> first getname, Folder
> Folder init
> Folder getname, 1
> Folder show, 1
>
> That's fine. It wasn't easy, but worth it. I post all this to soothe that
> nagging feeling that there's something else so obvious that I can't see it.
:^)

Since Folder._q_getname is only called for the URL component *past*
'Folder', I would think that the ValueError case would be an error
state.  Perhaps initialize idx to an illegal value (would -1 work
here?), and make Folder._q_getname something like this:

    try: val = int(name)
    except:
        return an error [page]: invalid folder selector
    if self.idx >= 0:
        return an error: multiple selectors
    self.idx = val

And presumably you need to check for the missing selector case in view
and show.

Well, this has been interesting.  I'm not sure if this is yet another
way to structure a Quixote tree or if it's just a very different view
of things.  Hope I've been of some help to you!

--
Anyone who says you can have a lot of widely dispersed people hack
away on a complicated piece of code and avoid total anarchy has never
managed a software project.  -- Andy Tanenbaum


reply