pythonenumspyqt5pyqt6

Are Enums optional in PyQt6


I have two python programs running in the same virtual environment using PyQt6.5 on MacOS Sonoma 14.5 and python 3.11. Both execute just fine. One uses the updated enums for PyQt6 for QMessageBox as such:

msgBox = QMessageBox(parent=self)
msgBox.setText('WHOOPS - Must have at least one (1) joystick attached')
msgBox.setStandardButtons(QMessageBox.StandardButton.Ok)
msgBox.setIcon(QMessageBox.Icon.Warning)
msgBox.exec()

while the other uses the PyQt5 format:

msgBox = QMessageBox(parent=self)
msgBox.setText('Whoops - Upload destination not set.\n' + 
    'Save animation file to implicitly set or\n' +
    'Edit metadata to explicitly set.')
msgBox.setStandardButtons(QMessageBox.Ok)
msgBox.setIcon(QMessageBox.Information)
ret = msgBox.exec_()    

Both work and I don't understand why. I do not have pyqt5 installed:

> pip freeze | grep -i pyqt
PyQt6==6.5.1
PyQt6-Qt6==6.5.1
PyQt6-sip==13.5.1

> python
>>> from PyQt5.QtCore import *
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'PyQt5'
>>> 

I do know that if I am missing the StandardButton and Icon enum specifiers in the first program it fails. The only plausible thing I can think of is that there is a fallback in PyQt6 that says if everything is PyQt5-compatible it works but that doesn't make much sense.

I expect to have to change the 40 QMessageBox instantiations to update to the PyQt6 form but haven't made the changes as the code seems to work fine. I have this code at the top of both programs to work for either PyQt5 or PyQt6:

# Import local version of PyQt, either 5 or 6
usedPyQt = None
try:
    # PyQt5 import block for all widgets
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    from PyQt5 import QtMultimedia as qm
    usedPyQt = 5
except:
    try:
        # PyQt6 import block for all widgets
        from PyQt6.QtCore import *
        from PyQt6.QtGui import *
        from PyQt6.QtWidgets import *
        from PyQt6 import QtMultimedia as qm
        usedPyQt = 6
    except:
        sys.stderr.write('WHOOPS - Unable to find PyQt5 or PyQt6 - Quitting\n')
        exit(10)

Any ideas why the PyQt5 format would work? I don't think it should.


Solution

  • tl;dr

    No, they are not. At least, not by default.

    Explanation

    The unexpected behavior is caused by another module ("qwt"), which internally requires the QtPy abstraction layer.

    Such abstraction layers (including similar projects such as Qt.py) try to overcome the change created since PyQt6, which requires every Qt enum to use its own namespace.

    This change was introduced to improve the behavior of enums in Python and the compatibility with Qt (since enums are now real python enums), but it also caused a huge drawback: despite the efforts from Qt developers to always use unique names, Python needs some object types to be declared within their own scope. In order to make PyQt enums real enums and appropriate Qt enums, they cannot be declared in the namespace of their parent class.

    At least, not safely and automatically.

    Compatibility layers such as QtPy try to work around that by programmatically recreating those attributes for the classes or modules they belong to.

    Since imported modules and their objects are normally kept in memory, externally overwritten attributes will be also seen and treated as well: if an external module [over]writes the Ok attribute for QMessageBox for the PyQt5.QtWidgets, any other script accessing the module from that point on will also "inherit" that Ok attribute (including the main one), at least if belonging to the same process environment.

    When modules such as QtPy are imported, they inspect each known Qt module (no matter the binding or the Qt version) and class, look for attributes related to flags and enums, and create references for the related module or class.

    By default, QMessageBox.Ok will raise an AttributeError when using PyQt6, unless QtPy or a similar module is loaded before using it.

    Note that:

    Conclusions

    Since Qt6, we should always use the full namespace for all enums in Python.

    Yet, Qt is known to use very long and verbose naming conventions, so the code can easily become cumbersome and difficult to read/debug.

    If your program needs fast loading timing, you should carefully consider if you do need the above mentioned compatibility layers.

    If that is not an important necessity and code simplicity/readability is more important, you can use the above (including "patching" done on your own), as long as you're aware of their possible drawbacks.

    In any case, yes: by default, PyQt6 (and, possibly, PySide in the upcoming future) always needs the full namespace syntax for enums.