Can anyone clarify why the following code is not behaving as expected.
All of the calls to remainingTime()
are returning 0 and hasExpired()
is returning True.
I would have expected first remainingTime()
to have returned something close to 1000ms and the second remainingTime()
to have returned around 500ms
deadline = QDeadlineTimer(1000)
print(f"deadline.remainingTime(): {deadline.remainingTime()}")
print(f"has expired: {deadline.hasExpired()}")
sleep(0.5)
print(f"deadline.remainingTime(): {deadline.remainingTime()}")
This code is being called from a PyQt5 button press so is operating in the main thread, not good practice, however I'm just trying to learn how to use QDeadlineTimer.
That looks like a bug in the QDeadlineTimer constructor that only happens in PyQt5 (at least until 5.15.9[1]).
It seems to work fine in PySide2 and even PyQt6 (probably due to the more strict enum system it introduced).
Using an integer as argument will result in calling the constructor that accepts a Qt.TimerType
as argument:
>>> timeout = 10
>>> QDeadlineTimer(timeout).timerType() == timeout
True
The problem might be in the sip file, as it seems that trying to use the explicit signature (int, type=Qt.TimerType
) is not accepted, even if help(QDeadlineTimer)
lists it:
>>> help(QDeadlineTimer)
...
| QDeadlineTimer(int, type: Qt.TimerType = Qt.CoarseTimer)
Still, trying to use the full signature, will raise an exception:
>>> QDeadlineTimer(2000, type=Qt.CoarseTimer)
...
TypeError: 'type' is an unknown keyword argument
The simple solution is to explicitly set the remaining time:
>>> deadline = QDeadlineTimer()
>>> deadline.setRemainingTime(1000)
WARNING: Please consider that the following "hack" should not be done lightly, as complex object structures (as foreign language bindings often are) may rely on rigid and delicate mechanisms that could be fatally broken. The fact that it works doesn't mean that it's completely safe to do it. If we're writing code for a software that requires absolute reliability, we should just do what explained above (or eventually use a basic subclass), unless we really know what we're doing and what that code does.
As a temporary workaround, we could create a monkey patch for the __init__
, until we know that the bug has been fixed in PyQt5 (see below).
The trick is to create a function that virtually overrides the __init__
of the class and checks if the first given argument actually is an integer.
def deadlineTimerInitFix(self, *args, **kwargs):
if len(args) and type(args[0]) is int:
remaining, *args = args
timerType = kwargs.pop('type', None) # fix the "type" argument issue
else:
remaining = 0
timerType = None
QDeadlineTimer.__origInit__(self, *args, **kwargs)
if remaining > 0:
self.setRemainingTime(remaining)
if timerType is not None:
self.setTimerType(timerType)
QDeadlineTimer.__origInit__ = QDeadlineTimer.__init__
QDeadlineTimer.__init__ = deadlineTimerInitFix
Which will result in the expected behavior:
>>> t = QDeadlineTimer(1000); t.remainingTime()
1000
Note that the type checking is done with if type() is
, not isinstance()
. In PyQt5 (as in PySide2 and, in "forgiveness mode" in PySide6 too) normal integers can be used for enums and isinstance(SomeQtEnumOrFlag, int)
will always be true.
For instance, setTimerType(0)
would be accepted as equivalent to setTimerType(Qt.PreciseTimer)
.
And if we used isinstance(args[0], int)
, it would use that first argument as remaining time even if using a valid Qt.TimerType
enum or flag.
This means that, with the fix above, in the case we wanted to create an arbitrary QDeadlineTimer that just specifies the timer type in the constructor, we must use the Qt.TimerType
enum.
# correct:
>>> t = QDeadlineTimer(Qt.VeryCoarseTimer)
>>> t.timerType() == Qt.VeryCoarseTimer
True
>>> t.timerType()
2
# not valid, as the value would be used for the duration:
>>> t = QDeadlineTimer(2)
>>> t.timerType() == Qt.VeryCoarseTimer
False
>>> t.timerType()
1 # as in Qt.CoarseTimer, the default type
Be aware that the above MUST only be done once (otherwise we'll get recursion), possibly as soon as the first PyQt import is done in the whole application.
Since we may need to use QDeadlineTimer in more than one script, and (hopefully) the bug may be fixed in the future, we should add some checks to avoid unnecessary calls.
I normally create a separate file that does this sort of patches (in order to be sure that they will be applied only when necessary).
The following assumes that the bug has been fixed since PyQt 5.15.10 (remember that only major and minor version numbers of PyQt and Qt match: the build/revision number never does).
This the content of a possible qtcompatfixes.py
file:
from PyQt5 import QtCore
if (
not hasattr(QtCore.QDeadlineTimer, '__origInit__')
and QtCore.PYQT_VERSION >> 8 & 255 < 15 # PyQt older than 5.15
or QtCore.PYQT_VERSION & 255 < 10 # 5.15.* but older than 5.15.10
):
def deadlineTimerInitFix(self, *args, **kwargs):
...
QtCore.QDeadlineTimer.__origInit__ = QtCore.QDeadlineTimer.__init__
QtCore.QDeadlineTimer.__init__ = deadlineTimerInitFix
Then, for any script that requires a QDeadlineTimer constructor with the remaining time:
from PyQt5 import whatever # or any PyQt5 related import statement
import qtcompatfixes
...
[1] According to this update from the PyQt maintainer, the issue has been fixed on the latest snapshot; if you're using local builds (or you're using a system that relies on latest official builds), just upgrade it; otherwise, if you're using PIP, wait for the next official release. In any case, the fix above should work properly and be consistent with new updates.