Search code examples
pythonpython-3.xdecoratorpython-decorators

Calling another member decorator from another member decorator in Python 3


I am trying to re-use a member function decorator for other member function decorator but I am getting the following error:

'function' object has no attribute '_MyClass__check_for_valid_token'

Basically I have a working decorator that checks if a user is logged in (@LOGIN_REQUIRED) and I would like to call this first in the @ADMIN_REQUIRED decorator (so the idea is to check that the user is logged in with the existing @LOGIN_REQUIRED decorator and then add some specific validation to check if the logged user is an Administrator in the @ADMIN_REQUIRED decorator.

My current code is like this:

class MyClass:
    def LOGIN_REQUIRED(func):
            @wraps(func)
            def decorated_function(self, *args, **kwargs):
                # username and token should be the first parameters
                # throws if not logged in
                self.__check_for_valid_token(args[0], args[1])
                return func(self, *args, **kwargs)
            return decorated_function
    
    @LOGIN_REQUIRED
    def ADMIN_REQUIRED(func):
        @wraps(func)
        def decorated_function(self, *args, **kwargs):
            is_admin = self.check_if_admin()

            if not is_admin:
                raise Exception()

            return func(self, *args, **kwargs)
        return decorated_function
        
    @ADMIN_REQUIRED
    def get_administration_data(self, username, token):
        # return important_data
        # currently throws 'function' object has no attribute '_MyClass__check_for_valid_token'

Do you have any idea how could I get this to work?


Some notes based on the comments and answers for clarification:

  1. The method __check_for_valid_token name can be changed to not run into name mangling issues. I was just using double underscore because it was a method supposedly only accessible by the class itself (private).
  2. There is no inheritance in "MyClass".
  3. The @LOGIN_REQUIRED code must run before the @ADMIN_REQUIRED code (as that is what someone expects, at least in my case).

Solution

  • I think this is possible, with two caveats; first, the decorator will have to move outside the class, and second, some adaptation will be required in regards to the name mangling. Let's tackle the first - first.

    Moving some functions

    Decorating a decorator directly may seem intuitive, but it probably won't result with what you want. You can, however, decorate an inner function - just like how @wraps is used. Due to how python parses code, the outer decorator will have to be defined outside (and before) the class, otherwise you'll get a NameError. The code should look something like this:

    def _OUTER_LOGIN_REQUIRED(func):
        @wraps(func)
        def decorated_function(self, *args, **kwargs):
            self.__check_for_valid_token(args[0], args[1])
                return func(self, *args, **kwargs)
            return decorated_function
        [Notice no code-changes to this function (yet)]
    
    class MyClass:
        # The following line will make the transition seamless for most methods
        LOGIN_REQUIRED = _OUTER_LOGIN_REQUIRED
    
        def ADMIN_REQUIRED(func):
            @wraps(func)
            @_OUTER_LOGIN_REQUIRED  # <-- Note that this is where it should be decorated
            def decorated_function(self, *args, **kwargs):
            ... [the rest of ADMIN_REQUIRED remains unchanged]
    
        @ADMIN_REQUIRED
        def get_administration_data(self, username, token):
        ... [this should now invoke LOGIN_REQUIRED -> ADMIN_REQUIRED -> the function]
    
        @LOGIN_REQUIRED
        def get_some_user_data(self, ...):
        ... [Such definitions should still work, as we added LOGIN_REQUIRED attribute to the class]
    

    If such a change is acceptable so-far, let's move on to

    Name Mangling

    As the name of __check_for_calid_token function is mangled (as its name starts with a dunder), you'll have to decide how to tackle it. There are two options:

    1. If there is no restriction, simply shorten the dunder to a single underscore (or rename as you like - as long as it doesn't start with more than one underscore).
    2. If the name mangling is important, you'll have to change the call in _OUTER_LOGIN_REQUIRED like so:
    def decorated_function(self, *args, **kwargs):
        self._MyClass__check_for_valid_token(args[0], args[1])
    

    This might affect inheritance, and should be tested thoroughly.

    Summary

    I've tested around this a bit on python 3.9, and it seems to work quite well. I noticed that login errors are raised before admin errors, as I assume is desired. Still, I only poked around in a shallow manner, and while I can't think of a reason for this to misbehave, I strongly recommend testing this thoroughly before committing to this method (especially if the code includes inheritance, which I didn't even touch).

    I hope this works for you, and if it doesn't - let us know where it breaks, and how.