I have this decorator in python 3.11 that sends a signal encoding the arguments to a function call that it decorates, here:
def register_action(signal=None):
def decorator(func):
def wrapper(self, *args, **kwargs): # self is the first argument for instance methods
result = func(self, *args, **kwargs)
history = {'function': func.__name__,
'args': args,
'kwargs': kwargs
}
try:
signal.emit(history, False)
except Exception as e:
self.logger.info(f'Unable to log tab action: {str(e)}')
return result
return wrapper
return decorator
The signal in question is defined in a class at the top, like so:
class MetadataView(MetaView):
logger = logging.getLogger(__name__)
update_tab_action_history = Signal(object, bool)
...
and the decorator is applied like this:
@register_action(signal=update_tab_action_history)
def _overlay_plot(self, parameters):
...
However, when this is called, I get the following error:
'PySide6.QtCore.Signal' object has no attribute 'emit'
I am reasonably certain that PySide6.QtCore.Signal
does in fact have an emit
attribute, so something else is going wrong... Any suggestions would be appreciated.
The MetaView class from which my class inherits inherits from QWidget.
It's always important to remember how Qt signals work in Python. I haven't been able to find any official Qt reference documentation, but since PySide is mostly based on PyQt and follows most of its concepts, the concept remains.
Here is an excerpt from the chapter "Support for Signals and Slots" in the PyQt docs:
A signal (specifically an unbound signal) is a class attribute. When a signal is referenced as an attribute of an instance of the class then PyQt6 automatically binds the instance to the signal in order to create a bound signal. This is the same mechanism that Python itself uses to create bound methods from class functions.
A bound signal has connect(), disconnect() and emit() methods that implement the associated functionality. It also has a signal attribute that is the signature of the signal that would be returned by Qt’s SIGNAL() macro.
When you're using the decorator, you're passing the signal at the class level, meaning that at that moment it's still an unbound signal.
Within the context of the wrapper, the reference is still to the same object, so it cannot be used as a proper (bound) signal, which is also clear by the fact that trying to access emit
raises an attribute error, confirming what the documentation explains.
In Python, signals are used through descriptors, and the actual bound signal is therefore created on request. In fact, when accessing a signal as an instance attribute, we actually receive different objects every time, causing the following apparently unintuitive result:
>>> o = QObject()
>>> o.objectNameChanged == o.objectNameChanged
False
This is because the descriptor actually returns a new object every time, the signal bound to the instance. Note that the new object is just the Python object that allows access to the signal, so, despite always having different Python objects, the signal is obviously the same.
So, how can we access the bound signal from another context, having the unbound signal as only reference?
One approach could be to not pass the actual signal to the decorator, but its name as a string, then we could use getattr(self, signalName)
to get the bound signal within the wrapper, but that's not a very elegant solution.
When accessing descriptor as attributes from instances, Python actually calls __get__()
on the descriptor, so we can just do that on our own:
boundSignal = signal.__get__(self)
boundSignal.emit(history, False)
# or simply
signal.__get__(self).emit(history, False)