Fork me on GitHub

flask自定义decorator的坑点

flask自定义decorator的坑点

在一次开发中需要做一个权限验证的修饰器,由于之前并没有太多的flask编程经验,没有写过这种修饰器,于是便直接写出了以下的代码:

1
2
3
4
5
6
def is_admin(func):
def wrapper(*args,**kargs):
if not session['is_admin']:
return redirect('/admin/login')
return func(args,kargs)
return wrapper

写完后直接运行报错:

1
2
existing endpoint function: %s' % endpoint)
AssertionError: View function mapping is overwriting an existing endpoint function: admin.wrapper

一开始的时候百思不得其解,并不懂这个错误出现的原因。后来联系报错的提示为已经存在了一个注册过的函数admin.wrapper.
可以判断为,经过修饰器修饰后,当传入flask自己的修饰器后修饰器获取到的当前函数的名称为wrapper,所以所有被这个修饰器修饰过得函数传入flask的修饰器中获取到的函数名都是wrapper,因此产生了冲突从而报错。

查看报错的地方的源码,位于app.py中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def add_url_rule(self, rule, endpoint=None, view_func=None,
provide_automatic_options=None, **options):
"""Connects a URL rule. Works exactly like the :meth:`route`
decorator. If a view_func is provided it will be registered with the
endpoint.

Basically this example::

@app.route('/')
def index():
pass

Is equivalent to the following::

def index():
pass
app.add_url_rule('/', 'index', index)

If the view_func is not provided you will need to connect the endpoint
to a view function like so::

app.view_functions['index'] = index

Internally :meth:`route` invokes :meth:`add_url_rule` so if you want
to customize the behavior via subclassing you only need to change
this method.

For more information refer to :ref:`url-route-registrations`.

.. versionchanged:: 0.2
`view_func` parameter added.

.. versionchanged:: 0.6
``OPTIONS`` is added automatically as method.

:param rule: the URL rule as string
:param endpoint: the endpoint for the registered URL rule. Flask
itself assumes the name of the view function as
endpoint
:param view_func: the function to call when serving a request to the
provided endpoint
:param provide_automatic_options: controls whether the ``OPTIONS``
method should be added automatically. This can also be controlled
by setting the ``view_func.provide_automatic_options = False``
before adding the rule.
:param options: the options to be forwarded to the underlying
:class:`~werkzeug.routing.Rule` object. A change
to Werkzeug is handling of method options. methods
is a list of methods this rule should be limited
to (``GET``, ``POST`` etc.). By default a rule
just listens for ``GET`` (and implicitly ``HEAD``).
Starting with Flask 0.6, ``OPTIONS`` is implicitly
added and handled by the standard request handling.
"""
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func)
options['endpoint'] = endpoint
methods = options.pop('methods', None)

# if the methods are not given and the view_func object knows its
# methods we can use that instead. If neither exists, we go with
# a tuple of only ``GET`` as default.
if methods is None:
methods = getattr(view_func, 'methods', None) or ('GET',)
if isinstance(methods, string_types):
raise TypeError('Allowed methods have to be iterables of strings, '
'for example: @app.route(..., methods=["POST"])')
methods = set(item.upper() for item in methods)

# Methods that should always be added
required_methods = set(getattr(view_func, 'required_methods', ()))

# starting with Flask 0.8 the view_func object can disable and
# force-enable the automatic options handling.
if provide_automatic_options is None:
provide_automatic_options = getattr(view_func,
'provide_automatic_options', None)

if provide_automatic_options is None:
if 'OPTIONS' not in methods:
provide_automatic_options = True
required_methods.add('OPTIONS')
else:
provide_automatic_options = False

# Add the required methods now.
methods |= required_methods

rule = self.url_rule_class(rule, methods=methods, **options)
rule.provide_automatic_options = provide_automatic_options

self.url_map.add(rule)
if view_func is not None:
old_func = self.view_functions.get(endpoint)
if old_func is not None and old_func != view_func:
raise AssertionError('View function mapping is overwriting an '
'existing endpoint function: %s' % endpoint)
self.view_functions[endpoint] = view_func

这里的view_functions是一个dict,存储endpoint-view_func键值对。而endpoint是指一个标记各个url的一个字符串,通常为函数的名称。endpoint与不同的函数一一对应。
出错的原因在于相同的endpoint被指向了两次,且指向的函数不同,但是这两个不同的函数却拥有相同的名字name,因此便对应了相同的endpoint。所以便导致了重复出现的错误。

那么怎么解决这个问题?这里使用了一个叫做functools的库中定义的一个修饰器@functool.wraps.
更改后的代码为:

1
2
3
4
5
6
7
def is_admin(func):
@functools.wraps(func)
def wrapper(*args,**kargs):
if not session['is_admin']:
return redirect('/admin/login')
return func(args,kargs)
return wrapper

再次调试可以看到这次程序运行后对应的函数的name不同,从而没有再出现bug。再次调试可以发现最后的函数的name与对应的endpoint值均不同。

注:@functools.wraps(func)的作用就是保留原有函数的名称和docstring

参考

记录一次使用Flask开发过程中的bug

后记

经过这次拍错发现自己对flask的实现机制产生了浓厚的兴趣。。。。那就立个flag,今年要把flask的源码好好看一看,至少弄懂flask背后的设计原理。