Code


Version 1 (modified by russellm, 4 years ago) (diff)

Summary of work on Class-based generic views

Class-Based Views

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.

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?

The most recent django-developers discussion on the topic discussed many of the available options. Here is a summary of the various approaches that have been proposed.

__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. However, this is entirely hidden by the implementation.

__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
  • x = AuthorView() returns x as a HTTPResponse, not an AuthorView instance. This violates expectations of normal class usage.

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.

Recommendation

Based on these discussions, plus in-person discussions at DjangoCon.eu, __copy__() on __call__() appears to be a slight front runner, with __new__() as a close runner up.