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).
RandomGuy
  • 648
  • 6
  • 21
  • why is `LOGIN_REQUIRED` defined inside `MyClass`? It should just be defined at the module level. Same with `ADMIN_REQUIRED` – juanpa.arrivillaga Aug 16 '23 at 19:41
  • @juanpa.arrivillaga I defined it inside class because it accesses specific members of the class instance (so basically it belongs to the class logically). I searched before about this and it seems possible to define decorators as part of classes, but maybe I am missing something? – RandomGuy Aug 17 '23 at 08:22
  • 1
    It's *possible* But you aren't using them as methods. This is unexpected. Because functions inside of class namespaces get treated as methods, although, you can just use them in the class body, but you won't be able to reference other functions from the namespace. But see the edit to my answer for how it is possible anyway (again, just very unusual). – juanpa.arrivillaga Aug 17 '23 at 08:47

3 Answers3

3

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.

micromoses
  • 6,747
  • 2
  • 20
  • 29
  • Thank you very much for your answer I can confirm that this works! :) The name mangling is not a problem for me, I can change the function name, I was just using double underscore because it was a method supposedly only accessible by the class itself (private). – RandomGuy Aug 17 '23 at 09:09
  • Also as note there is not inheritance in "MyClass". – RandomGuy Aug 17 '23 at 09:20
  • I really like this approach because later I can add easily new decorators for checking for other roles if necessary (such as `@MODERATOR_REQUIRED`, `@STAFF_REQUIRED` etc, those are just examples) that use the `@LOGIN_REQUIRED` as initial validation. – RandomGuy Aug 17 '23 at 09:59
1

The sanest way to do this is to keep both decorators seperate and apply them both:

from functools import wraps

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
        print("checking login")
        self._MyClass__check_for_valid_token(args[0], args[1])
        return func(self, *args, **kwargs)
    return decorated_function
    
def ADMIN_REQUIRED(func):
    @wraps(func)
    def decorated_function(self, *args, **kwargs):
        print("checking if admin")
        is_admin = self.check_if_admin()

        if not is_admin:
            raise Exception()

        return func(self, *args, **kwargs)
    return decorated_function


class MyClass:
    def __check_for_valid_token(self, username, token):
        return True
    def check_if_admin(self):
        return True
    @LOGIN_REQUIRED
    @ADMIN_REQUIRED
    def get_administration_data(self, username, token):
        # return important_data
        return 42

Note, the decorators are defined outside of the class, because they are not methods. They should have never been defined inside the class to begin with. Also note, you seem to want to use double-underscore name-mangling, so you are going to have to manually use _MyClass__check_for_valid_token instead of relying on the compile-time transformation of __check_for_valid_token. Probably, it is even better to simply not use double-underscore name mangling and use a single underscore.

If you really want something that applies both, then create a third, helper decorator:

def LOGIN_AND_ADMIN_REQUIRED(func):
    return LOGIN_REQUIRED(ADMIN_REQUIRED(func))


class MyClass:        
    @LOGIN_AND_ADMIN_REQUIRED
    def get_administration_data(self, username, token):
        # return important_data
        ...

But if you insist on making ADMIN_REQUIRED do both, then you can just:

def ADMIN_REQUIRED(func):
    # func = LOGIN_REQUIRED(func) here, ADMIN_REQUIRED would happen first
    @wraps(func)
    def decorated_function(self, *args, **kwargs):
        print("checking if admin")
        is_admin = self.check_if_admin()

        if not is_admin:
            raise Exception()

        return func(self, *args, **kwargs)

    return LOGIN_REQUIRED(decorated_function)

Note, if you really want to, you can nest the decorator definitions inside the class. This is ultimately a matter of style, since you are not prevented from doing it, but now you should really just use the explicit double decoration syntax, otherwise, you cannot refer to the other decorator because the class block doesn't create an enclosing scope:

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
            print("checking login")
            self.__check_for_valid_token(args[0], args[1])
            return func(self, *args, **kwargs)
        return decorated_function

    def ADMIN_REQUIRED(func):
        @wraps(func)
        def decorated_function(self, *args, **kwargs):
            print("checking if admin")
            is_admin = self.check_if_admin()

            if not is_admin:
                raise Exception()

            return func(self, *args, **kwargs)
        return decorated_function

    def __check_for_valid_token(self, username, token):
        return True

    def check_if_admin(self):
        return True

    @LOGIN_REQUIRED
    @ADMIN_REQUIRED
    def get_administration_data(self, username, token):
        # return important_data
        return 42

Note, if it is inside the class definition, you can just use __check_for_valid_token in LOGIN_REQUIRED.


juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • Interesting thank you very much for your answer. I actually tried this approach "But if you insist on making ADMIN_REQUIRED do both, then you can just:" as it is closely what I was looking for. Unfortunately there is a problem (well maybe not, but it doesn't work as I expected). The ADMIN_REQUIRED code is ran before LOGIN_REQUIRED at least according to my logs, I wanted the other way around. – RandomGuy Aug 17 '23 at 08:48
  • @RandomGuy see my edit, you can accomplish this with any of the approaches. – juanpa.arrivillaga Aug 17 '23 at 08:55
  • 1
    @RandomGuy I fixed them all up to go in that order, it's just an issue of when you apply the decorator. The decorator applied last is the decorator that runs first. – juanpa.arrivillaga Aug 17 '23 at 09:00
  • Oh I see, but then I must use the @LOGIN_AND_ADMIN_REQUIRED approach right? I can't use the "But if you insist on making ADMIN_REQUIRED do both" approach? The name mangling is not a problem for me, I can change the function name, I was just using double underscore because it was a method supposedly only accessible by the class itself (private). – RandomGuy Aug 17 '23 at 09:19
  • 1
    @RandomGuy you can't make ADMIN_REQUIRED do both and have both defined within the class without jumping through a lot of hoops, because you *can't refer to functions in the class namespace from other functions*. Note, this is why you have to use `self.some_other_method()` to call another instance method from within an instance method – juanpa.arrivillaga Aug 17 '23 at 09:51
  • Ok, thank you for your explanation, just one suggestion, make it more clear that `@ADMIN_REQUIRED` will be executed first in the "But if you insist on making ADMIN_REQUIRED do both" example (maybe with a statement as bold). I know that you added the comment but someone may miss it and implement it that way without realizing the order that the decorators are being called. – RandomGuy Aug 17 '23 at 10:04
0

If you want to decorate a decorator, you could add another wrapping layer to LOGIN_REQUIRED

class MyClass:
    def LOGIN_REQUIRED(dec):
        def decwrapper(func):
            wrapped = dec(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 wrapped(self, *args, **kwargs)
            return decorated_function
        return decwrapper

If you want to keep your decorator definitions as is, you have to move the @LOGIN_REQUIRED decoration from def ADMIN_REQUIRED to def get_administration_data()

@LOGIN_REQUIRED
@ADMIN_REQUIRED
    def get_administration_data(self, ar1, ar2):
       ...
Tzane
  • 2,752
  • 1
  • 10
  • 21
  • Thank you. Unfortunately none solve my issue exactly how I wanted (keeping the existing definition, without having to add 2 decorators explicitly on top of get_administration_data). I tried the first solution you proposed but that causes issues at the other functions that are already using the login decorator (TypeError: MyClass.LOGIN_REQUIRED..decwrapepr() takes 1 positional argument but 3 were given). So I assume the only solution is to explicitly use both decorators on the functions that require admin access, or add the check_for_valid_token function to the @ADMIN_REQUIRED directly? – RandomGuy Aug 16 '23 at 12:23
  • 1
    @RandomGuy Im not sure if there is another way to do it, but you could just add that one line of code from `@LOGIN_REQUIRED` to `@ADMIN_REQUIRED` as well. If you need more logic, just make it into a function that you call in both decorators. – Tzane Aug 16 '23 at 12:28
  • Thank you again. Yes that's what I did since self.__check_for_valid_token(args[0], args[1]) is what is common between both decorators. Other function, if more logic is needed like you suggested is also a good idea. I will wait more 1 or 2 days for other answers if nothing else shows up I will mark yours as the solution. :) – RandomGuy Aug 16 '23 at 12:47
  • 1
    @RandomGuy You should also check out the accepted answer in the other thread I linked, about making "decorator that applies decorators to decorators". Although, what you ended up doing is probably the least convoluted way of doing it :P – Tzane Aug 16 '23 at 13:03
  • Is this not what you are looking to do? https://stackoverflow.com/a/5952807/218663 – JonSG Aug 16 '23 at 13:55
  • @JonSG Unfortunately I couldn't get that to work. – RandomGuy Aug 16 '23 at 14:54
  • "If you want to decorate a decorator, you need to add another wrapping layer" no, you absolutely do not – juanpa.arrivillaga Aug 16 '23 at 19:59
  • @juanpa.arrivillaga Switched up the wording to be less assertive :) – Tzane Aug 16 '23 at 20:37
  • But this whole approach is just wrong.`wrapped = dec(func)` *calls the decorated function*, it doesn't retrieve the wrapped function, and then you `return decorated_function`. They are getting a `TypeErrror`, because the nesting is *making this not work*, because `MyClass().get_administration_data` is returning `decwrapper`. Since it is a bound method now, it can only be called like `MyClass().get_administration_data()`, but this will fail, because `decwrapper(func)` is being passed *self*, and anyway, `wrapped = dec(func)` is calling `MyClass.get_administration_data` – juanpa.arrivillaga Aug 16 '23 at 21:34
  • Like, not only is it wrong in this case, it is *wrong in general*. Nesting like this is to allow you to provide "arguments to teh decorator", e.g. `@some_deco(arg1, arg2)`. Most straightforward decorators (i.e. functions that define and return a wrapper function) **just work with nesting**. There is no magic required. It is just function functions that return functions. – juanpa.arrivillaga Aug 16 '23 at 21:36
  • If you look at that answer, it is assuming you are going to do: `@LOGIN_REQUIRED(ADMIN_REQUIRED)`, but that's **silly** since you don't need all that rigamarole, since you *can just apply both decorators on top of each other* – juanpa.arrivillaga Aug 16 '23 at 21:43