List [CTL]
BBS部署于:http://bbs.iv4n.xyz,,项目地址
接下来一点一点总结踩过的坑们:
flask_xxx
,新版的Flask扩展把flask.ext.xxx
改为了前面那个,我觉得算退步吧,一个ext
包模块化结构不是清晰很多吗flask_login
,user
必须要有id
属性,因为我用了MongoDB
所以开始没赋id
,之后看了文档解决class UserMixin(object): if not PY2: # pragma: no cover # Python 3 implicitly set __hash__ to None if we override __eq__ # We set it back to its default implementation __hash__ = object.__hash__ @property def is_active(self): return True @property def is_authenticated(self): return True @property def is_anonymous(self): return False def get_id(self): try: return text_type(self.id) except AttributeError: raise NotImplementedError('No `id` attribute - override `get_id`')
Flask的蓝图,这个东西通俗点说就是让项目能更具模块化,比如说同类视图的实现放在同一个文件里,然后不同功能创建不同文件,但假如没有蓝图很难实现,因为涉及相互导入的问题,即a包导入b包,b包中又导入b包,而有了蓝图的话可以先实现功能,然后在项目的
__init__.py
中为app
类注册实现功能蓝图相对包导入,这个写多自然就懂了。简单点说,
Python3
中使用from .. import xxx/from ..a import xxx
这种叫做相对包导入,假如写函数库的话这样会为调用者省掉很多麻烦,但是!:一不小心报错。因为相对导入只有在包内可实现,而包必须在顶层文件夹内包含__init__.py
,即使它为空。举例:/test |___ a.py(内含app实例) |___ /routes |___ b.py(需导入app实例)
我在
b.py
里写from ..a import app
,报错ValueError: attempted relative import beyond top-level package
因为test
本身不是一个包,所以b.py
无法相对导入上层文件夹里的变量,解决办法:在/test
下创建__init__
,然后将/test
当做包,在/test
同级目录下创建文件调用b.py
。在Flask
开发时,我在项目文件夹中的__init__.py
中实例化app/mongo/bootstrap
,然后在项目文件夹同级创建启动文件,启动文件中再为app
注册蓝图。假如不在外层启动的话,__init__.py
需要导入数据库蓝图注册,而数据库蓝图文件中又需要导入__init__.py
来实例化数据库对象,这就造成了相互导入,不报错就怪了另外
__init__.py
中的变量是会被提到比包内文件高一层的位置上,即直接是包级文件,举个例子:/a |___ b.py(内含class t) |___ __init__.py
这里假如我从外部导入类
t
的话需要from a.b import t
,但假如我在__init__.py
中写from .b import t
,我再从外部导入就只需要from a import t
,即把t
提升到了包级flask_login
中的user.loader
回调,文档中告诉你要实现函数:@login_manager.user_loader def load_user(userid): return get_user_obj(userid) if xxx else None
即通过这个函数返回
id
对应的用户对象,想想flask_login
的内部实现就可以理解,它在内部维护了一个登录用户的实例栈,通过每个用户唯一的id
来获取用户实例,可以阅读源码(注释写的非常清楚了):def user_loader(self, callback): ''' This sets the callback for reloading a user from the session. The function you set should take a user ID (a ``unicode``) and return a user object, or ``None`` if the user does not exist. :param callback: The callback for retrieving a user object. :type callback: callable ''' self.user_callback = callback return callback def reload_user(self, user=None): ''' This set the ctx.user with the user object loaded by your customized user_loader callback function, which should retrieved the user object with the user_id got from session. Syntax example: from flask_login import LoginManager @login_manager.user_loader def any_valid_func_name(user_id): # get your user object using the given user_id, # if you use SQLAlchemy, for example: user_obj = User.query.get(int(user_id)) return user_obj Reason to let YOU define this self.user_callback: Because we won't know how/where you will load you user object. ''' ctx = _request_ctx_stack.top if user is None: user_id = session.get('user_id') if user_id is None: ctx.user = self.anonymous_user() else: if self.user_callback is None: raise Exception( "No user_loader has been installed for this " "LoginManager. Refer to" "https://flask-login.readthedocs.io/" "en/latest/#how-it-works for more info.") user = self.user_callback(user_id) if user is None: ctx.user = self.anonymous_user() else: ctx.user = user else: ctx.user = user
这里因为我用的
MongoDB
,所以网上没有现成实现,几乎都是SqlArchemy
的,这里我给用户类添加了一个类方法class BaseUser(UserMixin): __slots__ = ['id', 'uname', 'passwd', 'passwd_hash', 'email', 'role'] def __init__(self, _id, uname=None, passwd=None, email=None, role="basic"): self.id=_id self.uname = uname self.passwd = passwd self.email = email self.role = role self.passwd_hash = hashlib.sha256(passwd.encode('utf-8')+(hashlib.md5(salt.encode("utf-8")).hexdigest()).encode("utf-8")).hexdigest() ## Other func @classmethod def query(cls, user_id): result = mongo.db.users.find_one({"_id": ObjectId(user_id)}) return cls(result["_id"], result["uname"], result["passwd"], result["email"], result["role"])
MongoDB
的数据库权限,MongoDB
的未授权访问漏洞是因为未开启数据库auth
,正确做法是先关闭auth
然后添加数据库用户,添加root
用户需到admin
库,别的数据库管理员需到指定库,也就是说添加那个库权限就到哪个库执行createUser()
(MongoDB 3.0
以前是addUser
),然后开启auth
在开放到公网,不过一般问题也不大,别监听0.0.0.0
就行了。此外,MongoDB
的权限也很奇怪,dbAdmin
居然没有读写数据库权限(一直说我无权限执行函数搞得我很迷惑),只有执行管理函数的权限,而读写权限需要readWrite
,如下:/* * Read:允许用户读取指定数据库 * readWrite:允许用户读写指定数据库 * dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profile * userAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户 * clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限。 * readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限 * readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限 * userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限 * dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限。 * root:只在admin数据库中可用。超级账号,超级权限 */ > use flask > db.createUser({ "user": "flask", "pwd": "flask", "roles": [ "role": "readWrite", "db": "flask" ] })
蓝图中的
ulr_for
函数,这个函数的前缀是需要实例化蓝图时的名字的,如main = Blueprint("a", __name__) url_for("a.xxx")
头像,头像我没有保存到项目文件里然后
open()
啥的,我直接把图片二进制base64
编码然后存进数据库了,或者其实不编码直接存也可以,因为MongoDB
本身就是Bson
格式存的数据,然后在后台创建了一个读取头像的API
sort
分页,我这样写的get_posts
函数,按页码select
数据:def get_posts_api(limit=0, skip=0, **kwargs): for i in kwargs.keys(): if type(i) != str or type(i) != int: continue kwargs[i], _ = flask_real_escape(kwargs[i]) _posts = mongo.db.posts.find(kwargs).sort([("date", -1)]).skip(skip).limit(limit) posts = list(_posts) for i in posts: i["content"] = mistune.markdown(i["content"], escape=True, hard_wrap=True) return posts
其中的
sort
函数查阅文档可知需要传进去一个元组列表,如sort([(a, 1), (b, -1)])
redirect
函数,我服务器前加了一层Nginx
反代,因为我总觉得Gunicorn
是个玩具,丢在公网上实在不放心,就只让它监听本地让Nginx
去请求它。Redirect
会返回重定向的URL
,然后set location
响应头,Nginx
配置需要加上proxy_set_header
,不然redirect
会直接返回127.0.0.1:2333/xxx
到用户浏览器server { listen 80; server_name 39.105.187.104; location / { proxy_pass http://127.0.0.1:2333; proxy_redirect off; proxy_set_header Host $host:$server_port; } }
Gunicorn
的部署,直接gunicorn wsgi:app
是不行的,提示找不到app
对象,因为我的启动函数不在项目包里,而直接从包启动又没有注册蓝图。没办法看文档另一种方法创建工厂函数,改写gunicorn 'wsgi:create_app()'
,ok注册的验证码我用了一个随机四个数的运算,当然其实没有什么卵用,恶意爬虫直接爬下来
eval
就完事了,我可能就是好玩吧。最初我是在routes/auth.py
中定义了全局变量的随机生成,本地测试一切正常(缓存的缘故),部署后就崩了,原因是第一次访问注册页生成一个验证码,然后post
数据的时候算作第二次访问,这时候验证码已经刷新了,而用户提交的依旧是第一次访问生成在html
里的验证码。解决办法将验证码保存到session全局变量中即可
Flask && Werkzeug源码阅读
Flask
其实Flask框架整体最亮眼的是它的四个全局变量概念(session/current_app/request/g
)以及上下文的概念,全局变量的实现是在Werkzeug
的Local
模块,所以会着重学习它的实现
上下文的实现:
class _AppCtxGlobals(object):
"""A plain object. Used as a namespace for storing data during an
application context.
Creating an app context automatically creates this object, which is
made available as the :data:`g` proxy.
.. describe:: 'key' in g
Check whether an attribute is present.
.. versionadded:: 0.10
.. describe:: iter(g)
Return an iterator over the attribute names.
.. versionadded:: 0.10
"""
def get(self, name, default=None):
"""Get an attribute by name, or a default value. Like
:meth:`dict.get`.
:param name: Name of attribute to get.
:param default: Value to return if the attribute is not present.
.. versionadded:: 0.10
"""
return self.__dict__.get(name, default)
def pop(self, name, default=_sentinel):
"""Get and remove an attribute by name. Like :meth:`dict.pop`.
:param name: Name of attribute to pop.
:param default: Value to return if the attribute is not present,
instead of raise a ``KeyError``.
.. versionadded:: 0.11
"""
if default is _sentinel:
return self.__dict__.pop(name)
else:
return self.__dict__.pop(name, default)
def setdefault(self, name, default=None):
"""Get the value of an attribute if it is present, otherwise
set and return a default value. Like :meth:`dict.setdefault`.
:param name: Name of attribute to get.
:param: default: Value to set and return if the attribute is not
present.
.. versionadded:: 0.11
"""
return self.__dict__.setdefault(name, default)
def __contains__(self, item):
return item in self.__dict__
def __iter__(self):
return iter(self.__dict__)
def __repr__(self):
top = _app_ctx_stack.top
if top is not None:
return '<flask.g of %r>' % top.app.name
return object.__repr__(self)
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)
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()
def _get_g(self):
return _app_ctx_stack.top.g
def _set_g(self, value):
_app_ctx_stack.top.g = value
g = property(_get_g, _set_g)
del _get_g, _set_g
def copy(self):
"""Creates a copy of this request context with the same request object.
This can be used to move a request context to a different greenlet.
Because the actual request object is the same this cannot be used to
move a request context to a different thread unless access to the
request object is locked.
.. versionadded:: 0.10
"""
return self.__class__(self.app,
environ=self.request.environ,
request=self.request
)
def match_request(self):
"""Can be overridden by a subclass to hook into the matching
of the request.
"""
try:
url_rule, self.request.view_args = \
self.url_adapter.match(return_rule=True)
self.request.url_rule = url_rule
except HTTPException as e:
self.request.routing_exception = e
def push(self):
"""Binds the request context to the current context."""
# If an exception occurs in debug mode or if context preservation is
# activated under exception situations exactly one context stays
# on the stack. The rationale is that you want to access that
# information under debug situations. However if someone forgets to
# pop that context again we want to make sure that on the next push
# it's invalidated, otherwise we run at risk that something leaks
# memory. This is usually only a problem in test suite since this
# functionality is not active in production environments.
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)
# Before we push the request context we have to ensure that there
# is an application context.
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
app_ctx = self.app.app_context()
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_request_ctx_stack.push(self)
# Open the session at the moment that the request context is available.
# This allows a custom open_session method to use the request context.
# Only open a new session if this is the first time the request was
# pushed, otherwise stream_with_context loses the session.
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(
self.app, self.request
)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
def pop(self, exc=_sentinel):
"""Pops the request context and unbinds it by doing that. This will
also trigger the execution of functions registered by the
:meth:`~flask.Flask.teardown_request` decorator.
.. versionchanged:: 0.9
Added the `exc` argument.
"""
app_ctx = self._implicit_app_ctx_stack.pop()
try:
clear_request = False
if not self._implicit_app_ctx_stack:
self.preserved = False
self._preserved_exc = None
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
# If this interpreter supports clearing the exception information
# we do that now. This will only go into effect on Python 2.x,
# on 3.x it disappears automatically at the end of the exception
# stack.
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
request_close = getattr(self.request, 'close', None)
if request_close is not None:
request_close()
clear_request = True
finally:
rv = _request_ctx_stack.pop()
# get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active.
if clear_request:
rv.request.environ['werkzeug.request'] = None
# Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)
assert rv is self, 'Popped wrong request context. ' \
'(%r instead of %r)' % (rv, self)
def auto_pop(self, exc):
if self.request.environ.get('flask._preserve_context') or \
(exc is not None and self.app.preserve_context_on_exception):
self.preserved = True
self._preserved_exc = exc
else:
self.pop(exc)
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb):
# do not pop the request stack if we are in debug mode and an
# exception happened. This will allow the debugger to still
# access the request object in the interactive shell. Furthermore
# the context can be force kept alive for the test client.
# See flask.testing for how this works.
self.auto_pop(exc_value)
if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)
def __repr__(self):
return '<%s \'%s\' [%s] of %s>' % (
self.__class__.__name__,
self.request.url,
self.request.method,
self.app.name,
)
因为它的底层实现是依赖Local
类型,所以先看werkzeug
中的实现
Werkzeug
Werkzeug是一个wsgi工具集,实现了web的常用功能,如request/response。它也是由Flask的作者开发的,Flask周边的Jinja、Werkzeug、flask_login都是由大佬一手开发orz
前面提到的四种全局变量概念就是用它实现的,比如在定义一个路由函数时,我们不需要为函数传入request变量,而可以直接由全局变量request(其实是request的代理)获取到我们当前请求的数据
# handle in Flask
@app.route("/")
def index():
return "x"
# handle in other web framework, such as Django
def index(request):
return HttpResponse("x")
看一下Flask源码,这些全局变量是什么东西:
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
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
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
_request_ctx_stack
和_app_ctx_stack
是LocalStack
对象,衍生的四种全局变量都是对上下文对象某个成员的代理,接下来看werkzeug/local
里的具体实现
Local类
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)
首先,__slots__
限定了实例只有两个属性'__storage__', '__ident_func__'
看到__init__
函数中是采用了object.__setattr__(self, x, x)
来赋值,而不是直接self.x = xx
或setattr(self, x, x)
原因是重写了类的__setattr__
方法后,这里想要调用原生的赋值行为(绕过重写的魔术方法),所以使用了这样的写法
初始化函数为对象创建了__storage__
字典和__ident_func__
方法,默认为threading.get_ident
(获取当前线程id),当然,假如是greenlet之类的协程的话是可以修改__ident_func__
,使其获取协程id来实现协程间数据隔离
__call__
魔术方法以实例自身为参数构造了一个LocalProxy
对象,待会看到LP
类的时候再说
所以可看出Local
对象实现了一个线程隔离的哈希表,它的核心是__storage__
,结构是:
{
`[thread id 1]: int`: {key: value},
`[thread id 2]: int`: {key: value},
}
在不同线程中向Local
对象set一个相同的key
值,它的存储也是隔离的
而__release_local__
方法的作用是释放当前线程的所有数据,即直接pop当前线程id
LocalStack类
class LocalStack(object):
"""This class works similar to a :class:`Local` but keeps a stack
of objects instead. This is best explained with an example::
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42
They can be force released by using a :class:`LocalManager` or with
the :func:`release_local` function but the correct way is to pop the
item from the stack after using. When the stack is empty it will
no longer be bound to the current context (and as such released).
By calling the stack without arguments it returns a proxy that resolves to
the topmost item on the stack.
.. versionadded:: 0.6.1
"""
def __init__(self):
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
def _get__ident_func__(self):
return self._local.__ident_func__
def _set__ident_func__(self, value):
object.__setattr__(self._local, '__ident_func__', value)
__ident_func__ = property(_get__ident_func__, _set__ident_func__)
del _get__ident_func__, _set__ident_func__
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError('object unbound')
return rv
return LocalProxy(_lookup)
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
"""Removes the topmost item from the stack, will return the
old value or `None` if the stack was already empty.
"""
stack = getattr(self._local, 'stack', None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()
@property
def top(self):
"""The topmost item on the stack. If the stack is empty,
`None` is returned.
"""
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
LocalStack
类其实就是将Local
封装了一层,在Local
的__storage__
哈希表里新增了{'stack': []}
作栈
首先为实例赋值了一个Local
对象,接着将成员Local
对象的__ident_func__
以property
装饰为getter/setter
,也就相当于将成员对象的属性提升到了自身,因为LocalStack
类也需要接口来修改自身的__ident_func__
来实现除线程外的数据安全。P.s. 这里少了个空行,排版看起来怪怪的,我刚开始还以为是个IndentError
接着它的push/pop
也就实现了一个栈(以list实现),即LocalStack.Local.__storage__[thread_id]["stack"] = []
,注意这里push
的赋值: self._local.stack = rv = []
,因为list是可变容器,所以下面的append
方法实际修改了两个变量(同一对象的引用),而pop
方法中,假如pop当前值后栈为空,则会直接release
当前线程id的kv。
而这里需要留意的是,Local
的__getattr__
方法,当字典的键不存在时抛出异常,而我们看到在push
方法的第一次初始化时会getattr
,但它却正常执行,原因在于,当reflet的getattr
函数设置default参数时会recover抛出的AttributeError
,见说明When a default argument is given, it is returned when the attribute doesn't exist; without it, an exception is raised in that case.
LocalManager
LocalManager
的作用是将Local/LocalStack
对象进行封装聚合,实现了修改get_ident
以及增加中间件的接口,很简单的功能:
class LocalManager(object):
"""Local objects cannot manage themselves. For that you need a local
manager. You can pass a local manager multiple locals or add them later
by appending them to `manager.locals`. Every time the manager cleans up,
it will clean up all the data left in the locals for this context.
The `ident_func` parameter can be added to override the default ident
function for the wrapped locals.
.. versionchanged:: 0.6.1
Instead of a manager the :func:`release_local` function can be used
as well.
.. versionchanged:: 0.7
`ident_func` was added.
"""
def __init__(self, locals=None, ident_func=None):
if locals is None:
self.locals = []
elif isinstance(locals, Local):
self.locals = [locals]
else:
self.locals = list(locals)
if ident_func is not None:
self.ident_func = ident_func
for local in self.locals:
object.__setattr__(local, '__ident_func__', ident_func)
else:
self.ident_func = get_ident
def get_ident(self):
"""Return the context identifier the local objects use internally for
this context. You cannot override this method to change the behavior
but use it to link other context local objects (such as SQLAlchemy's
scoped sessions) to the Werkzeug locals.
.. versionchanged:: 0.7
You can pass a different ident function to the local manager that
will then be propagated to all the locals passed to the
constructor.
"""
return self.ident_func()
def cleanup(self):
"""Manually clean up the data in the locals for this context. Call
this at the end of the request or use `make_middleware()`.
"""
for local in self.locals:
release_local(local)
def make_middleware(self, app):
"""Wrap a WSGI application so that cleaning up happens after
request end.
"""
def application(environ, start_response):
return ClosingIterator(app(environ, start_response), self.cleanup)
return application
def middleware(self, func):
"""Like `make_middleware` but for decorating functions.
Example usage::
@manager.middleware
def application(environ, start_response):
...
The difference to `make_middleware` is that the function passed
will have all the arguments copied from the inner application
(name, docstring, module).
"""
return update_wrapper(self.make_middleware(func), func)
def __repr__(self):
return '<%s storages: %d>' % (
self.__class__.__name__,
len(self.locals)
)
LocalProxy
前面Local
类和LocalStack
类的__call__
魔术方法:
# class Local
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
# class LocalStack
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError('object unbound')
return rv
return LocalProxy(_lookup)
这里的LocalStack
是通过闭包将自身实例包裹进一个lookup
函数并返回
LocalProxy
的实现:
@implements_bool
class LocalProxy(object):
"""Acts as a proxy for a werkzeug local. Forwards all operations to
a proxied object. The only operations not supported for forwarding
are right handed operands and any kind of assignment.
Example usage::
from werkzeug.local import Local
l = Local()
# these are proxies
request = l('request')
user = l('user')
from werkzeug.local import LocalStack
_response_local = LocalStack()
# this is a proxy
response = _response_local()
Whenever something is bound to l.user / l.request the proxy objects
will forward all operations. If no object is bound a :exc:`RuntimeError`
will be raised.
To create proxies to :class:`Local` or :class:`LocalStack` objects,
call the object as shown above. If you want to have a proxy to an
object looked up by a function, you can (as of Werkzeug 0.6.1) pass
a function to the :class:`LocalProxy` constructor::
session = LocalProxy(lambda: get_current_request().session)
.. versionchanged:: 0.6.1
The class can be instantiated with a callable as well now.
"""
__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')
def __init__(self, local, name=None):
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
if callable(local) and not hasattr(local, '__release_local__'):
# "local" is a callable that is not an instance of Local or
# LocalManager: mark it as a wrapped function.
object.__setattr__(self, '__wrapped__', local)
def _get_current_object(self):
"""Return the current object. This is useful if you want the real
object behind the proxy at a time for performance reasons or because
you want to pass the object into a different context.
"""
if not hasattr(self.__local, '__release_local__'):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')
def __repr__(self):
try:
obj = self._get_current_object()
except RuntimeError:
return '<%s unbound>' % self.__class__.__name__
return repr(obj)
def __bool__(self):
try:
return bool(self._get_current_object())
except RuntimeError:
return False
def __unicode__(self):
try:
return unicode(self._get_current_object()) # noqa
except RuntimeError:
return repr(self)
def __dir__(self):
try:
return dir(self._get_current_object())
except RuntimeError:
return []
def __getattr__(self, name):
if name == '__members__':
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)
def __setitem__(self, key, value):
self._get_current_object()[key] = value
def __delitem__(self, key):
del self._get_current_object()[key]
if PY2:
__getslice__ = lambda x, i, j: x._get_current_object()[i:j]
def __setslice__(self, i, j, seq):
self._get_current_object()[i:j] = seq
def __delslice__(self, i, j):
del self._get_current_object()[i:j]
__setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
__delattr__ = lambda x, n: delattr(x._get_current_object(), n)
__str__ = lambda x: str(x._get_current_object())
__lt__ = lambda x, o: x._get_current_object() < o
__le__ = lambda x, o: x._get_current_object() <= o
__eq__ = lambda x, o: x._get_current_object() == o
__ne__ = lambda x, o: x._get_current_object() != o
__gt__ = lambda x, o: x._get_current_object() > o
__ge__ = lambda x, o: x._get_current_object() >= o
__cmp__ = lambda x, o: cmp(x._get_current_object(), o) # noqa
__hash__ = lambda x: hash(x._get_current_object())
__call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw)
__len__ = lambda x: len(x._get_current_object())
__getitem__ = lambda x, i: x._get_current_object()[i]
__iter__ = lambda x: iter(x._get_current_object())
__contains__ = lambda x, i: i in x._get_current_object()
__add__ = lambda x, o: x._get_current_object() + o
__sub__ = lambda x, o: x._get_current_object() - o
__mul__ = lambda x, o: x._get_current_object() * o
__floordiv__ = lambda x, o: x._get_current_object() // o
__mod__ = lambda x, o: x._get_current_object() % o
__divmod__ = lambda x, o: x._get_current_object().__divmod__(o)
__pow__ = lambda x, o: x._get_current_object() ** o
__lshift__ = lambda x, o: x._get_current_object() << o
__rshift__ = lambda x, o: x._get_current_object() >> o
__and__ = lambda x, o: x._get_current_object() & o
__xor__ = lambda x, o: x._get_current_object() ^ o
__or__ = lambda x, o: x._get_current_object() | o
__div__ = lambda x, o: x._get_current_object().__div__(o)
__truediv__ = lambda x, o: x._get_current_object().__truediv__(o)
__neg__ = lambda x: -(x._get_current_object())
__pos__ = lambda x: +(x._get_current_object())
__abs__ = lambda x: abs(x._get_current_object())
__invert__ = lambda x: ~(x._get_current_object())
__complex__ = lambda x: complex(x._get_current_object())
__int__ = lambda x: int(x._get_current_object())
__long__ = lambda x: long(x._get_current_object()) # noqa
__float__ = lambda x: float(x._get_current_object())
__oct__ = lambda x: oct(x._get_current_object())
__hex__ = lambda x: hex(x._get_current_object())
__index__ = lambda x: x._get_current_object().__index__()
__coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o)
__enter__ = lambda x: x._get_current_object().__enter__()
__exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw)
__radd__ = lambda x, o: o + x._get_current_object()
__rsub__ = lambda x, o: o - x._get_current_object()
__rmul__ = lambda x, o: o * x._get_current_object()
__rdiv__ = lambda x, o: o / x._get_current_object()
if PY2:
__rtruediv__ = lambda x, o: x._get_current_object().__rtruediv__(o)
else:
__rtruediv__ = __rdiv__
__rfloordiv__ = lambda x, o: o // x._get_current_object()
__rmod__ = lambda x, o: o % x._get_current_object()
__rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o)
__copy__ = lambda x: copy.copy(x._get_current_object())
__deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)
首先,可以知道实例属性被限制了,而这里可以看到在__slots__
中又添加了__dict__
属性,看起来是矛盾的,也就是说其实还是能随意为实例新增属性的。但其实这里的目的是为了能让Proxy
对象转发被代理obj的__dict__
属性,所以用property
装饰器将方法转为属性,然后在__slots__
里添加了__dict__
__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')
初始化函数:
def __init__(self, local, name=None):
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
if callable(local) and not hasattr(local, '__release_local__'):
# "local" is a callable that is not an instance of Local or
# LocalManager: mark it as a wrapped function.
object.__setattr__(self, '__wrapped__', local)
必填参数local,赋值给_LocalProxy__local
,也就是__local
,私有变量的类外名称会转化为_classname__funcname
,因为调用的基类的__setatr__
方法,所以需要用它的类外名称。接着判断local参数是否存在__call__
魔术方法且无__release_local__
,也就是说这里Local
实例不会被赋值给__wrapped__
,只有通过call LocalStack
或自建的函数会被添加装饰标记
代理的核心——_get_current_obj
:
def _get_current_object(self):
"""Return the current object. This is useful if you want the real
object behind the proxy at a time for performance reasons or because
you want to pass the object into a different context.
"""
if not hasattr(self.__local, '__release_local__'):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)
首先判断是否为普通lookup
函数,如果是则直接调用返回。如是Local
实例,则通过实例重写后的__getattr__
获取name对应的值
总结它的行为:如果是LocalStack
实例化的代理,则返回顶部元素;如果是Local
实例化的,则返回__storage__
字典里实例化时__name__
对应的变量。也就是说,代理只代理一个对象(或栈的top)
剩下的部分就是重写了几乎所有的魔术方法,将它完全变成了一个代理对象,所有的操作符/运算符都是针对它所代理的local元素
代理的意义:
为什么需要在Local/LocalStack
前加一层代理?其实这是设计模式里的代理模式
假如说我们写这样的代码:
s = LocalStack()
s.push(obj1)
s.push(obj2)
def get_obj():
return s.pop()
# 1
p = LocalProxy(get_obj)
print(p)
print(p)
# 2
p = get_obj()
# or
p = s.top
第一种方法和第二种方法是不一样的,代理模式在每一次访问代理对象的时候都会动态获取,而不增代理的话,赋值后就无法再次动态获取了,当然用函数来获取也是可以的,但它就不太像操作对象的属性值了
了解werkzeug
自己实现的Local
类后可以回来看Flask
的上下文实现了
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
请求上下文和程序上下文都是LocalStack
实例,也就是线程(协程)安全的栈结构,四种全局变量:
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
四种全局变量构造函数的实参就是一个能获取前面两种栈的栈顶元素的函数:
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
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
这里分析一下RequestContext
,APPContext
和前者其实是差不多的
初始化函数:
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()
需要一个Flask app
实例作实参,以及wsgi
的environment
,下面是g
变量
def _get_g(self):
return _app_ctx_stack.top.g
def _set_g(self, value):
_app_ctx_stack.top.g = value
g = property(_get_g, _set_g)
del _get_g, _set_g
用property
设置了g
的getter/setter
,g
是从app上下文栈的栈顶的上下文对象里取得的
看一下AppContext
里g
的定义:
self.g = app.app_ctx_globals_class()
进入Flask
类看一看app_ctx_globals_class
#: In Flask 0.9 this property was called `request_globals_class` but it
#: was changed in 0.10 to :attr:`app_ctx_globals_class` because the
#: flask.g object is now application context scoped.
#:
#: .. versionadded:: 0.10
app_ctx_globals_class = _AppCtxGlobals
可见它是一个类,回到flask/ctx
查看类定义:
class _AppCtxGlobals(object):
"""A plain object. Used as a namespace for storing data during an
application context.
Creating an app context automatically creates this object, which is
made available as the :data:`g` proxy.
.. describe:: 'key' in g
Check whether an attribute is present.
.. versionadded:: 0.10
.. describe:: iter(g)
Return an iterator over the attribute names.
.. versionadded:: 0.10
"""
def get(self, name, default=None):
"""Get an attribute by name, or a default value. Like
:meth:`dict.get`.
:param name: Name of attribute to get.
:param default: Value to return if the attribute is not present.
.. versionadded:: 0.10
"""
return self.__dict__.get(name, default)
def pop(self, name, default=_sentinel):
"""Get and remove an attribute by name. Like :meth:`dict.pop`.
:param name: Name of attribute to pop.
:param default: Value to return if the attribute is not present,
instead of raise a ``KeyError``.
.. versionadded:: 0.11
"""
if default is _sentinel:
return self.__dict__.pop(name)
else:
return self.__dict__.pop(name, default)
def setdefault(self, name, default=None):
"""Get the value of an attribute if it is present, otherwise
set and return a default value. Like :meth:`dict.setdefault`.
:param name: Name of attribute to get.
:param: default: Value to set and return if the attribute is not
present.
.. versionadded:: 0.11
"""
return self.__dict__.setdefault(name, default)
def __contains__(self, item):
return item in self.__dict__
def __iter__(self):
return iter(self.__dict__)
def __repr__(self):
top = _app_ctx_stack.top
if top is not None:
return '<flask.g of %r>' % top.app.name
return object.__repr__(self)
g
变量是通过__dict__
设置实例属性来保存数据的
从1.0开始,g
变量存储在app上下文里,而不是请求上下文里,但它依然会在每一次请求重置,所以作用没有变:在一次请求间共享数据
#: In Flask 0.9 this property was called `request_globals_class` but it
#: was changed in 0.10 to :attr:`app_ctx_globals_class` because the
#: flask.g object is now application context scoped.
网上有人说之所以g
只在一次请求间可共享,是因为每次请求会重置g
变量,这是错误的,因为我们可以看到g
的实例化是在AppContext
的初始化函数中,而却没有留重置的接口,所以我们尝试打印每一次请求的app
上下文id:
from flask import Flask, _app_ctx_stack
app = Flask(__name__)
@app.route("/")
def index():
return str(id(_app_ctx_stack.top))
@app.route("/s")
def s():
return str(id(_app_ctx_stack.top))
app.run()
运行即可知道,每一次请求的AppContext
都是不同的对象,所以说,其实每一次的请求都创建了一个新的AppContext
,也就是在app.wsgi_app
中,由RequestContext
来隐式实例化AppContext
并压栈,而当一次请求结束时,app上下文栈和请求上下文栈都会被pop,这里在最后再分析
继续看请求上下文push
方法:
def push(self):
"""Binds the request context to the current context."""
# If an exception occurs in debug mode or if context preservation is
# activated under exception situations exactly one context stays
# on the stack. The rationale is that you want to access that
# information under debug situations. However if someone forgets to
# pop that context again we want to make sure that on the next push
# it's invalidated, otherwise we run at risk that something leaks
# memory. This is usually only a problem in test suite since this
# functionality is not active in production environments.
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)
# Before we push the request context we have to ensure that there
# is an application context.
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
app_ctx = self.app.app_context()
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_request_ctx_stack.push(self)
# Open the session at the moment that the request context is available.
# This allows a custom open_session method to use the request context.
# Only open a new session if this is the first time the request was
# pushed, otherwise stream_with_context loses the session.
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(
self.app, self.request
)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
看到它会确认当前AppContext
不为空,且栈顶上下文的app
为自身app
,接着将自身实例压入_request_ctx_stack
而pop
方法里:
def pop(self, exc=_sentinel):
"""Pops the request context and unbinds it by doing that. This will
also trigger the execution of functions registered by the
:meth:`~flask.Flask.teardown_request` decorator.
.. versionchanged:: 0.9
Added the `exc` argument.
"""
app_ctx = self._implicit_app_ctx_stack.pop()
try:
clear_request = False
if not self._implicit_app_ctx_stack:
self.preserved = False
self._preserved_exc = None
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
# If this interpreter supports clearing the exception information
# we do that now. This will only go into effect on Python 2.x,
# on 3.x it disappears automatically at the end of the exception
# stack.
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
request_close = getattr(self.request, 'close', None)
if request_close is not None:
request_close()
clear_request = True
finally:
rv = _request_ctx_stack.pop()
# get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active.
if clear_request:
rv.request.environ['werkzeug.request'] = None
# Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)
assert rv is self, 'Popped wrong request context. ' \
'(%r instead of %r)' % (rv, self)
pop当前上下文时,会先执行teardown_request
里的函数,这是Flask
的@teardown_request
装饰器注册的hook函数,最后断言pop出的请求上下文is
自身实例
查看flask/app
的源码:
def app_context(self):
"""Create an :class:`~flask.ctx.AppContext`. Use as a ``with``
block to push the context, which will make :data:`current_app`
point at this application.
An application context is automatically pushed by
:meth:`RequestContext.push() <flask.ctx.RequestContext.push>`
when handling a request, and when running a CLI command. Use
this to manually create a context outside of these situations.
::
with app.app_context():
init_db()
See :doc:`/appcontext`.
.. versionadded:: 0.9
"""
return AppContext(self)
def request_context(self, environ):
"""Create a :class:`~flask.ctx.RequestContext` representing a
WSGI environment. Use a ``with`` block to push the context,
which will make :data:`request` point at this request.
See :doc:`/reqcontext`.
Typically you should not call this from your own code. A request
context is automatically pushed by the :meth:`wsgi_app` when
handling a request. Use :meth:`test_request_context` to create
an environment and context instead of this method.
:param environ: a WSGI environment
"""
return RequestContext(self, environ)
在请求到来时,werkzeug
调用Flask
实例,接着创建一个请求上下文,而假如此时app栈为空,RequestContext
的push方法则会隐式实例化一个AppContext
并压栈
def wsgi_app(self, environ, start_response):
"""The actual WSGI application. This is not implemented in
:meth:`__call__` so that middlewares can be applied without
losing a reference to the app object. Instead of doing this::
app = MyMiddleware(app)
It's a better idea to do this instead::
app.wsgi_app = MyMiddleware(app.wsgi_app)
Then you still have the original application object around and
can continue to call methods on it.
.. versionchanged:: 0.7
Teardown events for the request and app contexts are called
even if an unhandled error occurs. Other events may not be
called depending on when an error occurs during dispatch.
See :ref:`callbacks-and-errors`.
:param environ: A WSGI environment.
:param start_response: A callable accepting a status code,
a list of headers, and an optional exception context to
start the response.
"""
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
def __call__(self, environ, start_response):
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app` which can be
wrapped to applying middleware."""
return self.wsgi_app(environ, start_response)
最后总结几点:
一般情况下
AppContext
的生命周期同RequestContext
,甚至准确点说它的生命周期比请求上下文还要短一点。它只是作一个获取当前应用的代理。是随请求到来时与请求上下文一起创建的,而并不是很多人所想的它是当前应用的上下文直观点验证,我们修改Flask的源码,分别在
AppContext
和RequestContext
的初始化函数里打印debug信息,并定义析构函数,在析构函数里也打印debug信息,运行后的结果:* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) request ctx init app ctx init app ctx del request ctx del 127.0.0.1 - - [20/Jan/2019 18:01:42] "GET / HTTP/1.1" 200 - request ctx init app ctx init app ctx del request ctx del 127.0.0.1 - - [20/Jan/2019 18:02:42] "GET /s HTTP/1.1" 200 -
可以清晰的看到它们的生命周期
当请求到来时,由
werkzeug
调用Flask
对象的__call__
魔术方法,接着调用wsgi_app
,创建一个请求上下文,接着将请求上下文push到请求上下文栈中,而它判断目前无应用上下文,就会隐式创建,并入栈,在请求结束后,会执行一系列hook函数,接着调用请求上下文的pop方法,此时会判断是否之前隐式创建了应用上下文,如是,则一起pop掉:# 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 = [] # ... def push(self): # ... if app_ctx is None or app_ctx.app != self.app: app_ctx = self.app.app_context() app_ctx.push() self._implicit_app_ctx_stack.append(app_ctx) else: self._implicit_app_ctx_stack.append(None) def pop(self, exc=_sentinel): """Pops the request context and unbinds it by doing that. This will also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. .. versionchanged:: 0.9 Added the `exc` argument. """ app_ctx = self._implicit_app_ctx_stack.pop() # ... if app_ctx is not None: app_ctx.pop(exc)
栈的意义,因为
Local
对象已经线程(协程)间数据隔离了,所以只需讨论单线程的情况。当单线程请求经历多个中间件时,AppContext
一层一层压栈,能保证获取到的总是目前处理的app
;而对于请求上下文,在写应用时不会有这个情况,之所以用栈结构是为了在写测试或离线脚本时手动with app.request_context()
时能数据隔离