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.