(Py)Qt6 readiness: Use typed enums#4833
Conversation
In PyQt6, in order to provide better type safety, enum values are no longer all lumped together under their owner classes, or thrown in a big pile inside `PyQt6.QtCore.Qt`. Now, every enum has its own, distinct type, which means (among other things) that enum values can be type checked. * Aspect ratio modes have the type `Qt.AspectRatioMode`. * Brush styles have the distinct type `Qt.BrushStyle`. So in PyQt6: - `Qt.KeepAspectRatio` becomes `Qt.AspectRatioMode.KeepAspectRatio` - `Qt.SolidPattern` becomes `Qt.BrushStyle.SolidPattern` - `QImage.Format_ARGB32` becomes `QImage.ImageFormat.Format_ARGB32` While still staying within PyQt5, convert our enum accesses over to the longer, class-member form of access, so that the code is more future-proof. (Typing benefits still aren't available until PyQt6. Even if accessed as class members, two members from different enums with the same underlying numeric value will test as equal.)
| self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) | ||
| self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) | ||
| self.cancel_button = self.buttonBox.addButton( | ||
| QDialogButtonBox.StandardButtons.Cancel) |
There was a problem hiding this comment.
Will StanardButtons.Cancel be translated? Our previous code had a manually translated _('Cancel')
| self.setIndentation(0) | ||
| self.setSelectionBehavior(QTreeView.SelectRows) | ||
| self.setSelectionBehavior(QAbstractItemView.SelectRows) | ||
| self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) |
There was a problem hiding this comment.
This line looks like a duplicate
There was a problem hiding this comment.
Good call, my regular-expression-based updates even turned them into identical duplicates, one can clearly go.
| def Cancel(self): | ||
| """Cancel the current render, if any""" | ||
| #QMetaObject.invokeMethod(self.worker, 'Cancel', Qt.DirectConnection) | ||
| #QMetaObject.invokeMethod(self.worker, 'Cancel', Qt.ConnectionType.DirectConnection) |
| self.setSelectionBehavior(QAbstractItemView.SelectRows) | ||
| self.setSelectionMode(QAbstractItemView.ExtendedSelection) | ||
| self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) | ||
| self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) |
| self.setSelectionBehavior(QAbstractItemView.SelectRows) | ||
| self.setSelectionMode(QAbstractItemView.ExtendedSelection) | ||
| self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) | ||
| self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) |
| self.win.dockProperties.visibilityChanged.connect(functools.partial(self.process, "dockProperties")) | ||
| self.win.dockVideo.visibilityChanged.connect(functools.partial(self.process, "dockVideo")) | ||
| self.win.dockEmojis.visibilityChanged.connect(functools.partial(self.process, "dockEmojis")) | ||
| for d in [ |
There was a problem hiding this comment.
This is actually more lines of code than he original, with the humble brag of not repeating any code, lol
There was a problem hiding this comment.
I ask you... which version is easier to glance at and understand?
There was a problem hiding this comment.
Fair question, but I'll throw one back at you: Which version will crash if self.win doesn't have a dockProperties attribute, or has one that's not a QDockWidget / lacks a visibilityChanged signal, and which version won't? The intent isn't merely to avoid cut-and-paste, it's to avoid assumptions about the structure of external classes.
|
A few Codacy issues:
|
|
@ferdnyc Thanks for taking a shot at this one! It's a big one, which seems fairly high-risk (as with any PR that changes 40+ files I suppose, lol). I'll give this a try locally (and hopefully @JacksonRG can do the same local testing). Let us run through a full set of regression tests, to verify we don't see a bunch of stack-traces. 👍 We'll keep you posted! |
…his is mostly work from ferdnyc and #4833. I've made a few additional changes (formatting, bug fixes)
|
Re-opening this PR, and waiting for changes from branch: https://github.com/OpenShot/openshot-qt/tree/qt6-prep-enums to be applied to this one. |
|
Merge conflicts have been detected on this PR, please resolve. |
|
Merge conflicts have been detected on this PR, please resolve. |
|
@ferdnyc so what's the status of this pull request? I found only two MR both closed and it looks like job was stalled. Still, some distributions (e.g. Gentoo) already try to avoid qt5 (https://bugs.gentoo.org/925718) for a good reason, so this job is appreciated! |
|
@pva There is no status. I am no longer involved in the project. |
@jonoomph @JacksonRG et al..
I've been plugging away, the past few weeks, at trying to come up with a PyQt6-compatible OpenShot. It's very slow going. (Libopenshot was easy, literally one line in the GUI code for the
openshot-playertool had to be changed. That's it! Other than that, it's already fully Qt6-compatible. OpenShot? Not so much.)The changes necessary to make it work are huge, and because Riverbank chose to make a clean break and drop everything that's deprecated from PyQt5 (even the stuff that Qt, itself, has kept in the Qt5 Compatibility Layer module), it may not be (realistically) possible to simultaneously support PyQt5 and PyQt6 in the same codebase. Branching and maintaining parallel versions, at least for a period, may be the only realistic option, unfortunately.
Because let's face it, eventually OpenShot will need to make the move to PyQt6. So, there are plenty of things that can be done now to get it at least "more ready", since a lot of mandatory-in-PyQt6 features are already available today in PyQt5, and have been for some time. We just haven't adopted them yet.
I'm going to make an effort to push as many of those adaptations as possible back into the PyQt5 code, so that the PyQ6-compatibility transition is at least less painful and world-breaking. This PR is the first of those changes.
I'm also going to make a plea for a bit of fast-tracking on this one, because while it's very focused, it's also very sweeping, and edit conflicts will crop up quickly if it's left open and unmerged while other changes get committed ahead of it. Obviously some conflicts are likely to come up, and I'll fix those as they come, but there is a certain degree of time-sensitivity to this PR, if no these changes. (Which are, in one sense, admittedly "totally unnecessary", but I hope I've adequately sold the reasons why they're still a good idea.)
Adoption of typed enums.
In PyQt6, in order to provide better type safety, enum values are no longer all lumped together under their owner classes, or thrown in a big pile inside
PyQt6.QtCore.Qt.Now, every enum has its own, distinct type, which means that enum
values can be type checked. It's no longer possible to do something
crazy like call
QImage.scale(newSize, Qt.SolidPattern)and havethat work, because
Qt.SolidPatternhas the value1which isinterpreted by
QImage::scale()as the modeQt.KeepAspectRatio.Mypy won't catch it either, because in PyQt5
Qt.SolidPattern == Qt.KeepAspectRatioreturnsTrue.After all, they both have the integer value
1.Safety through precise typing
In PyQt6 the enums have all been separated from each other.
They're partitioned into type silos, each type holding only
a set of members that represent all the possible values for
that specific enum.So even though
KeepAspectRatiohas the value1in theQt.AspectRatioModeenum, andSolidPatternis1in theQt.BrushStyleenum, in PyQt6 they won't be equal or interchangeable with each other, because they'll have their own, separate type classes.Aspect ratio modes have the type
Qt.AspectRatioMode.Brush styles have the distinct type
Qt.BrushStyle....Which means they have to be accessed that way. Exclusively that way, in PyQt6.
So, come PyQt6:
Qt.KeepAspectRatiobecomesQt.AspectRatioMode.KeepAspectRatioQt.SolidPatternbecomesQt.BrushStyle.SolidPatternQImage.Format_ARGB32becomesQImage.ImageFormat.Format_ARGB32And so on, for Every. Single. Enum!
Like
Qt.green? Instead cozy up toQt.GlobalColor.green.Friends with
Qt.Key_Escape? EmbraceQt.Key.Key_Escape.Want
Qt.AA_ShareOpenGLContexts? Hope you're ready forQt.ApplicationAttribute.AA_ShareOpenGLContexts.It does make for much stronger typing, which when combined with
mypy and code that's type hinted means stronger code, period.
This comparison:
Qt.AspectRatioMode.KeepAspectRatio == Qt.BrushStyle.SolidPatternwill now properly return
False, yay!The per-enum classes are already present in PyQt5, they're just
not type-siloed into distinct, non-equivalent classes. (That
previous comparison still returns
True, in PyQt5.) But they'rethere, so we can choose to start using them now, since we'll
HAVE to eventually.
But it's definitely a big adjustment. (At least by making the
switch before it's forced, we have a safety net in the event
that we've missed any old-style enum accesses. Instead of a
traceback, the code will still function as expected. Only in
PyQt6 do the old-style "short" enums get taken away.)
Secondary changes
The PR also carries a couple of minor additional changes that got swept up in the enum typing. (Meaning, they were changes that also appeared on lines where enums changed, so they got committed together.)
It replaces all of our remaining
QRegExpusage withQRegularExpression. QRegExp has been deprecated long time now, and it's removed from PyQt6. QRegularExpression has been the official replacement since Qt 5.12.It replaces some uses of outdated, aliased, trailing-underscore methods with their unaliased counterparts. (Mostly
QDialog/QMessageBox.exec_()calls, which haven't required the trailing underscore for, again, a long time. Since Python 3.0, in fact. Python 2 was picky about builtin words as class methods, but Python 3 isn't, so it's fine to just callQDialog.exec()and those aliases have been deprecated for a while. They're also removed in PyQt6.)I'll submit a separate PR at some point that fixes the rest of them, but a few got swept up in this change.
Note, BTW, that certain methods are still aliased, if they're Python KEYWORDS. Like,
QWidget::raise()is stillPyQt{5,6}.QtWidgets.QWidget.raise_(), because calling a methodraiseis still a terrible idea in any version of Python.