flask 源码解析:请求

这是 flask 源码解析系列文章的其中一篇,本系列全部文章列表:python

简介

对于物理链路来讲,请求只是不一样电压信号,它根本不知道也不须要知道请求格式和内容究竟是怎样的;
对于 TCP 层来讲,请求就是传输的数据(二进制的数据流),它只要发送给对应的应用程序就好了;
对于 HTTP 层的服务器来讲,请求必须是符合 HTTP 协议的内容;
对于 WSGI server 来讲,请求又变成了文件流,它要读取其中的内容,把 HTTP 请求包含的各类信息保存到一个字典中,调用 WSGI app;
对于 flask app 来讲,请求就是一个对象,当须要某些信息的时候,只须要读取该对象的属性或者方法就好了。ide

能够看到,虽然是一样的请求数据,在不一样的阶段和不一样组件看来,是彻底不一样的形式。由于每一个组件都有它自己的目的和功能,这和生活中的事情一个道理:对于一样的事情,不一样的人或者同一我的不一样人生阶段的理解是不同的。函数

这篇文章呢,咱们只考虑最后一个内容,flask 怎么看待请求。ui

请求

咱们知道要访问 flask 的请求对象很是简单,只须要 from flask import request

from flask import request

with app.request_context(environ):
    assert request.method == 'POST'

前面一篇文章 已经介绍了这个神奇的变量是怎么工做的,它最后对应了 flask.wrappers:Request 类的对象。
这个类内部的实现虽然咱们还不清楚,可是咱们知道它接受 WSGI server 传递过来的 environ 字典变量,并提供了不少经常使用的属性和方法可使用,好比请求的 method、path、args 等。
请求还有一个不那么明显的特性——它不能被应用修改,应用只能读取请求的数据。

这个类的定义很简单,它继承了 werkzeug.wrappers:Request,而后添加了一些属性,这些属性和 flask 的逻辑有关,好比 view_args、blueprint、json 处理等。它的代码以下:

from werkzeug.wrappers import Request as RequestBase


class Request(RequestBase):
    """
    The request object is a :class:`~werkzeug.wrappers.Request` subclass and
    provides all of the attributes Werkzeug defines plus a few Flask
    specific ones.
    """

    #: The internal URL rule that matched the request.  This can be
    #: useful to inspect which methods are allowed for the URL from
    #: a before/after handler (``request.url_rule.methods``) etc.
    url_rule = None

    #: A dict of view arguments that matched the request.  If an exception
    #: happened when matching, this will be ``None``.
    view_args = None

    @property
    def max_content_length(self):
        """Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
        ctx = _request_ctx_stack.top
        if ctx is not None:
            return ctx.app.config['MAX_CONTENT_LENGTH']

    @property
    def endpoint(self):
        """The endpoint that matched the request.  This in combination with
        :attr:`view_args` can be used to reconstruct the same or a
        modified URL.  If an exception happened when matching, this will
        be ``None``.
        """
        if self.url_rule is not None:
            return self.url_rule.endpoint

    @property
    def blueprint(self):
        """The name of the current blueprint"""
        if self.url_rule and '.' in self.url_rule.endpoint:
            return self.url_rule.endpoint.rsplit('.', 1)[0]

    @property
    def is_json(self):
        mt = self.mimetype
        if mt == 'application/json':
            return True
        if mt.startswith('application/') and mt.endswith('+json'):
            return True
        return False

这段代码没有什难理解的地方,惟一须要说明的就是 @property 装饰符可以把类的方法变成属性,这是 python 中常常见到的用法。

接着咱们就要看 werkzeug.wrappers:Request

class Request(BaseRequest, AcceptMixin, ETagRequestMixin,
              UserAgentMixin, AuthorizationMixin,
              CommonRequestDescriptorsMixin):

    """Full featured request object implementing the following mixins:

    - :class:`AcceptMixin` for accept header parsing
    - :class:`ETagRequestMixin` for etag and cache control handling
    - :class:`UserAgentMixin` for user agent introspection
    - :class:`AuthorizationMixin` for http auth handling
    - :class:`CommonRequestDescriptorsMixin` for common headers
    """

这个方法有一点比较特殊,它没有任何的 body。可是有多个基类,第一个是 BaseRequest,其余的都是各类 Mixin
这里要讲一下 Mixin 机制,这是 python 多继承的一种方式,若是你但愿某个类能够自行组合它的特性(好比这里的状况),或者但愿某个特性用在多个类中,就可使用 Mixin。
若是咱们只须要能处理各类 Accept 头部的请求,能够这样作:

class Request(BaseRequest, AcceptMixin)
    pass

可是不要滥用 Mixin,在大多数状况下子类继承了父类,而后实现须要的逻辑就能知足需求。

咱们先来看看 BaseRequest:

class BaseRequest(object):
    def __init__(self, environ, populate_request=True, shallow=False):
        self.environ = environ
        if populate_request and not shallow:
            self.environ['werkzeug.request'] = self
        self.shallow = shallow

能看到实例化须要的惟一变量是 environ,它只是简单地把变量保存下来,并无作进一步的处理。Request 的内容不少,其中至关一部分是被 @cached_property 装饰的方法,好比下面这种:

@cached_property
    def args(self):
        """The parsed URL parameters."""
        return url_decode(wsgi_get_bytes(self.environ.get('QUERY_STRING', '')),
                          self.url_charset, errors=self.encoding_errors,
                          cls=self.parameter_storage_class)

    @cached_property
    def stream(self):
        """The stream to read incoming data from.  Unlike :attr:`input_stream`
        this stream is properly guarded that you can't accidentally read past
        the length of the input.  Werkzeug will internally always refer to
        this stream to read data which makes it possible to wrap this
        object with a stream that does filtering.
        """
        _assert_not_shallow(self)
        return get_input_stream(self.environ)

    @cached_property
    def form(self):
        """The form parameters."""
        self._load_form_data()
        return self.form

    @cached_property
    def cookies(self):
        """Read only access to the retrieved cookie values as dictionary."""
        return parse_cookie(self.environ, self.charset,
                            self.encoding_errors,
                            cls=self.dict_storage_class)

    @cached_property
    def headers(self):
        """The headers from the WSGI environ as immutable
        :class:`~werkzeug.datastructures.EnvironHeaders`.
        """
        return EnvironHeaders(self.environ)

@cached_property 从名字就能看出来,它是 @property 的升级版,添加了缓存功能。咱们知道
@property 能把某个方法转换成属性,每次访问属性的时候,它都会执行底层的方法做为结果返回。
@cached_property 也同样,区别是只有第一次访问的时候才会调用底层的方法,后续的方法会直接使用以前返回的值。
那么它是如何实现的呢?咱们能在 werkzeug.utils 找到它的定义:

class cached_property(property):

    """A decorator that converts a function into a lazy property.  The
    function wrapped is called the first time to retrieve the result
    and then that calculated result is used the next time you access
    the value.

    The class has to have a `__dict__` in order for this property to
    work.
    """

    # implementation detail: A subclass of python's builtin property
    # decorator, we override __get__ to check for a cached value. If one
    # choses to invoke __get__ by hand the property will still work as
    # expected because the lookup logic is replicated in __get__ for
    # manual invocation.

    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, _missing)
        if value is _missing:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

这个装饰器同时也是实现了 __set____get__ 方法的描述器
访问它装饰的属性,就会调用 __get__ 方法,这个方法先在 obj.__dict__ 中寻找是否已经存在对应的值。若是存在,就直接返回;若是不存在,调用底层的函数
self.func,并把获得的值保存起来,再返回。这也是它能实现缓存的缘由:由于它会把函数的值做为属性保存到对象中。

关于 Request 内部各类属性的实现,就不分析了,由于它们每一个具体的实现都不太同样,也不复杂,无外乎对 environ 字典中某些字段作一些处理和计算。
接下来回过头来看看 Mixin,这里只用 AcceptMixin 做为例子:

class AcceptMixin(object):

    @cached_property
    def accept_mimetypes(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT'), MIMEAccept)

    @cached_property
    def accept_charsets(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT_CHARSET'),
                                   CharsetAccept)

    @cached_property
    def accept_encodings(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT_ENCODING'))

    @cached_property
    def accept_languages(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT_LANGUAGE'),
                                   LanguageAccept)

AcceptMixin 实现了请求内容协商的部分,好比请求接受的语言、编码格式、相应内容等。
它也是定义了不少 @cached_property 方法,虽然本身没有 __init__ 方法,可是也直接使用了
self.environ,所以它并不能直接使用,只能和 BaseRequest 一块儿出现。

参考资料

相关文章
相关标签/搜索