@url tag - getting rid of urlpatterns

78 views
Skip to first unread message

Ilya Semenov

unread,
Aug 29, 2007, 12:54:23 PM8/29/07
to Django users
I've been using django for almost a year and I was always frustrated
by its cumbersome urlpatterns system. While it is really flexible, it
doesn't provide any shortcuts for widely-used url and views naming
schemes.

Let me show in examples what I mean.

As everyone, I started with the tutorial and worked with the following
system:

=== urls.py ===
urlpatterns = patterns('',
(r'^$', 'apps.app1.views.index'),
(r'^news/$', 'apps.app1.views.news'),
(r'^members/profile/$', 'apps.app1.views.members.profile'),
(r'^members/secure/$', 'apps.app1.views.members.secure'),
(r'^app2/page/$', 'apps.app2.views.page'), # some other app
...
)

It becomes quite a large and hardy maintainable list when the number
of applications and views grows. I do believe in DRY principle and I
didn't like much that I had to repeat the URL bases and parent module
names again and again. So I kept reading the docs and involved the
system to the following:

=== urls.py ===
urlpatterns = patterns('',
(r'^', include('apps.app1.urls')),
(r'^app2', include('apps.app2.urls')),
)

=== apps/app1/urls/__init__.py ===
urlpatterns = patterns('apps.app1.views',
(r'^index/$', 'index'),
(r'^news/$', 'news'),
(r'^members', include('apps.app1.urls.members'),
)

=== apps/app1/urls/members.py ===
urlpatterns = patterns('apps.app1.views.members',
(r'^profile/$', 'profile'),
(r'^secure/$', 'secure'),
)

=== apps/app1/views/__init__.py ===
def index(request):
...
def news(request):
...

=== apps/app1/views/members.py ===
def profile(request):
....
def secure(request):
...

(I skipped app2.* files for easier reading)

While this system had less redundancy and easier to maintain (the DRY
benefits), it suffered from another DRY problem - there were two
packages with the same structure (apps.app1.urls.* and
apps.app1.views.*), with the highly related content. When I was
renaming a view, I had to browse through two package structures and
change the things twice. Still frustrating, you see.

So what I decided, why do we have to keep urlpatterns apart of the
views? Why wouldn't I put them in the same files? The architecture
became:

=== urls.py ===
urlpatterns = patterns('',
(r'^', include('apps.app1.views')), # note views, not urls
(r'^app2', include('apps.app2.views')),
)

=== apps/app1/views/__init__.py ===
urlpatterns = patterns(__name__, # sic!
(r'^index/$', 'index'),
(r'^news/$', 'news'),
(r'^members', include('apps.app1.views.members'),
)

def index(request):
...
def news(request):
...

=== apps/app1/views/members.py ===
urlpatterns = patterns(__name__,
(r'^profile/$', 'profile'),
(r'^secure/$', 'secure'),
)

def profile(request):
....
def secure(request):
...

This was a good change. I had 50% less files, and I put the related
info within the same modules. Also note the usage of __name__, which
increased the DRY factor a bit more :-)

Yet I wasn't fully satisfied. Whenever I renamed a view I had to patch
the urlpatterns as well.
I also remembered an old inconvenience I always felt with views
modules. If I place views functions in a views module, and place like
"normal" helper functions there as well, they got mixed. By looking at
the code it is sometimes hard to understand which function is a view,
and which function is a helper.
So what I created is the @url decorator which solved the both
problems:

=== urls.py ===
urlpatterns = patterns('',
(r'^', include('apps.app1.views')),
(r'^app2', include('apps.app2.views')),
)

=== apps/app1/views/__init__.py ===
@url(r'^index/$')
def index(request):
...

@url(r'^news/$')
def news(request):
...

urlpatterns += include_urlpatterns(r'^members',
'apps.app1.views.members')

=== apps/app1/views/members.py ===
@url(r'^profile/$)
def profile(request):
....

@url(r'^secure/$)
def secure(request):
...

@url(r'^path1/$', '^path2/$') # you can specify several patterns
def multipath_view(request):
...

def helper(): # easily distinguishable - no @url!
...

Summarizing, the benefits are:
- no more creating and supporting urlpattern maps (less files, less
code, more DRY)
- have the url associated with a view in-place
- easily see if a function is a view
- fully compatible with other chained decorators

Implementation problems, or possible improvements:
- it is hackish
- the speed isn't constant time, it is O(N) where N is the number of
currently loaded modules
- I would like to make it support the no-arguments syntax:

@url()
def profile(request):
....

and have the decorator automatically pull the function name as the url
pattern, but that doesn't seem possible if there are further
decorators (like @user_passes_test or @render_to)

The source code can be found at http://www.djangosnippets.org/snippets/395/

Ivan Sagalaev

unread,
Aug 30, 2007, 2:02:20 AM8/30/07
to django...@googlegroups.com
Ilya Semenov wrote:
> === apps/app1/views/__init__.py ===
> @url(r'^index/$')
> def index(request):
> ...
>
> @url(r'^news/$')
> def news(request):

While the decorator looks nice it creates in my opinion at least as many
problems as it solves.

1. You can apply decorators only to custom views, not to generic views.
And as generic views are commonly used one still should keep a separate
urls.py for them.

2. One of the best things (again, in my opinion) of urls.py is that it
shows whole user interface of an app in one place. You loose this
feature with decorators scattered all over views.

BTW, do you know that you can use function objects instead of their
names in url definition? So this:

urlpatterns = patterns(__name__,


(r'^index/$', 'index'),

becomes just:

urlpatterns = patterns('',
(r'^index/$', index),

Ilya Semenov

unread,
Aug 30, 2007, 4:08:19 AM8/30/07
to Django users
Ivan,

Thanks for reviewing the snippet.

> While the decorator looks nice it creates in my opinion at least as many
> problems as it solves.
>
> 1. You can apply decorators only to custom views, not to generic views.
> And as generic views are commonly used one still should keep a separate
> urls.py for them.

First of all, the decorator is fully optional. It doesn't replace
urlpatterns, it gently appends to them. There's nothing wrong in
writing:

urlpatterns = urlpatterns('',
('^news/$', 'django.views.generic.object_list',
{'queryset':......})
)

@url('^edit_news/$')
def edit_news(request):
....

Second, I think the use of generic views is over-estimated. Generic
views do not even support access restrictions (@user_passes_test) and
thus are most of the time proxied via custom one-line views.

> 2. One of the best things (again, in my opinion) of urls.py is that it
> shows whole user interface of an app in one place. You loose this
> feature with decorators scattered all over views.

While I see the rationale in your words, that position is very
arguable.

Encapsulation is one of the greatest programming principles. From the
architectural point of view, the app-level urls.py shouldn't bother
what members area urls are there in members module, since it can
safely delegate the responsibility to manage the list of members urls
and just pull the data when needed.

> BTW, do you know that you can use function objects instead of their
> names in url definition? So this:
>
> urlpatterns = patterns(__name__,
> (r'^index/$', 'index'),
>
> becomes just:
>
> urlpatterns = patterns('',
> (r'^index/$', index),

Sure, that way it is more natural. Alas, that won't work, since index
function is defined later in the module code. Alternatively, one
should put urlpatterns to the very end of the module file, which just
doesn't feel right to me.

daev

unread,
Aug 30, 2007, 4:55:19 AM8/30/07
to Django users
On 30 авг, 12:08, Ilya Semenov <seme...@inetss.com> wrote:

> Second, I think the use of generic views is over-estimated. Generic
> views do not even support access restrictions (@user_passes_test) and
> thus are most of the time proxied via custom one-line views.

Look at this:
http://www.djangoproject.com/documentation/url_dispatch/#passing-callable-objects-instead-of-strings
You can wrap even generic views with decorator

Ivan Sagalaev

unread,
Aug 30, 2007, 4:59:46 AM8/30/07
to django...@googlegroups.com
Ilya Semenov wrote:
> Second, I think the use of generic views is over-estimated. Generic
> views do not even support access restrictions (@user_passes_test) and
> thus are most of the time proxied via custom one-line views.

Actually they support decoration perfectly well, right in urls.py:

from django.views.generic.list_detail import object_detail
from some_decorators import decorator

urlpatterns = patterns('',
(r'...', decorator(object_detail), { ... }),
)

Though I agree that generic views require some time get used to...

Gary Wilson

unread,
Aug 30, 2007, 5:08:59 PM8/30/07
to Django users
On Aug 30, 3:08 am, Ilya Semenov <seme...@inetss.com> wrote:
> Second, I think the use of generic views is over-estimated.

I tend to agree a bit here. I like having all my URLs in one file and
all my views in another. Using generic views usually clutters the
urls file too much for me. Generic views usually don't save enough
code to be worth it in my experience, it just moves the code to a
different place. Also, if I ever wanted to change the view up to do
more custom things, I would have to undue the generic view setup and
create a regular view anyway, so why not just do that in the first
place.

> > 2. One of the best things (again, in my opinion) of urls.py is that it
> > shows whole user interface of an app in one place. You loose this
> > feature with decorators scattered all over views.

Yes, I think this is a very nice thing about having a file dedicated
to url conf. It give you a nice summary of what the application does
and how it's organized. I would not like having to scan through all
my views to figure out the URL structure.

> While I see the rationale in your words, that position is very
> arguable.
>
> Encapsulation is one of the greatest programming principles. From the
> architectural point of view, the app-level urls.py shouldn't bother
> what members area urls are there in members module, since it can
> safely delegate the responsibility to manage the list of members urls
> and just pull the data when needed.

Isn't this what include() is for:
(r'^members/', include('myapp.members'))

All the members URLs can be encapsulated in the members module.

Gary

Reply all
Reply to author
Forward
0 new messages