views:

379

answers:

2

Hi,

I'm trying to do something like these proposed signal decorators. In addition to having a decorator that connects the decorated method to a signal (with the signal's sender as an argument to the decorator), I would like to use the decorator on class methods.

I'd like to use the decorator like so:

class ModelA(Model):

    @connect.post_save(ModelB)
    @classmethod
    def observe_model_b_saved(cls, sender, instance, created, **kwargs):
        # do some stuff
        pass

The decorator is:

from django.db.models import signals
def post_save(sender):
    def decorator(view):
        signals.post_save.connect(sender=sender, receiver=view)
        return view
    return decorator

The error I get when I do this is:

File "/Library/Python/2.6/site-packages//lib/python2.6/site-packages/django/dispatch/dispatcher.py", line 78, in connect
AssertionError: Signal receivers must be callable.

I guess the problem is that @classmethod returns a class method object which is not callable. I don't really understand how classmethod works under the hood, but I surmise from this reference page that the class method object is not translated into a callable until it is accessed from the class, e.g., ModelA.observe_model_b_saved. Is there any way that I can both (1) define my method as a class or instance method on a model, and (2) connect it to a signal using a decorator directly on the method definition? Thanks!

+1  A: 

It's not clear from your example code, so I'd be asking if the signal listener actually has to be a @classmethod? I.e. Will a regular method do (and then use self.__class__ if you still need to access the class itself)? Does it need to be a method at all (can you just use a function)?

Another option might be to use a second method to listen to the signal and delegate the call to the @classmethod:

class ModelA(Model): 

    @classmethod 
    def do_observe_model_b_saved(cls, sender, instance, created, **kwargs): 
        # do some stuff 
        pass 

    @connect.post_save(ModelB) 
    def observe_model_b_saved(self, sender, instance, created, **kwargs): 
        self.do_observe_model_b_saved(sender, instance, created, **kwargs)
Tom
Thanks I think I am going to go with a solution someone suggested to another version of this question: class decorators. The class decorator can create the signal connection after the entire class is done loading and this obviates the problem of not being able to access the class method directly. http://stackoverflow.com/questions/2366713/can-a-python-decorator-of-an-instance-method-access-the-class
DGGenuine
A: 

Could you make it a @staticmethod instead? That way, you can just swap the order of the decorators.

class ModelA(Model):

    @staticmethod
    @connect.post_save(ModelB)
    def observe_model_b_saved(sender, instance, created, **kwargs):
        # do some stuff
        pass

You'd have to refer to the class by full name instead of getting passed the cls argument, but this would allow you to keep a similar code organization.

Matt
Thanks, but that solution won't work for me because in the methods I depend upon accessing the class dynamically in order to retrieve members that maybe overloaded. E.g. the superclass defines a method that relies upon some attribute, but then the subclass overloads the attribute. I need the overloaded attribute and not the one that comes from naming explicitly the class by name.
DGGenuine