CherryPy Routing Using the RoutesDispatcher

27 Oct

In our last article, we introduced CherryPy’s default dispatcher that maps URL components to a tree of objects and their respective methods. One drawback to this approach is that you must instantiate the hierarchy of objects ourselves, or define a spaghetti of nested classes. In this article we’ll take a look at a more sophisticated method of mapping URLs to page handlers based on the Routes project, which in turn is based on routing concepts from Ruby on Rails.

RoutesDispatcher

In contrast to the default dispatcher, where the object structure defines the available routes, using the RoutesDispatcher entails that we predefine the set of available URLs and assign the components of each to specific page handlers in our application. Let’s take a look at an example of the default dispatcher versus the routes dispatcher. Here is our application:

# main.py
import cherrypy
import wsgiref.handlers

class Root:
  @cherrypy.expose
  def index(self):
    cherrypy.InternalRedirect("hello")
class HelloWorld:
  @cherrypy.expose
  def index(self):
    return "Hello world!"
class GoodbyeWorld:
  @cherrypy.expose
  def index(self,num=None):
    return "Goodbye World!"

def main():
  root = Root()
  root.hello = HelloWorld()
  root.goodbye = GoodbyeWorld()
  app = cherrypy.tree.mount(root,"/")
  wsgiref.handlers.CGIHandler().run(app)

if __name__ == '__main__':
  main()

Using the DefaultDispatcher the following picture depicts the translation of URL components to a page handler, with the default root URL “/” highlighted in blue:

Here is the application emulating the same URL-to-handler matching using the RoutesDispatcher:

# main.py
import cherrypy
import wsgiref.handlers

class HelloWorld:
  @cherrypy.expose
  def index(self):
    return "Hello world!"
class GoodbyeWorld:
  @cherrypy.expose
  def index(self,num=None):
    return "Goodbye World!"

def main():
  hw = HelloWorld()
  gw = GoodbyeWorld()
  mapper = cherrypy.dispatch.RoutesDispatcher()
  mapper.connect('home',"/",controller=hw)
  mapper.connect('hello','/hello',controller=hw)
  mapper.connect('goodbye','/goodbye',controller=gw)
  app = cherrypy.tree.mount(None,config={"/":{"request.dispatch": mapper}})
  wsgiref.handlers.CGIHandler().run(app)

if __name__ == '__main__':
  main()

The pictorial representation of this dispatcher would look like so:

There are several things to notice, including that the Root class is no longer needed. Another thing to notice that the HelloWorld.index() page handler is mapped twice, to two different URLs, obviating the need for the redirecting requests using the default dispatcher. A side effect of multiple routes to a single controller, however, is that a client-side redirect is not issued, CherryPy itself just serves the same content from two different locations. This is actually a faster approach, since the client does not have to re-issue their request to get to the right page, but may create some “code debt” that may need to be maintained across versions of your applications.

A bit of improvement is made by use of the RoutesDispatcher, but not so much that it makes it worthwhile to adopt yet. Let’s take a look at some more examples and see if we can convince ourselves that this is indeed a good investment in our development effort. First let’s change our HelloWorld.index() method to take an optional argument for the number of times to print “Hello World!”:

class HelloWorld:
  @cherrypy.expose
  def index(self,num=1):
    msg = ''
    for x in range(0,int(num)):
      msg+= "Hello World!"
    return msg

Next let’s create a route to handle this request:

mapper.connect('hello-2','/hello/:num', controller=hw, num=1)

The colon prefix is used to denote that an URL component is actually a request parameter. Thus :num is provided as a keyword argument to HelloWorld.index() method. You can define default values for URL components within the URL pattern, and these will override any default values within the class methods. For instance, if our previous example had been:

# main.py
class HelloWorld:
  @cherrypy.expose
  def index(self,num=1):
    msg = ''
    for x in range(1,int(num)):
      msg+= "Hello World!"
    return msg
  # ...
def main(self):
  mapper.connect('hello-2','/hello/:num', controller=hw, num=None)
  # ...

A request to http://localhost:8080/hello will result in an error, since num will always equal None in this case. Knowing this, I would not define default values in the method signatures, leaving that for the route definitions to supply. Another word of caution, CherryPy’s RoutesDispatcher provides a wrapper for the Routes library, and reuses some of the same method names, but there are several important differences between the two.

Specifically, the RoutesDispatcher.connect() method must provide a pattern name, an URL, and a controller t o map the request to. This is true even when the URL pattern has the :contoller keyword parameter defined. In contrast, the default URL pattern for Routes which is “/:controller/:action/:id“, would have obviated the need to define a controller or a name for the route. Another important difference is that you must provide an actual object instance for the controller to the connect() method, not just a class name. Perhaps the two are related, but I have not had a chance to look into this closer.

Even so, the easy specification and handling of request parameters within URL patterns is a big enough reason to choose the RoutesDispatcher over the default, but the decision really becomes a no-brainer when you start planning your application around serving resources in a RESTful manner. We’ll take a look at providing REST style URL patterns for CherryPy applications in our next post, Advanced Routing in CherryPy.

About these ads

7 Responses to “CherryPy Routing Using the RoutesDispatcher”

  1. Sal October 31, 2008 at 1:33 am #

    I noticed that at http://code.google.com/p/appmecha/ you have routes-1.10.1.zip packaged and ready to go. However, I’m not sure why. Any reason?

  2. delagoya October 31, 2008 at 3:07 am #

    Yes, you actually need it to use CherryPy’s RoutesDispatcher. I neglected to mention this in the article, but have a note in my next one about this in the series, RESTful routing in CherryPy.

    BTW it’s taking me a long time for this next one, since RoutesDispatcher does not do RESTful routing, so I am having to actually dig into the source to see where I need to monkey patch that functionality in.

  3. Sal November 4, 2008 at 2:00 am #

    But I thought routes (and thus RoutesDispatcher) does RESTful Urls. Seems like I’m missing something here. Maybe this is a sign of a project being born. :)

  4. delagoya November 4, 2008 at 3:52 am #

    Routes does support RESTful URL handling, but the wrapper that is CherryPy’s RoutesDispatcher does not. It took a bit of leg work on my end to figure out why, but now all that is finished and I am just cleaning up the source code and the post for #3 in the series. Please look forward for it tomorrow :-)

  5. Mike Rhodes June 3, 2009 at 9:21 am #

    Firstly, thanks for these posts—they’ve greatly reassured me of CherryPy and AppEngine’s compatibility.

    Secondly, I found a small bug: in the redefinition of HelloWorld to print multiple times, you need to start the “for x in range(…)” from 0 rather than 1, as follows:

    for x in range(0,int(num)):
    msg+= “Hello World!”

    otherwise you’ll always get one too few “Hello World!” messages (at least, I did!).

  6. delagoya June 3, 2009 at 10:37 am #

    Ah, Thanks for that Mike. I thought I was through making off by one errors. Maybe I should make a series on app unit and integration testing…

  7. Jasper July 7, 2009 at 4:21 pm #

    Above code did not work for me, but after I changed the
    line:

    wsgiref.handlers.CGIHandler().run(app)

    into:

    cherrypy.quickstart(app)

    It did work.

    Thanks!

Comments are closed.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: