Opened 15 months ago

Last modified 15 months ago

#6554 enhancement new

t.w.wsgi.WSGIResource should allow children

Reported by: lvh Owned by:
Priority: normal Milestone:
Component: web Keywords:
Cc: jknight Branch:
Author: Launchpad Bug:

Description

t.w.wsgi.WSGIResource explicitly does not allow children to be added with putChild, or retrieved with getChildWithDefault. The reasoning for this is that once you've hit it, everything should be delegated to the WSGI app.

I would like to be able to do so nonetheless. I ran into this issue demoing how Twisted can serve WSGI while still doing Twistedy stuff. Obviously, if you're trying to show how nicely things play together, the following URL pattern:

/
/chat
/sockjs

... is nicer than this one:

/wsgi/
/wsgi/chat
/sockjs

The obvious behavior, I think, would be for putChild and getChildWithDefault to work just like they do on t.w.r.Resource: static children get preference over dynamic behavior.

I think there is precedent for this behavior. t.w.r.Resource itself prefers statically registered children (i.e. put there with putChild) over dynamically produced children (from getChild). I don't think it's a big leap to suggest that whatever the WSGI app does internally is something akin to the dynamic resource behavior, and putChild should get precedence.

On IRC, _habnabit volunteered a resource that did a generalized version of this: a wrapping resource that allows you to register children, and, when it can't find them, delegates to a composed leaf resource, hiding it's own presence to that resource. I modified it slightly and added some tests:

Tests:

from twisted.trial import unittest
from twisted.web import iweb, resource
from twistyflask import server
from zope import interface

class ChildrenFirstResourceTests(unittest.TestCase):
    """
    Tests for the resource that delegates to children before
    delegating to a leaf.
    """
    def setUp(self):
        self.leaf = _FakeLeafResource()
        self.resource = server.ChildrenFirstResource(self.leaf)

        self.child = resource.Resource()
        self.child.isLeaf = True
        self.resource.putChild("c", self.child)


    def test_getStaticChild(self):
        """
        When attempting to get a statically registered child resource, that
        child is returned.
        """
        request = _FakeRequest(["a", "b"], ["c", "d"])
        child = resource.getChildForRequest(self.resource, request)
        self.assertIdentical(child, self.child)
        self.assertEqual(request.prepath, ["a", "b", "c"])
        self.assertEqual(request.postpath, ["d"])


    def test_getChild(self):
        """
        When attempting to get a child resource that wasn't statically
        registered, the leaf is returned (which would have ``render``
        called on it).

        The request's prepath and postpath are unchanged, making the
        delegating resource "invisible".
        """
        request = _FakeRequest(["a", "b"], ["x", "y"])
        child = resource.getChildForRequest(self.resource, request)
        self.assertEqual(child, self.leaf)

        self.assertEqual(request.prepath, ["a", "b"])
        self.assertEqual(request.postpath, ["x", "y"])


    def test_render(self):
        """
        When rendering, the resource delegates to its leaf.
        """
        self.assertIdentical(self.leaf.request, None)
        request = _FakeRequest(["a", "b"], ["x", "y"])
        body = self.resource.render(request)
        self.assertEqual(body, "Hello from the leaf")
        self.assertIdentical(self.leaf.request, request)

        self.assertEqual(request.prepath, ["a", "b"])
        self.assertEqual(request.postpath, ["x", "y"])



@interface.implementer(iweb.IRequest)
class _FakeRequest(object):
    """
    A fake request with a prepath and a postpath.
    """
    def __init__(self, prepath, postpath):
        self.prepath = prepath
        self.postpath = postpath



@interface.implementer(resource.IResource)
class _FakeLeafResource(resource.Resource):
    """
    A fake leaf resource.
    """
    isLeaf = True

    def __init__(self):
        self.request = None
        resource.Resource.__init__(self)


    def render(self, request):
        self.request = request
        return "Hello from the leaf"

Implementation:

from twisted.web import resource


class ChildrenFirstResource(resource.Resource):
    """
    A resource that delegates to statically registered children before
    giving up and delegating to a given leaf resource.
    """
    def __init__(self, leaf):
        resource.Resource.__init__(self)
        self.leaf = leaf


    def getChild(self, child, request):
        """
        Reconstructs the request's postpath and prepath as if this
        resource wasn't there, then delegates to the leaf.

        This gets called when ``getChildWithDefault`` failed, i.e. we're
        handing it over to the leaf.
        """
        request.postpath.insert(0, request.prepath.pop())
        return self.leaf


    def render(self, request):
        """
        Delegates the requests to the leaf.
        """
        return self.leaf.render(request)

As for whether or not this entire thing is even necessary: I ran into it, and _habnabit wrote the above composing resource for the exact same purpose :)

As for the generalized version vs amending WSGIResource: I don't really care. The generalized version can be tested separately (as demonstrated above). That said, it's only useful for things that explicitly don't allow putChild...

Change History (1)

comment:1 Changed 15 months ago by DefaultCC Plugin

  • Cc jknight added
Note: See TracTickets for help on using tickets.