请求上下文对象
之前在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