NFicano
NFicano

Reputation: 1075

Flask hit decorator before before_request signal fires

I'm using Flask and using the before_request decorator to send information about requests to an analytics system. I'm now trying to create a decorator that would prevent sending these events on a few routes.

The problem I'm running into is getting my decorator to get called before the before_request signal gets fired.

def exclude_from_analytics(func):

    @wraps(func)
    def wrapped(*args, **kwargs):
        print "Before decorated function"
        return func(*args, exclude_from_analytics=True, **kwargs)

    return wrapped

# ------------------------

@exclude_from_analytics
@app.route('/')
def index():
    return make_response('..')

# ------------------------

@app.before_request
def analytics_view(*args, **kwargs):
    if 'exclude_from_analytics' in kwargs and kwargs['exclude_from_analytics'] is True:
       return

Upvotes: 15

Views: 10555

Answers (2)

klenwell
klenwell

Reputation: 7148

Here's a variation on @Mark Hildreth's answer that does wrap and return a function:

from functools import wraps
from flask import Flask, request, g

app = Flask(__name__)

def exclude_from_analytics(*args, **kw):
    def wrapper(endpoint_method):
        endpoint_method._skip_analytics = True

        @wraps(endpoint_method)
        def wrapped(*endpoint_args, **endpoint_kw):
            # This is what I want I want to do. Will not work.
            #g.skip_analytics = getattr(endpoint_method, '_skip_analytics', False)
            return endpoint_method(*endpoint_args, **endpoint_kw)
        return wrapped
    return wrapper

@app.route('/')
def no_skip():
    return 'Skip analytics? %s' % (g.skip_analytics)

@app.route('/skip')
@exclude_from_analytics()
def skip():
    return 'Skip analytics? %s' % (g.skip_analytics)

@app.before_request
def analytics_view(*args, **kwargs):
    if request.endpoint in app.view_functions:
        view_func = app.view_functions[request.endpoint]
        g.skip_analytics = hasattr(view_func, '_skip_analytics')
        print 'Should skip analytics on {0}: {1}'.format(request.path, g.skip_analytics)

app.run(debug=True)

The reason why it does not work quite as simply as I expected and hoped has to something do with the Flask context stack and the order in which callbacks are applied. Here is a timeline of method calls (based on some debug statements since removed):

$ python test-flask-app.py
# Application Launched
DECORATOR exclude_from_analytics
DECORATOR wrapper
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

# REQUEST: /
DECORATOR app.before_request: analytics_view
> Should skip analytics on /: False
ENDPOINT no_skip
127.0.0.1 - - [14/May/2016 16:10:39] "GET / HTTP/1.1" 200 -

# REQUEST: /skip
DECORATOR app.before_request: analytics_view
> Should skip analytics on /skip: True
DECORATOR wrapped
ENDPOINT skip
127.0.0.1 - - [14/May/2016 16:12:46] "GET /skip HTTP/1.1" 200 -

I would prefer to set g.skip_analytics from within the wrapped function. But because that is not called until after the analytics_view @app.before_request hook, I had to follow Mark's example and set the _skip_analytics attr on the endpoint method loaded in what I'm calling the application (as opposed to request) context which gets invoked only at launch.

For more on flask.g and app context, see this StackOverflow answer.

Upvotes: 4

Mark Hildreth
Mark Hildreth

Reputation: 43071

You can use the decorator to simply put an attribute on the function (in my example below, I'm using _exclude_from_analytics as the attribute). I find the view function using a combination of request.endpoint and app.view_functions.

If the attribute is not found on the endpoint, you can ignore analytics.

from flask import Flask, request

app = Flask(__name__)

def exclude_from_analytics(func):
    func._exclude_from_analytics = True
    return func

@app.route('/a')
@exclude_from_analytics
def a():
    return 'a'

@app.route('/b')
def b():
    return 'b'

@app.before_request
def analytics_view(*args, **kwargs):
    # Default this to whatever you'd like.
    run_analytics = True

    # You can handle 404s differently here if you'd like.
    if request.endpoint in app.view_functions:
        view_func = app.view_functions[request.endpoint]
        run_analytics = not hasattr(view_func, '_exclude_from_analytics')

    print 'Should run analytics on {0}: {1}'.format(request.path, run_analytics)

app.run(debug=True)

The output (ignoring static files...)

Should run analytics on /a: False
127.0.0.1 - - [24/Oct/2013 15:55:15] "GET /a HTTP/1.1" 200 -
Should run analytics on /b: True
127.0.0.1 - - [24/Oct/2013 15:55:18] "GET /b HTTP/1.1" 200 -

I have not tested to see if this works with blueprints. Additionally, a decorator that wraps and returns a NEW function could cause this to not work since the attribute might be hidden.

Upvotes: 22

Related Questions