Flask部分源码阅读

原创
2018/09/25 17:21
阅读数 822

Flask主要依赖于WerkzeugJinja这两个库,是很简洁的Python Web框架。

  • Werkzeug 是一个WSGI的工具包,是Flask的核心库。
  • Jinja 则是一个模板渲染的库,主要负责渲染返回给客户端的html文件。

各个文件的功能简介

文件名 功能简介
__init__.py 主要是添加了一些常用的模块或者对象,方便导入
_compat.py 主要是做 Python3Python2 之间的兼容处理,还有pypy的兼容处理
app.py 实现与定义WSGI对象的主要模块(WSGI规范可以参考PEP333PEP3333)
blueprints.py 是Blueprints的实现与定义模块。Blueprints的作用是项目的模块划分,类似于Django的APP
cli.py 主要是Flask的命令行功能实现和命令行解析
config.py 实现与配置相关等功能
ctx.py 实现需要上下文管理相关的类与方法
debughelpers.py 定义一些错误提示或者检查的方法或类,增强开发者的体验
globals.py 定义所有全局对象或方法的文件
helpers.py 各种帮助类工具类和方法的文件
logging.py 日志模块的实现文件
sessions.py 定义和实现cookiesession的文件
signals.py 定义和处理信号的文件,主要以blinker库实现
templating.py 实现了与Jinja交互的接口文件
testing.py 实现测试需要的帮助类或者函数。一般不用于生产环境
views.py 定义和实现了视图的类
wrappers.py 实现WSGI规范中的Wrapper,Request和Response

主要类的简单解析

主要类的关系

  1. 主要类的关系

    Flask_主要类的关系.png

  2. Session类的关系

    Flask_Session关系.png

  3. Wrapper类的关系

    Flask_Wrapper关系.png

Context 机制

Werkzeug.local模块

参考之http://python.jobbole.com/87738/

Pyhton标准库中thread.local,实现了线程局部变量,使得每个线程可以有私有变量,具有线程隔离性,可以通过线程安全的方式获取或者改变线程中的变量。

Werkzeug.local的功能与thread.local类似,不过Werkzeug.local在支持线程的基础上,加上了greenlet的协程支持。

在Flask的上下文管理中,主要依赖了Werkzeug.local来实现的。

Werkzeug.local主要有4个类:

  • Local
  • LocalStack
  • LocalProxy
  • LocalManager

Local

# werkzeug.local

...
try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident
...

class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

  • __storage__: 字典,用来保存不同线程/协程中的线程变量。字典中的Key为线程/协程的ID。
  • __ident_func__: 这个是保存获取当前线程/协程的ID函数。如果是协程,则是greenlet.getcurrent。如果是线程,则是thread. get_ident

在Local中调用local.xxx时,会调用__getattr__函数,在__storage__中用当前线程/协程ID作为Key然后再查找变量,代码: self.__storage__[self.__ident_func__()][name]

LocalStack

LocalStack则是使用Local实现一个栈来存储线程/协程的变量。

LocalStack还实现了push、pop、top等方法或属性。调用这些属性或者方法时,该类会根据当前线程/协程的ID,在Local实例中对相应的数值进行操作。

AppContext应用上下文

AppContext在请求、Flask的CLI命令及其他活动期间,保存整个应用程序级别的数据。类似于RequestContext是保存请求期间的数据一样。

# ctx.py

class AppContext(object):
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

        # Like request context, app contexts can be pushed multiple times
        # but there a basic "refcount" is enough to track them.
        self._refcnt = 0

    def push(self):
        """Binds the app context to the current context."""
        self._refcnt += 1
        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()
        _app_ctx_stack.push(self)
        appcontext_pushed.send(self.app)

    def pop(self, exc=_sentinel):
        """Pops the app context."""
        try:
            self._refcnt -= 1
            if self._refcnt <= 0:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_appcontext(exc)
        finally:
            rv = _app_ctx_stack.pop()
        assert rv is self, 'Popped wrong app context.  (%r instead of %r)' \
            % (rv, self)
        appcontext_popped.send(self.app)

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)
# globals.py

from functools import partial
from werkzeug.local import LocalStack, LocalProxy

...

def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)

...

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app

...

# context locals
_app_ctx_stack = LocalStack()

...

current_app = LocalProxy(_find_app)
g = LocalProxy(partial(_lookup_app_object, 'g'))

从代码中可以看到,_app_ctx_stack是一个werkzeug.local.LocalStack的栈,current_appg是通过代理,每次获取栈顶元素AppContextappg属性。

AppContext的实现目的,主要目的是:

  • 使得一个进程中,可以多个Flask App共存,比如Werkzeug内置的Middleware可以将两个Flask App组合成一个WSGI应用,这种情况下,每个App都有自己独立的AppContext
  • 可以使得一些App的全局属性,在离线的CLI命令或者单元测试等非Web环境中中进行访问,而不需要构造出请求,只需要将AppContext push到栈顶。

AppContext中有属性_refcnt,这个值是记录该上下文是否已经没被使用,默认值为0,每次push()则增1,pop()则减1,直至_refcnt减至0,才会触发teardown_appcontext的函数。

RequestContext请求上下文

RequestContext在整个请求处理期间,保存整个请求级别的数据。

# ctx.py

class RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.

    Do not attempt to use this class directly, instead use
    :meth:`~flask.Flask.test_request_context` and
    :meth:`~flask.Flask.request_context` to create this object.

    When the request context is popped, it will evaluate all the
    functions registered on the application for teardown execution
    (:meth:`~flask.Flask.teardown_request`).

    The request context is automatically popped at the end of the request
    for you.  In debug mode the request context is kept around if
    exceptions happen so that interactive debuggers have a chance to
    introspect the data.  With 0.4 this can also be forced for requests
    that did not fail and outside of ``DEBUG`` mode.  By setting
    ``'flask._preserve_context'`` to ``True`` on the WSGI environment the
    context will not pop itself at the end of the request.  This is used by
    the :meth:`~flask.Flask.test_client` for example to implement the
    deferred cleanup functionality.

    You might find this helpful for unittests where you need the
    information from the context local around for a little longer.  Make
    sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
    that situation, otherwise your unittests will leak memory.
    """

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.flashes = None
        self.session = None

        # Request contexts can be pushed multiple times and interleaved with
        # other request contexts.  Now only if the last level is popped we
        # get rid of them.  Additionally if an application context is missing
        # one is created implicitly so for each level we add this information
        self._implicit_app_ctx_stack = []

        # indicator if the context was preserved.  Next time another context
        # is pushed the preserved context is popped.
        self.preserved = False

        # remembers the exception for pop if there is one in case the context
        # preservation kicks in.
        self._preserved_exc = None

        # Functions that should be executed after the request on the response
        # object.  These will be called before the regular "after_request"
        # functions.
        self._after_request_functions = []

        self.match_request()

# globals.py

from functools import partial
from werkzeug.local import LocalStack, LocalProxy

...

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

...

# context locals
_request_ctx_stack = LocalStack()

...

request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))

...

从代码中可以看到,_request_ctx_stack 是一个werkzeug.local.LocalStack的栈,request session是通过代理,每次获取栈顶元素RequestContextrequestsession属性。

RequestContext的实现目的,主要目的是:

  • 保证了每个请求之间的信息完全隔离,避免冲突。
  • 同时,可以简单的使用from flask import request获取当前的请求。

一个线程只执行一个请求,所以RequestContext对于每个线程是唯一的。

当Flask应用开始处理请求时,会将当前RequestContext``push到栈顶,也会同时将AppContext``push到它的栈顶。当请求结束时,会将RequestContext``pop出,然后将AppContext也从其栈顶pop出。

流程

Flask_Context出入栈流程.png

  • 在经过步骤A后,globals.py_request_ctx_stack_app_ctx_stack的top分别为当前的req_ctxapp_ctx,经过LocalProxy的代理后,在Flask中所有的from flask import session, request, g,都是来自栈顶元素的req_ctx或者app_ctx
  • 所以在步骤B中,对请求进行处理的都是当前线程的request。
  • 在步骤C中,会将_request_ctx_stack_app_ctx_stack的栈顶元素弹出。

RequestContext中的_implicit_app_ctx_stack是为了防止在处理请求中有如下操作,使得app_ctxreq_ctx出栈错误:

# 在请求处理中
from

with req_ctx:
   ...
# 如果没有`_implicit_app_ctx_stack`做判断,会出错。AppContext中的`_refcnt`作用也类似

请求接收到返回响应流程

流程的图里,当req_ctx.push()后,就是开始调用self.full_dispatch_request()进行请求的处理了。

Flask_full_dispatch_request的过程.png

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部