I have been using a GUI library that allows you to connect signals to signal handlers using a connect function, for example:
widget.connect(signal, callback)
Means that the function callback
will be run whenever the signal is fired from the widget. In an attempt to make my code nicer, and remove a series of connect
calls from my constructor, I decided to use a decorator, which works well:
def callback(widget, signal) def decorate(f): widget.connect(signal, f) return f return decorate ... @callback(widget, signal) def do_something(): ...
This works excellently until I needed to do this in a class - the function is decorated before it is bound to the class, meaning the callback function given doesn't get the instance of the class that owns it, making it useless. Is there a way to get this to work?
A relatively simple solution to this is possible. We begin by using the decorator to mark the function. When instances are constructed, we search for these marks and register the callbacks.
More specifically, the mark and recapture pattern works by using the decorator to mark the function before it is bound, and then find it amongst the bound methods of the instance in the constructor.
First we use a decorator to do the marking (we use a set to allow for multiple marks on one method):
def callback(*args): def decorate(f): try: f._marks.add(args) except AttributeError: f._marks = {args} return f return decorate
Then we use the inspect module to find the bound versions of the marked functions, and connect them:
def connect_callbacks(obj): for _, f in inspect.getmembers(obj, inspect.ismethod): try: marks = f.__func__._marks except AttributeError: continue for widget, signal in marks: widget.connect(signal, f)
__func__
is the name of the original, unbound function. This allows us to access the marks that we applied earlier, facilitating our recapture.
We can then simply create our class and decorate our functions, remembering to connect our callbacks in the constructor:
class Test: def __init__(self): ... connect_callbacks(self) @callback(widget, signal) def test(): ...
This allows us to connect the bound methods with a decorator.
Edit: I have published a tiny library on github that does this for you - it's called recap.