I'm trying to add an access_rights decorator to my Bottle app to check permissions when accessing a route. However, it's not getting the decorated function's arguments, which causes an error when trying to call my decorated function again.
Here's an example of code using the decorator:
#route('/users')
#access_rights({'POST': ['admin']})
def users(user):
pass
The user parameter comes from a Bottle plugin I wrote that gets the user from the token passed with the request. This is my current decorator:
def access_rights(permissions):
def decorator(f):
def wrapper(*args, **kwargs):
# Check permissions rights here (not implemented yet)
return f(*args, **kwargs)
return wrapper
return decorator
With this, I get TypeError: users() takes exactly 1 argument (0 given) when doing a GET /users, meaning args and kwargs were both empty. However, when I change the decorator as follows, it works:
def access_rights(permissions):
def decorator(f):
return f
return decorator
I haven't worked with decorators a lot, but from my understanding, both implementations above should call the users function with its original parameters, yet for some reason the first one doesn't get the parameters. Why is that?
Your route handler, function users, expects one parameter.
But your decorator, access_rights, which you wrap around users, isn't passing a user param; it's just passing any params that it received (and, in this case, there aren't any, hence the "0 given" part of the error message).
An example should help clarify. Here's a small but complete working app, based on your original code:
from bottle import route, Bottle
app = Bottle()
def access_rights(permissions):
def decorator(f):
def wrapper(*args, **kwargs):
# Check permissions rights here (not implemented yet)
the_user = 'ron' # hard-coded for this example
return f(the_user, *args, **kwargs)
return wrapper
return decorator
#app.route('/users')
#access_rights({'POST': ['admin']})
def users(user):
return ['hello, {}'.format(user)]
app.run(host='127.0.0.1', port=8080, debug=True)
Note that the only substantial change I made was to have access_rights actually pass a user param on down the line. (How it determines the user is, naturally, up to you--presumably it's the "not implemented yet" part that you called out in your comment).
Hope that helps!
Related
Say, I have a hand-crafted #login-required decorator:
from functools import wraps
def login_required(decorated_function):
"""Decorator to check if user is logged in."""
#wraps(decorated_function)
def wrapper(*args, **kwargs):
if False: # just to check it's working
return decorated_function(*args, **kwargs)
else:
flash('You need to login, to access this page')
return redirect(url_for('login'))
return wrapper
and a function, decorated with #app.route() and #login_required (endpoint for login omitted for brevity):
#app.route('/')
#login_required
def index():
return "Hello!"
Now, if I try to access /, as expected, it won't let me and will redirect to the login page.
Though, if I swipe the the order of the decorators i.e.:
#login_required
#app.route('/')
def index():
return "Hello!"
then I am able to access /, even though I shouldn't be.
I am aware that Flask documentation on the subject states:
When applying further decorators, always remember that the route() decorator is the outermost.
I have also seen other questions on the same issue.
What I'm curious about is not what is the proper way to do it (#app.route() decorator must be outermost - got it), but rather why it is working this way (i.e. what is the mechanics behind it).
I took a look at #app.route() source code:
def route(self, rule, **options):
def decorator(f):
endpoint = options.pop('endpoint', None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
This answer, helped me to understand mechanism of decorators, more or less. Though, I have never seen function just returned (without calling it) before, so I did a little experiment myself (which turned out to be workable, of course):
def my_decorator():
def decorator (function):
return function
return decorator
#my_decorator()
def test():
print('Hi')
test()
So, I would like to understand:
Why order of decorators matter in the exact case above and for #app.route() and other decorators in general (which is the same answer, I guess)? What confuses me, is that #app.route() just adds url rule to the app (i.e. self.add_url_rule(rule, endpoint, f, **options) and returns the function, that's it, so why would order matter?
Does #app.route() overrides all the decorators above it (how if so)?
I am also aware, that decorators application order is from bottom to top, though it doesn't make things any clearer, for me. What am I missing?
You have almost explained it yourself! :-) app.route does
self.add_url_rule(rule, endpoint, f, **options)
But the key is that f here is whatever function was decorated. If you apply app.route first, it adds a URL rule for the original function (without the login decorator). The login decorator wraps the function, but app.route has already stored the original unwrapped version, so the wrapping has no effect.
It may help to envision "unrolling" the decorators. Imagine you did it like this:
# plain function
def index():
return "Hello!"
login_wrapped = login_required(index) # login decorator
both_wrapped = app.route('/')(login_wrapped) # route decorator
This is the "right" way where the login wrap happens first and then the route. In this version, the function that app.route sees is already wrapped with the login wrapper. The wrong way is:
# plain function
def index():
return "Hello!"
route_wrapped = app.route('/')(index) # route decorator
both_wrapped = login_wrapped(route_wrapped) # login decorator
Here you can see that what app.route sees is only the plain unwrapped version. The fact that the function is later wrapped with the login decorator has no effect, because by that time the route decorator has already finished.
I'm doing a blog for my self from scratch and everything is working, even the sessions.
Now I'm trying to limit the admin with a decorator called #require_login. I think I'm doing something really wrong, besides the fact that is not working.
Here is my decorator:
def requirir_login(func):
request = make_response()
if session['logged_in'] == True:
return func
else:
print("no hay sesion registrada webon")
and here is it used, decorating the admin function:
#app.route("/admin")
#requirir_login
def admin():
users = User.objects
return render_template("admin.html", users=users)
My logic behind this is to check if there is a session then return the admin function. If not I wanted to check in the terminal that message for test purposes.
I haven't decided what to do if there is not a session yet. I would possibly redirect to the log-in page or something.
Your decorator needs to provide a wrapper function, which will be called in place of the decorated function. Only when that wrapper is being called is an actual request being routed and can you test the session:
from functools import wraps
def requirir_login(func):
#wraps(func)
def wrapper(*args, **kwargs):
if session['logged_in']:
return func(*args, **kwargs)
else:
print("no hay sesion registrada webon")
return wrapper
When a decorator is applied, it is called an its return value replaces the decorated function. Here wrapper is returned, so that now becomes your view function.
The wrapper passes on all arguments untouched, making your decorator suitable for any view function regardless of the arguments they expect to be passed in from the route.
I also made a few other changes to improve the functionality of your decorator:
You don't need to test for == True; that is what if is for, to test if the result of an expression is true or not.
I used the #functools.wraps() decorator to give your wrapper the same name and documentation string as the original wrapped view function, always helpful when debugging.
You could indeed use a redirect to the login form if you have one:
return redirect(url_for('login'))
if your login view is named login.
I have a bunch of decorators in my Flask routes that I am trying to condense into one (including #app.route).
I have the following #route function:
from functools import wraps
def route(route, method):
def decorator(f):
print 'decorator defined'
print 'defining route'
app.add_url_rule(route, methods=method, view_func=f)
print 'route defined'
#wraps(f)
def wrapper(*args, **kwargs):
print 'Hello'
# do stuff here such as authenticate, authorise, check request json/arguments etc.
# these will get passed along with the route and method arguments above.
return f(*args, **kwargs)
return wrapper
return decorator
and a sample route:
#route('/status', ['GET'])
def status():
return Response('hi', content_type='text/plain')
The route is getting defined, but wrapper() never gets called, which is really odd. When I move app.add_url_rule outside of the decorator to the end of the file, then wrapper() gets called; so decorator defined statement prints on Flask startup, and Hello prints when I hit the GET /status route as expected.
However, when I put app.add_url_rule back into the decorator as shown above, decorator defined prints on startup but when I call GET /status, it does not print Hello as if app.add_url_rule overrides the wrapper() function that I defined somehow.
Why does this happen? It looks like app.add_url_route hijacks my function in some odd/unexpected way.
How can I get wrapper() to be called once the route is hit, while defining app.add_url_rule in the decorator?
You registered the original function, not the wrapper, with Flask. Whenever the route matches, Flask calls f, not wrapper, because that's what you registered for the route.
Tell Flask to call wrapper when the route matches, instead:
def route(route, method):
def decorator(f):
print 'decorator defined'
print 'defining route'
#wraps(f)
def wrapper(*args, **kwargs):
print 'Hello'
# do stuff here such as authenticate, authorise, check request json/arguments etc.
# these will get passed along with the route and method arguments above.
return f(*args, **kwargs)
app.add_url_rule(route, methods=method, view_func=wrapper)
print 'route defined'
return wrapper
return decorator
This has been driving me crazy because it should be so simple, but there must be some Python quirk I'm missing. I have a decorator that I'm trying to apply to a Flask route, but for some reason none of the decorators in my views.py seem to be getting loaded.
decorators.py
def admin_required(func):
"""Require App Engine admin credentials."""
#wraps(func)
def decorated_view(*args, **kwargs):
if users.get_current_user():
if not users.is_current_user_admin():
abort(401) # Unauthorized
return func(*args, **kwargs)
return redirect(users.create_login_url(request.url))
return decorated_view
views.py
#admin_required
#blueprint.route('/')
def index():
return render_template('index.html')
The admin_required decorator function is not being called (index.html is loaded without a redirect), and I cannot figure out why.
Short answer: change the order of the decorators; blueprint.route only "sees" your undecorated function.
Decorators are applied inside-out, in loose analogy to function calls. Thus your function definition is equivalent to:
def index():
return render_template('index.html')
index = blueprint.route('/')(index)
index = admin_required(index)
Note how blueprint.route is passed the index function before it gets wrapped by admin_required. Of course, admin_required does eventually get applied to the index name in the module, so if you were to call index directly, it would go through both decorators. But you're not calling it directly, you're telling flask's request processor to call it.
I am creating a Python Flask app and created the decorator and views below. The decorator works great when viewing the index, but when you logout and it redirects using the url_for index it throws a builderror. Why would
def logged_in(fn):
def decorator():
if 'email' in session:
return fn()
else:
return render_template('not-allowed.html', page="index")
return decorator
#app.route('/')
#logged_in
def index():
email = session['email']
return render_template('index.html', auth=True, page="index", marks=marks)
#app.route('/sign-out')
def sign_out():
session.pop('email')
print(url_for('index'))
return redirect(url_for('index'))
Any ideas? The error is: BuildError: ('index', {}, None)
The problem here is that decorator() function which you return has different name than the function it is decorating, so the URL builder can't find your index view. You need to use wraps() decorator from functools module to copy the name of the original function. Another problem (which you still have to encounter) is that you don't accept the arguments in your decorator and pass it to the original function. Here's is the corrected decorator:
from functools import wraps
def logged_in(fn):
#wraps(fn)
def decorator(*args, **kwargs):
if 'email' in session:
return fn(*args, **kwargs)
else:
# IMO it's nicer to abort here and handle it in errorhandler.
abort(401)
return decorator
A bit more explanations: in Python decorator is a function which takes another function as its argument and returns a function as its result. So the following
#logged_in
def index(): pass
is essentially identical to
def index(): pass
index = logged_in(index)
The problem in this case was that what your logged_in decorator returns is not the original function, but a wrapper (called decorator in your code), which wraps the original function. This wrapper has a different name (decorator) than the original function it is wrapping. Now app.route() decorator, which you call after logged_in, sees this new function and uses its name (decorator) to register a route for it. Here lies the problem: you want the decorated function to have the same name (index), so it could be used in url_for() to get a route for it. That's why you need to copy the name manually
decorator.__name__ = fn.__name__
or better use update_wrapper and wraps helpers from functools module, which do that and even more for you.