5

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?

1 Answers1

11

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.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • 3
    Thanks for such a comprehensive explanation. The funny thing is: right after posting the question — I went though [this article](https://ains.co/blog/things-which-arent-magic-flask-part-1.html), which basically explains how to write your own `@app.route()` (_and thus, how `@app.route()` works in Flask_), and understood it myself =). –  Nov 24 '17 at 09:48