I used to do crazy things like hard-code URLs in my templates:

<a href="/accounts/create/">Create your account</a>  

or

<a href="/accounts/view/?id={{ the_id }}">View account {{ the_id }}</a>  

Ever since webapp2 came around, I've been really into the URL routing helpers, which make it much easier to keep long lists of URLs organized. I started using a new project layout that makes routing really simple -- maybe even fun.

Before I get to the interesting part I need to digress slightly. A while back, I posted a question on Stack Overflow asking whether it was better to break your app apart into separate routes in app.yaml or inside a WSGIApplication. Put simply: should you create one WSGIApplication object and route everything there? Or many separate ones, each with their own routing in app.yaml?

  • The short answer was: It doesn't really matter.
  • The longer answer was: if you're particularly sensitive about start-up time or memory, you might break things apart.

I decided to use one big app. Having a single app makes unit testing with ext.testbed a bit easier.

In addition to making testing easier, I've been using the Python 2.7 runtime which lends itself nicely to a single WSGIApplication: I put my code in main.py and then route /.* to main.app and don’t think about it anymore. But how are you supposed to keep track of all the URLs, Handlers, methods, and routes in your app as it grows larger and larger?

webapp2 to the rescue.

Take a look at routes.py:

from webapp2_extras.routes import RedirectRoute

from handlers.account import AccountHandler


__all__ = ['application_routes']

application_routes = []

_route_info = [  
  ('account.list',   'GET', '/accounts/',                 AccountHandler, 'list'),
  ('account.create', None,  '/accounts/create/',          AccountHandler, 'create'),
  ('account.view',   'GET', '/accounts/<id:\d+>/',        AccountHandler, 'view'),
  ('account.delete', None,  '/accounts/<id:\d+>/delete/', AccountHandler, 'delete'),
  ('account.update', None,  '/accounts/<id:\d+>/update/', AccountHandler, 'update'),
]

for name, methods, pattern, handler_cls, handler_method in _route_info:  
  # Allow a single string, but this has to be changed to a list.
  # None here means any method
  if isinstance(methods, basestring):
    methods = [methods]

  # Create the route
  route = RedirectRoute(name=name, template=pattern, methods=methods,
                        handler=handler_cls, handler_method=handler_method)

  # Add the route to the public list
  application_routes.append(route)

There are a few things to notice here:

  1. You could type out all the boiler-plate every time. I don't think routes have enough distinguishing factors to merit that. I removed the boiler plate and put together a list of tuples.
  2. Each tuple defines a route: (name, methods, URL pattern, HandlerClass, handler_method).
  3. Routes are named as thing.action (thing is singular -- not things).
  4. The method can be 'GET', 'POST', None, or any other method (or list of methods). None simply means "I don't care".
  5. URL patterns allow fancy matching (<id:\d+>).
  6. Rather than having many handlers (CreateUserHandler, DeleteUserHandler), I use one handler per thing and have the methods correspond to the actions (UserHandler.create, UserHandler.delete). If necessary, I can use self.request.method to distinguish between intent (ie, "show me the account creation form" versus "create my account and rediect me somewhere").
  7. The handler method name (AccountHandler.list) and the action part of the route name (account.list) are identical. This makes linking easy -- you know the route's name because it lines up with your code.
  8. I break from PEP8 about lining data up vertically. This is data you'd put in a table. I don't like jagged tables.

Now to tie this all together, I just put some configuration for webapp2 into config.py:

__all__ = ['webapp2_config']

webapp2_config = {  
  'webapp2_extras.sessions': {
    'secret_key': 'Put some magical secret here.', 
  },
  'webapp2_extras.jinja2': {
    'environment_args': {
      'autoescape': True,
    },
  },
}
and hook it all up in main.py:

import webapp2

from config import webapp2_config  
from routes import application_routes


app = webapp2.WSGIApplication(routes=application_routes, config=webapp2_config)  
After that, your handlers might look like this:

import webapp2  
from webapp2_extras import auth  
from webapp2_extras import jinja2  
from webapp2_extras import sessions


class BaseHandler(webapp2.RequestHandler):  
  @webapp2.cached_property
  def jinja2(self):
    return jinja2.get_jinja2(app=self.app)

  @webapp2.cached_property
  def auth_config(self):
    return {'login_url': self.uri_for('login'),
            'logout_url': self.uri_for('logout')}

  @webapp2.cached_property
  def auth(self):
    return auth.get_auth()

  @webapp2.cached_property
  def session_store(self):
    return sessions.get_store(request=self.request)

  def dispatch(self):
    """Override dispatch to persist session data."""
    try:
      super(BaseHandler, self).dispatch()
    finally:
      self.session_store.save_sessions(self.response)

  def render_template(self, template, context=None):
    context = context or {}

    extra_context = {
      'request': self.request,
      'uri_for': self.uri_for,
    }

    # Only override extra context stuff if it's not set by the template:
    for key, value in extra_context.items():
      if key not in context:
        context[key] = value

    rendered = self.jinja2.render_template(template, **context)
    self.response.write(rendered)
from handlers import base


class AccountHandler(base.BaseHandler):  
  def create(self):
    if self.request.method == 'POST':
      # Create the account
      pass

      # Redirect them to the dashboard page
      return self.redirect(self.uri_for('account.view'))

    else:
      return self.render_template('account_create.html')

And your templates can now create links that look like this:

<a href="{{ uri_for('account.create', my_param='something') }}">Create your account</a>  

or

<a href="{{ uri_for('account.view', id=the_id) }}">View account {{ the_id }}</a>  

Extra bonus: If you delete a URL and the template tries to look it up, you’ll get an exception — and so will your unit tests. I guess this could be a scary thing if you don’t have tests, but hopefully it is helpful.