PySide2 non-migration
I was about to finish another sprint (implementing a saner copy&paste system, as I hinted
earlier). So I ran mypy
over the sources and saw that it
complains about a bunch of things, which are caused by an issue with PyQt5
.
PyQt5
seems to not like classes, which explicitly inherit from multiple "Q
" classes.
E.g. something like that:
class SomeMixin(QtCore.QWidget):
def someMethod(self):
self.update()
class SomeLineEdit(SomeMixin, QtCore.QLineEdit):
...
QLineEdit
is a subclass of QWidget
, so the above should be perfectly fine. But not for
PyQt5
. When I tried to make a minimal example, it just segfaulted, but I vaguely remember seeing
some exception being raised. To workaround that, I had to make such mixin classes not inherit from a
"Q
" class (e.g. just object
), which is perfectly fine at runtime. But mypy
has no idea that
this mixin is only used with some kind of QWidget
and that self.update()
is a valid method. So I
have to make mypy
suppress all those false-positive warnings, which makes the code look ugly, and
I lose type checking for any real issues.
Besides that PyQt5
's support for type annotations was not so great anyway. I'm maintaining my own
set of stubs for it, which are based on the original PyQt5
stubs, but with lots of manual tweaks
to make them actually useful.
So when I saw that Qt
now officially includes Python
bindings in the shape of
PySide2
, I was interested in evaluating a migration. There was
an open ticket for adding stubs, but that was fixed
for version 5.13.
It's easy to install via pip
, so I made a quick test to see if PySide2
was also suffering from
the inheritance issue above. It wasn't, so let's give a real migration a try.
PySide2
looks sufficiently similar to PyQt5
, that you could think a simple s/PyQt5/PySide2/
might already be enough. For some projects it might already be that, plus some more trivial renames
like pyqtSignal
→Signal
or pyqtProperty
→Property
.
But there are more subtle differences, which make it much harder (at least for noisicaä)...
- The
connect()
method of signals returns a boolean. Apparently it can fail (no idea under which conditions...), so you should probably check the return value. Which is very unpythonic - it should just raise an exception. And inPyQt5
it returns aConnection
instance, which can be passed todisconnect()
. That's the only way to disconnect alambda
function (without carrying a reference to that function around), which I do a lot, so that's annoying. - The way how a class level
Signal
attribute gets turned into an instance levelSignalInstance
attribute looks odd. InPyQt5
thepyqtSignal
implements the property protocol, so accessing the attribute on an instance returns the appropriatepyqtBoundSignal
instance. InPySide2
the metaclass does somehow create aSignalInstance
for eachSignal
and injects that into the instance's__dict__
, though I haven't really figured out when this actually happens. The problem for me is that there doesn't seem to be a way to get from anSignal
instance and the owning object to theSignalInstance
. Which I do in some helper function, which saves me a lot of boilerplate code. I could find a workaround, but that is really ugly... Parsing thestr()
of theSignal
instance... I won't say more. Too embarrassing. - Signals cannot use an
Enum
as the type. That bug is already 5 years old and for an ancient version ofPySide
. The workaround is to declare those signals with typeobject
(and lose some type safety). QSettings.value()
does not return the default value, if it is0
orFalse
. That seems like a plain and simple bug.
Those are the issues, which I have found so far. At least the unittests are now passing, but that
doesn't really mean that much, because the test coverage for the UI code isn't that great. And
getting there wasn't easy, because PySide2
is also very crash happy. So instead of a nice Python
exception telling me where and what was wrong, I just got the unhelpful "Segmentation fault (core
dumped)" message. I had to perform the "install from source" dance to get a version with debug
symbols, so gdb
could at least tell me something about the problem.
And now I'm getting this exception:
Traceback (most recent call last):
[...]
File "/home/pink/noisicaa/build/noisicaa/ui/control_value_connector.py", line 62, in __init__
self.valueChanged.connect(self.__onValueEdited)
TypeError: connect() takes 3 positional arguments but 4 were given
Sorry, but that simply does not make sense.
It would have been nice to have a viable alternative to PyQt5
, and perhaps PySide2
is that, if
you're starting a new project from scratch. But migrating noisicaä does not seem worth the effort,
at least not now. There is at least some hope that development of PySide2
gets a boost, at least
for a while, now that it has been included in the Qt
canon. Let's give it some more time.