Python decorator on instance method
Asked Answered
B

3

3

Anyone know what is wrong with this code?

def paginated_instance_method(default_page_size=25):
    def wrap(func):
        @functools.wraps(func)
        def inner(self, page=1, page_size=default_page_size, *args, **kwargs):
            objects = func(self=self, *args, **kwargs)
            return _paginate(objects, page, page_size)
        return inner
    return wrap

class Event(object):
    ...
    @paginated_instance_method
    def get_attending_users(self, *args, **kwargs):
        return User.objects.filter(pk__in=self.attending_list)

I get the following error:

    Traceback (most recent call last):
      File "<console>", line 1, in <module>
      File "/Users/zarathustra/Virtual_Envs/hinge/hinge_services/hinge/api/decorators.py", line 108, in wrap
        def inner(self, page=1, page_size=default_page_size, *args, **kwargs):
      File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/functools.py", line 33, in update_wrapper
        setattr(wrapper, attr, getattr(wrapped, attr))
    AttributeError: 'Event' object has no attribute '__name__'

The reason why I thought this would work is because, through trial and error, I got the following decorator working like a charm for classmethods:

def paginated_class_method(default_page_size=25):
    def wrap(func):
        @functools.wraps(func)
        def inner(cls, page=1, page_size=default_page_size, *args, **kwargs):
            objects = func(cls=cls, *args, **kwargs)
            return _paginate(objects, page, page_size)
        return inner
    return wrap
Banana answered 26/2, 2013 at 20:16 Comment(3)
Your decorator doesn't make sense. The decorator is supposed to take a function as an argument, but yours takes only one argument, which apparently a number (default_page_size). Were you intending to decorate with something like @paginated_instance_method(10)?Atcliffe
@Atcliffe Yes, I was trying to do just that. I am new to decorators, but I'll add an edit to explain why I thought it would work.Banana
Can you show how you used that other decorator?Atcliffe
A
1

Your decorator has an extra level of indirection which is throwing things off. When you do this:

@paginated_instance_method
def get_attending_users(self, *args, **kwargs):
    return User.objects.filter(pk__in=self.attending_list)

You are doing this:

def get_attending_users(self, *args, **kwargs):
    return User.objects.filter(pk__in=self.attending_list)
get_attending_users = paginated_instance_method(get_attending_users)

That is what decorators do. Note that paginated_instance_method is called with get_attending_users as its argument. That means that in your decorator, the argument default_page_size is set to the function get_attending_users. Your decorator returns the function wrap, so get_attending_users is set to that wrap function.

Then when you then call Event().get_attending_users() it calls wrap(self), where self is your Event instance. wrap is expecting the argument to be a function, and tries to return a new function wrapping that function. But the argument isn't a function, it's an Event object, so functools.wrap fails when trying to wrap it.

I have a hunch that what you're trying to do is this:

@paginated_instance_method()
def get_attending_users(self, *args, **kwargs):
    return User.objects.filter(pk__in=self.attending_list)

That is, you want paginated_instance_method to take an argument. But even if you want to use the default value of that argument, you still have to actually call paginated_instance_method. Otherwise you just pass the method as the argument, which is not what paginated_instance_method is expecting.

The reason it "worked" for a classmethod is that a classmethod takes the class as the first argument, and a class (unlike an instance) does have a __name__ attribute. However, I suspect that if you test it further you'll find it's not really doing what you want it to do, as it's still wrapping the class rather than the method.

Atcliffe answered 26/2, 2013 at 20:36 Comment(1)
It had the parentheses in the other usage -- that was the important difference. So it never really "worked" for the paginated_class_method. Thanks!Banana
B
3

paginated_instance_method is not a decorator, it is a function that returns a decorator. So

@paginated_instance_method()
def get_attending_users(self, *args, **kwargs):

(Note the parentheses.)

Boong answered 26/2, 2013 at 20:32 Comment(0)
A
1

Your decorator has an extra level of indirection which is throwing things off. When you do this:

@paginated_instance_method
def get_attending_users(self, *args, **kwargs):
    return User.objects.filter(pk__in=self.attending_list)

You are doing this:

def get_attending_users(self, *args, **kwargs):
    return User.objects.filter(pk__in=self.attending_list)
get_attending_users = paginated_instance_method(get_attending_users)

That is what decorators do. Note that paginated_instance_method is called with get_attending_users as its argument. That means that in your decorator, the argument default_page_size is set to the function get_attending_users. Your decorator returns the function wrap, so get_attending_users is set to that wrap function.

Then when you then call Event().get_attending_users() it calls wrap(self), where self is your Event instance. wrap is expecting the argument to be a function, and tries to return a new function wrapping that function. But the argument isn't a function, it's an Event object, so functools.wrap fails when trying to wrap it.

I have a hunch that what you're trying to do is this:

@paginated_instance_method()
def get_attending_users(self, *args, **kwargs):
    return User.objects.filter(pk__in=self.attending_list)

That is, you want paginated_instance_method to take an argument. But even if you want to use the default value of that argument, you still have to actually call paginated_instance_method. Otherwise you just pass the method as the argument, which is not what paginated_instance_method is expecting.

The reason it "worked" for a classmethod is that a classmethod takes the class as the first argument, and a class (unlike an instance) does have a __name__ attribute. However, I suspect that if you test it further you'll find it's not really doing what you want it to do, as it's still wrapping the class rather than the method.

Atcliffe answered 26/2, 2013 at 20:36 Comment(1)
It had the parentheses in the other usage -- that was the important difference. So it never really "worked" for the paginated_class_method. Thanks!Banana
F
0

This is really easy, but tricky at first view. Look at pep 318.

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

This is equivalent to:

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

You have an extra wrapper, which takes a decorator's args to use it in the wrapped functions (closure design pattern). So your decorator will look like this:

@dec(arg=True)
def func(arg1, arg2, ...):
    pass

Equivalent to:

def func(arg1, arg2, ...):
    pass
func = dec(arg=True)(func)
Filamentary answered 6/2, 2014 at 12:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.