Search code examples
pythonflaskdecoratorpython-decorators

View function mapping is overwriting an existing endpoint function when using a decorator


I am using the decorator below to authenticate endpoints in my application.

from google.appengine.api import users
from flask import redirect, render_template, request
from google.appengine.ext import ndb


def authenticate_admin(func):
    def authenticate_and_call(*args, **kwargs):
        user = users.get_current_user()
        if user is None:
            return redirect(users.create_login_url(request.url))
        else:
            email = user.email()
            register_user_if_required(email, user)
            if not users.is_current_user_admin():
                return redirect_to_unauthorized(email)
            return func(*args, **kwargs)

    def redirect_to_unauthorized(email):
        return render_template('xxxx/vvvv.html',
                               email=email,
                               users=users)

    return authenticate_and_call


def register_user_if_required(email, user):

I have the following endpoint which only allows administrators to access it.

@admin_routes.route('/xxxx')
@authenticate_admin
def xxxxx():
    return render_template('xxxx/xxxxx.html',
                           user=user,
                           logout=users.create_logout_url('/'))

And it works in the sense that only administrator's can access the above endpoint. However when I try to add a new endpoint with the same annotation but a different fancy url I get an error. Here is the code for the endpoint.

@admin_routes.route('/xxxx/bbbbbb')
@authenticate_admin
def abc():
    .....
    return render_template('xxxx/xxxx/zzzzz.html',
                           user=user,
                           breadcrumb=breadcrumb)

And here is the error I get when I run my app.

Traceback (most recent call last):
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/runtime/wsgi.py", line 240, in Handle
    handler = _config_handle.add_wsgi_middleware(self._LoadHandler())
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/runtime/wsgi.py", line 299, in _LoadHandler
    handler, path, err = LoadObject(self._handler)
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/runtime/wsgi.py", line 85, in LoadObject
    obj = __import__(path[0])
  File "/Users/vinay/App-Engine/xxxxx/main.py", line 61, in <module>
    app.register_blueprint(admin_routes)
  File "/Users/vinay/App-Engine/xxxxx/server/lib/flask/app.py", line 62, in wrapper_func
    return f(self, *args, **kwargs)
  File "/Users/vinay/App-Engine/xxxxx/server/lib/flask/app.py", line 889, in register_blueprint
    blueprint.register(self, options, first_registration)
  File "/Users/vinay/App-Engine/xxxxx/server/lib/flask/blueprints.py", line 153, in register
    deferred(state)
  File "/Users/vinay/App-Engine/xxxxx/server/lib/flask/blueprints.py", line 172, in <lambda>
    s.add_url_rule(rule, endpoint, view_func, **options))
  File "/Users/vinay/App-Engine/xxxxx/server/lib/flask/blueprints.py", line 76, in add_url_rule
    view_func, defaults=defaults, **options)
  File "/Users/vinay/App-Engine/xxxxx/server/lib/flask/app.py", line 62, in wrapper_func
    return f(self, *args, **kwargs)
  File "/Users/vinay/App-Engine/xxxxx/server/lib/flask/app.py", line 984, in add_url_rule
    'existing endpoint function: %s' % endpoint)
AssertionError: View function mapping is overwriting an existing endpoint function: admin_routes.authenticate_and_call
Traceback (most recent call last):
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ereporter/ereporter.py", line 240, in emit
    record.exc_info)
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/api/datastore.py", line 2520, in RunInTransactionCustomRetries
    return RunInTransactionOptions(options, function, *args, **kwargs)
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/api/datastore.py", line 2630, in RunInTransactionOptions
    ok, result = _DoOneTry(function, args, kwargs)
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/api/datastore.py", line 2650, in _DoOneTry
    result = function(*args, **kwargs)
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ereporter/ereporter.py", line 270, in __EmitTx
    handler=self.__RelativePath(os.environ['PATH_TRANSLATED']))
  File "/Users/vinay/Desktop/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/runtime/request_environment.py", line 113, in __getitem__
    return self._request.environ[key]
KeyError: 'PATH_TRANSLATED'
Logged from file wsgi.py, line 263
INFO     2015-08-09 03:19:14,731 module.py:812] default: "GET / HTTP/1.1" 500 -

Solution

  • You need to ensure your decorator wrapper has the same name as the wrapped view function, otherwise all your views look like the same endpoint (authenticate_and_call).

    You can do so with the @functool.wraps() utility:

    from functools import wraps
    
    def authenticate_admin(func):
        @wraps(func)
        def authenticate_and_call(*args, **kwargs):
            user = users.get_current_user()
            if user is None:
                return redirect(users.create_login_url(request.url))
            else:
                email = user.email()
                register_user_if_required(email, user)
                if not users.is_current_user_admin():
                    return redirect_to_unauthorized(email)
                return func(*args, **kwargs)
    
        def redirect_to_unauthorized(email):
            return render_template('Admin/UnauthorizedAdmin.html',
                                   email=email,
                                   users=users)
    
        return authenticate_and_call
    

    This ensures that metadata such as the function name is copied over from func to the authenticate_and_call wrapper. From there on out @Flask.route() can pick up that name to use as the endpoint name.