Reminiscing this and that, on the web since 1995

Nicer URL mappings in Django

Published tagged , , .

URL mappings in django consists more or less of a list of (regex, handler) tuples, if the regular expression matches the requested URL, the handler is used to serve the request. The regular expressions themselfes tends to be rather long and not as readable as I would want them to. Let's see if we can fix that.

Update : Django 2.0 was recently released and supports a scheme simliar to what I present here (but more generic). If you use this, consider upgrading to Django 2.0 and see the documentation on the URL dispatcher.

Here's a few lines of urls.py from the main app of my blog server, as an example:

url(r'^(?P<lang>(sv|en)?)$',          index, {'year': None}, name='index'),
url(r'^(?P<year>[0-9]{4})/(?P<lang>(sv|en)?)$', index, name='index'),
    post_detail, name='post_detail'),
url(r'^img/(?P<slug>[a-z0-9_-]+)',    image_view, name='image_view'),
url(r'^tag/(?P<slug>[a-z0-9-]+)\.(?P<lang>(sv|en))$', tagged, name='tagged'

As you see, the regular expressions is rather repetitive. In particular, (?P<name>[a-z0-9-]+) comes up a lot. Since we know we are talking about URLs, we should be able to improve a lot. How about we assume by default that parameters should match the format [a-z0-9-]+, and note beforehand when parameters should look like something else. How about like this:

url.bind('year', '[0-9]{4}')
url.bind('lang', '(sv|en)')
url(r'^<lang>?$',               index, {'year': None}, name='index'),
url(r'^<year>/<lang>?$',        index,       name='index'),
url(r'^<year>/<slug>\.<lang>$', post_detail, name='post_detail'),
url(r'^img/<imgid>',            image_view,  name='image_view'),
url(r'^tag/<slug>\.<lang>$',    tagged,      name='tagged'),

Admittedly, this is one lines more but it is a few characters shorter, and more important, I think it is much easier to read. Luckily, this is Python, so it is quite easy to make the second version work. Here's one way of doing it:

class UrlShortcut:
    def __init__(self):
        self.bound = {}
        self.default = r'[a-z0-9-]+'
    def bind(self, name, pattern):
        self.bound[name] = pattern
    def __call__(self, pattern, *args, **kwargs):
        def djangify(match):
            p = match.group(1)
            return r'(?P<%s>%s)' % (p, self.bound.get(p, self.default))
        return django_url(re.sub(r'<(\w+)>', djangify, pattern),
	                      *args, **kwargs)

url = UrlShortcut()

Write a comment

Your name (or pseudonym).

Not published, except as gravatar.

Your homepage / presentation.

No formatting, except that an empty line is interpreted as a paragraph break.