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
- QtGui.QPixmap.grabWidget
- QtCore.qInstallMessageHandler
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.
Note: This is not included on our tests, as we cannot reproduce this using PyQt4 4.11.4, CY2017. It's likely that this issue persists in e.g. Maya version < 2017.
# PySide, untested
>>> 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, untested
>>> 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.QHeaderView.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)
Note: Qt.QtCompat.setSectionResizeMode is a older way this was handled and has been left in for now, but this will likely be removed in the future.
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.
Note: This is not included on our tests, as we cannot reproduce this using PySide2 (build commit date 2017-08-25
), CY2018. It's likely that this issue persists in e.g. Maya version < 2018.
# PySide2, untested
>>> 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.
The method of capturing a widget to a pixmap changed between Qt4 and Qt5.
PySide and PyQt4:
# PySide
>>> from Qt import QtGui, QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> button = QtWidgets.QPushButton("Hello world")
>>> pixmap = QtGui.QPixmap.grabWidget(button)
PySide2 and PyQt5
# PySide2
>>> from Qt import QtGui, QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> button = QtWidgets.QPushButton("Hello world")
>>> pixmap = button.grab()
Use compatibility wrapper.
# PySide2
>>> from Qt import QtCompat, QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> button = QtWidgets.QPushButton("Hello world")
>>> pixmap = QtCompat.QWidget.grab(button)