Qt.py

repository·master·Indexed 21 days ago

https://github.com/mottosso/qt.py

A compatibility layer that allows developers to write Qt code once and run it across multiple Python bindings, including PySide6, PyQt6, PySide2, and PyQt5. It provides a unified API following PySide6 conventions, a QtCompat module for bridging binding differences (such as loadUi), and a QtSiteConfig system for runtime customization of exposed modules and class locations.

Tokens
12.2K
Snippets
42
Records
53
Agent score
77%

What's inside Qt.py

  1. Available Qt.py usage examples

    master

    The following examples demonstrate specific patterns for using Qt.py:

    • loadUi: Demonstrates how to add a base instance argument to the loadUi functionality.
    • QtSiteConfig: Demonstrates how to expose additional Qt modules and classes via configuration.
  2. Access the current QApplication via QApplication.instance()

    master

    QtWidgets.qApp is not included in Qt.py because it can become out of sync with the active QApplication.

    Workaround: Use QtWidgets.QApplication.instance() to reliably access the current application instance.

    # PySide2 workaround
    >>> from Qt import QtWidgets
    >>> app = QtWidgets.QApplication(sys.argv)
    >>> app == QtWidgets.QApplication.instance()
    True
  3. Handle QtWidgets.QAction.triggered signal arguments

    master

    The triggered signal behavior varies:

    • PySide: Does not accept arguments. Calling .emit(True) will raise a TypeError.
    • PyQt4: Requires a boolean argument. Calling .emit() without arguments will raise a TypeError.

    Note: This issue is often environment-specific (e.g., older versions of Maya).

    # PySide, untested
    >>> from Qt import QtCore, QtWidgets
    >>> obj = QtCore.QObject()
    >>> action = QtWidgets.QAction(obj)
    >>> action.triggered.emit()
    True
    >>> action.triggered.emit(True)
    Traceback (most recent call last):
    ...
    TypeError: triggered() only accepts 0 arguments, 2 given!
  4. Handle QtCore.Slot keyword argument incompatibility

    master

    PySide allows using result=None as a keyword argument in QtCore.Slot to set the return type. This causes a TypeError in PyQt4 (both Python 2 and 3) because it expects a string or bytes for the return type, not NoneType.

    Workaround: Do not use result=None when targeting PyQt4 bindings.

    # 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'
  5. Use fully qualified Enums for Qt6 compatibility

    master

    Qt6 (PySide6 and PyQt6) is moving from short-form Enums (e.g., QFont.Bold) to fully qualified Python Enums (e.g., QFont.Weight.Bold).

    Best Practices:

    • For maximum compatibility with both Qt5 and Qt6, always use the fully qualified enum name.
    • Short enums are not compatible with PyQt6.
    • Using short enums in Qt6 can lead to value conflicts (e.g., QColorSpace.NamedColorSpace.AdobeRgb vs QColorSpace.Primaries.AdobeRgb).

    Conversion: Use the Qt_convert_enum.py script included with Qt.py to automatically update your codebase.

  6. Understand the test suite breakdown

    master

    The test suite is organized into two main categories:

    Membership Tests (membership-*)

    These tests generate the common members dictionary stored in /.members.

    • membership-begin: Cleans up files from previous runs.
    • membership-*-*: Maps available bindings for specific Python and Qt versions.
    • membership-end: Combines individual mappings into a final common members dictionary and creates reference .md files.

    Functional Tests (test-*)

    These run the testing suite against specific Python/Qt environments.

    • test-begin: Prepares files required for other tests.
    • test-*-*-impl: Runs implementation tests defined in test.py.
    • test-*-*-caveats: Tests edge cases defined in CAVEATS.md.
    • test-*-*-examples: Tests the code provided in the /examples directory.
  7. Handle QtGui.QPixmap.grabWidget differences

    master

    The method for capturing a widget to a pixmap changed between Qt4 and Qt5:

    • PySide and PyQt4: Use QtGui.QPixmap.grabWidget(widget).
    • PySide2 and PyQt5: Use widget.grab().

    Workaround: Use the QtCompat wrapper: QtCompat.QWidget.grab(widget).

    # PySide2 workaround
    >>> from Qt import QtCompat, QtWidgets
    >>> app = QtWidgets.QApplication(sys.argv)
    >>> button = QtWidgets.QPushButton("Hello world")
    >>> pixmap = QtCompat.QWidget.grab(button)
  8. Accessing static members and enums in Qt 6

    master

    In Qt 6, instances of classes (e.g., QFont()) no longer provide direct access to static members or enums (e.g., font.Bold). You must use the fully qualified enum path via the class type.

    Incorrect (Qt 5 style):

    font = QFont()
    font.setWeight(font.Bold)

    Correct (Qt 6 style):

    font = QFont()
    font.setWeight(QFont.Weight.Bold)

    While you can use font.setWeight(type(font).Weight.Bold), using the explicit class name is the recommended approach.

  9. Handle QtGui.QAbstractItemModel.createIndex incompatibility

    master

    In PySide, the last argument (id) in createIndex can be negative and is maintained. In PyQt4, this value is coerced into an undefined unsigned integer (a very large positive number).

    Workaround: When using PyQt4, always validate that the returned id is within the expected range (e.g., between 0 and your maximum expected index) before using it as a list index.

    # PyQt4
    >>> from Qt import QtGui
    >>> model = QtGui.QStandardItemModel()
    >>> index = model.createIndex(0, 0, -1)
    >>> int(index.internalId()) == 18446744073709551615
    True
  10. Handle QtCore.QItemSelection emptiness check

    master

    The attributes for checking if a selection is empty differ between bindings:

    • PySide/PySide2: Supports both isEmpty() and empty().
    • PyQt4/PyQt5: Only supports isEmpty().

    Workaround: Use the len() function on the selection object, which is supported by all bindings, to check for emptiness.

    # PyQt4
    >>> from Qt import QtCore
    >>> selection = QtCore.QItemSelection()
    >>> len(selection)
    0
  11. Handle QtGui.QRegExpValidator constructor differences

    master

    In PySide, the QtGui.QRegExpValidator constructor can take just a QRegExp instance. In PyQt4 (versions <= 4.8.4), you must provide a parent argument (e.g., None), otherwise a TypeError is raised.

    # 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: ...
  12. Handle QtWidgets.QHeaderView.setResizeMode renaming

    master

    In Qt 5, setResizeMode was renamed to setSectionResizeMode.

    • PySide2: Uses setSectionResizeMode.
    • PySide: Uses setResizeMode.
    • PyQt5: Uses setSectionResizeMode.

    Workarounds:

    1. Use the QtCompat wrapper: QtCompat.QHeaderView.setSectionResizeMode(header, mode).
    2. Use a conditional check on the __binding__ variable.
    # PyQt5 workaround
    >>> 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)