14

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
NFicano
  • 1,065
  • 1
  • 11
  • 26
  • 1
    Not a duplicate, but might give you some ideas: http://stackoverflow.com/questions/14367991/flask-before-request-add-exception-for-specific-route – Mark Hildreth Oct 24 '13 at 19:28
  • That's what we are currently doing but as the number of routes and codebase grows, its definitely not a scalable solution. – NFicano Oct 24 '13 at 19:38
  • My thought was to have the "exclude_from_analytics" decorator put an attribute on the view function itself, then use the Flask API to get the view function from the endpoint and check the attribute. – Mark Hildreth Oct 24 '13 at 19:45
  • FYI, `app.route()` **must** be the topmost decorator. Otherwise your wrapper function is never used. Right now you have this: `exclude_from_analytics(app.route(index))` - as you can see, the original function is passed to `app.route()` – ThiefMaster Oct 24 '13 at 20:11

2 Answers2

21

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.

Mark Hildreth
  • 42,023
  • 11
  • 120
  • 109
3

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.

Community
  • 1
  • 1
klenwell
  • 6,978
  • 4
  • 45
  • 84