patrick.mchugh4
patrick.mchugh4

Reputation: 85

Decorator Causing All Functions to Return True

In an attempt to debug an online permissions system I was having problems with, I've created a couple of classes and functions designed to replicate the issue offline. Unfortunately, I'm having a problem, in which turning a function into a decorator completely changes the results of a decorated function.

My code is as follows, I've kept it as basic as possible, to illustrate my point.

  1. Setting up User class:

    class User(object):
    
        def __init__(self, forename=None, surname=None, logged_in=True, exists=True, poop=False, admin=False):
            self.forename = forename
            self.surname = surname
            self.logged_in = logged_in
            self.exists = exists
            self.poop = poop
            self.admin = admin
    
        def __repr__(self):
            return f'User: {self.forename} {self.surname}.'
    
    user1 = User('Paddy', 'McHugh', True, True, False, True)
    user2 = User('Rodney', 'Donger', False, False, True, False)
    user3 = User('Bob', 'Dangler', True, True, True, True)
    
  2. Creating functions to test against a user:

    def user_just_is(user):
        return user.exists
    
    
    def user_is_poop(user):
        return user.poop
    
    
    def user_is_admin(user):
        return user.admin
    
  3. Testing those functions against the chosen user with a regular function:

    class Permissions2(object):
    
        def __init__(self):
            pass
    
        def requires(self, *args):
            user = user2
    
            if not user.logged_in:
                print('You\'re not logged in, please log in.')
                return
    
            if not all(i(user) for i in args):
                print('Not all of the conditions were met.')
            else:
                print('All of the conditions were met.')
    
    
    Permissions2().requires(user_just_is, user_is_poop, user_is_admin)
    
  4. Testing those functions against the chosen user with a decorator function:

    class Permissions(object):
    
        def __init__(self):
            pass
    
        def requires(self, *args):
            user = user2
    
            def decorator(func):
                @wraps(func)
                def allower(*args, **kwargs):
                    if not user.logged_in:
                        print('You\'re not logged in, please log in.')
                        return
    
    
                    if not all(i(user) for i in args):
                        print('Not all of the conditions were met.')
                        return
    
                    return func(*args, **kwargs)
    
                return allower
    
            return decorator
    
    
    @Permissions.requires(user_just_is, user_is_poop, user_is_admin)
    def print_stuff():
        print('All of the conditions were met.')
    
    print_stuff()
    

I'd expect the outcome of both the regular and decorator function to be the same. That if User.logged_in == False, then it would print: 'You're not logged in, please log in.'. That if all the Boolean variables were True, it would print: 'All of the conditions were met.'. That if any of the conditions were False, it would print: 'Not all of the conditions were met.'.

The decorator function still returns the 'You're not logged in, please log in', but if User.logged_if == True, then the other Booleans don't matter, it always returns True to the all() function and print 'All of the conditions were met.'.

What is it about putting it in a decorator that means all() seems to return True to all of the tested functions?

Upvotes: 1

Views: 73

Answers (1)

blhsing
blhsing

Reputation: 106618

The args parameter for your allower function shadows the args parameter of requires, so when you iterate over args here:

if not all(i(user) for i in args):

you are not iterating through the list of functions passed in to requires as args anymore, but rather the args passed to the decorated function. You should rename the parameter to avoid the naming conflict.

Moreover, you're defining Permissions.requires as an instance method so its first parameter is self, the object that the method is bound to, so when you call:

@Permissions.requires(user_just_is, user_is_poop, user_is_admin)

user_just_is is passed as self, rather than becoming part of args. Since requires does not actually make use of self, it should be defined as a static method instead.

So with the above issues fixed, your Permissions class should look like:

class Permissions(object):
    @staticmethod
    def requires(*conditions):
        user = user2

        def decorator(func):
            @wraps(func)
            def allower(*args, **kwargs):
                if not user.logged_in:
                    return print('You\'re not logged in, please log in.')

                if not all(i(user) for i in conditions):
                    return print('Not all of the conditions were met.')

                return func(*args, **kwargs)

            return allower

        return decorator

Upvotes: 2

Related Questions