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