Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Flask's url_for throw an error when using a decorator on that item in Python?

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)

like image 847
chromedude Avatar asked Jan 01 '13 21:01

chromedude


1 Answers

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.

like image 169
Audrius Kažukauskas Avatar answered Oct 22 '22 07:10

Audrius Kažukauskas