python - PySide6 signal not emitting properly when passed to decorator - Stack Overflow

I have this decorator in python 3.11 that sends a signal encoding the arguments to a function call that

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.

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.

Share Improve this question edited Mar 28 at 17:25 KBriggs asked Mar 26 at 11:55 KBriggsKBriggs 1,4345 gold badges21 silver badges45 bronze badges 3
  • 1 The signal you're passing is an unbound signal, which is a class member and has no connect/emit attributes. Signals actually work only with instances, then they have those attributes and can be used appropriately. There may be ways to inspect the unbound signal object and get the bound one for the instance, but I believe that the easiest way to do so would be to just pass the signal name (as a string) and then use getattr(self, <signalName>).emit(...). – musicamante Commented Mar 26 at 20:13
  • Thank you, your suggested approach works. Is it just me, or is that behavior very unintuitive? In any case, if you want to post that answer I will accept it. – KBriggs Commented Mar 27 at 12:37
  • 1 What behavior? If you're referring to the fact that signals are unbound until the class is instantiated, that's how it's expected to be: "usable" signals can only be used by instances and need to be bound to them in order to provide proper management. In Python they're wrapped as descriptors, so it may be possible to use related functionalities to avoid the string attribute lookup, I'll do some research later and eventually post an answer with my findings. – musicamante Commented Mar 27 at 14:22
Add a comment  | 

1 Answer 1

Reset to default 1

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)

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744146941a4560466.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信