0

I want to mark class methods with decorators in such a way that a GUI can be automatically built to interact with the class. The simplest case for this is a 'button' method, which requires no arguments.

Here is how I define the decorator:

class Button(Control):
    def __init__(self, callback: Callable, label: Optional[str] = None):
        super(Button, self).__init__()
        self.callback = callback
        self.label = label

    def __call__(self, *args, **kwargs):
        return self.callback(*args, **kwargs)


def button(label: Optional[str]):
    """Decorator to denote function as one-shot button press"""
    def decorator(f):
        obj = Button(f, label)
        return obj
    return decorator

class SignalGenerator(BaseMockInstrument):
    def __init__(self):
        super(SignalGenerator, self).__init__()

    @button('Reset')
    def reset(self):
        pass

a = SignalGenerator()
a.reset() #TypeError: reset() missing 1 required positional argument: 'self'
a.reset(a) # Works

But this doesn't work because reset is called without the self argument. TypeError: reset() missing 1 required positional argument: 'self'

I think this is happening because during the decorator call, reset is a function, but it is not a bound instance method (because how could it be when the object doesn't exist yet).

So, the instance isn't passed to the callback.

How do I fix the bug and have the callback appropriately bound, and is there a better way to approach the problem overall?

orbita
  • 47
  • 5
  • I think you have forgotten to pass the arguments to the callback. Did you mean this: `return self.callback(*args, **kwargs)`? – quamrana Oct 14 '22 at 17:27
  • @quamrana This doesn't work either, because the SignalGenerator object isn't passed as an argument to reset(). so generator.reset() still throws the error (but generator.reset(generator)) does not. – orbita Oct 14 '22 at 17:35

2 Answers2

0

This question is similar to another which I answered like this.

The solution to your question is similar in that the code below returns a function rather that the instance of Button.

def button(label):
    """Decorator to denote function as one-shot button press"""
    def decorator(f):
        obj = Button(f, label)
        def wrapper(*args, **kwargs):
            obj(*args, **kwargs)
        return wrapper
    return decorator

# The code below here is my test code only.
class SignalGenerator:

    @button('Reset')
    def reset(self):
        print('reset pressed')

s = SignalGenerator()
s.reset()

Output:

reset pressed

quamrana
  • 37,849
  • 12
  • 53
  • 71
  • Could you explain why this works and the above doesn't? – orbita Oct 14 '22 at 19:29
  • Same as my answer linked above the short answer is `no`. Longer answer is that inside `button()` there is `wrapper()` which is returned and replaces `reset()`. This means that `s.reset()` actually calls `wrapper()`, but with `s` as the first item in `args`. The call `obj(*args, **kwargs)` now has `s` and so `self.callback(*args, **kwargs)` now has `s` and now the final call to `reset()` has the correct `self`. – quamrana Oct 14 '22 at 19:35
0

You can use descriptors [Python-docs] to get the instance Button accessed on.

class Button(Control):
    def __init__(self, callback: Callable, label: Optional[str] = None):
        super(Button, self).__init__()
        self.callback = callback
        self.label = label
        self.bound = None
    def __get__(self, instance, _):
        self.bound = instance
        return self
    def __call__(self, *args, **kwargs):
        return self.callback(self.bound, *args, **kwargs)
Javad
  • 2,033
  • 3
  • 13
  • 23
orbita
  • 47
  • 5