How do function decorators work and how to use them for Flask routes
How do you limit routes so they can be accessed if and only if certain condition are met? For example, limiting a route for users who are both logged in and have the admin
role:
@login_required
@roles_required(["admin"])
def admin():
return render_template("admin.html")
1. What are decorators?
Python decorators are syntactic sugar for functions that return functions. Let’s take a look at a simple example of what this means.
def my_function():
print("Inside my_function")
def decorator_func(func_to_decorate):
def wrapper_func():
print("Runs before func_to_decorate")
func_to_decorate()
print("Runs after func_to_decorate")
return wrapper_func
my_function_decorated = decorator_func(my_function)
my_function_decorated()
# Output:
# "Runs before func_to_decorate"
# "Inside my_function"
# "Runs after func_to_decorate"
We can see that the decorator_func
returns the function wrapper_func
, which simply calls the function that we pass into decorator_func
. Now let’s do the same thing but using the decorator syntax.
@decorator_func
def another_function():
print("Inside another_function")
another_function()
# Outputs:
# "Runs before func_to_decorate"
# "Inside another_function"
# "Runs after func_to_decorate"
So we can see that @decorator
is just a shortcut for my_function_decorated = decorator_func(my_function)
2. How to create a login_required decorator?
Now let’s look at how to create a login_required
decorator for our routes. The flask documentation has a good example for it.
from functools import wraps
from flask import g, request, redirect, url_for
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
Used like so:
@login_required
def login_route():
# ...
It looks similar to our decorator_func
above, except that it does a check first to see if the g
object has a user, and if it doesn’t, redirects the person to the login
page. If the user does exist, then it returns the f
function, passing in all args
and kwargs
.
3. Why should we use @wraps?
The other noticeable difference is @wraps
, imported from functools. The main purpose of using @wraps
is so that the metadata of the returned wrapper function will point to the decorated function. An example will make this clear:
from functools import wraps
def decorator_func(func_to_decorate):
@wraps(func_to_decorate)
def wrapper_func():
func_to_decorate()
return wrapper_func
def decorator_func_without_wraps(func_to_decorate):
def wrapper_func():
func_to_decorate()
return wrapper_func
@decorator_func
def my_function():
print("Inside my_function")
@decorator_func_without_wraps
def another_function():
print("Inside another_function")
my_function.__name__
# 'my_function'
another_function.__name__
# 'wrapper_func'
Here we have two decorators, one which uses @wraps
, and another which doesn’t. When we call __name__
on the decorated function, we see that the one that uses @wraps
correctly returns the name of the decorated function. But the other decorated function returns the __name__
of the wrapper function, which is not useful. That’s why we use @wraps
.
4. How can we create a decorator that accepts arguments?
Next, we want to look at how we can create a decorator function that accepts arguments like @roles_required(["admin"])
.
from functools import wraps
from flask_login import current_user
def roles_required(roles):
def decorator_function(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not set(roles).issubset({r.name for r in current_user.roles}):
return { error: 'Not an admin' }
return f(*args, **kwargs)
return wrapper
return decorator_function
It looks similar except we wrap the decorator function inside roles_required
, so that it can accept arguments.
roles_required
takes in theroles
argument and when it’s called it returnsdecorator_function
decorator_function
returnswrapper
functionwrapper
function returnsf
, which is the decorated function
5. Using multiple decorators together
Let’s go back to our first example:
@login_required
@roles_required(["admin"])
def admin():
return render_template("admin.html")
Now we understand that linking two decorators together is just the same as:
login_required(roles_required(["admin"])(admin))
The general formula for creating a decorator function is:
- create the decorator function that accepts your_function
- create a wrapper function inside the decorator function (and use
@wraps
to decorate the wrapper function) - have the wrapper function return your_function with all the arguments passed in
- have the decorator function return the wrapper function
Acknowledgement: Some code snippets from a great answer on SOF