Ticket #6554 enhancement new
t.w.wsgi.WSGIResource should allow children
|Reported by:||lvh||Owned by:|
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:
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"
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...