pythonenumspyqtpyqt5pyqt6

Is there any way to obtain a listing of enum names and values in a typedef for QFlags?


As such I (questioner) am no longer looking for an answer but I am not sure on what grounds to recommend closing. It could possibly be helpful to someone.

Here's an example of what I mean (based on Qt5 documentation):

This type is used to signify an object's orientation.

Constant Value
Qt::Horizontal 0x1
Qt::Vertical 0x2

Orientation is used with QScrollBar for example.
The Orientations type is a typedef for QFlags<Orientation>. It stores an OR combination of Orientation values.

What I'm trying to obtain (automatically, programmatically) is a dict like this:

{
    "Horizontal": 1,
    "Vertical": 2
}

...with a view to giving a "readable" representation of the result which delivers a value of this Orientation enum.

Obviously these values are a subset of "generic" values from an enormous list of QtCore.Qt constants, with values which are often int, often equal to 1 or to 2, and often ORed together to produce binary values as results. This Orientation enum appears to be used in many contexts. In my particular case, I want to find the result of QSizePolicy.expandingDirections(), which returns a value in this Orientation enum.

I have tried exploring both the Qt.Orientation enum class and the Qt.Orientations "flags" class, i.e. I've looked at dir(Qt.Orientation), etc.

Usually dir on an enum class will show all the possible constant values. Not in this case. Is there any way of obtaining them?

The problem outlined above only applies to PyQt5, but doesn't in PyQt6.


Solution

  • The PyQt6 way

    Since PyQt6 all Qt enums are real Python enums, which makes such job much easier.

    For example:

    {e.name:e.value for e in Qt.Orientation}
    

    Yet, this hides some of the aliases and OR flags used in the enum, and even using dir() would be pointless: for instance, dir(Qt.DockWidgetArea) doesn't include AllDockWidgetAreas nor NoDockWidgetArea.

    A more complete approach makes use of enum's __members__, which returns a mapping with every enum name and relative value, including aliases:

    {e.name:e.value for e in Qt.DockWidgetArea}
    >>> {
        'LeftDockWidgetArea': 1, 
        'RightDockWidgetArea': 2, 
        'TopDockWidgetArea': 4,
        'BottomDockWidgetArea': 8
    }
    
    {n:getattr(Qt.DockWidgetArea, n).value for n in Qt.DockWidgetArea.__members__}
    >>> {
        'LeftDockWidgetArea': 1, 
        'RightDockWidgetArea': 2, 
        'TopDockWidgetArea': 4
        'BottomDockWidgetArea': 8, 
        'AllDockWidgetAreas': 15, 
        'NoDockWidgetArea': 0
    }
    
    

    The above only works with PyQt6, though, and will raise exceptions in PyQt5 (including the latest versions, even though they accept the namespace behavior used for PyQt6), because it is a "fake enum": therefore, it is not iterable, and it lacks the __members__ attribute.

    Issues with PyQt5

    The older PyQt5 implements Qt enums as simpler attribute members; while it makes code writing/reading easier, it also results in implementation issues like yours: getting a comprehensive dictionary of each enum is much more difficult (not to mention missing proper introspection useful for many aspects, including debugging: there is no name relation for any given enum or flag value).

    This is one of the reasons behind the PyQt6 change (also added in PySide6, even though it's still currently using a "forgiveness" mode), which makes enum work exactly as expected, with the cost of having more long and verbose names; if possible, switch to PyQt6, as Qt5 has already been declared obsolete.

    Another important problem is that those attributes are not always consistently exposed for all classes, and in some cases some of them are even missing depending on the query (similarly to the DockWidgetArea case above), which is why using dir() isn't always appropriate.

    A possible solution

    Since in PyQt5 the enum values are always in the "parent" name space (eg: Qt, QAbstractItemView, etc.), the only possibility is to iter through all members in that namespace (their __dict__), and check their types.

    In PyQt5, enum types always respect the "Qualified name", accessible through the __qualname__ attribute (except for very old PyQt5 versions): no matter the import approach, things like QtCore.Qt.AlignmentFlag will always have Qt.AlignmentFlag as their __qualname__. This allows us to get the namespace (eg: Qt) in which the enum type exists.

    As long as we always provide a "Qt enum type", then, the following simple example may suffice:

    def getEnumDict(enum):
        base = enum.__qualname__.rsplit('.', maxsplit=1)[0]
        namespace = globals()[base]
        res = {}
        for k, v in namespace.__dict__.items():
            if isinstance(v, enum):
                res[k] = v
        return res
    

    The above will always return a mapping with all enum names and values, including aliases and OR flags registered within the enum, as long as the argument refers to an enum parent namespace that is accessible in the global namespace:

    print(simpleEnumDict(Qt.DockWidgetArea))
    >>> {
        'AllDockWidgetAreas': 15, 'TopDockWidgetArea': 4, 
        'NoDockWidgetArea': 0, 'BottomDockWidgetArea': 8, 
        'DockWidgetArea_Mask': 15, 'LeftDockWidgetArea': 1, 
        'RightDockWidgetArea': 2
    }
    

    The order of the results is obviously unreliable, being it a mapping (a dict). I'll leave to the reader to find appropriate ways to sort items according to their needs.

    Drawbacks

    There are some enum and flag types that have inconsistent names. Most importantly, due to historical reasons, some widely used flags/enums are named opposed to their "code-logic" interpretation: flag types have generic and shorter names, while their enum type name ends with "Flag", while in other cases the names are even inconsistent; for example:

    Always remember to check the documentation and verify which name refers to a flag or an enum type: these object names are hardcoded and, as far as I know, their relations cannot be inspected (eg: you cannot programmatically get an Qt.AlignmentFlag enum type from a Qt.Alignment flag type), so you may consider creating a global dictionary for such exceptions to be considered when doing such level of introspection.

    Obviously, the simplest solution would be to always ensure that you call the function using a Qt enum, not a flag name, nor a bitwise composition of enums (which results in a flag).

    That may not always be possible, though, especially for complex cases that may require dynamic inspection. Specifically, if you need to get a mapping for a flag or flag type received from a property getter of an externally created Qt object, there is no immediate way to get that mapping of the base enum type.

    This means that we need to take care of accepting:

    We also need to consider the inaccessible relations between inconsistent names: as explained above, while most flags are simply named in the plural form of their enum, some flag type names cannot be easily converted to enum type names. Some plural forms are also irregular (eg: Query->Queries).

    Another important aspect to be aware of is that the above function only considers names and modules available in the global scope: supposing that the above function is defined in some separate module/file, and used arbitrarily as an "utility function", it's quite possible that it could be called with an enum belonging to a namespace of which the local scope is not aware of.

    The following example is an attempt to improve of the above, trying to consider all those aspects, while also allowing to accept any enum/flag, whether they are types or values; it does it by:

    NOTE: due to the extensive amount of Qt modules and classes, the hardcoded dict of exception may need to be updated.

    from PyQt5.QtCore import Qt
    
    # store the private `sip.enumtype` and `sip.wrappertype` types, later used for
    # comparison, in order to avoid arbitrary queries
    QtEnumType = type(Qt.ItemFlag)
    QtFlagType = type(Qt.ItemFlags)
    
    # exceptions for enums/flags with inconsistent names, stored as {flag:enum}
    QtEnumExceptions = {
        # QtCore.Qt
        'WindowFlags': 'WindowType', 
        'Alignment': 'AlignmentFlag', 
        'SplitBehavior': 'SplitBehaviorFlags', 
        'InputMethodQueries': 'InputMethodQuery', 
        # QtWidgets.QAbstractSpinBox
        'StepEnabled': 'StepEnabledFlag', 
        # QtWidgets.QStyle
        'State': 'StateFlag', 
        # further inconsistencies may exist, please add comments
    }
    
    def getEnumDict(enum):
        # get the enum/flag type for enum/flag values
        if isinstance(enum.__class__, (QtEnumType, QtFlagType)):
            enum = enum.__class__
    
        # if it is a flag type, try to get the enum type from it, and the 
        # "parent" object (the namespace) for which the enum is a member
        if isinstance(enum, QtFlagType):
            base = enum.__qualname__.rsplit('.', maxsplit=1)[0]
            try:
                namespace = globals()[base]
            except KeyError:
                import sys
                for _name, _mod in sys.modules.items():
                    if _name.startswith('PyQt5') and hasattr(_mod, base):
                        namespace = getattr(_mod, base)
                        break
                else:
                    raise TypeError('{} not found in any module'.format(
                        enum.__name__))
            if (
                hasattr(namespace, enum.__name__)
                and isinstance(getattr(namespace, enum.__name__), QtEnumType)
            ):
                enum = getattr(namespace, enum.__name__)
            else:
                if enum.__name__ in QtEnumExceptions:
                    name = QtEnumExceptions[enum.__name__]
                elif enum.__name__.endswith('s'):
                    name = enum.__name__[:-1]
                elif not hasattr(namespace, name):
                    raise TypeError('{} not found in {}'.format(
                        enum.__name__, namespace.__name__))
                enum = getattr(namespace, name)
        else:
            base = enum.__qualname__.rsplit('.', maxsplit=1)[0]
            namespace = globals()[base]
    
        res = {}
        for k, v in namespace.__dict__.items():
            if isinstance(v, enum):
                res[k] = v
        return res
    

    Some output examples:

    # note that I used "DockWidgetAreas", which is a flag type
    >>> print(getEnumDict(Qt.DockWidgetAreas))
    {
        'BottomDockWidgetArea': 8, 'NoDockWidgetArea': 0, 
        'DockWidgetArea_Mask': 15, 'LeftDockWidgetArea': 1, 
        'RightDockWidgetArea': 2, 'AllDockWidgetAreas': 15, 
        'TopDockWidgetArea': 4
    }
    
    # calling the function using arguments from a different namespace
    >>> from PyQt5 import QtCore
    >>> print(getEnumDict(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop))
    {
        'AlignRight': 2, 'AlignBaseline': 256, 'AlignHorizontal_Mask': 31, 
        'AlignVertical_Mask': 480, 'AlignLeading': 1, 'AlignTrailing': 2, 
        'AlignCenter': 132, 'AlignJustify': 8, 'AlignVCenter': 128, 
        'AlignLeft': 1, 'AlignTop': 32, 'AlignBottom': 64, 
        'AlignAbsolute': 16, 'AlignHCenter': 4
    }
    
    
    >>> from PyQt5.QtWidgets import *
    >>> app = QApplication([])
    >>> print(getEnumDict(QWidget().windowFlags()))
    {
        'ToolTip': 13, 'Window': 1, 'MSWindowsFixedSizeDialogHint': 256,
        ..., # contents skipped for convenience
        'NoDropShadowWindowHint': 1073741824
    }