django rest framework version, parser and source code analysis of python Django

1, rest framework version introduction

In the previous article, we introduced three authentication functions of the rest framework, perform_ Authentication, permissions and throttles, which are inherited from the View method through APIView and reflected by the dispatch method through initialize_ The request method encapsulates a request(Request class), and then calls the initial method to execute the authentication function. Before that, the initial method also has the operation of assignment, but what is the assignment and function? Let's observe through the source code.


1. Source code analysis of rest Framework version

The method of initial function is as follows:

    def initial(self, request, *args, **kwargs):
        self.format_kwarg = self.get_format_suffix(**kwargs)

        neg = self.perform_content_negotiation(request)
        request.accepted_renderer, request.accepted_media_type = neg

        version, scheme = self.determine_version(request, *args, **kwargs)
        request.version, request.versioning_scheme = version, scheme
        # Authority authentication related
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)

Let's start with version. We can find that the Chinese version of version is the version, and by calling self determine_ Version method. The returned parameters are received by the. Finally, the received parameters are assigned to version and versioning in the request_ In the scheme object.

determine_ The version function is as follows:

 def determine_version(self, request, *args, **kwargs):
        if self.versioning_class is None:
            return (None, None)
        scheme = self.versioning_class()
        return (scheme.determine_version(request, *args, **kwargs), scheme)

By determining_ The version function can find that the parameter needs to pass request, and self versioning_class is empty. According to our previous source code analysis, we can infer that this variable is the default global configuration in APIView method, so we can add versioning when customizing the version_ Class or configuration is in the global. It can be found from the code that this method is not traversed, and it is followed by scheme = self versioning_class () calls the function, that is, versioning defined in the subclass_ Class is not a list, but a function object. Determine is called through the scheme object_ Version method, we can know that we must pass in determine when defining our own class_ Version function.

Finally, return returns two parameters. The first one is based on determine_ The version function sends a request to get the version number, and the second return parameter returns versioning_class this function.

So we continue to return to the initial function and find that the two returned parameters are assigned to version and scheme, and version and scheme are assigned to request version, request.versioning_scheme. It is concluded that when we subclass custom functions, if we want to obtain version information, we can use request Version to get the version. If you want to get the object of the custom class method, you can use request versioning_ Get scheme (if there is no subclass, find it from the parent class).

2. Use of rest Framework version

urls are as follows:

from django.conf.urls import url
from api import views

urlpatterns = [
    url(r'^api/v1/auth/$', views.AuthView.as_view()),
    url(r'^api/v1/order/$', views.OrderView.as_view()),
    url(r'^api/v1/users/$', views.UsersView.as_view()),
]

The api/view is as follows:

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
from django.http import JsonResponse

class ParamVersion(object):
    def determine_version(self, request, *args, **kwargs):
        version = request.query_params.get('version')
        return version
        
class UsersView(APIView):
    authentication_classes = []
    permission_classes = []
    throttle_classes = []
    versioning_class = ParamVersion

    def get(self, request, *args, **kwargs):
        print(request.version, request.versioning_scheme)
        return HttpResponse('ok')

Access at this time http://127.0.0.1:8000/api/v1/users/?version=v1 Print:

v1 <api.views.ParamVersion object at 0x000001CF29E7DD08>

It can be found that the version and ParamVersion function objects can be obtained through request, and a query is defined in the Reuqest class_ Params function, which is used to return the parameters of the GET request in the native request (specifically, there are many functions that return the request, which can be viewed in the request class by themselves).

3.BaseVersioning class method

BaseVersioning class is django's built-in version class. As the parent class (subclass inheritance) of multiple class methods, this method is used to define the default version parameters and version selection.

BaseVersioning source code is as follows:

class BaseVersioning:
    default_version = api_settings.DEFAULT_VERSION
    allowed_versions = api_settings.ALLOWED_VERSIONS
    version_param = api_settings.VERSION_PARAM

    def determine_version(self, request, *args, **kwargs):
        msg = '{cls}.determine_version() must be implemented.'
        raise NotImplementedError(msg.format(
            cls=self.__class__.__name__
        ))

    def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
        return _reverse(viewname, args, kwargs, request, format, **extra)

    def is_allowed_version(self, version):
        if not self.allowed_versions:
            return True
        return ((version is not None and version == self.default_version) or
                (version in self.allowed_versions))

See api_settings we know that we can set the global through settings, so we can change the default parameters, allowed versions and how the incoming values are received through global settings.

settings.py is as follows:

REST_FRAMEWORK = {
    "DEFAULT_VERSION": 'v1',
    "ALLOWED_VERSIONS": ['v1', 'v2'],
    "VERSION_PARAM": 'version'
}

The BaseVersioning class also has a reverse method, which is to get the url back to the user by obtaining the value of the parameter passed after the routing, and then calling the reverse method of django.

urls.py is as follows:

from django.conf.urls import url
from api import views

urlpatterns = [
    url(r'^api/v1/auth/$', views.AuthView.as_view()),
    url(r'^api/v1/order/$', views.OrderView.as_view()),
    url(r'^api/v1/users/$', views.UsersView.as_view(), name='uuu'),
]

api/views.py is as follows:

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
from rest_framework.versioning import BaseVersioning


class ParamVersion(BaseVersioning):
    def determine_version(self, request, *args, **kwargs):
        version = request.query_params.get('version')
        return version
        
class UsersView(APIView):
    authentication_classes = []
    permission_classes = []
    throttle_classes = []
    versioning_class = ParamVersion

    def get(self, request, *args, **kwargs):
        u1 = request.versioning_scheme.reverse(viewname='uuu', request=request)
        print(request.version, request.versioning_scheme, u1)
        return HttpResponse('ok')

Access at this time http://127.0.0.1:8000/api/v1/users/?version=v2 Print:

v2 <api.views.ParamVersion object at 0x00000286D85413C8> http://127.0.0.1:8000/api/v1/users/

QueryParameterVersioning class uses

The QueryParameterVersioning method inherits from the BaseVersioning class. This method obtains the version by means of GET request, and can also obtain the URL through the built-in reverse class.

The source code of QueryParameterVersioning is as follows:

class QueryParameterVersioning(BaseVersioning):
    invalid_version_message = _('Invalid version in query parameter.')

    def determine_version(self, request, *args, **kwargs):
        version = request.query_params.get(self.version_param, self.default_version)
        if not self.is_allowed_version(version):
            raise exceptions.NotFound(self.invalid_version_message)
        return version

    def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
        url = super().reverse(
            viewname, args, kwargs, request, format, **extra
        )
        if request.version is not None:
            return replace_query_param(url, self.version_param, request.version)
        return url

You can find the determine in QueryParameterVersioning_ The version method passes request query_ params. GET gets the parameters in the GET request through the global configuration attribute defined by us, self version_ Param obtains the key value and the default attribute, and then passes is_allowed_version determines whether we have passed the value. If not, the default value will be displayed.
The reverse method calls the reverse method of the parent BaseVersioning.

is_ allowed_ The version function is as follows:

    def is_allowed_version(self, version):
        if not self.allowed_versions:
            return True
        return ((version is not None and version == self.default_version) or
                (version in self.allowed_versions))

The api/view is as follows:

from rest_framework.versioning import QueryParameterVersioning
from django.shortcuts import render, HttpResponse
class UsersView(APIView):
    authentication_classes = []
    permission_classes = []
    throttle_classes = []
    versioning_class = QueryParameterVersioning

    def get(self, request, *args, **kwargs):
        u1 = request.versioning_scheme.reverse(viewname='uuu', request=request)
        print(request.version, request.versioning_scheme, u1)
        return HttpResponse('ok')

At this time, the data printed is the same as before.

The URLPathVersioning class uses

The URLPathVersioning method inherits the BaseVersioning class. This method encapsulates the version and URL path. You can use the version to put it in the route and obtain the URL by calling the reverse method of the parent class.

The source code of URLPathVersioning is as follows:

class URLPathVersioning(BaseVersioning):
	"""

    urlpatterns = [
        re_path(r'^(?P<version>[v1|v2]+)/users/$', users_list, name='users-list'),
        re_path(r'^(?P<version>[v1|v2]+)/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
    ]
    """
    invalid_version_message = _('Invalid version in URL path.')
    def determine_version(self, request, *args, **kwargs):
        version = kwargs.get(self.version_param, self.default_version)
        if version is None:
            version = self.default_version

        if not self.is_allowed_version(version):
            raise exceptions.NotFound(self.invalid_version_message)
        return version

    def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
        if request.version is not None:
            kwargs = {} if (kwargs is None) else kwargs
            kwargs[self.version_param] = request.version

        return super().reverse(
            viewname, args, kwargs, request, format, **extra
        )

From the source code comments, it can be seen that this kind of method obtains the data on the route through the get method of kwargs, and the method of defining the route can be (? P[v1|v2] +) to define the version. If there is nothing, the information of the default version will be returned.

The reverse method judges the request first Version is not there, the existence is assigned to kwargs, then the reverse function of the BaseVersioning method of the parent class is invoked, so we can know that in URLPathVersioning, we only need to import request, and the internal operation will be assigned.

urls.py is as follows:

from django.conf.urls import url
from api import views

urlpatterns = [
    url(r'^api/v1/auth/$', views.AuthView.as_view()),
    url(r'^api/v1/order/$', views.OrderView.as_view()),
    url(r'^api/(?P<version>[v1|v2]+)/users/$', views.UsersView.as_view(), name='uuu'),
]

app/views.py is as follows:

from rest_framework.versioning import URLPathVersioning
from django.shortcuts import render, HttpResponse

class UsersView(APIView):
    authentication_classes = []
    permission_classes = []
    throttle_classes = []
    versioning_class = URLPathVersioning

    def get(self, request, *args, **kwargs):
    	# reverse(viewname='uuu', request={'version':'v1'})
        u1 = request.versioning_scheme.reverse(viewname='uuu', request=request)
        print(request.version, request.versioning_scheme, u1)
        return HttpResponse('ok')

Access at this time http://127.0.0.1:8000/api/v1/users/ Print:

v1 <rest_framework.versioning.URLPathVersioning object at 0x000001ACD1710488> http://127.0.0.1:8000/api/v1/users/

Generally, we define versions in routing, so for convenience, we can configure versioning_class to enable global use.

settings.py is as follows:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ['api.utils.auth.Authtication', ],
    "UNAUTHENTICATED_USER": None,
    "UNAUTHENTICATED_TOKEN": None,
    "DEFAULT_PERMISSION_CLASSES": ['api.utils.permissions.MyPermission', ],
    "DEFAULT_THROTTLE_CLASSES": ['api.utils.throttle.UserThrottle'],
    "DEFAULT_THROTTLE_RATES": {
        "scope": '3/m',
        "user": '5/m',
    },
    "DEFAULT_VERSION": 'v1',
    "ALLOWED_VERSIONS": ['v1', 'v2'],
    "VERSION_PARAM": 'version',
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning"
}

2, Introduction to rest framework parser

When the data is returned, if the request is returned through the post request, the specification of the post request data needs to be followed:

  1. Request header content type: application / x-www / form urlencoded (request.post requests to request.body to parse data)
  2. post request data format requirements: name = cyy & age = 18 & Gender = female

1.rest framework parser source code analysis

Generally, APIView encapsulates similar requests in the request class. After the native request is packaged, it can be called through the packaged function. In fact, the parser is encapsulated in the data function in the request class.

initialize_ The request function is as follows:

    def initialize_request(self, request, *args, **kwargs):
        """
        Returns the initial request object.
        """
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )

You can find that the parsers object in the Request class calls get_parsers() method. get_ The parsers function is as follows:

    def get_parsers(self):
        return [parser() for parser in self.parser_classes]

It can be found by calling self parser_ The classes object traverses every function, so we can know from here that if you want to define a parser in a subclass, you need to write self parser_ Classes this list.

The Request/data function is as follows:

   @property
    def data(self):
        if not _hasattr(self, '_full_data'):
            self._load_data_and_files()
        return self._full_data

You can see that the @ property decorator is added to the data function, so you are using request Data does not need parentheses, and we can see that the data function calls load_data_and_files function.

_ load_ data_ and_ The source code of the files function is as follows:

    def _load_data_and_files(self):

        if not _hasattr(self, '_data'):
            self._data, self._files = self._parse()
            if self._files:
                self._full_data = self._data.copy()
                self._full_data.update(self._files)
            else:
                self._full_data = self._data

            if is_form_media_type(self.content_type):
                self._request._post = self.POST
                self._request._files = self.FILES

You can see_ load_ data_ and_ The files method calls self again_ Parse(), and the returned parameters are given to self_ data, self._ files.

self._ The source code of parse() function is as follows:

    def _parse(self):
        media_type = self.content_type
        try:
            stream = self.stream
        except RawPostDataException:
            if not hasattr(self._request, '_post'):
                raise
            if self._supports_form_parsing():
                return (self._request.POST, self._request.FILES)
            stream = None

        if stream is None or media_type is None:
            if media_type and is_form_media_type(media_type):
                empty_data = QueryDict('', encoding=self._request._encoding)
            else:
                empty_data = {}
            empty_files = MultiValueDict()
            return (empty_data, empty_files)

        parser = self.negotiator.select_parser(self, self.parsers)

        if not parser:
            raise exceptions.UnsupportedMediaType(media_type)

        try:
            parsed = parser.parse(stream, media_type, self.parser_context)
        except Exception:
            self._data = QueryDict('', encoding=self._request._encoding)
            self._files = MultiValueDict()
            self._full_data = self._data
            raise

        try:
            return (parsed.data, parsed.files)
        except AttributeError:
            empty_files = MultiValueDict()
            return (parsed, empty_files)

Can see_ The parse function obtained self content_ Type (that is, the parameter of content_type in the request header).
Let's look directly at parser = self negotiator. select_parser (self, self. Parsers) passed in self Parsers (parser), through self Select of negotiator object_ Parser method, and then return the value to parser.

DefaultContentNegotiation class / select_ The parser function is as follows:

 def select_parser(self, request, parsers):
 
        for parser in parsers:
            if media_type_matches(parser.media_type, request.content_type):
                return parser
        return None

It can be found that the media in this method is obtained by looping parsers_ Type request header, which returns the parser of the request header according to the supported request header.

Then we continue down to parsed = parser Parse (stream, media_type, self. Parser_context) shows that this method is executed by calling the parse function of the subclass.

JSONParser class

rest_ The framework has built-in parsers for us, and they all inherit the BaseParser class, which has a parse function. There are many parsers that inherit the BaseParser class, but each source code process is similar. Here we can learn the whole process of the parser through the source code of JSONParser.

The JSONParser/parse function is as follows:

    def parse(self, stream, media_type=None, parser_context=None):
        parser_context = parser_context or {}
        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)

        try:
            decoded_stream = codecs.getreader(encoding)(stream)
            parse_constant = json.strict_constant if self.strict else None
            return json.load(decoded_stream, parse_constant=parse_constant)
        except ValueError as exc:
            raise ParseError('JSON parse error - %s' % str(exc))

decoded_stream receives a request The parameters returned by body, and then through json Load parses the json format into the form of a dictionary.

2. Use of rest framework parser

urls.py is as follows:

from django.conf.urls import url
from api import views

urlpatterns = [
    url(r'^api/v1/auth/$', views.AuthView.as_view()),
    url(r'^api/v1/order/$', views.OrderView.as_view()),
    url(r'^api/(?P<version>[v1|v2]+)/users/$', views.UsersView.as_view(), name='uuu'),
    url(r'^api/(?P<version>[v1|v2]+)/parser/$', views.ParserView.as_view(), name='ddd'),
]

api/views.py is as follows:

from rest_framework.parsers import JSONParser


class ParserView(APIView):
    authentication_classes = []
    permission_classes = []
    throttle_classes = []
    parser_classes = [JSONParser]

    def post(self, request, *args, **kwargs):
        print(request.data)

        return HttpResponse('ParserView')

Access at this time http://127.0.0.1:8000/api/v1/parser/ Display:

The console Prints:

{'name': 'sehun', 'age': 18, 'gender': 'male'}

In order to use each one, we continue to put it into the global configuration.

settings.py is as follows:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ['api.utils.auth.Authtication', ],
    "UNAUTHENTICATED_USER": None,
    "UNAUTHENTICATED_TOKEN": None,
    "DEFAULT_PERMISSION_CLASSES": ['api.utils.permissions.MyPermission', ],
    "DEFAULT_THROTTLE_CLASSES": ['api.utils.throttle.UserThrottle'],
    "DEFAULT_THROTTLE_RATES": {
        "scope": '3/m',
        "user": '5/m',
    },
    "DEFAULT_VERSION": 'v1',
    "ALLOWED_VERSIONS": ['v1', 'v2'],
    "VERSION_PARAM": 'version',
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
    "rest_framework": ["rest_framework.parsers.JSONParser", ["rest_framework.parsers.FormParser"]],
}

At this point, the parser can be configured globally.

Keywords: Python Django source code analysis

Added by Kano on Fri, 04 Mar 2022 20:58:08 +0200