zachary.com

personal pages

All ad proceeds donated to charity.

sixtyPercent: Cochlear Implants, Aviation, Technlology, and Philosophy 2006/02/02

More on Integrating CherryPy and Routes

I wrote before about an experiment with integrating Routes with CherryPy. I've now updated that experiment, and am beginning to look at putting the code into production. First I'll quickly cover how, then talk about why.

The "how" part is pretty simple, thanks to the nice implementations on the part of the CherryPy and Routes authors. The mapRequest function does the work of taking the cherry request object and handing it off to Routes. Since I'm retrofitting Routes onto an existing cherrypy application, I emulate the "index", "default", and "exposed" semantics. Routes doesn't say what to do with the mapped route, only that it did find a match. I maintain a dictionary of controllers (registered with the mapConnect function). As in cherrypy, the controller can be a function, object, module, etc. Here's the code:

import urllib
import cherrypy
import routes

mapper = routes.Mapper()
controllers = {}

def redirect( url ):
    raise cherrypy.HTTPRedirect( url )

def mapRequest():
    # sets up the current cherrypy request object with the Routes URL map

    # tell routes to use the cherrypy threadlocal object
    config = routes.request_config()
    if hasattr(config, 'using_request_local'):
        config.request_local = lambda : cherrypy.request
        config = routes.request_config()

    # hook up the routes variables for this request
    config.mapper = mapper
    config.host = cherrypy.request.headerMap['Host']
    config.protocol = cherrypy.request.scheme
    config.redirect = redirect
    config.mapper_dict = mapper.match( cherrypy.request.path )

    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
    
    return None

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


@cherrypy.expose
def home():
    return "...."

# '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()

class RouteRoot:

    @cherrypy.expose
    def default( self, *args, **kwargs ):
        handler = appserver.mapRequest()

        if handler:
            kwargs.update( cherrypy.request.mapper_dict )
            return handler( **kwargs )

        raise cherrypy.NotFound( cherrypy.request.path )

cherrypy.root = RouteRoot()
cherrypy.server.start()

***
highlight file error
***

Now for the 'why'. I had three pain points with the CherryPy object traversal approach to URLs. First, I'm tired of restructuring my application every time I want to adjust the URL-space. Worse, I find myself avoiding that task because it's a pain, and dealing with the existing URLs. I don't design my application all at once -- it evolves over time, and sometimes the implementation decisions have to be revisited.

Second, in CherryPy it's very difficult to have URLs like /archive/2005/12/1/blog.post.title/edit or /archive/2005/12/1/blog.post.title/preview. I find myself doing this a lot, and with CherryPy I pretty much have to write my own URL dispatching functions after the "/archive" part.

Third, CherryPy -- and for that matter most other web frameworks -- don't help me out with URL generation. In my templates and code, I don't want to write literal URLs -- I want to refer to item symbolically. Routes takes care of this nicely, and that's what the URL function above is for.

Update, Feb 26, 2006: I had a chance to talk to Robert Brewer from the CherryPy team at the PyCon 2006 conference in Dallas. He's taken up the Routes integration, and suggested a slightly different approach using a custom request class. Thanks Robert!

by David Creemer : 2006/02/02 : Categories python (permalink)



All content Copyright 2003-2005, David Z Creemer