Nylithius
Nylithius

Reputation: 45

apply decorator class to class method

Consider following decorator:

class connector(object):
    def __init__(self, signal):
        self.signal = signal
    def __call__(self, slot_func):
        def wrapper(*args, **kwargs):
            slot_func(*args, **kwargs)
        self.signal.connect(wrapper)

And following signal, and class with method I need to decorate:

from signalslot import Signal

update = Signal()

class manager(object):
    # SOME CODE CUT
    @connector(update)
    def update(self):
        print("I'm updating, yay!!!!")

As you can see I need to pass decorator some additional arguments, and in that case - signal I need to connect to. How to pass self as well?

The reason I ask that, because it fails with following error if I try to apply this decorator to method, not a function:

TypeError: update() missing 1 required positional argument: 'self'

More specifically, if I try to emit the signal:

update.emit()

And yes, I use "signalslot" in that project.

Upvotes: 3

Views: 1242

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1122062

self must be a positional argument, not a keyword argument:

def wrapper(self, *args, **kwargs):
    #       ^^^^ You can't use "self=None" here.
    slot_func(self, *args, **kwargs)

If you need to differentiate between functions and methods, implement a descriptor instead.

However, if you are trying to connect a signal, you'll need to do so to a bound method on each instance. You'd be better of connecting your signals at instance creation time:

class manager(object):
    def __init__(self):
        update.connect(self.update)

    def update(self):
        print("I'm updating, yay!!!!")

When manager.__init__ is called, you have have a new instance and it is then you can create one self.update bound method to receive the signal.

You can still use decorator for this, but you can, at best register at class level what functions can act as signal handlers; you'd have to enumerate all functions on your class at instance-creation time and bind all those signals then:

class connector(object):
    def __init__(self, signal):
        self.signal = signal
    def __call__(self, slot_func):
        slot_func._signal_handler = self.signal
        return slot_func

and a separate class decorator to wrap the class.__init__ method:

from inspect import getmembers, isfunction

def connectsignals(cls):
    signal_handlers = getmembers(
        cls, lambda m: isfunction(m) and hasattr(m, '_signal_handler'))
    init = getattr(cls, '__init__', lambda self: None)
    def wrapper(self, *args, **kwargs):
        init(self, *args, **kwargs)
        for name, handler in signal_handlers:
            handler._signal_handler.connect(handler.__get__(self))
    cls.__init__ = wrapper
    return cls

Decorate the class as well as the signal handlers:

@connectsignals
class manager(object):
    @connector(update)
    def update(self):
        print("I'm updating, yay!!!!")

The decorator then connects all handlers each time a new instance is created:

>>> class Signal(object):
...     def connect(self, handler):
...         print('connecting {!r}'.format(handler))
...
>>> update = Signal()
>>> @connectsignals
... class manager(object):
...     @connector(update)
...     def update(self):
...         print("I'm updating, yay!!!!")
...
>>> manager()
connecting <bound method manager.update of <__main__.manager object at 0x105439ac8>>
<__main__.manager object at 0x105439ac8>

You may want to check if the signalslot project uses weak references to track signal handlers however, as you'll either have a problem of circular references to any instances you create (where a manager instance is kept alive because a signal is still referencing a bound method to that instance), or where your signal handlers are cleaned up too early because your bound methods are stored in weak references, and thus won't have any other references to keep them alive.

Looking at the signalslot source code, I see that the current iteration of the project uses hard references, so your manager instances are never going to be cleared unless you do so explicitly. For this reason alone I'd avoid using methods as signal handlers. Take a look at using python WeakSet to enable a callback functionality if you want to use weak references instead.

Upvotes: 3

Related Questions