Home > database >  Why do rebound methods of PyQt classes raise a TypeError
Why do rebound methods of PyQt classes raise a TypeError

Time:04-30

Making my code compatible with PyQt5/6 and PySide2/6, I wrote

if not hasattr(QtCore.QDateTime, 'toPython'):  # fix for PyQt5/6
    QtCore.QDateTime.toPython = QtCore.QDateTime.toPyDateTime

Run with PyQt5 or PyQt6, this resulted in

TypeError: toPyDateTime(self): first argument of unbound method must have type 'QDateTime'

when the function gets called:

QtCore.QDateTime.currentDateTime().toPython()

But if I change the call into

QtCore.QDateTime.toPython(QtCore.QDateTime.currentDateTime())

there is no error.

However, when I change the first piece of code to

if not hasattr(QtCore.QDateTime, 'toPython'):  # fix for PyQt5/6
    QtCore.QDateTime.toPython = lambda self: QtCore.QDateTime.toPyDateTime(self)

everything works fine whatever way I call toPython function. Why do I need the lambda expression here at all?

Added. To explain the behavior I'd expect, there is a piece of simple code:

class A:
    def __init__(self) -> None:
        print(f'make A ({hex(id(self))})')

    def foo(self) -> None:
        print(f'A ({hex(id(self))}) foo')


class B:
    def __init__(self) -> None:
        print(f'make B ({hex(id(self))})')


B.bar = A.foo

b: B = B()  # prints “make B (0x7efc04c67f10)” (or another id)
B.bar(b)    # prints “A (0x7efc04c67f10) foo” (same id, no error)
b.bar()     # same result as before, no error

On the contrary, the following code doesn't work:

from PyQt6.QtCore import QDateTime

QDateTime.toPython = QDateTime.toPyDateTime
t: QDateTime = QDateTime.currentDateTime()
QDateTime.toPython(t)  # no error
t.toPython()           # raises TypeError

CodePudding user response:

This is due to an implementation difference between PyQt and PySide. In the former, most methods of classes are thin wrappers around C-functions which don't implement the descriptor protocol (i.e. they don't have a __get__ method). So, in this respect, they are equivalent to built-in functions, like len:

>>> type(len)
<class 'builtin_function_or_method'>
>>> type(QtCore.QDateTime.toPyDateTime) is type(len)
True
>>> hasattr(QtCore.QDateTime.toPyDateTime, '__get__')
False

By contrast, most PySide methods do implement the descriptor protocol:

>>> type(QtCore.QDateTime.toPython)
<class 'method_descriptor'>
>>> hasattr(QtCore.QDateTime.toPython, '__get__')
True

This means that if you reversed your compatibilty fix, it would work as expected:

>>> from PySide2 import QtCore
QtCore.QDateTime.toPyDateTime = QtCore.QDateTime.toPython
>>> QtCore.QDateTime.currentDateTime().toPyDateTime()
datetime.datetime(2022, 4, 29, 11, 52, 51, 67000)

However, if you want to keep your current naming scheme, using a wrapper function (e.g. like lambda) is essentially the best you can do. All user-defined Python functions support the descriptor protocol, which is why your example using simple user-defined classes behaves as expected. The only improvement that might be suggested is to use partialmethod instead. This will save writing some boiler-plate code and has the additional benefit of providing more informative error-messages:

>>> QtCore.QDateTime.toPython = partialmethod(QtCore.QDateTime.toPyDateTime)
>>> d = QtCore.QDateTime.currentDateTime()
>>> d.toPython()
datetime.datetime(2022, 4, 29, 12, 13, 15, 434000)
>>> d.toPython(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.10/functools.py", line 388, in _method
    return self.func(cls_or_self, *self.args, *args, **keywords)
TypeError: toPyDateTime(self): too many arguments

I suppose the only remaining point here is the question of why exactly PyQt and PySide differ in their implementations. You'd probably have to ask the author of PyQt to a get a definitive explanation regarding this. My guess would be that it's at least partly for historical reasons, since PyQt has been around a lot longer than PySide - but there are no doubt several other technical considerations as well.

  • Related