Reputation: 369
I'm building a webapp where different views will have different amounts of 'wrapped functionality' (e.g. authentication, logging/error handling, database access etc), and be able to easily share this functionality between views.
I'm thinking Pluggable Views would be good way to handle this, by repeatedly subclassing views to build up layers of functionality which wraps the main operation of the view.
However I'm struggling to work out the best way to implement this. I'm thinking of chaining up decorators but the inheritance doesn't seem to work well.
For example, a simplified view with some custom logging and error handling:
from flask.views import View
class LoggedView(View):
def __init__(self,template):
self.template=template
#Decorator method for error handling and logging
def log_view(self,view):
def decorator(**kwargs):
try:
#Set up custom logging
self.log = .....
#Execute view
return view(**kwargs)
except CustomApplicationError as e:
#Log and display error
self.log.error(e)
return render_template('error.html',error=str(e))
return decorator
decorators=[log_view]
#This can be overridden for more complex views
def dispatch_request(self):
return render_template(self.template)
View can be used like:
app.add_url_rule('/index', view_func=LoggedView.as_view('index',template='index.html'))
Then if I wanted to build upon this view to also add user authentication:
class RestrictedView(LoggedView):
#Decorator method for user access validation
def validate_access(self,view):
def decorator(**kwargs):
g.user=session.get('user')
if g.user is None:
return redirect(url_for('login'))
#Execute view
return view(**kwargs)
return decorator
#How to add this functionality to the decorator chain? e.g. I dont think this works:
decorators.append(validate_access)
Then I want to repeat this subclassing to add further functionality such as database access
Any suggestions would be much appreciated!
Upvotes: 1
Views: 819
Reputation: 1121614
decorators
is a list, a mutable structure. You can't just append to it in a subclass. The name decorators
is not defined in a subclass, and if you'd append to LoggedView.decorators
you'd be appending to the wrong list!
You'd have to create a new list object in a subclass to mask the attribute on the base class; you can contruct one by concatenating to the base class sequence; I used tuples here to make this clearer:
class LoggedView(View):
decorators = (log_view,)
class RestrictedView(LoggedView):
decorators = LoggedView.decorators + (validate_access,)
Note that decorators are not methods, they are not bound to the view when they are applied, so there is no self
argument.
If you need access to the view instance from a decorator, then don't use View.decorators
, those decorate a simple function that when called creates the view before calling View.dispatch_request()
on that view; it is this simple function that is returned when you call View.as_view()
. On the other hand, if you need to be able to access the wrapper a decorator produces when registering the route or (in the other direction) when looking up the registered view for an endpoint, then using View.decorators
is exactly correct.
You'd either decorate the methods directly (including dispatch_request()
) or implement your own mechanism in dispatch_request()
:
import inspect
class LoggedView(View):
method_decorators = (log_view,)
#This can be overridden for more complex views
def dispatch_request(self):
# decorate methods
cls = type(self)
members = vars(type(self)).items()
for name, object in members:
if not inspect.isfunction(object):
continue
if name == 'dispatch_request':
continue
# add bound decorated functions to the view
for d in self.method_decorators:
setattr(self, name, d(object).__get__(self, cls))
# dispatch
return render_template(self.template)
This is the path that the Flask-RESTFul project uses to allow specifying decorators for all methods on a view with one line.
Then pull the self
argument from the wrapper call arguments (but do pass it on to the wrapped function):
def log_view(view):
def decorator(self, **kwargs):
try:
#Set up custom logging
self.log = .....
#Execute view
return view(self, **kwargs)
except CustomApplicationError as e:
#Log and display error
self.log.error(e)
return render_template('error.html',error=str(e))
return decorator
I'd define the decorator functions themselves outside of the view classes.
Upvotes: 2