diff --git a/Qt.py b/Qt.py index 4aadc9ac..c3c7292e 100644 --- a/Qt.py +++ b/Qt.py @@ -3,8 +3,8 @@ DOCUMENTATION Qt.py was born in the film and visual effects industry to address the growing need for the development of software capable of running - with more than one flavour of the Qt bindings for Python. - + with more than one flavour of the Qt bindings for Python. + Supported Binding: PySide, PySide2, PySide6, PyQt4, PyQt5 1. Build for one, run with all @@ -47,7 +47,7 @@ import json -__version__ = "1.3.10" +__version__ = "1.4.0" # Enable support for `from Qt import *` __all__ = [] @@ -112,7 +112,6 @@ "QGenericArgument", "QGenericReturnArgument", "QItemSelection", - "QItemSelectionModel", "QItemSelectionRange", "QIODevice", "QLibraryInfo", @@ -152,7 +151,6 @@ "QSize", "QSizeF", "QSocketNotifier", - "QStringListModel", "QSysInfo", "QSystemSemaphore", "QT_TRANSLATE_NOOP", @@ -197,8 +195,6 @@ "qUnregisterResourceData", "qVersion", "qWarning", - "Signal", - "Slot" ], "QtGui": [ "QAbstractTextDocumentLayout", @@ -271,7 +267,6 @@ "QQuaternion", "QRadialGradient", "QRegion", - "QRegExpValidator", "QResizeEvent", "QSessionManager", "QShortcutEvent", @@ -930,6 +925,9 @@ def createWidget(self, class_name, parent=None, name=""): These members from the original submodule are misplaced relative PySide2 +NOTE: For bindings where a member is not replaced, they still + need to be added such that they are added to Qt.py + """ _misplaced_members = { "PySide6": { @@ -943,6 +941,9 @@ def createWidget(self, class_name, parent=None, name=""): "QtCore.QItemSelection": "QtCore.QItemSelection", "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtCore.QRegularExpression": "QtCore.QRegExp", + "QtStateMachine.QStateMachine": "QtCore.QStateMachine", + "QtStateMachine.QState": "QtCore.QState", "QtGui.QRegularExpressionValidator": "QtGui.QRegExpValidator", "QtGui.QShortcut": "QtWidgets.QShortcut", "QtGui.QAction": "QtWidgets.QAction", @@ -961,14 +962,16 @@ def createWidget(self, class_name, parent=None, name=""): "QtCompat.qInstallMessageHandler", _qInstallMessageHandler ], "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", - "QtMultimedia.QSound": "QtMultimedia.QSound", + "QtWidgets.QActionGroup": "QtGui.QActionGroup", }, "PySide2": { - "QtGui.QStringListModel": "QtCore.QStringListModel", "QtGui.QStringListModel": "QtCore.QStringListModel", "QtCore.Property": "QtCore.Property", "QtCore.Signal": "QtCore.Signal", "QtCore.Slot": "QtCore.Slot", + "QtCore.QRegExp": "QtCore.QRegExp", + "QtWidgets.QShortcut": "QtWidgets.QShortcut", + "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", "QtCore.QItemSelection": "QtCore.QItemSelection", @@ -989,7 +992,6 @@ def createWidget(self, class_name, parent=None, name=""): "QtCompat.qInstallMessageHandler", _qInstallMessageHandler ], "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", - "QtMultimedia.QSound": "QtMultimedia.QSound", }, "PyQt5": { "QtCore.pyqtProperty": "QtCore.Property", @@ -1000,6 +1002,8 @@ def createWidget(self, class_name, parent=None, name=""): "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], "sip.isdeleted": ["QtCompat.isValid", _isvalid], "QtWidgets.qApp": "QtWidgets.QApplication.instance()", + "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", + "QtCore.QRegExp": "QtCore.QRegExp", "QtCore.QCoreApplication.translate": [ "QtCompat.translate", _translate ], @@ -1009,10 +1013,13 @@ def createWidget(self, class_name, parent=None, name=""): "QtCore.qInstallMessageHandler": [ "QtCompat.qInstallMessageHandler", _qInstallMessageHandler ], + "QtWidgets.QShortcut": "QtWidgets.QShortcut", "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", - "QtMultimedia.QSound": "QtMultimedia.QSound", }, "PySide": { + "QtCore.Property": "QtCore.Property", + "QtCore.Signal": "QtCore.Signal", + "QtCore.Slot": "QtCore.Slot", "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", "QtGui.QStringListModel": "QtCore.QStringListModel", @@ -1020,18 +1027,21 @@ def createWidget(self, class_name, parent=None, name=""): "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", + "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", "QtGui.QPrinter": "QtPrintSupport.QPrinter", + "QtWidgets.QShortcut": "QtWidgets.QShortcut", "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], "shiboken.isValid": ["QtCompat.isValid", _isvalid], "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QRegExp": "QtCore.QRegExp", "QtCore.QCoreApplication.translate": [ "QtCompat.translate", _translate ], @@ -1042,7 +1052,6 @@ def createWidget(self, class_name, parent=None, name=""): "QtCompat.qInstallMessageHandler", _qInstallMessageHandler ], "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", - "QtGui.QSound": "QtMultimedia.QSound", }, "PyQt4": { "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", @@ -1055,9 +1064,11 @@ def createWidget(self, class_name, parent=None, name=""): "QtCore.pyqtSlot": "QtCore.Slot", "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", + "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", + "QtWidgets.QShortcut": "QtWidgets.QShortcut", "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", "QtGui.QPrinter": "QtPrintSupport.QPrinter", @@ -1068,6 +1079,7 @@ def createWidget(self, class_name, parent=None, name=""): "sip.isdeleted": ["QtCompat.isValid", _isvalid], "QtCore.QString": "str", "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QRegExp": "QtCore.QRegExp", "QtCore.QCoreApplication.translate": [ "QtCompat.translate", _translate ], @@ -1078,7 +1090,6 @@ def createWidget(self, class_name, parent=None, name=""): "QtCompat.qInstallMessageHandler", _qInstallMessageHandler ], "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", - "QtGui.QSound": "QtMultimedia.QSound", } } @@ -1141,6 +1152,12 @@ def createWidget(self, class_name, parent=None, name=""): "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", }, + "QFont":{ + "setWeight": "QtGui.QFont.setWeight", + }, + "Qt": { + "MidButton": "QtCore.Qt.MiddleButton", + }, }, "PyQt5": { "QWidget": { @@ -1161,6 +1178,12 @@ def createWidget(self, class_name, parent=None, name=""): "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", }, + "QFont":{ + "setWeight": "QtGui.QFont.setWeight", + }, + "Qt": { + "MidButton": "QtCore.Qt.MiddleButton", + }, }, "PySide": { "QWidget": { @@ -1179,6 +1202,12 @@ def createWidget(self, class_name, parent=None, name=""): "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", }, + "QFont":{ + "setWeight": "QtGui.QFont.setWeight", + }, + "Qt": { + "MidButton": "QtCore.Qt.MiddleButton", + }, }, "PyQt4": { "QWidget": { @@ -1197,6 +1226,12 @@ def createWidget(self, class_name, parent=None, name=""): "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", }, + "QFont":{ + "setWeight": "QtGui.QFont.setWeight", + }, + "Qt": { + "MidButton": "QtCore.Qt.MiddleButton", + }, }, } @@ -1275,6 +1310,7 @@ def _reassign_misplaced_members(binding): """ + for src, dst in _misplaced_members[binding].items(): dst_value = None @@ -1293,6 +1329,7 @@ def _reassign_misplaced_members(binding): if len(dst_parts) > 1: dst_member = dst_parts[1] + # Get the member we want to store in the namesapce. if not dst_value: try: @@ -1439,7 +1476,7 @@ def _pyside6(): if hasattr(Qt, "_QtWidgets"): Qt.QtCompat.setSectionResizeMode = \ Qt._QtWidgets.QHeaderView.setSectionResizeMode - + def setWeight(func): def wrapper(self, weight): weight = { @@ -1460,7 +1497,7 @@ def wrapper(self, weight): wrapper.__name__ = func.__name__ return wrapper - + decorators = { "QFont": { @@ -1469,7 +1506,6 @@ def wrapper(self, weight): } _reassign_misplaced_members("PySide6") - _build_compatibility_members("PySide6") _build_compatibility_members("PySide6", decorators) diff --git a/README.md b/README.md index 1460b0b6..608d23cf 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Qt.py enables you to write software that runs on any of the 4 supported bindings | Date | Version | Event |:---------|:----------|:---------- +| May 2024 | [1.4.0][] | Added support for Qt 6 | Jan 2024 | [1.3.9][] | Run CI on Github Actions, instead of Travis CI. | Sep 2020 | [1.3.0][] | Stability improvements and greater ability for `QtCompat.wrapInstance` to do its job | Jun 2019 | [1.2.1][] | Bugfixes and [additional members](https://github.com/mottosso/Qt.py/releases/tag/1.2.0) @@ -34,9 +35,11 @@ Qt.py enables you to write software that runs on any of the 4 supported bindings [1.2.1]: https://github.com/mottosso/Qt.py/releases/tag/1.2.1 [1.3.0]: https://github.com/mottosso/Qt.py/releases/tag/1.3.0 [1.3.9]: https://github.com/mottosso/Qt.py/releases/tag/1.3.9 +[1.4.0]: https://github.com/mottosso/Qt.py/releases/tag/1.4.0 ##### Guides +- [Qt 6 Transition Guide](#qt-6-transition-guide) - [Developing with Qt.py](https://fredrikaverpil.github.io/blog/2016/07/25/developing-with-qtpy/) - [Dealing with Maya 2017 and PySide2](https://fredrikaverpil.github.io/blog/2016/07/25/dealing-with-maya-2017-and-pyside2/) - [Vendoring Qt.py](https://fredrikaverpil.github.io/blog/2017/05/04/vendoring-qtpy/) @@ -64,6 +67,7 @@ Qt.py enables you to write software that runs on any of the 4 supported bindings - [Projects using Qt.py](#projects-using-qtpy) - [Projects similar to Qt.py](#projects-similar-to-qtpy) - [Developer guide](#developer-guide) +- [Qt 6 transition guide](#qt-6-transition-guide)

@@ -589,3 +593,174 @@ cd Qt.py python .\setup.py sdist bdist_wheel python -m twine upload .\dist\* ``` + +
+
+
+ +### Qt 6 Transition Guide + +| Replace | With | Notes +|:--------|:-----|:---------------------------- +| `QFont().setWeight(...)` | `QtCompat.QFont.setWeight(font, ...)` +| `QFont().setWeight(QFont().Bold)` | `QFont().setWeight(QFont.Bold)` | Instance of class doesn't have the enums, apparently +| `QEvent().Resize` | `QEvent.Resize` | Instance of class doesn't have the enums, seems to apply overall +| `QtCore.Qt.MidButton` | `QtCompat.QtCore.Qt.MidButton` +| `QLabel.setPixmap(str)` | `QLabel.setPixmap(QPixmap())` | Can't take a string anymore (tested in Maya 2025.0) +| `QModelIndex.child` | `QModel.index` | This one is apparently from Qt 4 and should not have been in Qt.py to begin with +| | Submit your known issues here! | + +##### Removed Members + +Many members were removed from Qt.py due to no longer existing in PySide 6. + +> If you find where they went, or think some were removed in error, please submit a pull-request! + +```json +"QtCore": [ + "QAbstractState", + "QAbstractTransition", + "QEventTransition", + "QFinalState", + "QSignalTransition", + "QTextCodec", + "QTextDecoder", + "QTextEncoder", + "QtCriticalMsg", + "QtDebugMsg", + "QtFatalMsg", + "QtSystemMsg", + "QtWarningMsg", + "qChecksum", + "QPictureIO", +], +"QtMultimedia": [ + "QAbstractVideoBuffer", + "QAbstractVideoSurface", + "QAudio", + "QAudioDeviceInfo", + "QAudioFormat", + "QAudioInput", + "QAudioOutput", + "QVideoFrame", + "QVideoSurfaceFormat" +], +"QtNetwork": [ + "QNetworkConfiguration", + "QNetworkConfigurationManager", + "QNetworkSession", +], +"QtOpenGL": [ + "QGL", + "QGLContext", + "QGLFormat", + "QGLWidget" +], +"QtSql": [ + "QSql", + "QSqlDatabase", + "QSqlDriver", + "QSqlDriverCreatorBase", + "QSqlError", + "QSqlField", + "QSqlIndex", + "QSqlQuery", + "QSqlQueryModel", + "QSqlRecord", + "QSqlRelation", + "QSqlRelationalDelegate", + "QSqlRelationalTableModel", + "QSqlResult", + "QSqlTableModel" +], +"QtSvg": [ + "QSvgGenerator", + "QSvgRenderer", +], +"QtWidgets": [ + "QActionGroup", + "QDesktopWidget", + "QDirModel", + "QKeyEventTransition", + "QMouseEventTransition", + "QUndoCommand", + "QUndoGroup", + "QUndoStack", +], +"QtX11Extras": [ + "QX11Info" +], +"QtXml": [ + "QXmlAttributes", + "QXmlContentHandler", + "QXmlDTDHandler", + "QXmlDeclHandler", + "QXmlDefaultHandler", + "QXmlEntityResolver", + "QXmlErrorHandler", + "QXmlInputSource", + "QXmlLexicalHandler", + "QXmlLocator", + "QXmlNamespaceSupport", + "QXmlParseException", + "QXmlReader", + "QXmlSimpleReader" +], +"QtXmlPatterns": [ + "QAbstractMessageHandler", + "QAbstractUriResolver", + "QAbstractXmlNodeModel", + "QAbstractXmlReceiver", + "QSourceLocation", + "QXmlFormatter", + "QXmlItem", + "QXmlName", + "QXmlNamePool", + "QXmlNodeModelIndex", + "QXmlQuery", + "QXmlResultItems", + "QXmlSchema", + "QXmlSchemaValidator", + "QXmlSerializer" +] +``` + +##### Static Members Missing from Instances + +An overall change is that instances of classes, like `QFont()` no longer provides access to static members, such as `QFont.Bold`. So things like: + +```py +font = QFont() +font.setWeight(font.Bold) +``` + +Must be replaced with: + +```py +font = QFont() +font.setWeight(QFont.Bold) +``` + +Or: + +```py +font.setWeight(type(font).Bold) +``` + +Tedious and seemingly unnecessary.. But there you have it! + +##### Notes + +Qt.py 1.4.0, released in May 2024, added support for Qt 6 whilst preserving compatibility with Qt 4 and 5. That means that in most cases, code you've already written for Qt 4 or 5 will now continue to work with Qt 6, such as Maya 2025. + +However, some changes between 5 and 6 require up-front work by you the developer to make your codebase run on Qt 6 whilst continuing to run on Qt 4 and 5. + +The above is what we know, please do submit issues and pull-request with what else you find! + +- https://github.com/mottosso/Qt.py/issues/new + +**See also** + +The official PySide2 to PySide6 transition guide, which is especially helpful since Qt.py is modeled after PySide2. + +- https://doc.qt.io/qtforpython-6/gettingstarted/porting_from2.html diff --git a/tests.py b/tests.py index fef4ea51..6439b656 100644 --- a/tests.py +++ b/tests.py @@ -21,7 +21,7 @@ except ImportError: # Fallback: Define assert_raises using unittest if the import fails import unittest - + def assert_raises(expected_exception, callable_obj=None, *args, **kwargs): """ Custom implementation of assert_raises using unittest. @@ -36,7 +36,7 @@ def assert_raises(expected_exception, callable_obj=None, *args, **kwargs): function_that_raises_some_exception() """ context = unittest.TestCase().assertRaises(expected_exception) - + # If callable_obj is provided, directly call the function with the context manager if callable_obj: with context: @@ -409,7 +409,7 @@ def test_load_ui_returntype(): import sys from Qt import QtWidgets, QtCore, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -423,7 +423,7 @@ def test_load_ui_baseinstance(): """Tests to see if the baseinstance loading loads a QWidget on properly""" import sys from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -438,7 +438,7 @@ def test_load_ui_signals(): """Tests to see if the baseinstance connects signals properly""" import sys from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -457,7 +457,7 @@ def test_load_ui_mainwindow(): import sys from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -477,7 +477,7 @@ def test_load_ui_dialog(): import sys from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -497,7 +497,7 @@ def test_load_ui_dockwidget(): import sys from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -517,7 +517,7 @@ def test_load_ui_customwidget(): import sys from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -560,12 +560,12 @@ def test_load_ui_pycustomwidget(): # append the path to ensure the future import can be loaded 'relative' to the tempdir sys.path.append(self.tempdir) - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: app = QtWidgets.QApplication.instance() - + win = QtWidgets.QMainWindow() QtCompat.loadUi(self.ui_qpycustomwidget, win) @@ -584,7 +584,7 @@ def test_load_ui_invalidpath(): """Tests to see if loadUi successfully fails on invalid paths""" import sys from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -606,7 +606,7 @@ def test_load_ui_invalidxml(): from xml.etree import ElementTree from Qt import QtWidgets, QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -624,7 +624,7 @@ def test_load_ui_existingLayoutOnDialog(): '"Dialog", which already has a layout' with ignoreQtMessageHandler([msgs]): - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -645,7 +645,7 @@ def test_load_ui_existingLayoutOnMainWindow(): '"", which already has a layout' with ignoreQtMessageHandler([msgs]): - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -666,7 +666,7 @@ def test_load_ui_existingLayoutOnDockWidget(): '"", which already has a layout' with ignoreQtMessageHandler([msgs]): - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -687,7 +687,7 @@ def test_load_ui_existingLayoutOnWidget(): '"Form", which already has a layout' with ignoreQtMessageHandler([msgs]): - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: @@ -998,7 +998,7 @@ def test_qtcompat_base_class(): import Qt from Qt import QtWidgets from Qt import QtCompat - + if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: