[Twisted-web] Re: some questions about twisted.web

David Bolen db3l.net at gmail.com
Wed Apr 8 18:07:00 EDT 2009


Jack Moffitt <jack at chesspark.com> writes:

> When the first getChild is called how do I know it's the first
> getChild call so that I do the special logic?  Perhaps that's what is
> confusing me.

Well, "first getChild" is ambiguous to me.  If you mean the getChild
on the instance that is supposed to do non-default mapping, I'd
suggest you know simply because the Resource class that implements it
has to get instantiated and reached somehow by the normal lookup
mechanism, so once it gets control, by definition that's the right
time.

Or put another way, you only instantiate and install into the tree the
object supporting the new lookup mechanism just where you need it, so
as far as that object instance is concerned, if it's being called, it
needs the special logic.

This of course assumes that you aren't trying to implement a single class
for both lookup types, but rather have a Resource subclass designed for
use as the new lookup mechanism.

If you made such a class your Root object, then essentially your whole
site would be mapped using the new mechanism.  If you stuck it further
down (such as via a putChild on lower level resources, or by
dynamically returning it in some othera custom getChild), then the new
lookups start taking place beyond that point in the URL.

An important point mentioned in some of the other responses is how the
isLeaf Resource attribute gets used in traversal.  When Twisted is
working its way down a URL and traversing objects through getChild, if
it hits one with isLeaf=True, it will stop at that point and let that
object render the result, even if the URL hasn't been exhausted.

During rendering the Resource will have access to the prepath and
postpath attributes on the current request indicating the portion of
the URL used to reach the current Resource, and any remaining portion.

So two approaches you might follow:

* Intercept the getChild lookup mechanism itself within the instance
  of your specific Resource object - use whatever mapping approach you
  want, and return an appropriate Resource.  That Resource object,
  when initially instantiated for use by the site, might have its own
  configuration file, cache of instantiated Resource objects, or
  whatever.

  Assuming that the mapping process yields the final Resource for the
  matched URL you'll want to ensure the resource you return is used
  for the rendering, even though request.postpath at that point may
  not be empty.  One way to handle this would be to ensure that
  isLeaf=True on any returned Resource (your mapping class can even
  set this if you don't want to require authors of the Resource
  objects themselves to care) so that traversal stops at that point
  and they are directly called to render the page.

  One drawback to using isLeaf on the returned resource might be that
  the request postpath won't be accurate during rendering as it will
  still reflect the location of your mapping Resource rather than the
  rendering resource.  If that's an issue (say for computed links
  during rendering), you could alternatively leave isLeaf=False in the
  returned Resource but ensure that its getChild() always returns
  itself, which would let the normal Twisted mechanism eat through the
  URL but never leave the Resource.

  Or yet another approach - if you manipulate request.prepath/postpath
  during a getChild call, you'll control the traversal, so simply
  clearing postpath (presumably after appending it to prepath) should
  guarantee the traversal stops with the Resource object you're
  returning, while leaving prepath/postpath accurate during rendering.

* Define the mapping Resource itself with isLeaf=True, and then during
  its render() operation, do whatever dynamic lookups you need to in
  order to locate the appropriate Resource, and return the result of
  its render() operation.

  This approach also allows the mapping operation itself to be
  deferrable since its occurring during the render operation rather
  than the initial object traversal.
  
  In this case, since you are in control of calling the final Resource
  object's render(), there's no worry about further object traversal,
  but you may still want to manipulate prepath/postpath in the active
  request object to have appropriate values for use by the final
  rendering Resource.

As an example of the latter, I have a site that accepts URLs of the
form:

   http://<site>/approval/<key>/x/y/z

where <key> is a unique key handed out to clients via email to gate
their access to their data.  I need to use <key> to validate and
identify the client, but the rest of the URL is a fairly static
association of Resource objects per the normal Twisted "putChild"
setup.  However, once the client is validated, a job UUID for them is
automatically appended to the URL for use by any eventual Resource.

I'll grant that this scenario is not necessarily the greatest argument
for the Twisted traversal mechanism versus an RE-based mapper
(assuming the latter lets you save portions of the matched URL for the
use of the rendering object), but it does show a dynamic (and
deferred) processing mechanism within twisted.web.

I have an ApprovalRoot Resource subclass (with isLeaf=True), that was
tied in under my site's Root object (with putChild), as shown below.
It uses the approval key to do some database lookups and validations,
then re-uses the same getChildForRequest function Twisted itself uses
for traversal on the remainder of the path (popping off the approval
key first), via an internal Resource tree.  In this case, since the
second level lookup is processing the Request object, prepath/postpath
end up correct for the render() call without further intervention.

If you replace the lookup with your own traversal mapping object, it
could just as easily map resources in other ways.  Of course, if the
mapping mechanism had no requirement for deferrable lookups, doing the
processing during the getChild operations is probably a little more
logical.

-- David

          - - - - - - - - - - - - - - - - - - - - - - - - -

During initialization, site setup includes (among other resources):

    # Main site URL entry points
    self.root = Root(self.options)
    msg_root = ApprovalRoot(db, self.options)
    self.root.putChild('approval', msg_root)

which uses the following class:

class ApprovalRoot(Resource):
    """Act as root of the approval tree, which is accessed from URLs in
    messages, and always include the message key as the first part of
    request.postpath.  Strips off the key, validates it, and then passes
    control on to appropriate job or file based objects depending on the
    remainder of the URL.
    
    This is almost identical to normal child lookup by non-leaf objects,
    but handled at render time since the message key validation is a
    deferred operation."""

    isLeaf = True

    def __init__(self, db, options):
        Resource.__init__(self)
        self.db = db
        self.loader = options['loader']

        # Use a separate resource as the root of the remaining URL processing
        # since the isLeaf on ourselves would defeat any child search

        self.job_root = Resource()

    def putChild(self, path, child):
        """Permit simulated children, so that the overall structure of the
        web site can still be established in a higher level function"""
        self.job_root.putChild(path, child)

    def _db_retrieveJobUuid(self, key):
        sql = sa.select([schema.jobs.c.uuid, schema.messages.c.expiration],
                        sa.and_(schema.jobs.c.uuid ==
                                schema.messages.c.job_uuid,
                                schema.messages.c.key == key))

        r = sql.execute().fetchone()

        if not r:
            raise _Unavailable
        elif (r.expiration and r.expiration < datetime.utcnow()):
            raise NoResource('The email approval key has expired')
        else:
            return r.uuid

    def _cb_render(self, job_uuid, request):
        # Transfer control to the appropriate child for rendering.  In the
        # case of a top level render, modify the postpath to include the job
        # uuid as an argument.
        if request.postpath and not request.postpath[0]:
            request.postpath.append(job_uuid.hex)
        child = getChildForRequest(self.job_root, request)
        r = child.render(request)
        if r != NOT_DONE_YET:
            request.write(r)
            request.finish()

    def _cb_render_err(self, failure, request):
        if failure.check(NoResource):
            request.write(failure.value.render(request))
            request.finish()
            return
        return failure

    def _finishRequest(self, value, request):
        request.finish()
        return value

    def render(self, request):
        if len(request.postpath) < 1:
            return ErrorPage(http.NOT_FOUND,
                             'Missing approval reference', '').render(request)

        # We only render message key failures, so if the URL has no further
        # segments beyond the key, add a trailing "/" to trigger the child
        # lookup for the default handler.
        if len(request.postpath) == 1:
            request.redirect(request.prePathURL() + '/' +
                             request.postpath[0] + '/')
            request.finish()
        else:
            msg_key = request.postpath.pop(0)
            d = self.db.run(self._db_retrieveJobUuid, msg_key)
            d.addCallback(self._cb_render, request)
            d.addErrback(self._cb_render_err, request)
            d.addErrback(self._finishRequest, request)
            d.addErrback(log.err)
        return NOT_DONE_YET




More information about the Twisted-web mailing list