pythonqtpysidepyside2

QTimer with Python lambda function runs with previous data


I have a GUI project which uses PySide2 with Python 3.8, which performs some background tasks in a QThread. Within that QThread, I have QTimer member object that has to periodically run a function passing it different data each time. I am not using QTimer.singleShot static function because I need to stop/reset the timer if need be for some specific scenario.

Once the target function runs successfully, it invokes another function which is responsible for passing new data to the timer and run it again. The function is connected with timer.timeout.connect(lambda: function_name(data))and before passing it new data I stop and disconnect the previous slot and stop the timer if the timer was active. On the second run of the timer when I pass it new data, the target function still runs with the old data somehow.

The following code is the issue reproduced in an isolated manner:


from PySide2.QtCore import QTimer
from PySide2 import QtWidgets
import time
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
FORMAT = "[%(asctime)s] %(funcName)s: %(message)s"
logging.basicConfig(format=FORMAT)

class QtTest(QtWidgets.QWidget):
    
    def __init__(self):
        super(QtTest, self).__init__()
        logger.info("Class initialized")
        self.timer = QTimer()
        self.timer.setSingleShot(True)
        self.timer.setInterval(1000)
        self.timer.timeout.connect(lambda: self.target_func(0))
        self.timer.start()
        
        
    def target_func(self, data):
        logger.info("Target function called with data: {}".format(data))
        time.sleep(1.5)
        if self.timer.isActive():
            logger.info("Timer reset")
            self.timer.stop()
            self.timer.timeout.disconnect()
            
        self.func_done(data)
        
    def func_done(self, data):
        logger.info("Tasks completed for data: {}".format(data))
        
        if data == 0:
            logger.info("Setting next timer with data 1")
            #I have tried the following as well
            #self.timer.timeout.disconnect()
            #self.timer.stop()

            self.timer.setInterval(1000)
            self.timer.timeout.connect(lambda: self.target_func(1))
            self.timer.start()


def init_app():
    app = QtWidgets.QApplication([])

    gui = QtTest()
    gui.show()
    app.exec_()
    
if __name__ == "__main__":
    print("QT app running")
    init_app()
    test = QtTest()

This is the output of the test code:

QT app running
[2024-07-30 22:44:53,204] __init__: Class initialized
[2024-07-30 22:44:54,255] target_func: Target function called with data: 0
[2024-07-30 22:44:55,757] func_done: Tasks completed for data: 0
[2024-07-30 22:44:55,757] func_done: Setting next timer with data 1
[2024-07-30 22:44:56,808] target_func: Target function called with data: 0
[2024-07-30 22:44:58,309] func_done: Tasks completed for data: 0
[2024-07-30 22:44:58,309] func_done: Setting next timer with data 1
[2024-07-30 22:44:58,309] target_func: Target function called with data: 1
[2024-07-30 22:44:59,810] target_func: Timer reset
[2024-07-30 22:44:59,811] func_done: Tasks completed for data: 1

What I am expecting is that once the timer is set with new value of 1, target function prints 1 as well.

I have also tried using default values for lambdas as described in What do lambda function closures capture? with same result:

self.timer.timeout.connect(lambda x=1: self.target_func(x))

Solution

  • I was able to fix the problem by disconnecting the timeout slot irrespective of its active status before restarting the timer:

    def func_done(self, data):
        logger.info("Tasks completed for data: {}".format(data))
        
        if data == 0:
            logger.info("Setting next timer with data 1")
            self.timer.timeout.disconnect()
            self.timer.setInterval(1000)
            self.timer.timeout.connect(lambda: self.target_func(1))
            self.timer.start()
    

    which resulted in the expected behaviour:

    [2024-07-31 00:18:27,188] __init__: Class initialized
    [2024-07-31 00:18:28,139] target_func: Target function called with data: 0
    [2024-07-31 00:18:29,640] func_done: Tasks completed for data: 0
    [2024-07-31 00:18:29,640] func_done: Setting next timer with data 1
    [2024-07-31 00:18:30,592] target_func: Target function called with data: 1
    [2024-07-31 00:18:32,093] func_done: Tasks completed for data: 1