[python] pprika:基于werkzeug编写的web框架(4) ——请求上下文与helpers

请求上下文对象

之前在wsgi_app中提到了ctx,它需要在请求的开始绑定,结束阶段解绑↓ (去掉了不相关部分)

def wsgi_app(self, environ, start_response):
    ctx = RequestContext(self, environ)# 请求上下文对象
    try:
        try:
            ctx.bind()  # 绑定请求上下文并匹配路由
        except Exception as e:
    finally:
        ctx.unbind()

 上述标橙的RequestContext即是实现该功能重要的类 ↓

 

RequestContext

class RequestContext(object):
    def __init__(self, app, environ):
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = Request(environ)  # 即全局变量request

    def bind(self):

    def unbind(self):

    def match_request(self):

其中app.url_map是之前提及的Map的实例,调用它的bind_to_environ方法可以得到url_adapter,后者是进行路由匹配,即获取该请求url所对endpoint的重要对象。

而Request的实例即为全局可用的请求对象pprika.request,跟flask中的那个一样。此处继承了 werkzeug.wrappers.Request 添加了一些属性 ↓

 

Request

class Request(BaseRequest):
    def __init__(self, environ):
        self.rule = None
        self.view_args = None
        self.blueprint = None
        self.routing_exception = None
        super().__init__(environ)

    def __load__(self, res):
        self.rule, self.view_args = res

        if self.rule and "." in self.rule.endpoint:
            self.blueprint = self.rule.endpoint.rsplit(".", 1)[0]

    @property
    def json(self):
        if self.data and self.mimetype == 'application/json':
            return loads(self.data)

其中的属性如rule、view_args等都是将来在pprika.request上可以直接调用的。

property装饰的json方法使用了json.load将请求体中的data转化为json,方便之后restful模块的使用

__load__方法在路由匹配时使用 ↓

class RequestContext(object):

    def match_request(self):
        try:
            res = self.url_adapter.match(return_rule=True)
            self.request.__load__(res)
        except HTTPException as e:
            self.request.routing_exception = e
            # 暂存错误,之后于handle_user_exception尝试处理

调用url_adapter.match将返回(rule, view_args)元组,并以此调用request.__load__初始化reques.blueprint,这也是为什么当发生路由错误(404/405)时无法被蓝图处理器捕获,因为错误发生于 url_adapter.match,__load__未调用,request.blueprint保持None,因此错误总被认为是发生于app(全局)上。

那么ctx.bind、ctx.unbind都做了什么事情?

class RequestContext(object):

    def bind(self):
        global _req_ctx_ls
        _req_ctx_ls.ctx = self
        self.match_request()

    def unbind(self):
        _req_ctx_ls.__release_local__()

主要是关于_req_ctx_ls中ctx属性的设置与清空,然后就是match_request的调用

其中 _req_ctx_ls 表示:request_context_localStorage,顾名思义,是用来存储请求上下文的对象,而且是线程隔离的 ↓

 

_req_ctx_ls

from werkzeug.local import LocalProxy, Local

_req_ctx_ls = Local()  # request_context_localStorage, 只考虑一般情况:一个请求一个ctx
request = LocalProxy(_get_req_object)

此处的实现类似于bottle中对 threading.local 的使用,通过Local保证各线程/协程之间的数据隔离,而LocalProxy则是对Local的代理,它接收Local实例或函数作为参数,将所有请求转发给代理的Local,主要是为了多app、多请求的考虑引入(实际上pprika应该不需要它)。

在flask中 _req_ctx_ls是LocalStack的子类,以栈结构保存请求上下文对象,但pprika并不考虑测试、开发时可能出现的多app、多请求情况,因此简化了。同时比起flask,pprika还有session、current_app、g对象还没实现,但原理大体相同。

def _get_req_object():
    try:
        ctx = _req_ctx_ls.ctx
        return getattr(ctx, 'request')
    except KeyError:
        raise RuntimeError('脱离请求上下文!')

因此 request = LocalProxy(_get_req_object) 实际上表示使用request时,使用的就是 _req_ctx_ls.ctx.request,而_req_ctx_ls.ctx又会在wsgi_app中通过ctx.bind初始化为RequestContext实例,并在最后情空,做到一个请求对应一个请求上下文。

至此请求上下文已经实现,之后就可以通过 from pprika import request 使用request全局变量。

 

简要介绍Helpers

还记得之前 wsgi_app 与 handle_exception 用到的函数make_response吗?它负责接受视图函数返回值并生成响应对象。

 

make_response

 1 def make_response(rv=None):
 2     status = headers = None
 3 
 4     if isinstance(rv, (BaseResponse, HTTPException)):
 5         return rv
 6 
 7     if isinstance(rv, tuple):
 8         len_rv = len(rv)
 9         if len_rv == 3:
10             rv, status, headers = rv
11         elif len_rv == 2:
12             if isinstance(rv[1], (Headers, dict, tuple, list)):
13                 rv, headers = rv
14             else:
15                 rv, status = rv
16         elif len_rv == 1:
17             rv = rv[0]
18         else:
19             raise TypeError(
20                 '视图函数返回值若为tuple至少要有响应体body,'
21                 '可选status与headers,如(body, status, headers)'
22             )
23 
24     if isinstance(rv, (dict, list)):
25         rv = compact_dumps(rv)
26         headers = Headers(headers)
27         headers.setdefault('Content-type', 'application/json')
28     elif rv is None:
29         pass
30     elif not isinstance(rv, (str, bytes, bytearray)):
31         raise TypeError(f'视图函数返回的响应体类型非法: {type(rv)}')
32 
33     response = Response(rv, status=status, headers=headers)
34     return response

其中rv为视图函数返回值,可以是(body, status, headers)三元组、或仅有body、或body与status,body与headers的二元组、或响应实例,还可以是HTTPException异常实例。根据不同的参数会采取不同的处理措施,最终都是返回Response实例作为响应对象,总体上近似于flask.app.make_response,但没那么细致。

在行号为24-27处是为restful功能考虑的,它会将dict、list类型的rv尝试转化为json。

 

compact_dumps

本质上是对json.dumps的一个封装,实现紧凑的json格式化功能

from json import dumps
from functools import partial

json_config = {'ensure_ascii': False, 'indent': None, 'separators': (',', ':')}
compact_dumps = partial(dumps, **json_config)

 

结语

这样pprika的主要功能就已经都讲完了,作为框架可以应对小型demo的程度

下一篇将介绍blueprint的实现,使项目结构更有组织

[python] pprika:基于werkzeug编写的web框架(5) ——蓝图blueprint

posted @ 2020-05-31 21:38  NoNoe  阅读(177)  评论(0编辑  收藏  举报