django-easyurls: Making it easier to read and write Django URLs

By making assumptions, like year is usually a 4-digit number and id 1 or more digits, django-easyurls takes much of the repetition out of defining URLs, using a syntax that's shorter and easier to read.

Compare the following:

# standard
urlpatterns += patterns('django.views.generic.date_based',
    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[-\w]+)/$',
        'object_detail', info_dict),
    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/$',
        'archive_day',   info_dict),
    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/$', 'archive_month', info_dict),
    url(r'^(?P<year>\d{4})/$', 'archive_year',  info_dict),
)

# use easyurls, and let it generate the regex for you
from easyurls import regex as p
urlpatterns += patterns('django.views.generic.date_based',
    url(p('<year>/<month:mon>/<day>/<slug>'), 'object_detail', info_dict),
    url(p('<year>/<month:mon>/<day>'),        'archive_day',   info_dict),
    url(p('<year>/<month:mon>'),              'archive_month', info_dict),
    url(p('<year>'),                          'archive_year',  info_dict),
)

These two sets of URL patterns are functionally equivalent -- the same regex is passed to url() in both cases, but the second is shorter and clearer. Why keep repeating that year is a 4-digit number, month is 3 letters, and day a 1 or 2 digit number? Also, since 99% of the time one wants the pattern to start with '^', and end with '/$', why keep repeating it? If you don't want them, you can say so.

django-easyurls works by defining names for patterns and generating regular expressions for you. By default, the name of the captured variable is the name of the pattern. This can be overriden, as is done above where the "mon" pattern is used for "month", instead of the default \d{1,2}.

Here's a list of the default patterns:

>>> from easyurls import regex as p
>>> for name in sorted(p.patterns):
...     print '%5s: %s' % (name,p.patterns[name])
  day: \d{1,2}
   id: \d+
  mon: [a-z]{3}
month: \d{1,2}
    n: \d+
 slug: [\w-]+
  tag: \w+
 year: \d{4}

To use a different name for a pattern, or different pattern for a name, add the pattern after the name, prefixing the pattern with ":".

# default for month is \d{1,2}
>>> print p('<month>')
^(?P<month>\d{1,2})/$

# using [a-z]{3} for month
>>> print p('<month:mon>')
^(?P<month>[a-z]{3})/$

# using [a-z]{3} for mmm
>>> print p('<mmm:mon>')
^(?P<mmm>[a-z]{3})/$

It's easy to add new or override existing patterns:

>>> p['yy'] = r'\d{2}'
>>> p['mm'] = r'\d{2}'
>>> p['dd'] = r'\d{2}'

>>> print p('<year:yy>/<month:mm>/<day:dd>')
^(?P<year>\d{2})/(?P<month>\d{2})/(?P<day>\d{2})/$

By default, if no pattern is found, \d+ is assumed.

>>> print p('releases/<project_id>')
^releases/(?P<project_id>\d+)/$

For flexibility, you can always use a regular expression.

# regex for unknown "zip_code"
>>> print p('zip/<zip_code:\d{5}>')
^zip/(?P<zip_code>\d{5})/$

# override slug, allowing "."
>>> print p('<slug:[\w-.]+>')
^(?P<slug>[\w-.]+)/$

For demonstration, and testing, purposes here's how prepending and appending or '^', '/', and '$' is handled:

>>> print p('')
^$
>>> print p('foo$')
^foo$
>>> print p('foo/')
^foo/$
>>> print p('/')
^/$

Prepending of '^' and appending of '/' and '$' can be disabled.

>>> p('foo', anchor=False, terminate=False, append_slash=False)
'foo'

Getting django-easyurls

django-easyurls lives in a mercurial repository on Bitbucket, available here: http://bitbucket.org/orutherfurd/django-easyurls/. Alternatively, you can download the single file easyurls.py, which is all you need.

Tagged with: annoucement django

posted March 23rd, 2009 at 2:23 p.m.