-
Notifications
You must be signed in to change notification settings - Fork 253
Add PyQt6 support #431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add PyQt6 support #431
Conversation
I wanted to ask how to solve the two failing PyQt6 tests. It looks like PyQt6 is capturing the Do we accept this limitation and I just update the tests so they pass, or should I look into extracting the original exception and raise it in the The exception raised by the test: ERROR: tests.test_load_ui_invalidpath
Tests to see if loadUi successfully fails on invalid paths
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/runner/.local/lib/python3.10/site-packages/PyQt6/uic/ui_file.py", line 35, in __init__
document = ElementTree.parse(ui_file)
File "/usr/lib/python3.10/xml/etree/ElementTree.py", line 1222, in parse
tree.parse(source, parser)
File "/usr/lib/python3.10/xml/etree/ElementTree.py", line 569, in parse
source = open(source, "rb")
FileNotFoundError: [Errno 2] No such file or directory: 'made/up/path'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/runner/work/Qt.py/Qt.py/tests.py", line 622, in test_load_ui_invalidpath
assert_raises(IOError, QtCompat.loadUi, 'made/up/path')
File "/usr/lib/python3.10/unittest/case.py", line 738, in assertRaises
return context.handle('assertRaises', args, kwargs)
File "/usr/lib/python3.10/unittest/case.py", line 201, in handle
callable_obj(*args, **kwargs)
File "/home/runner/work/Qt.py/Qt.py/Qt.py", line 798, in _loadUi
return Qt._uic.loadUi(uifile, baseinstance)
File "/home/runner/.local/lib/python3.10/site-packages/PyQt6/uic/load_ui.py", line 86, in loadUi
return DynamicUILoader(package).loadUi(uifile, baseinstance)
File "/home/runner/.local/lib/python3.10/site-packages/PyQt6/uic/Loader/loader.py", line 62, in loadUi
return self.parse(filename)
File "/home/runner/.local/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 995, in parse
ui_file = UIFile(filename)
File "/home/runner/.local/lib/python3.10/site-packages/PyQt6/uic/ui_file.py", line 37, in __init__
self._raise_exception("invalid Qt Designer file", detail=str(e))
File "/home/runner/.local/lib/python3.10/site-packages/PyQt6/uic/ui_file.py", line 92, in _raise_exception
raise UIFileException(self._ui_file, message, detail=detail)
PyQt6.uic.exceptions.UIFileException: made/up/path: invalid Qt Designer file: [Errno 2] No such file or directory: 'made/up/path' |
CAVEATS.md
Outdated
#### 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](https://doc.qt.io/qtforpython-6/considerations.html#doing-a-smooth-transition-from-the-old-enums) 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`. | ||
|
||
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 Qt6. | ||
|
||
For Qt5 and Qt6 support, for maximum compatibility 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. | ||
|
||
This https://stackoverflow.com/a/72658216 post contains a script that makes it easy | ||
to convert code to fully qualified enums. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this as the biggest issue with PyQt6 in Qt.py. From experience as one of the few people using PyQt bindings most devs only test their code in PySide and won't run into the missing short enum's.
Once I got into running the tests, I discovered that Qt4 and the older versions of Qt5 in the VFXPLATFORM docker images don't support the new enum names. So this can't just be solved by dropping Qt4 support, we would have to consider dropping support for older versions of Qt5.
I considered building some sort of compatibility mapping of all enums, but it's not just QtCore.Qt
that has this enum issue. A lot of classes have their own Enums like QFont.Weight.Bold
. Alternatively, adding a enum lookup method like get_enum
in test.py could work but seems unlikely to be used by users unless somehow forced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just read up on #413 (comment) and looks like we are planning to drop Qt4 support. If we also drop support for older versions of Qt5 then we can just encourage using the fully qualified enum names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been using the https://stackoverflow.com/a/72658216 and ran into problems with it not supporting sub-classes. For example it didn't find QTreeWidget.ScrollPerPixel
because that is defined on its superclass QAbstractItemView.ScrollMode.ScrollPerPixel
.
We have a lot of code we will need to convert so I'm working on improving it using ast
to parse the .pyi file and adding support for using PySide6 which the original would not have supported.
I found out that Qt.py has a cli that I never noticed so I'm thinking of adding it there. Though this may be its own merge request.
@@ -603,17 +603,21 @@ python -m twine upload .\dist\* | |||
|
|||
| Replace | With | Notes | |||
|:--------|:-----|:---------------------------- | |||
| `QFont().fromString(...)` | `QtCompat.QFont.fromString(font, ...)` | Qt6 adds extra data to font strings and changes weight if you want to pass a font string from Qt6 to Qt4/5 use this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes it possible to take a font string saved in Qt6 easily be loaded in Qt5. By default it would log a warning and not load the font.
>>> fs = "Arial,7,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
>>> from PySide2.QtGui import QFont
>>> f = QFont()
>>> f.fromString(fs)
QFont::fromString: Invalid description 'Arial,7,-1,5,400,0,0,0,0,0,0,0,0,0,0,1'
False
>>> f
<PySide2.QtGui.QFont(,-1,-1,5,50,0,0,0,0,0) at 0x000001196C664480>
>>> from PyQt5.QtGui import QFont
>>> f = QFont()
>>> f.fromString(fs)
QFont::fromString: Invalid description 'Arial,7,-1,5,400,0,0,0,0,0,0,0,0,0,0,1'
False
>>> f
<PyQt5.QtGui.QFont object at 0x00000119698EFD80>
| `QtCore.Qt.MidButton` | `QtCompat.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 | ||
| `QApplication.exec_()` | `QtCompat.QApplication.exec_()` | `exec` is no longer a reserved keyword in python 3 so Qt6 is removing the underscore from `exec_`. Qt.py is using exec_ to preserve compatibility with python 2. The same applies for `QCoreApplication`. | ||
| `QAction().setShortcut(Qt.SHIFT\|Qt.Key_Backspace)` | `QAction().setShortcut(QKeySequence(Qt.Modifier.SHIFT\|Qt.Key.Key_Backspace))` | PyQt6 doesn't accept `QKeyCombination` objects for shortcuts. To work around this cast them to `QKeySequence` objects. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another PyQt6 only issue. When building keyboard shortcuts, the PyQt6 bindings don't accept the new QKeyCombination
objects so you have to cast them to QKeySequence
before passing them.
PySide6:
>>> from PySide6 import QtCore, QtGui
>>> act = QtGui.QAction()
>>> act.setShortcut(QtCore.Qt.Modifier.SHIFT | QtCore.Qt.Key.Key_Backspace)
PyQt6:
>>> from PySide6 import QtCore, QtGui
>>> QtGui.QAction().setShortcut(QtCore.Qt.Modifier.SHIFT | QtCore.Qt.Key.Key_Backspace)
>>> from PyQt6 import QtCore, QtGui
>>> QtGui.QAction().setShortcut(QtCore.Qt.Modifier.SHIFT | QtCore.Qt.Key.Key_Backspace)
Traceback (most recent call last):
File "C:\blur\dev\preditor\preditor\gui\console.py", line 586, in keyPressEvent
self.executeCommand()
File "C:\blur\dev\preditor\preditor\gui\console.py", line 469, in executeCommand
cmdresult, wasEval = self.executeString(commandText)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\blur\dev\preditor\preditor\gui\console.py", line 422, in executeString
cmdresult = eval(compiled, __main__.__dict__, __main__.__dict__)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<ConsolePrEdit>", line 1, in <module>
TypeError: setShortcut(self, shortcut: Union[QKeySequence, QKeySequence.StandardKey, Optional[str], int]): argument 1 has unexpected type 'QKeyCombination'
PyQt6 fix:
>>> QtGui.QAction().setShortcut(QtGui.QKeySequence(QtCore.Qt.Modifier.SHIFT | QtCore.Qt.Key.Key_Backspace))
| `QtCore.Qt.MidButton` | `QtCompat.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 | ||
| `QApplication.exec_()` | `QtCompat.QApplication.exec_()` | `exec` is no longer a reserved keyword in python 3 so Qt6 is removing the underscore from `exec_`. Qt.py is using exec_ to preserve compatibility with python 2. The same applies for `QCoreApplication`. | ||
| `QAction().setShortcut(Qt.SHIFT\|Qt.Key_Backspace)` | `QAction().setShortcut(QKeySequence(Qt.Modifier.SHIFT\|Qt.Key.Key_Backspace))` | PyQt6 doesn't accept `QKeyCombination` objects for shortcuts. To work around this cast them to `QKeySequence` objects. | ||
| int(QMainWindow().windowState()) | QtCompat.enumValue(QMainWindow().windowState()) | Consistent interface to convert an enum to an `int` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a compatibility function to convert enums to int values.
For user prefs I often store the int value of flags and enums in prefs files and restore them. The new enum implementation for both PySide6 and PyQt6 require converting to int differently. For PyQt4/5 you can just cast them to ints(for some bindings they are already ints). In Qt6 you have to call enum.value
to get the enum value.
>>> from PySide6 import QtCore
>>> int(QtCore.Qt.WindowState.WindowActive)
Traceback (most recent call last):
File "C:\blur\dev\preditor\preditor\gui\console.py", line 586, in keyPressEvent
self.executeCommand()
File "C:\blur\dev\preditor\preditor\gui\console.py", line 469, in executeCommand
cmdresult, wasEval = self.executeString(commandText)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\blur\dev\preditor\preditor\gui\console.py", line 422, in executeString
cmdresult = eval(compiled, __main__.__dict__, __main__.__dict__)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<ConsolePrEdit>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'WindowState'
>>> QtCore.Qt.WindowState.WindowActive.value
8
Great initiative. I'm in the start of a house-move at the moment and will be slow to reply, but I would like invite others to join in on the conversation too so if you are reading this, do chime in. |
- Don't mutate the current test suite binding when testing None. This was causing the tests after it runs to use the wrong binding. - Restore the original working directory to prevent file lock issues when testing on windows. - run_tests.py now includes the failing bindings in the summary exception for ease of debugging.
- QtCompat.QFont.fromString adds support for Qt6 font strings in Qt4/5. - QtCompat.enumValue interface for converting Qt enums to int values.
This removes the UIFileException exception if the cause exception is not PyQt6 specific. This should make it easier to handle exceptions consistently between bindings. Updated readme documentation per mottosso#429
I chose the latter. For known exceptions we are testing for |
b4505d8
to
91d707f
Compare
Tests for Qt_convert_enum.py are written but require at least python 3.7 to run successfully. These tests will be enabled once the underlying testing suite is updated to current requirements.
Add the binding information for PyQt6 support.
PyQt6 seems to have removed a lot of backwards compatible support that PySide6 has not. This introduces a few issues that should be figured out before this is merged. I will add additional notes calling out the specifics.