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:
- 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.
- Each tuple defines a route:
(name, methods, URL pattern, HandlerClass, handler_method)
. - Routes are named as
thing.action
(thing
is singular -- notthings
). - The method can be
'GET'
,'POST'
,None
, or any other method (or list of methods). None simply means "I don't care". - URL patterns allow fancy matching
(<id:\d+>)
. - Rather than having many handlers (
CreateUserHandler
,DeleteUserHandler
), I use one handler perthing
and have the methods correspond to the actions (UserHandler.create
,UserHandler.delete
). If necessary, I can useself.request.method
to distinguish between intent (ie, "show me the account creation form" versus "create my account and rediect me somewhere"). - The handler method name (
AccountHandler.list
) and theaction
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. - 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.