[Python] Django Source Code Reading View Starting from Zero ①

View version ② FormView

Introduction

"Reading the source code" is one of the ways to learn programming efficiently. The famous Become a Hacker (original title: How To Become A Hacker) also says:

But let's just say it doesn't work for books and workshop courses. Many, or perhaps most, hackers have studied in their own way. Useful is (a) reading the code, and (b) writing the code.

However, I feel that there are many people who often "write code" but are not very good at "reading code". (It is often more difficult to "read" in terms of difficulty ...) In this article, I'll read Django, Python's major web framework, to give you the feel of reading the source code. I hope this will increase the number of people who want to read open source code!

This time, as the first step, we will follow the flow of class-based View.

I don't know how to write it in Qiita's first post, so if you make a mistake in quoting, it would be helpful if you could point it out in the comments.

environment

We will proceed in the following environment. Django is the latest 2.2.17 at the time of writing (November 2020), you don't have to be too particular about the Python version, but this time we will proceed with 3.8.5. When reading the source code, it is recommended to use your favorite editor or IDE that can use tag jumps such as PyCharm and Eclipse.

Check the installation location with $ python -c" import django; print (django .__ path__) " for the Django source code, or check the Official Repository Let's pull from .17).

$ python --version
Python 3.8.5
$ python -m django --version
2.2.17

Prerequisite knowledge

--I can understand Python 3.x somehow --Somehow understand Django's Tutorial

Try to read

View basics

Sample code: https://github.com/tsuperis/read_django_sample

Views can be written in both functions / classes in Django.

hoge/views.py


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


def function_based_view(request):
    """Function-based view"""
    return HttpResponse('function_based_view')


class ClassBasedView(View):
    """Class-based view"""
    def get(self, request):
        return HttpResponse('class_based_view GET')

    def post(self, request):
        return HttpResponse('class_based_view POST')

core/urls.py


from django.urls import path

from hoge import views as hoge_views

urlpatterns = [
    path('func/', hoge_views.function_based_view, name='hoge_func'),
    path('cls/', hoge_views.ClassBasedView.as_view(), name='hoge_cls'),
]

this is,

--When accessing / func /, "function_based_view" is returned regardless of the HTTP request method. --When requesting / cls / with GET, "class_based_view GET" is returned. --When you make a POST request to / cls /, "class_based_view POST" is returned. --When requesting / cls / other than GET / POST, Method Not Allowed with HTTP status 405 is returned.

It means that · · · Why do function-based views and class-based views behave differently?

First of all, if you look at the simple function base view function_based_view and urls.py, it seems that the function is [django.urls.path](https://docs.djangoproject.com/en/2.2/ref/urls/# It seems to function as View if set in the second argument of path).

So what about class-based views? If you look at the urls.py, you'll notice that the biggest difference is as_view (). Let's read from here.

View.as_view()

Type a command in the interpreter to see where it is, or use the tag jump feature to browse the class directly.

>>> from django.views import View
>>> View.__module__
'django.views.generic.base'

(django installation path)/django/views/generic/base.py


    # -- (A)
    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        # -- (B)
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        # -- (C)
        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # -- (D)
        # 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=())
        return view

(A) Argument

Ignore the classonlymethod decorator this time.

Recognize something like classmethod (a decorator attached to a method that can be called in a non-instantiated class). This is why you can write ClassBasedView.as_view () instead of ClassBasedView (). As_view ().

Returning to the main line, there are two arguments

cls is the convention of class methods and contains ClassBasedView. Arguments that start with **, such as ** initkwargs, are called variable-length keyword arguments and are treated as dict types by taking arbitrary named arguments.

For example, if you call as_view (name ='view', number = 1), the contents of initkwargs will be {'name':'view', number = 1}.

>>> def kw(hoge, **kwargs):
...     print(f'kwargs: {kwargs}')
... 
>>> kw('hoge', name='view', number=123)
kwargs: {'name': 'view', 'number': 123}

(B) Check arguments in a loop

There is no difficult processing, so I will go crispy. It seems that initkwargs (dict type) is looped and the argument name is checked.

As an error condition

  1. key (argument name) exists in ClassBasedView.http_method_names
  2. key does not exist in the attribute of the ClassBasedView class

What is the http_method_names of 1? It is defined at the beginning of the View class.

(django installation path)/django/views/generic/base.py


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

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

That is, in this loop all argument names

  1. Not get, post, put, patch, delete, head, options, trace
  2. Exists as a class attribute name such as as_view, http_method_names

I found that I was checking.

(C) Creating a view function

This is the heart of this time. Defines the view function. If you also look at the return value of as_view, you can see that this function is returned.

Let's compare the function-based view we created earlier with the view function.

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

Since * args is called a variable length argument and the input is arbitrary, the only required argument of the view function is the first argument. In other words, it can be called as function_based_view, so it can be used as a function-based view.

Let's see the continuation. I instantiate the class and then call the setup`` dispatch method.

Instantiation

__init__ looks like this. I have set the named argument of as_view as an attribute of the instance, but since I have not specified an argument for as_view this time, I will ignore it.

(django installation path)/django/views/generic/base.py


    def __init__(self, **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.items():
            setattr(self, key, value)

setup method

It's easy here, so I'll just put the code on it. The self.request, which you often see when using a class-based view, is set at this time.

(django installation path)/django/views/generic/base.py


    def setup(self, request, *args, **kwargs):
        """Initialize attributes shared by all view methods."""
        self.request = request
        self.args = args
        self.kwargs = kwargs

dispatch method

request.method contains the HTTP request method name literally. You're trying to get the same instance method as the HTTP request method name with getattr.

In other words

--When you make a GET request to / cls /, "class_based_view GET" is returned. --Call self.get --When you make a POST request to / cls /, "class_based_view POST" is returned. --Call self.post --When requesting / cls / other than GET / POST, Method Not Allowed with HTTP status 405 is returned. --Call self.http_method_allowed

Seems to be coming from here.

(django installation path)/django/views/generic/base.py


    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.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)  #here
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

view function summary

I will briefly summarize the processing flow of the view function.

  1. Instantiate the View class
  2. Set self.request`` self.kwargs self.args
  3. Call the instance method with the same HTTP request method name

(D) update_wrapper

It's not Django code, but it's important when creating original decorators, so I'll briefly introduce it.

console


>>> from functools import update_wrapper
>>> def view():
...     """This is the view function"""
...     pass
... 
>>> def dispatch():
...     """This is the dispatch function"""
...     pass
... 
>>> view.__doc__
'This is the view function'
>>> update_wrapper(view, dispatch)
<function dispatch at 0x7f90328194c0>
>>> view.__doc__
'This is the dispatch function'

The example looks like this, do you understand? When you call the function, view.__doc__ is replaced with dispatch.__doc__. In addition to __doc__, there is Attribute to be overwritten, but basically the metadata is Used for replacement purposes.

In this case, it's used to let a new function called view inherit the metadata of the class body and the dispatch method.

Summary

A simple summary of the as_view process is" Create a view function that calls the instance method corresponding to the HTTP request method ".

I tried to follow the source code in a hurry, but when I actually read it

--Read after confirming the actual operation ――For parts that are not related to the main line, take a quick look at the function name and contents to get a feel for it.

I think that is the point. You can follow the detailed processing, but if you don't understand the whole thing, you won't know what you were doing.

next time

I haven't decided in particular, but I'll read FormView or Form. I'd like to read Model as well, but it seems to be long due to metaprogramming, so I'll try it after I get used to Qiita a little more. .. ..

I wrote the continuation

View version ② FormView

Recommended Posts

[Python] Django Source Code Reading View Starting from Zero ①
Code wars kata starting from zero
Let Code Day58 Starting from Zero "20. Valid Parentheses"
Let Code Day16 Starting from Zero "344. Reverse String"
Let Code Day49 starting from zero "1323. Maximum 69 Number"
Python explosive environment construction starting from zero (Mac)
Let Code Day89 "62. Unique Paths" Starting from Zero
Let Code Day 55 "22. Generate Parentheses" Starting from Zero
Code wars kata starting from zero Sudoku, Nampre
Let Code Table of Contents Starting from Zero
Let Code Day18 starting from zero "53. Maximum Subarray"
Let Code Day 13 "338. Counting Bits" Starting from Zero
Let Code Day71 Starting from Zero "1496. Path Crossing"
Let Code Day 61 "7. Reverse Integer" starting from zero
Let Code Day 82 "392. Is Subsequence" Starting from Zero
Let Code Day51 Starting from Zero "647. Palindromic Substrings"
Let Code Day 50 "739. Daily Temperatures" Starting from Zero
Let Code Day 15 "283. Move Zeroes" starting from zero
Let Code Day14 starting from zero "136. Single Number"
Install python from source
Reading CSV data from DSX object storage Python code
Let Code Day 43 "5. Longest Palindromic Substring" Starting from Zero
Let Code Day74 starting from zero "12. Integer to Roman"
Let Code Day 42 "2. Add Two Numbers" Starting from Zero
Let Code Day57 Starting from Zero "35. Search Insert Position"
Let Code Day47 Starting from Zero "14. Longest Common Prefix"
Let Code Day78 Starting from Zero "206. Reverse Linked List"
Django starting from scratch (part: 2)
Django starting from scratch (part: 1)
Install ansible from source code
ChIP-seq analysis starting from zero
Stop Omxplayer from Python code
Let Code Day 44 "543. Diameter of Binary Tree" starting from zero
Let Code Day 64 starting from zero "287. Find the Duplicate Number"
[Tweepy] Re: Twitter Bot development life starting from zero # 1 [python]
Remove one-line comments containing Japanese from source code in Python
Let Code Day 84 starting from zero "142. Linked List Cycle II"
Let Code Day24 Starting from Zero "21. Merge Two Sorted Lists"
Let Code Day12 Starting from Zero "617. Merge Two Binary Trees"
Let Code Day2 Starting from Zero "1108. Defanging an IP Address"
Let Code Day70 Starting from Zero "295. Find Median from Data Stream"
Let Code Day81 "347. Top K Frequent Elements" Starting from Zero
Let Code Day48 Starting from Zero "26. Remove Duplicates from Sorted Array"
Let Code Day87 Starting from Zero "1512. Number of Good Pairs"
Install Python from source with Ansible
Let Code Day67 Starting from Zero "1486. XOR Operation in an Array"
Let Code Day56 Starting from Zero "5453. Running Sum of 1d Array"
Let Code Day7 starting from zero "104. Maximum Depth of Binary Tree"
Let Code Day86 Starting from Zero "33. Search in Rotated Sorted Array"
Let Code Day92 Starting from Zero "4. Median of Two Sorted Arrays"
Let Code Day5 starting from zero "1266. Minimum Time Visiting All Points"
View Mac desktop notifications from Python
Execute Python code from C # GUI
[Python] Read the Flask source code
Re: Competitive Programming Life Starting from Zero Chapter 1.2 "Python of Tears"
Let Code Day 35 "160. Intersection of Two Linked Lists" Starting from Zero
Let Code Day83 Starting from Zero "102. Binary Tree Level Order Traversal"
Let Code Day 40 Starting from Zero "114. Flatten Binary Tree to Linked List"
Let Code Day 91 "153. Find Minimum in Rotated Sorted Array" starting from zero
Let Code Day59 starting from zero "1221. Split a String in Balanced Strings"
Let Code Day 11 Starting from Zero "1315. Sum of Nodes with Even-Valued Grandparent"