There are cases where Qt.py is not handling incompatibility issues.
- QtCore.QAbstractItemModel.createIndex
- QtCore.QItemSelection
- QtCore.Slot
- QtWidgets.QAction.triggered
- QtGui.QRegExpValidator
- QtWidgets.QHeaderView.setResizeMode
- QtWidgets.qApp
- QtCompat.wrapInstance
Tests
Code blocks in this document are automatically tested at each commit before being accepted into the project. In order for your code to run successfully, follow these guidelines.
- Each caveat MUST contain (1) a header, (2) description, (3) one or more examples and (4, optional) a solution.
- Each caveat MUST have a header prefixed with four hashtags, e.g.
#### My Heading
. - Each example MAY NOT use more than one (1) binding at a time, e.g. both PyQt5 and PySide.
- Each example MUST visualise return value and any exceptions thrown.
- An example MUST reside under a heading, e.g.
#### My Heading
- The first line of each example MUST be
# MyBinding
, whereMyBinding
is the binding you intend to test with, such asPySide
orPyQt4
. - Examples MAY indicate either Python 2 or 3 as
# MyBinding, Python2
- Examples MUST be in doctest format. See other caveats for samples.
- Examples MUST
import Qt
(where appropriate), NOT e.g.import PyQt5
. - Examples MAY include
untested
in which case the continuous integration mechanism will look the other way, e.g.# PyQt4, untested
In PySide, somehow the last argument (the id) is allowed to be negative and is maintained. While in PyQt4 it gets coerced into an undefined unsigned value.
# PySide
>>> from Qt import QtGui
>>> model = QtGui.QStandardItemModel()
>>> index = model.createIndex(0, 0, -1)
>>> int(index.internalId()) == -1
True
# PyQt4
>>> from Qt import QtGui
>>> model = QtGui.QStandardItemModel()
>>> index = model.createIndex(0, 0, -1)
>>> int(index.internalId()) == 18446744073709551615
True
I had been using the id as an index into a list. But the unexpected return value from PyQt4 broke it by being invalid. The workaround was to always check that the returned id was between 0 and the max size I expect.
– @justinfx
PySide has the QItemSelection.isEmpty
and QItemSelection.empty
attributes while PyQt4 only has the QItemSelection.isEmpty
attribute.
# PySide2
>>> from Qt import QtCore
>>> func = QtCore.QItemSelection.isEmpty
>>> func = QtCore.QItemSelection.empty
# PyQt5
>>> from Qt import QtCore
>>> func = QtCore.QItemSelection.isEmpty
>>> func = QtCore.QItemSelection.empty
Traceback (most recent call last):
...
AttributeError: type object 'QItemSelection' has no attribute 'empty'
They both support the len(selection)
operation.
# PyQt4
>>> from Qt import QtCore
>>> selection = QtCore.QItemSelection()
>>> len(selection)
0
# PySide
>>> from Qt import QtCore
>>> selection = QtCore.QItemSelection()
>>> len(selection)
0
PySide allows for a result=None
keyword param to set the return type. PyQt4 crashes:
# PySide
>>> from Qt import QtCore, QtWidgets
>>> slot = QtCore.Slot(QtWidgets.QWidget, result=None)
# PyQt4, Python2
>>> from Qt import QtCore, QtWidgets
>>> slot = QtCore.Slot(QtWidgets.QWidget)
>>> slot = QtCore.Slot(QtWidgets.QWidget, result=None)
Traceback (most recent call last):
...
TypeError: string or ASCII unicode expected not 'NoneType'
# PyQt4, Python3
>>> from Qt import QtCore, QtWidgets
>>> slot = QtCore.Slot(QtWidgets.QWidget)
>>> slot = QtCore.Slot(QtWidgets.QWidget, result=None)
Traceback (most recent call last):
...
TypeError: bytes or ASCII string expected not 'NoneType'
PySide cannot accept any arguments. In PyQt4, QAction.triggered
signal requires a bool arg.
# PySide
>>> from Qt import QtCore, QtWidgets
>>> obj = QtCore.QObject()
>>> action = QtWidgets.QAction(obj)
>>> action.triggered.emit() # Note the return value (!)
True
>>> action.triggered.emit(True)
Traceback (most recent call last):
...
TypeError: triggered() only accepts 0 arguments, 2 given!
# PyQt4
>>> from Qt import QtCore, QtWidgets
>>> obj = QtCore.QObject()
>>> action = QtWidgets.QAction(obj)
>>> action.triggered.emit(True)
>>> action.triggered.emit()
Traceback (most recent call last):
...
TypeError: QAction.triggered[bool] signal has 1 argument(s) but 0 provided
Affects | Version |
---|---|
PyQt4 | <= 4.8.4 |
In PySide, the constructor for QtGui.QRegExpValidator()
can just take a QRegExp
instance, and that is all.
In PyQt4 you are required to pass some form of a parent argument, otherwise you get a TypeError:
# PySide, untested
>>> from Qt import QtCore, QtGui
>>> regex = QtCore.QRegExp("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
>>> validator = QtGui.QRegExpValidator(regex)
>>> validator = QtGui.QRegExpValidator(regex, None)
Traceback (most recent call last):
...
TypeError: ...
# PyQt4, untested
>>> from Qt import QtCore, QtGui
>>> regex = QtCore.QRegExp("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
>>> validator = QtGui.QRegExpValidator(regex, None)
>>> validator = QtGui.QRegExpValidator(regex)
Traceback (most recent call last):
...
TypeError: ...
setResizeMode
was renamed setSectionResizeMode
in Qt 5.
# PySide2
>>> from Qt import QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> view = QtWidgets.QTreeWidget()
>>> header = view.header()
>>> header.setResizeMode(QtWidgets.QHeaderView.Fixed)
Traceback (most recent call last):
...
AttributeError: 'PySide2.QtWidgets.QHeaderView' object has no attribute 'setResizeMode'
# PySide
>>> from Qt import QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> view = QtWidgets.QTreeWidget()
>>> header = view.header()
>>> header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
Traceback (most recent call last):
...
AttributeError: 'PySide.QtGui.QHeaderView' object has no attribute 'setSectionResizeMode'
Use compatibility wrapper.
# PySide2
>>> from Qt import QtWidgets, QtCompat
>>> app = QtWidgets.QApplication(sys.argv)
>>> view = QtWidgets.QTreeWidget()
>>> header = view.header()
>>> QtCompat.setSectionResizeMode(header, QtWidgets.QHeaderView.Fixed)
Or a conditional.
# PyQt5
>>> from Qt import QtWidgets, __binding__
>>> app = QtWidgets.QApplication(sys.argv)
>>> view = QtWidgets.QTreeWidget()
>>> header = view.header()
>>> if __binding__ in ("PyQt4", "PySide"):
... header.setResizeMode(QtWidgets.QHeaderView.Fixed)
... else:
... header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
qApp
is not included in Qt.py due to the way Qt keeps this up to date with the currently active QApplication.
Qt implicitly updates this variable through monkey patching whenever a new QApplication is instantiated. This means that our variable quickly goes out of date and is not updated at the same time.
# PySide2
>>> from Qt import QtWidgets
>>> "qApp" in dir(QtWidgets)
False
Use QApplication.instance()
instead.
Technically, there is no difference between the two, apart from more characters to type.
# PySide2
>>> from Qt import QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> app == QtWidgets.QApplication.instance()
True
QtCompat.wrapInstance
differs across sip
and shiboken
in subtle ways.
# PySide2
>>> from Qt import QtCompat, QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> button = QtWidgets.QPushButton("Hello world")
>>> button.setObjectName("MySpecialButton")
>>> pointer = QtCompat.getCppPointer(button)
>>> widget = QtCompat.wrapInstance(long(pointer))
>>> assert isinstance(widget, QtWidgets.QWidget), widget
>>> assert widget.objectName() == button.objectName()
>>> widget == button
False
# PyQt5
>>> from Qt import QtCompat, QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> button = QtWidgets.QPushButton("Hello world")
>>> button.setObjectName("MySpecialButton")
>>> pointer = QtCompat.getCppPointer(button)
>>> widget = QtCompat.wrapInstance(long(pointer))
>>> assert isinstance(widget, QtWidgets.QWidget), widget
>>> assert widget.objectName() == button.objectName()
>>> widget == button
True
Note the False
for PySide2 and True
for PyQt5.