Qt.py icon indicating copy to clipboard operation
Qt.py copied to clipboard

signal.connect() TypeError in pyqt5 when signal has an argument

Open eyeteajay opened this issue 1 year ago • 9 comments

Using this code example:

import logging
from Qt import QtCore
from Qt import QtWidgets

class MainWind(QtWidgets.QMainWindow):
    willclose = QtCore.Signal(int)
    
    def __init__(self, *args, **kw):
        super(MainWind, self).__init__(*args, **kw)
        but = QtWidgets.QPushButton("hi")
        self.setCentralWidget(but)
    
    def closeEvent(self, evt):
        closecode = 10
        logging.info("will close '%s': %s: %s", self.objectName(), closecode, self)
        self.willclose.emit(closecode)
        return super(MainWind, self).closeEvent(evt)

class Runner(object):
    def __init__(self, wind):
        wind.willclose.connect(self.didclose)
        self._wind = wind
    
    @QtCore.Slot(int)
    def didclose(self, closecode):
        logging.info("didclose signal called with: %r", closecode)
    
    def run(self):
        self._wind.show()
        exitcode = QtWidgets.QApplication.instance().exec_()
        return exitcode

logging.basicConfig(level=logging.DEBUG)
app = QtWidgets.QApplication([])
wind = MainWind()
r = Runner(wind)
r.run()

produces a TypeError when trying to connect a signal using python3 & PyQt5: TypeError: connect() failed between MainWind.willclose[int] and didclose()

However, the same code runs fine using python3 & PySide2. An undecorated didclose() method will also run using PyQt5, and a slot method that takes no arguments will also run.

eyeteajay avatar Jun 10 '23 03:06 eyeteajay

I don't think the os matters, but this was on a Mac. And the pip environments I tested with are:

Package       Version
------------- ----------
pip           22.3.1
PyQt5         5.15.9
PyQt5-Qt5     5.15.2
PyQt5-sip     12.12.1
Qt.py         1.3.8
setuptools    65.5.0
types-PySide2 5.15.2.1.5

vs.

pip           22.3.1
PySide2       5.13.2
Qt.py         1.3.8
setuptools    65.5.0
shiboken2     5.13.2
types-PySide2 5.15.2.1.5

eyeteajay avatar Jun 10 '23 03:06 eyeteajay

Thanks for reporting this. Does this happen when using PyQt5 directly, without Qt.py? Could this be a PyQt5 bug, rather than a Qt.py bug?

mottosso avatar Jun 12 '23 06:06 mottosso

Looks like PyQt5 requires inheriting from QObject not python's object to use the Slot decorator. This code change fixes the original example, and will run for both PyQt5 and PySide2.

- class Runner(object):
+ class Runner(QtCore.QObject):
    def __init__(self, wind):
+         super().__init__()
        wind.willclose.connect(self.didclose)
        self._wind = wind

Looks like this is not a Qt.py issue but a low level difference in how PySide2 and PyQt5 work. I wonder if there are any sip.setapi features for PyQt5 that would address issues like this.

MHendricks avatar Jun 12 '23 18:06 MHendricks

Nice catch; yes I expected PySide to require this too. This is true for the C++ side too, so I would consider PySide to be at fault for allowing it.

Is there anything Qt.py can do to disallow it cross binding? (thinking) That could help users get the error early and on every binding.

mottosso avatar Jun 13 '23 06:06 mottosso

@MHendricks thanks! I had tested it with QObject but I forgot to initialize it, so I thought it didn't matter in that case. It makes sense that it should require QObject, so I agree it's more Pyside's fault.

eyeteajay avatar Jun 13 '23 18:06 eyeteajay

@mottosso I think there should be some decorator magic that could examine the mro of the class of the unbound method and see if it inherits from QObject? If I can come up with anything I'll share

eyeteajay avatar Jun 13 '23 22:06 eyeteajay

Great, then let's consider this issue fixed once we can prevent PySide from allowing signals in non-QObject classes.

mottosso avatar Jun 14 '23 08:06 mottosso

I've found another inconsistency when creating a QApplication between PyQt5 and PySide2. PyQt5 requires passing a list, but PySide2 does not.

>>> from PySide2 import QtWidgets
>>> QtWidgets.QApplication()
<PySide2.QtWidgets.QApplication(0x23563d1ecc0) at 0x000002353342A0F0>
>>> exit()
>>> from PyQt5 import QtWidgets
>>> QtWidgets.QApplication()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: QApplication(argv: List[str]): not enough arguments
>>> QtWidgets.QApplication([])
<PyQt5.QtWidgets.QApplication object at 0x00000264E3274160>

MHendricks avatar Jul 18 '23 22:07 MHendricks

I've found another inconsistency when creating a QApplication between PyQt5 and PySide2. PyQt5 requires passing a list, but PySide2 does not.

Sounds like a good fit for another issue with a dedicated PR. I'd vouch to make PyQt5 be OK with no argument, by just subclassing it with our own wrapper. Up for it?

mottosso avatar Jul 19 '23 06:07 mottosso