Class-Based Views

Update: Class-based generic views have landed [14254] for 1.3. See http://docs.djangoproject.com/en/dev/topics/class-based-views/. Everything below this notice is historical.

The proposal to add a class-based framework for describing views (#6735) has been around for a while. It's been proposed for inclusion in Django 1.0, 1.1, and 1.2. This is a summary of the state of debate, with the intention of landing a patch for the 1.3 release.

To see the current state of work on this feature, seethe 'class-based-views' branch on Russell Keith-Magee's bitbucket repository

The brief

At present, generic views are defined as methods. If you want to customize them, you have to pass in arguments to the view at time of use in urls.py. This is cumbersome:

  • urls.py rapidly becomes large an unwieldly
  • complex behaviors that need to be wrapped in callables depend on the underlying views handling callable arguments (which isn't implemented universally)
  • There's no such thing as a simple extension -- you can't easily say "use that view, but change one argument"; you have to reproduce the full argument list.
  • .. and much more

Moving to a class-based structure means that the complexities of defining and customizing generic views can be handled by subclassing. urls.py returns to being a simple process of pointing at a class. Extension logic can be arbitrarily complex, abstracted behind methods on the generic class. It also means we can provide an entry point for HTTP method-based dispatch -- e.g., a GET() method to handle GET requests, POST() to handle posts, etc.

The problem

However, the devil is in the detail. There are several issues that any class-based view solution needs to address:

  • Deployment: How do you deploy an instance of the view
  • URLResolver interaction: Does the approach require any special handling in URLResolver to identify and/or instantiate the view?
  • Subclassing: How easy is it to subclass and override a specific method on a view class?
  • Thread safety: Does each request get a clean instance of self to work with? If so, how is this achieved?
  • Ease of testing: Does testing the view require any special handling (such as a specialized method to instantiate the view)
  • Ease of decoration: Does the proposed technique pose any impediments to easily decorating views?

View instantiation and thread safety

The most recent django-developers discussion on the topic, and an older thread discussed many of the available options on instantiating the views and protecting them from threading issues. Here is a summary of the various approaches that have been proposed.

Some examples showing the essence of each of them without the other details: http://bitbucket.org/spookylukey/django-class-views-experiments/src/tip/

Store state on request, not view

Pass around the request object via method arguments, store arbitrary state on the request or a special "state" object or dict that is passed around or attached to the request.

Document that state should be stored on the request, storing it on the view instance is unsafe. Additionally, override __setattr__ to make setting state on self raise an error or warning.

Alternatively, override __getattr__/__setattr__ to access self.request.state under the hood while on the surface it looks as if the state is stored on the view instance.

Example usage and view would be the same as shown below in "__call__ and copy()".

Arguments for:

  • Avoids messy and non-idiomatic hacks.
  • Avoids copying or creating new view instance on every request.
  • All the options for actually protecting against thread-unsafety involve some kind of "surprising" behavior. The surprising behavior here (can't store state on self) is explicit and fails immediately, rather than failing confusingly and only under certain circumstances.

Arguments against:

  • It's unusual to have a class where you can't store state on self while in __init__.

__call__() and copy()

Implementation

Example usage:

    url(r'^detail/author/(?P<pk>\d+)/$', views.AuthorDetail(), name="author_detail"),

Example class:

class AuthorView(View):
    def GET(self, request, *args, **kwargs)
        return self.render_to_response('author_list.html', {'authors': Author.objects.all()})

This approach proposes that an class instance be placed in urls.py; the instance has a __call__() method, and when that method is invoked, it takes a shallow copy of the instance defined in urls.py, and returns the result of invoking the request method (e.g., GET()). This achieves thread safety by ensuring that every request is given a clean instance on the view class to work on.

No special handling is required in UrlResolver -- the class instance is a callable, so it appears just like an existing view function.

Arguments against:

  • The "copy on __call__()" approach is a little messy. Normal Python idiom wouldn't lead users to expect that __call__() would cause a copy to be created.
  • The abstraction of the copy-on-call can leak in surprising ways. Some users will try to set up state using an __init__ method (common practice). If any of the state they attach to self in __init__ is mutable (list, dict, object, etc) and they mutate it in the view, this will fail (but not immediately, or in obvious ways).

__new__()`

Implementation

Example usage:

    url(r'^detail/author/(?P<pk>\d+)/$', views.AuthorDetail, name="author_detail"),

Example class:

class AuthorView(View):
    def GET(self, request, *args, **kwargs)
        return self.render_to_response('author_list.html', {'authors': Author.objects.all()})

This approach is much the same as the __copy__() on __call__() approach, except that __new__() is used to create the new instance.

Arguments against:

  • You can't use arguments to __init__() to instantiate a class view (although you can add a configure class method to replace this usage)
  • x = AuthorView() returns x as a HTTPResponse, not an AuthorView instance. This violates expectations of normal class usage.

classmethod

Implementation

Example usage:

    url(r'^detail/author/(?P<pk>\d+)/$', views.AuthorDetail.as_view, name="author_detail"),

Example class:

class AuthorView(View):
    def GET(self, request, *args, **kwargs)
        return self.render_to_response('author_list.html', {'authors': Author.objects.all()})

This is very similar to the __new__() approach, except that we have to add a classmethod call to create the callable.

Arguments against:

  • You can't use arguments to __init__() to instantiate a class view (although you can add a configure class method to replace this usage)
  • The URLconf is slightly more verbose

classmethod2

Implementation.

Similar to classmethod, but as_view returns a view functions when called, and arguments to __init__() can be passed to the as_view call.

Example usage:

    url(r'^detail/author/(?P<pk>\d+)/$', views.AuthorDetail.as_view(), name="author_detail"),
    url(r'^detail/author/(?P<pk>\d+)/foo/$', views.AuthorDetail.as_view(foo=True), name="author_detail"),

Arguments against:

  • The URLconf is slightly more verbose
  • If you do AuthorDetail(foo=True).as_view(), instead of AuthorDetail.as_view(foo=True), you will not get an exception, but foo=True will effectively be ignored.

HTTPResponse subclassing

Implementation

This approach exploits the fact that the aim of a view is to produce a HttpResponse instance; so it shortcuts the process, and makes a 'view' the constructor for the HttpResponse.

Arguments against:

  • Binds the Response object the concept of a view. A view isn't 'is-a' response, in the OO-sense.
  • Makes it difficult or impossible to subclass HttpResponse and use that subclass
  • Makes it difficult to use HttpResponse-returning methods; you can't just call out to a subview/utility method that returns a HttpResponse instance, because the view needs to return 'self'.

UrlResolver view instantiation

Rather than try to make the view look like a callable, this approach modifies UrlResolver so that it can identify when a class-based view is in use, and instantiate an instance when one is detected.

Arguments against:

  • Requires special cases in UrlResolver which almost inevitably involve isinstance(ViewClass) or some analog.
  • Decorators become extremely difficult to use; wrapping methods or __call__ on an uninstantiated class is hard.

Recommendation

Based on these discussions, classmethod2 appears to be the winner.

Class Hierarchy

There are several ways to lay out the tree of classes that will make up both the base views and the new generic views.

The current recommended approach is to use mixins to achieve certain parts of functionality (e.g. "rendering a form", "accepting form arguments", etc.), then combine these into generic views. While mixins are a relatively unused part of Python, and multiple inheritance can cause some odd bugs, the idea is to have the fact mixins are used as more of an implementation detail rather than part of the public API.

Method-based dispatch

This involves having the base view automatically call self.GET for GET requests, self.POST for POST requests, and so forth. This has the advantage of saving boilerplate for several types of view, but the disadvantage of getting in the way if you do want to share a lot of code between GET and POST methods.

The recommended solution is that the very base view have only a dispatch() method (or a similar name) that gets called per-request, and then a subclass that also ships with Django that does method-based dispatch, which would probably be used by a lot of the class-based generic views.

Storing request, args, kwargs on self

One more controversial move is, if we have one instance per request (which is looking more likely), whether the request object and any positional/keyword arguments from the URL should be only passed around in the actual function calls, or stored on self.

Advantages:

  • Allows much less fragile code (functions are far less subclassable if it's been decided in advance they will never see the request object)
  • Cleaner method signatures

Disadvantages:

  • Won't work if there's not one instance per request

The current recommendation is that, if one instance per request is chosen, that these are stored on the instance of the view (using the names request, args and kwargs), and that the dispatch() and GET(), POST(), etc. methods still take these as parameters, but all other methods (like render(), or get_form()) don't.

Methods for everything

Some attempts contain methods for everything - one to get the context instance, one to get the context data, one to get the whole context object, and so on. In real-life usage, this turns out to be both very verbose to override as well as being mostly unused (if I want to override how things are rendered, it's a lot easier to just provide a new render() function than to override five different other methods, and the logic flow can be changed as well).

For this reason, the current recommendation is to break things down into moderately-sized chunks, but not too small - 5 lines or more. Things like template names/patterns should probably be provided as overrideable attributes on the class, however (just not which context instance one should use).

Justification

There are quite a few sets of class-based views out there already; they include:

How to help

Class-based views were originally developed as a separate application on Github. However, work has now moved to the 'class-based-views' branch on Russell Keith-Magee's bitbucket repository. There are a few things yet to be done:

  • A simple readme for getting started
  • Testing it in real applications
  • More test coverage
  • Support for ModelForms that mimics the current generic views
  • Full documentation

Fork the Github project if you want to help out.

Last modified 14 years ago Last modified on Nov 5, 2010, 12:53:56 PM
Note: See TracWiki for help on using the wiki.
Back to Top