user2268507
user2268507

Reputation:

Python decorator - func_wrapper() takes no arguments (1 given)

I am trying my hand at writing my first decorator but am a little lost. I would like this decorator to check that a request came from a particular address before executing the main function.

At the moment I have:

def check_referrer(url):
    def func_wrapper():
        if request.referrer == url:
            return render_template('index.html', error=None)
        else:
            return render_template('login.html', error="some_error")
    return func_wrapper

@app.route('/index', methods = ['GET'])
@check_referrer("/venue/login")
def index():
    return

@app.route /venue/login (this code has been simplified)

@app.route('/venue/login', methods=['GET', 'POST'])
def login():

    error = None

    if login_valid():
        return redirect(url_for('index'))                                                                                   
    else:
        error = 'Invalid Credentials. Please try again.'

    return render_template('login.html', error=error)

1) I am sure there are a few issues with what I am doing but I first need to understand why I am getting the error:

TypeError: func_wrapper() takes no arguments (1 given)

I thought I was only passing an argument to check_referrer.

2) Are my return statements correct?

Any help would be greatly appreciated!

Upvotes: 1

Views: 2979

Answers (2)

NeoWang
NeoWang

Reputation: 18513

The key to understanding python decorator is this:

@decorator
def func():
    pass

is equivalent with:

func = decorator(func)

Now you can understand why your code does not work:@check_referrer("/venue/login") returns a function func_wrapper that takes no argument, so func_wrapper can not be a decorator.

You can define a decorator that take no arguments with 2 levels of inner functions. To make a decorator that takes argument(s), you need another level of inner function, as davidism's code shows.

Here is what happens:

decorator = check_referrer(url) 
decorated = decorator(index) 
index = decorated

Upvotes: 0

davidism
davidism

Reputation: 127210

Consider using Flask-Login instead to handle authentication and redirect if the user is not logged in. (Or closely examine how it works.) It handles this much more robustly than the function you've written.


The function you've written is not a decorator, and has many other issues. First, it needs to be properly structured. It needs 3 layers:

  1. Layer 1 function takes the the url parameter and produces a decorator.
  2. Layer 2 function decorates a function and returns a wrapped function.
  3. Layer 3 is the decorated view.

request.referrer contains the entire url, not just the path matching a route. Use urlparse to get the path. There is no guarantee that a client's browser will send a referrer, or the correct referrer, so you should not rely on this value.

The decorated function needs to accept arbitrary arguments that may be passed to the view. The decorated function should properly wrap the original function by using wraps.

It is not a good idea to just render different templates from the same view. Rather than rendering index.html or login.html, you should be redirecting to the relevant views. If you need to pass messages along with the redirect, put them in the session.

from functools import wraps
from urllib.parse import urlparse
# or from urlparse import urlparse for py2
from flask import request, session, redirect

def check_referrer(path):
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            if not request.referrer:
                session['error'] = 'no referrer'
                return redirect('login')

            referrer_path = urlparse(request.referrer).path

            if referrer_path != path:
                session['error'] = 'expected referrer {!r}'.format(path)
                return redirect('login')

             return f(*args, **kwargs)

        return decorated

    return decorator

Upvotes: 2

Related Questions