| « A radical act of obedience | Dejavu 1.4.0 (Python ORM) release » |
While at PyCon in Dallas this weekend, I got a chance to hear David Creemer talk about how he's using CherryPy (among lots of other tools) to deliver a site that's getting 250,000 hits a day before it's even been officially launched. He mentioned during the "lightning" (5 minute) talk that, of his entire toolkit, CherryPy and SQLObject were two of the tools that "mostly worked, except..." I spoke with him after the talk about his concerns, and the big CherryPy issue was dispatching: he prefers a Routes-style dispatch mechanism, which makes changes to the design easier.
He had previously posted his cherrypy+routes script, which I read but hadn't done anything about. I mentioned to him yesterday that it might be better, when overriding the dispatch mechanism, to do so in a custom Request class, rather than in a single exposed default method on the cherrypy tree. Here's a first crack at what that would look like; I haven't tested it but it's more to get the idea of custom Request classes out there than to be a working patch ![]()
import urllib
import cherrypy
from cherrypy import _cphttptools
import routes
mapper = routes.Mapper()
controllers = {}
def redirect( url ):
raise cherrypy.HTTPRedirect( url )
def mapConnect( name, route, controller, **kwargs ):
controllers[ name ] = controller
mapper.connect( name, route, controller=name, **kwargs )
def mapFinalize():
mapper.create_regs( controllers.keys() )
def URL( name, query = None, doseq = None, **kwargs ):
uri = routes.url_for( name, **kwargs )
if not uri:
return "/UNKNOWN-%s" % name
if query:
uri += '?' + urllib.urlencode(query, doseq)
return uri
class RoutesRequest(_cphttptools.Request):
def main(self, path=None):
"""Obtain and set cherrypy.response.body from a page handler."""
if path is None:
path = self.object_path
page_handler = self.mapPathToObject(path)
virtual_path = path.split("/")
# Decode any leftover %2F in the virtual_path atoms.
virtual_path = [x.replace("%2F", "/") for x in virtual_path if x]
kwargs = self.params.copy()
kwargs.update( cherrypy.request.mapper_dict )
try:
body = page_handler(*virtual_path, **kwargs)
except Exception, x:
x.args = x.args + (page_handler,)
raise
cherrypy.response.body = body
def mapPathToObject(self, objectpath):
"""For path, return the corresponding exposed callable (or raise NotFound).
path should be a "relative" URL path, like "/app/a/b/c". Leading and
trailing slashes are ignored.
"""
# tell routes to use the cherrypy threadlocal object
config = routes.request_config()
if hasattr(config, 'using_request_local'):
config.request_local = lambda: self
config = routes.request_config()
# hook up the routes variables for this request
config.mapper = mapper
config.host = self.headerMap['Host']
config.protocol = self.scheme
config.redirect = redirect
config.mapper_dict = mapper.match( objectpath )
if config.mapper_dict:
c = config.mapper_dict.pop( 'controller', None )
if c:
controller = controllers[c]
# we have a controller, now emulate cherrypy's index/default/callable semantics:
action = config.mapper_dict.pop( 'action', 'index' )
meth = getattr( controller, action, None )
if not meth:
meth = getattr( controller, 'default', None )
if not meth and callable( controller ) and action == 'index' :
meth = controller
if meth and getattr( meth, 'exposed', False ):
return meth
raise cherrypy.NotFound( objectpath )
# 'authui' is a module with login/logout functions
mapConnect( name = 'home', route = '', controller = home )
mapConnect( name = 'auth', route = 'auth/:action', controller = authui,
requirements = dict( action='(login|logout)' ) )
mapFinalize()
cherrypy.server.request_class = RoutesRequest
cherrypy.server.start()
Look, Ma, no root!
This is a good improvement on David Creamer's original code. One change needs to be made to make it work past the first use. The line in the mapPathToObject method:
config.request_local = lambda: self
should be changed to:
config.request_local = lambda: cherrypy.request
Otherwise, the second request (works the first time) will try to access an object with no mapper_dict attribute and an AttributeError is raised.
Still and all, this is excellent!
And I can't believe I mispelled David's last name in my last post - my apologies to David; that should be Creemer.