PyQt6: QEvent.type() returns int instead of QEvent.Type

Florian Bruhin me at the-compiler.org
Thu Apr 28 20:16:31 BST 2022


On Thu, Apr 28, 2022 at 09:42:46AM +0100, Phil Thompson wrote:
> On 27/04/2022 17:25, Florian Bruhin wrote:
> > On Wed, Apr 27, 2022 at 03:58:22PM +0100, Phil Thompson wrote:
> > > 
> > > On 26/04/2022 20:49, Florian Bruhin wrote:
> > > > On Tue, Apr 26, 2022 at 09:36:25PM +0200, Florian Bruhin wrote:
> > > > > On Tue, Apr 26, 2022 at 09:19:05AM +0100, Phil Thompson wrote:
> > > > > >
> > > > > > On 21/04/2022 10:37, Florian Bruhin wrote:
> > > > > > > Hey,
> > > > > > >
> > > > > > > With PyQt5:
> > > > > > >
> > > > > > >     >>> evtype = QEvent(QEvent.Type.User).type()
> > > > > > >     >>> evtype
> > > > > > >     1000
> > > > > > >     >>> type(evtype)
> > > > > > >     <class 'PyQt5.QtCore.QEvent.Type'>
> > > > > > >
> > > > > > > and even:
> > > > > > >
> > > > > > >     >>> evtype = QEvent(QEvent.Type.User + 1).type()
> > > > > > >     >>> evtype
> > > > > > >     1001
> > > > > > >     >>> type(evtype)
> > > > > > >     <class 'PyQt5.QtCore.QEvent.Type'>
> > > > > > >
> > > > > > > but with PyQt6, the type information gets lost:
> > > > > > >
> > > > > > >     >>> evtype = QEvent(QEvent.Type.User).type()
> > > > > > >     >>> evtype
> > > > > > >     1000
> > > > > > >     >>> type(evtype)
> > > > > > >     <class 'int'>
> > > > > > >
> > > > > > > From what I understand, it's not possible to convert arbitrary values
> > > > > > > into an IntEnum:
> > > > > > >
> > > > > > >     >>> QEvent.Type(QEvent.Type.User + 1)
> > > > > > >     [...]
> > > > > > >     ValueError: 1001 is not a valid QEvent.Type
> > > > > > >
> > > > > > > But least for types which are part of QEvent.Type, calling .type()
> > > > > > > should perhaps return the IntEnum value again instead of falling back to
> > > > > > > an int without any type information? Given that IntEnum is an int
> > > > > > > subclass, this should be a backwards-compatible change too.
> > > > > >
> > > > > > I've been adopting a piecemeal approach to this sort of thing so far. For
> > > > > > example having QEvent.type() return an int and adding an extra QEvent ctor
> > > > > > that accepts an int, and similar with gesture types. However the issue you
> > > > > > point out in your other email (new enum members in later versions of Qt) is
> > > > > > something I hadn't considered.
> > > > > >
> > > > > > I think the solution is to take the approach you suggest above and apply it
> > > > > > to all enums (no matter what their base type is). In other words, when
> > > > > > converting from Python to a C++ enum both a Python enum and an int will be
> > > > > > accepted. When converting from a C++ enum to Python then the corresponding
> > > > > > enum member will be returned or an int if there is no such member.
> > > > > >
> > > > > > This would mean that there is no need for me to apply special treatment to
> > > > > > individual methods (as the change is implemented in the sip module) and the
> > > > > > approach should be future-proof.
> > > > > >
> > > > > > Thoughts?
> > > > >
> > > > > Hm, I don't really like the lost type safety when accepting ints.
> > > > > However, at the same time I can't think of a proper way to solve the
> > > > > "new enum members" problem.
> > > > >
> > > > > I tried coercing Python into having some kind of special
> > > > > SomeEnum.missing(42) value instead, which acts like a member of the
> > > > > enum, but can also hold an arbitrary value. enum.py sure is some crazy
> > > > > black magic. I bet it would be possible somehow (custom enum metaclass
> > > > > defining __instancecheck__ perhaps?), but at this point there is so
> > > > > much
> > > > > black magic involved I'm not sure it would be a better solution.
> > > >
> > > > I spoke too soon, here is something that seems to work, somehow.
> > > > The point about "probably too much black magic" still stands, though.
> > > 
> > > Nice. The obvious problem is the knowledge of the enum internals.
> > 
> > Right. The code still works properly when removing the setting of
> > _value2member_map_, though. I was copying this from a similar suggestion
> > using _missing_ initially: https://stackoverflow.com/a/57179436
> > 
> > There, it's only needed so that calling CustomEnum(3) later returns the
> > same object again. I believe this isn't needed in PyQt's case, and
> > actually it doesn't work properly either (since we're not using
> > _missing_).
> 
> Enum members must be singletons.
> 
> > Is it strange that the user gets an enum value back, for which the
> > invariant   SomePyQtEnum(member.value) == member   does not hold true?
> > 
> > I don't believe it really is, given that the PyQt enum really doesn't
> > know what member the value corresponds to after all.
> > 
> > One thing that might require some extra care is making the values
> > pickleable I suppose:
> > 
> >     >>> pickle.loads(pickle.dumps(SomePyQtEnum._from_qt(3)))
> >     Traceback (most recent call last):
> >       File "<stdin>", line 1, in <module>
> >       File "/usr/lib/python3.10/enum.py", line 385, in __call__
> >         return cls.__new__(cls, value)
> >       File "/usr/lib/python3.10/enum.py", line 710, in __new__
> >         raise ve_exc
> >     ValueError: 3 is not a valid SomePyQtEnum
> > 
> > I know nothing about pickle, but if you're still interested in this
> > approach, I can try to make it work too.
> 
> I think we need a way to make it work.
> 
> > > I'm still favouring the int approach. I can't think of a case that
> > > code
> > > would start to break when a newer version of Qt was used with an
> > > older PyQt
> > > (which is the main problem).
> > 
> > My main gripe with that approach is that it allows it to pass ints
> > instead of enum members again (even corresponding to a different enum,
> > of course).
> 
> Agreed, but I'm more interested in allowing people to do the right thing
> rather than preventing them from doing the wrong thing.
> 
> > If you'd prefer not messing with the enums directly, I'd prefer an
> > approach which at least tries to be type-safe to some degree, e.g. via a
> > sip.UnknownEnumMember(QEvent.Type, 1001) or somesuch.
> > 
> > That way:
> > 
> > - ints can't be accidentally passed where enum members are expected
> > - Passing a sip.UnknownEnumMember belonging to a different enum would
> >   still raise a TypeError of some sorts.
> > - In summary, whatever I get out of PyQt I can only pass into PyQt at
> >   the correct place again, which seems like a great thing.
> > - Type checkers could probably made to understand it statically
> >   (I think?).
> > - From the __repr__, it would still be clear what kind of thing I'm
> >   dealing with, rather than just seeing 1001 and having no idea where it
> >   comes from.
> 
> See the attached. This has an acceptably minimal knowledge of enum
> internals. Note that I have adopted the Flags naming convention for
> pseudo-members, but I'm not sure wether this is Ok for negative values.

I think there is no inherent limitation of what can be in a Python name.
The only limitations are syntactic, but e.g. this works:

    >>> class T(): pass
    ...
    >>> t = T()
    >>> setattr(t, "-1 hello world", True)
    >>> getattr(t, "-1 hello world")
    True

and the functional enum API doesn't hold you back either:

    >>> E = enum.Enum("E", "-1")
    >>> list(E)
    [<E.-1: 1>]

so I suppose it won't be a problem.

I'd still prefer something like f"sip_unknown_{cpp}" or so, though:
Still shouldn't conflict with real enum values, but it at least
generates valid identifiers for positive values, and it makes it a bit
clearer to the user what's going on.

> So if we can solve the pickle problem I think I'd be Ok to take this
> approach.

I think the most elegant way to do this is via the _missing_ classmethod
on enums. This is public API:
https://docs.python.org/3/library/enum.html#supported-sunder-names

The drawback about the approach is that it's possible to do
SomePyQtEnum(1337) and that happily returns a value, so at this point,
the cpp2py() can probably just be implemented in _missing_ altogether.

See the attached file. The _missing_ method needs to use cpp2py so that
even values which haven't been passed through cpp2py before work fine.

This means I had to rewrite cpp2py slightly to avoid triggering
_missing_. The __members__ attribute is documented too:
https://docs.python.org/3/library/enum.html#iteration

Alternatively, "if cpp in list(etype):" could be used, but that builds
up an unnecessary list ("cpp in etype" will not work, raising a
TypeError).

If we didn't want to use missing (so that SomePyQtEnum(1337) does not
work), I suppose it can be solved by defining __reduce_ex__ on the enum
type somehow, but I tried, and it never was called (only enum.py's was).

Florian

-- 
            me at the-compiler.org | https://www.qutebrowser.org 
       https://bruhin.software/ | https://github.com/sponsors/The-Compiler/
       GPG: 916E B0C8 FD55 A072 | https://the-compiler.org/pubkey.asc
             I love long mails! | https://email.is-not-s.ms/
-------------- next part --------------
import pickle

from enum import Enum


class SomePyQtEnum(Enum):

    one = 1
    two = 2

    @classmethod
    def _missing_(etype, value):
        return cpp2py(etype, value)


def cpp2py(etype, cpp):
    if cpp in etype.__members__:
        return etype(cpp)

    try:
        return etype._sip_unknowns[cpp]
    except KeyError:
        pass
    except AttributeError:
        etype._sip_unknowns = {}

    member = object.__new__(etype)
    member._name_ = f"sip_unknown_{cpp}"
    member._value_ = cpp

    etype._sip_unknowns[cpp] = member

    return member


def py2cpp(py):

    return py.value


e1 = cpp2py(SomePyQtEnum, 1)
e2 = cpp2py(SomePyQtEnum, 2)
e3minus = cpp2py(SomePyQtEnum, -3)
e4 = cpp2py(SomePyQtEnum, 4)
e4a = cpp2py(SomePyQtEnum, 4)

print(e1)
print(e2)
print(e3minus)
print(e4)
print(e4a)
print(e4 is e4a)

print(py2cpp(e1))
print(py2cpp(e2))
print(py2cpp(e3minus))
print(py2cpp(e4))

print(pickle.loads(pickle.dumps(cpp2py(SomePyQtEnum, 4))))
# Try loading a value that has not been passed through cpp2py first.
# Generated via: print(pickle.dumps(cpp2py(SomePyQtEnum, 5)))
print(pickle.loads(b'\x80\x04\x95#\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x0cSomePyQtEnum\x94\x93\x94K\x05\x85\x94R\x94.'))
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 833 bytes
Desc: not available
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20220428/70127ccf/attachment.sig>


More information about the PyQt mailing list