How does django's View class work
Asked Answered
Q

2

15

I'm diving into Django's generic views, figuring out how they return a simple HttpResponse object, like a simple view function would.
I have written a simple project for testing, and I added some logging commands to the basic View classed defined in the file django/views/generic/base.py, so that I can track what is happening under the hood.

I have some questions that came up during my research.
I've been trying to keep this post short, however, for the a complete understanding I felt it essential to include the code snippets and the logs.
I will be really thankful to anyone who takes the time to give some helpful comment, possibly answering some of my questions.


urls.py

from django.conf.urls import patterns, url
from views import WelcomeView

urlpatterns = patterns('',
    url(r'^welcome/(?P<name>\w+)/$', WelcomeView.as_view()),
)


views.py

from django.http import HttpResponse
from django.views.generic import View

class WelcomeView(View):
    def get(self, request, name):
        return HttpResponse('What is up, {0}?'.format(name))


django/views/generic/base.py

class View(object):
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):

        #####logging
        logging.error('*** View.__init__ is started with kwargs: {0} ***'.format(
            repr(kwargs)))

        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """

        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in kwargs.iteritems():
            setattr(self, key, value)

        #####logging
        logging.error('*** View.__init__ reached its end. No return value. ***')

    @classonlymethod
    def as_view(cls, **initkwargs):

        #####logging
        logging.error('*** View.as_view is started with initkwargs: {0} ***'.format(
            repr(initkwargs)))

        """
        Main entry point for a request-response process.
        """

        # sanitize keyword arguments
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError(u"You tried to pass in the %s method name as a "
                                u"keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError(u"%s() received an invalid keyword %r" % (
                    cls.__name__, key))

        def view(request, *args, **kwargs):

            #####logging
            logging.error('*** View.as_view.view is called with args: {0};\
                and kwargs: {1} ***'.format(
                repr(args),
                repr(kwargs)))

            self = cls(**initkwargs) 
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get

            #####logging
            logging.error('*** View.as_view.view reached its end.\ 
                Now calls dispatch() and returns the return value.')

            return self.dispatch(request, *args, **kwargs)

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())

        #####logging
        logging.error('*** View.as_view reached its end. Now returns view. ***')
        return view



    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.

        #####logging
        logging.error('*** View.dispatch called, with args: {0};\
            and kwargs: {1} ***'.format(
            repr(args),
            repr(kwargs)))

        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        self.request = request
        self.args = args
        self.kwargs = kwargs

        #####logging
        logging.error('*** View.dispatch reached its end.\ 
            Now calls handler and returns the return value. ***')
        return handler(request, *args, **kwargs)

    def http_method_not_allowed(self, request, *args, **kwargs):
        allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
        logger.warning('Method Not Allowed (%s): %s', request.method, request.path,
            extra={
                'status_code': 405,
                'request': self.request
            }
        )
        return http.HttpResponseNotAllowed(allowed_methods)


Logs from some test requests

Django version 1.4.5, using settings 'try1.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

ERROR:root:*** View.as_view is started with initkwargs: {} ***
ERROR:root:*** View.as_view reached its end. Now returns view. ***
ERROR:root:*** View.as_view.view is called with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.__init__ is started with kwargs: {} ***
ERROR:root:*** View.__init__ reached its end. No return value. ***
ERROR:root:*** View.as_view.view reached its end. Now calls dispatch() and returns the return value.
ERROR:root:*** View.dispatch called, with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.dispatch reached its end. Now calls handler and returns the return value. ***
[24/Feb/2013 12:43:19] "GET /welcome/Dude/ HTTP/1.1" 200 17

ERROR:root:*** View.as_view.view is called with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.__init__ is started with kwargs: {} ***
ERROR:root:*** View.__init__ reached its end. No return value. ***
ERROR:root:*** View.as_view.view reached its end. Now calls dispatch() and returns the return value.
ERROR:root:*** View.dispatch called, with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.dispatch reached its end. Now calls handler and returns the return value. ***
[24/Feb/2013 12:43:32] "GET /welcome/Dude/ HTTP/1.1" 200 17

[24/Feb/2013 12:44:43] "GET /welcome/ HTTP/1.1" 404 1939

ERROR:root:*** View.as_view.view is called with args: (); and kwargs: {'name': u'Bro'} ***
ERROR:root:*** View.__init__ is started with kwargs: {} ***
ERROR:root:*** View.__init__ reached its end. No return value. ***
ERROR:root:*** View.as_view.view reached its end. Now calls dispatch() and returns the return value.
ERROR:root:*** View.dispatch called, with args: (); and kwargs: {'name': u'Bro'} ***
ERROR:root:*** View.dispatch reached its end. Now calls handler and returns the return value. ***
[24/Feb/2013 12:44:59] "GET /welcome/Bro/ HTTP/1.1" 200 16


After all, my questions

1.
According to the logs, as_view is called before View.init.
Does it mean that it calls a View method even before creating a View instance?

2.
Why isn't as_view() called after it is executed for the first call?
I'm not yet an expert at Python's imports, compilation and memory use,
but I have this feeling that they play some role here.

3.
In the definition of view(), what does the following snippet do?

self = cls(**initkwargs)

According to the logs, it triggers View.init.
Is it that it creates a new View instance with initkwargs and assigns it to the instance in use (self)?
If so, why is it needed?

4.
How can we make use of initkwargs (arguments of as_view)?

Quezada answered 24/2, 2013 at 19:47 Comment(0)
T
23

The underlying implementation of these views involves some fairly advanced Python, so if you're a relative beginner it's not surprising if you find some of this code confusing.

  1. The main thing you should understand is what the @classmethod decorator does on the definition of as_view(). This means that this method is not a normal method, which is called on an instance of the class (and takes the instance as the self parameter), but a classmethod, which is called on the class itself (and takes the class as the cls parameter). Some languages refer to that as a static method, although in Python that's a third type of method that we don't need to go into here.

  2. This is because of how the view is defined in the urlconf. You correctly put WelcomeView.as_view() - what this does is call the as_view classmethod at the time that the urlconf is imported.

  3. As we know from point 1, cls is the view class itself. As normal with a class, when you call it, you get an object. So, as you say, what we're doing here is instantiating the class, then assigning that instance to a variable called self, as if we were inside a method of that instance. The point here is that as I said above, as_view is called at import time, and it returns a function - view - that is in turn called by the URL dispatcher when a browser requests that URL. So inside that function, we construct and call the rest of the class that makes up the class-based view. As to why it's needed, see below.

  4. The __init__ method takes care of setting each member of initargs to an instance attribute, where you can access it in your view code via the usual self.whatever syntax.

So, why is all this necessary?

Class-based views come with a huge potential gotcha, which is that any class instantiated directly in the URLconf (or anywhere else at module level) will persist throughout the entire lifetime of the process. And the way that Django is commonly deployed - via WSGI - usually means that one process can last for many many requests. And if you have something persisting across multiple requests, you have the potential for some really nasty thread-safety bugs - if you set something to an instance attribute in one request, for example, it will be visible in subsequent requests.

So, this code not only ensures that each request gets a new instance, it also makes it really hard to break that request isolation by dynamically constructing the instance each time inside the view function.

Topple answered 24/2, 2013 at 20:53 Comment(0)
S
2

1. Firstly as_view() is a class method. This is a method that can be called on a class rather than an instance of a class. And in this case you can see it's being call on View, which is a class rather than an instance.

2. as_view() is called when the url.conf module is loaded - it returns the function view(). It's this function that's called every time a view is requested - as_view doesn't need to be called again.

3. In the scope of the view() function, the cls variable is the View class (eg DetailView, ListView, or whatever child of View is calling the function). Refering to the first argument of a class method as cls is a coding style specification from PEP8. It's similar to the way we refer to the first argument of am instance method as self. So

self = cls(**initkwargs)

is basically the same as

self = View(**initkwargs) or self = DetailView(**initkwargs)

(depending on which class is inheriting this function).

This is, as you say, instantiating a new instance of the class. Up to this point, a View object has yet to be instantiated.

4. Lastly the initkwargs are used when an instance of the class is created. It's really as simple as adding each key, value pair as attributes of the new view object -

for key, value in kwargs.iteritems():
    setattr(self, key, value)
Shunt answered 24/2, 2013 at 21:3 Comment(1)
where can I find documentation on the initkwargs that can be passed in to as_view? I need to know how to specify the template that should be used to render when the url gets calledDefective

© 2022 - 2024 — McMap. All rights reserved.