pyqt signal emit with object instance or None
Asked Answered
M

3

7

I would like to have a pyqt signal, which can either emit with a python "object" subclass argument or None. For example, I can do this:

valueChanged = pyqtSignal([MyClass], ['QString'])

but not this:

valueChanged = pyqtSignal([MyClass], ['None'])
TypeError: C++ type 'None' is not supported as a pyqtSignal() type argument type

or this:

valueChanged = pyqtSignal([MyClass], [])
TypeError: signal valueChanged[MyObject] has 1 argument(s) but 0 provided

I also tried None without the quotes and the c++ equivalent "NULL". But neither seems to work. What is it I need to do to make this work?

Magdau answered 21/8, 2012 at 7:48 Comment(1)
I think [this][1] answer might do what you want [1]: https://mcmap.net/q/897781/-pyqt-signal-with-arguments-of-arbitrary-type-pyqt_pyobject-equivalent-for-new-style-signalsNeurotomy
R
2

TLDR; There's no real solution to this problem. PyQt treats None in a special way when defining signal overloads, so the simplest solution is to use object instead. For an explanation of exactly why this is, see below.


There are a couple of reasons why what you tried didn't work.

Firstly, the type argument of pyqtSignal accepts either a python type-object or a string giving the name of a C++ object (i.e. a Qt class). Strings can't be used to specify python types - and in any case, None is a value, not a type.

Secondly, None is special-cased by PyQt to allow the default overload of the signal to be used without explicitly selecting the signature. So, even if type(None) were used when defining signal overloads, it still wouldn't solve the problem.

(With hindsight, it appears that a better design choice would have been for PyQt to use an internal sentinel object for the default, rather than special-casing None. However, it's possibly too late to change that now. The behaviour remains unchanged in PyQt5 & PyQt6 [as of 13-03-2023]).

Just to spell this all out more clearly, here's some example code:

class MyClass: pass

class X(QtCore.QObject):
    valueChanged = QtCore.pyqtSignal([MyClass], [type(None)])

x = X()
x.valueChanged.connect(
    lambda *a: print('valueChanged[]:', a))
x.valueChanged[MyClass].connect(
    lambda *a: print('valueChanged[MyClass]:', a))
x.valueChanged[type(None)].connect(
    lambda *a: print('valueChanged[type(None)]:', a))

print('emit: valueChanged[]')
x.valueChanged.emit(MyClass())
print('\nemit: valueChanged[type(None)]')
x.valueChanged[type(None)].emit(MyClass())
print('\nemit: valueChanged[MyClass]')
x.valueChanged[MyClass].emit(MyClass())

Output:

emit: valueChanged[]
valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)

emit: valueChanged[type(None)]
valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)

emit: valueChanged[MyClass]
valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db940>,)
valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db940>,)
valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db940>,)

As you can see, when type(None) is used as an overload, it becomes impossible to select anything other than the default overload (i.e. the first one specified in the list of arguments). All three behave in exactly the same way, because they all select the same MyClass overload.

This also means that attempting to emit None will fail, because that value can never match the type of the selected overload (which defaults to MyClass).

A simple work-around would be to use object instead of type(None) - but this has the downside of allowing literally any value to be passed. So if preserving type is important, another possible solution might be to define your own singleton sentinel class to use instead of None.

Remora answered 21/8, 2012 at 17:49 Comment(0)
H
6

A hacky workaround for this is to define your signal to emit object. Since None and MyClass both inherit from object it works as expected. This requires no casting in the slot.

Working in PyQt5 5.11.3 and Python 3.5.6

Signal

valueChanged = QtCore.pyqtSignal(object)

valueChanged.emit(MyClass())
valueChanged.emit(None)

Slot

valueChanged.connect(slot)

def slot(my_class_instance):
    if my_class_instance:
        my_class_instance.some_method()
    else:
        # my_class_instance is None
Homeric answered 5/9, 2019 at 17:58 Comment(0)
C
3

You don't need to specify a second option - it will be accepting a pointer for your class type, which can already be a None value:

valueChanged = pyqtSignal(MyClass)

# both will work as is
inst.valueChanged.emit(MyClass())
inst.valueChanged.emit(None)
Cwmbran answered 21/8, 2012 at 16:53 Comment(2)
Is there any way for this to work with PyQt5 / Python 3.5+ then? I can only think of passing a wrapper class to the signal instead, i.e. a class with a single attribute which can be either None or a MyClass object. It seems a bit overkill to me though.Seraphim
@Seraphim Its a few years later, but if you look at my answer there is already a standard python object None and your custom type have in common - the object type.Homeric
R
2

TLDR; There's no real solution to this problem. PyQt treats None in a special way when defining signal overloads, so the simplest solution is to use object instead. For an explanation of exactly why this is, see below.


There are a couple of reasons why what you tried didn't work.

Firstly, the type argument of pyqtSignal accepts either a python type-object or a string giving the name of a C++ object (i.e. a Qt class). Strings can't be used to specify python types - and in any case, None is a value, not a type.

Secondly, None is special-cased by PyQt to allow the default overload of the signal to be used without explicitly selecting the signature. So, even if type(None) were used when defining signal overloads, it still wouldn't solve the problem.

(With hindsight, it appears that a better design choice would have been for PyQt to use an internal sentinel object for the default, rather than special-casing None. However, it's possibly too late to change that now. The behaviour remains unchanged in PyQt5 & PyQt6 [as of 13-03-2023]).

Just to spell this all out more clearly, here's some example code:

class MyClass: pass

class X(QtCore.QObject):
    valueChanged = QtCore.pyqtSignal([MyClass], [type(None)])

x = X()
x.valueChanged.connect(
    lambda *a: print('valueChanged[]:', a))
x.valueChanged[MyClass].connect(
    lambda *a: print('valueChanged[MyClass]:', a))
x.valueChanged[type(None)].connect(
    lambda *a: print('valueChanged[type(None)]:', a))

print('emit: valueChanged[]')
x.valueChanged.emit(MyClass())
print('\nemit: valueChanged[type(None)]')
x.valueChanged[type(None)].emit(MyClass())
print('\nemit: valueChanged[MyClass]')
x.valueChanged[MyClass].emit(MyClass())

Output:

emit: valueChanged[]
valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)

emit: valueChanged[type(None)]
valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)

emit: valueChanged[MyClass]
valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db940>,)
valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db940>,)
valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db940>,)

As you can see, when type(None) is used as an overload, it becomes impossible to select anything other than the default overload (i.e. the first one specified in the list of arguments). All three behave in exactly the same way, because they all select the same MyClass overload.

This also means that attempting to emit None will fail, because that value can never match the type of the selected overload (which defaults to MyClass).

A simple work-around would be to use object instead of type(None) - but this has the downside of allowing literally any value to be passed. So if preserving type is important, another possible solution might be to define your own singleton sentinel class to use instead of None.

Remora answered 21/8, 2012 at 17:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.