1

I have a class that handles the API calls to a server. Certain methods within the class require the user to be logged in. Since it is possible for the session to run out, I need some functionality that re-logins the user once the session timed out. My idea was to use a decorator. If I try it like this

class Outer_Class():
    class login_required():
        def __init__(self, decorated_func):
            self.decorated_func = decorated_func

        def __call__(self, *args, **kwargs):
            try:
                response = self.decorated_func(*args, **kwargs)
            except:
                print('Session probably timed out. Logging in again ...')
                args[0]._login()
                response = self.decorated_func(*args, **kwargs)
            return response

    def __init__(self):
        self.logged_in = False
        self.url = 'something'
        self._login()

    def _login(self):
        print(f'Logging in on {self.url}!')
        self.logged_in = True

    #this method requires the user to be logged in
    @login_required
    def do_something(self, param_1):
        print('Doing something important with param_1')
        if (): #..this fails
            raise Exception()

I get an error. AttributeError: 'str' object has no attribute '_login' Why do I not get a reference to the Outer_Class-instance handed over via *args? Is there another way to get a reference to the instance?

Found this answer How to get instance given a method of the instance? , but the decorated_function doesn't seem to have a reference to it's own instance.

It works fine, when Im using a decorator function outside of the class. This solves the problem, but I like to know, if it is possible to solve the this way.

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
IgNoRaNt23
  • 68
  • 6
  • 1
    Any particular reason you made your decorator a class instead of a function? You wouldn't have to implement the descriptor protocol manually if you used a function, and a lot of other stuff would work out nicer too. – user2357112 Nov 18 '19 at 08:10
  • Count your arguments. I would bet a coin (not more without seeing the code that raises the error...) that at the time of the call in the decorated function, `args[0]` is `param1` and not `self`. Just use a print to make sure... – Serge Ballesta Nov 18 '19 at 08:15
  • `if ()` is always false... – Mad Physicist Nov 18 '19 at 08:36
  • @SergeBallesta yes, args only holds param_1 and not self. But I already mentioned that in the description (at least that the instance reference is not part of args) – IgNoRaNt23 Nov 18 '19 at 08:57
  • @user2357112supportsMonica So this works, if the decorator function just looks like an outside function with this interface `def login_required(decorated_func):`. Since this is not a @staticmethod one would expect the first argument to be self, but it points to the decorated function. At least args holds the reference to the instance – IgNoRaNt23 Nov 18 '19 at 09:30
  • @IgNoRaNt23 you've implemented a custom type, why do you expect it to work like a method? – juanpa.arrivillaga Nov 18 '19 at 09:34
  • @juanpa.arrivillaga Good question. All the solutions I got so far are confusing to me (and probably my colleagues) or have other shortcomings. For example it doesnt seem right to have a function outside of the class, which calls a method from within, but its the one with the cleanest interfaces. Once I try to put it inside the class the interfaces become really akward. Would have to have a solution that ties the function to the class and is also intuitive. – IgNoRaNt23 Nov 18 '19 at 10:04

2 Answers2

1

The problem is that the magic of passing the object as the first hidden parameter only works for a non static method. As your decorator returns a custom callable object which is not a function, it never receives the calling object which is just lost in the call. So when you try to call the decorated function, you only pass it param_1 in the position of self. You get a first exception do_something() missing 1 required positional argument: 'param_1', fall into the except block and get your error.

You can still tie the decorator to the class, but it must be a function to have self magic work:

class Outer_Class():
    def login_required(decorated_func):
        def inner(self, *args, **kwargs):
            print("decorated called")
            try:
                response = decorated_func(self, *args, **kwargs)
            except:
                print('Session probably timed out. Logging in again ...')
                self._login()
                response = decorated_func(self, *args, **kwargs)
            return response
        return inner
    ...
    #this method requires the user to be logged in
    @login_required
    def do_something(self, param_1):
        print('Doing something important with param_1', param_1)
        if (False): #..this fails
            raise Exception()

You can then successfully do:

>>> a = Outer_Class()
Logging in on something!
>>> a.do_something("foo")
decorated called
Doing something important with param_1
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Thanks for the response. It's working fine and made my code a bit more readable. However, I'm still missing, why `login_required` is (or looks like) a static method here. And even worse, if I use the `@staticmethod`-decorator on `login_required`, the code breaks. Also for the unchanged code above, PyCharm shows the warning, that `'Outer_Class' object is not callable`. I know that's just the stupid IDE, but some of my coworkers will point this out and I want to at least be able to explain it. – IgNoRaNt23 Nov 18 '19 at 13:55
  • The decorator function is called at class build time, as `func(x)` and not as `obj.func(x)`. So it cannot receive a `self` object. But I would be reluctant to use that in production code without comments in red flashing font, because it uses a method from a class before the class is fully constructed. Unsure whether there is a risk that it breaks in a future release of Python, nor whether it will work on other Python flavors like Jython or even Cython. – Serge Ballesta Nov 18 '19 at 14:08
  • Ok, got it, thank you. What are my alternatives? A decorator seemed to be the best way to apply this functionality to multiple functions of this class. – IgNoRaNt23 Nov 18 '19 at 14:25
  • If you do not intend to use on other Python versions than the good C-Python, and you add a strong comment on it, nothing is really bad : it works and gives a nice encapsulation. Furthermore, if it is was to break in a future version, it would be enough to make the decorator a plain function outside of the class. From there on, you are on your own :-) – Serge Ballesta Nov 18 '19 at 15:07
  • @IgNoRaNt23 fundamentally, there is *nothing* wrong or atypical about a helper function in the same module as your class which you use in that class. Just mark it as part of the non-public API be calling it `def _login_required` and stop trying to work againt the language. You *can* use a custom callable object, but then you must re-implement the descriptor protocol as it exists for functions if you want it to work like a function, which is over-engineering it. The simplest thing is just to put the function outside your class. A nested class is *much* stranger than that in Python. – juanpa.arrivillaga Nov 18 '19 at 20:56
  • @IgNoRaNt23 fundamentally, in this case, it isn't acting as a method at all, but as a normal function, that just happens to be in the class namespace (which seems to be the main issue for you). I would consider this strange and unidiomatic, much more so than the solution you already had of simply defining the function outside the class. That, or implement the descriptor protocol for your nested class, but that is maybe just as strange, and very much over-engineered in my opinion. – juanpa.arrivillaga Nov 18 '19 at 20:58
0

You have the command of

args[0]._login()

in the except. Since args[0] is a string and it doesn't have a _login method, you get the error message mentioned in the question.

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
  • Yes, that is perfectly clear to me. The question is, why 'self' from the _login-interface is not part of args, like it is, when I use a decorator function from outside the class. – IgNoRaNt23 Nov 18 '19 at 08:53
  • @IgNoRaNt23 I think the reason might be that you are calling a method of the same instance. – Lajos Arpad Nov 18 '19 at 08:56