Version 7 (modified by 14 years ago) ( diff ) | ,
---|
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?
View instantiation and thread safety
The most recent django-developers discussion on the topic 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.
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 or setattribute to make setting state on self raise an error or warning.
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 mysteriously and only under concurrency.
Arguments against:
- It's unusual to have a class where you can't store state on self.
__call__() and copy()
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 is once again un-threadsafe (but will appear to work fine locally).
__new__()
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
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, plus in-person discussions at DjangoCon.eu, __copy__() on __call__() appears to be a slight front runner, with __new__() as a close runner up.
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:
- Andrew Godwin's baseviews (https://bitbucket.org/andrewgodwin/baseviews/), which was developed alongside a project being built, and has some information about why certain patterns were chosen in the README.
- bkondle's django-baseviews (http://github.com/bkonkle/django-baseviews)
How to help
Class-based views are being developed as a separate application on Github. 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.