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

Phil Thompson phil at riverbankcomputing.com
Thu Apr 28 09:42:46 BST 2022


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.

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

Phil
-------------- next part --------------
A non-text attachment was scrubbed...
Name: e1.py
Type: text/x-python
Size: 864 bytes
Desc: not available
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20220428/9d543bc4/attachment-0001.py>


More information about the PyQt mailing list