Skip to content

Latest commit

 

History

History
437 lines (338 loc) · 12.8 KB

File metadata and controls

437 lines (338 loc) · 12.8 KB

Caveats

There are cases where Qt.py is not handling incompatibility issues.



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.

  1. Each caveat MUST contain (1) a header, (2) description, (3) one or more examples and (4, optional) a solution.
  2. Each caveat MUST have a header prefixed with four hashtags, e.g. #### My Heading.
  3. Each example MAY NOT use more than one (1) binding at a time, e.g. both PyQt5 and PySide.
  4. Each example MUST visualise return value and any exceptions thrown.
  5. An example MUST reside under a heading, e.g. #### My Heading
  6. The first line of each example MUST be # MyBinding, where MyBinding is the binding you intend to test with, such as PySide or PyQt4.
  7. Examples MAY indicate either Python 2 or 3 as # MyBinding, Python2
  8. Examples MUST be in doctest format. See other caveats for samples.
  9. Examples MUST import Qt (where appropriate), NOT e.g. import PyQt5.
  10. Examples MAY include untested in which case the continuous integration mechanism will look the other way, e.g. # PyQt4, untested



QtGui.QAbstractItemModel.createIndex

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
Usecase

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




QtCore.QItemSelection

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'
Workaround

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



QtCore.Slot

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'



QtWidgets.QAction.triggered

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



QtGui.QRegExpValidator

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: ...



QtWidgets.QHeaderView.setResizeMode

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'
Workaround

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.



QtWidgets.qApp

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
Workaround

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

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.

QtGui.QPixmap.grabWidget

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()
Workaround

Use compatibility wrapper.

# PySide2
>>> from Qt import QtCompat, QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> button = QtWidgets.QPushButton("Hello world")
>>> pixmap = QtCompat.QWidget.grab(button)

Fully Qualified Enums

In Qt6 both PySide6 and PyQt6 are moving from custom Enum classes to python Enums. This means you should be moving from short form Enums QFont.Bold to fully qualified Enum names QFont.Weight.Bold.

PySide6 currently has a forgiveness mode where you can still use QFont.Bold on a Qt class object but no longer let you use QFont().Bold on a instance of the class. PyQt6 doesn't let you use either of these options and you must use the fully qualified Enum name QFont.Weight.Bold.

There have already been a few short enum name conflicts introduced. QtGui.QColorSpace for example has the enums QColorSpace.NamedColorSpace.AdobeRgb with a value of 3 and QColorSpace.Primaries.AdobeRgb with a value of 2. The value doesn't match so you may not be passing the value you expect when using short enums. You can use the --show dups mode of Qt_convert_enum.py to generate a listing of duplicates currently found in PySide6.

PySide, PyQt4 and older releases of PySide2 and PyQt5 can only use short enums and are not compatible with fully qualified enum names. If you need to support Qt4 and Qt5 then use short enum's. Unfortunately your code won't easily work with PyQt6.

For maximum compatibility with Qt5 and Qt6 moving forward, you should always use the fully qualified enum name. Even if you only plan to support PySide5/6 you are encouraged to use the fully qualified names for future proofing.

To convert existing code from short to fully qualified enum names use the Qt_convert_enum.py script included with Qt.py.

$ pip3 install Qt.py PySide2
$ python3 .../Qt_convert_enum.py /path/to/code/directory/to/update

This will search every .py file in the directory recursively and list any short enums that need replaced in each file.

To actually update the code add --write flag. This updates existing files and does not make backups of the existing files, so make sure to do that first.

To check for enum use regression you can add --check. This will change the return code to the number of enums that require changing. A return code of zero indicates that no enum changes are required. This check can only find and fix code that is directly using enums on Qt class names. If you used self.EnumName or other methods of accessing the enum objects the --partial check may find them. This is unable to automatically fix your code but shows you possible code that you will need to manually fix.