PySide2: How to make a decorated slot execute on its worker thread?

Viewed 270

Using Python 3.7 and PySide2, I created a worker object on a dedicated QThread to execute a long-running function. This is illustrated in the code below.

import threading
from time import sleep
from PySide2.QtCore import QObject, QThread, Signal, Slot
from PySide2.QtWidgets import QApplication

class Main(QObject):   
    signal_for_function = Signal()

    def __init__(self):
        print('The main thread is "%s"' % threading.current_thread().name)
        super().__init__()
        self.thread = QThread(self)
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.thread.start()
        self.signal_for_function.connect(self.worker.some_function)

def some_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

class Worker(QObject):
    # @some_decorator
    def some_function(self):
        print('some_function is running on thread "%s"' % threading.current_thread().name)

app = QApplication()
m = Main()
m.signal_for_function.emit()

sleep(0.100)
m.thread.quit()
m.thread.wait()

If I use some_function without the decorator, I get this as expected:

The main thread is "MainThread"
some_function is running on thread "Dummy-1"

However, if I apply a decorator (i.e. uncomment "@some_decorator"), I get:

The main thread is "MainThread"
some_function is running on thread "MainThread"

Why does this happen, and how do I make the decorated function run on the worker thread as I intented to?

1 Answers

Solution:

You must use @functools.wrap:

import functools
# ...

def some_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Output:

The main thread is "MainThread"
some_function is running on thread "Dummy-1"

Explanation:

To analyze the difference of using @functools.wrap or not then the following code must be used:

def some_decorator(func):
    print(func.__name__, func.__module__, func.__doc__, func.__dict__)

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) 
    print(wrapper.__name__, wrapper.__module__, wrapper.__doc__, wrapper.__dict__)
    return wrapper

By removing @functools.wrap you should get the following:

some_function __main__ None {}
wrapper __main__ None {}

By not removing @functools.wrap you should get the following:

some_function __main__ None {}
some_function __main__ None {'__wrapped__': <function Worker.some_function at 0x7f610d926a60>}

The main difference is in __name__, in the case of @functools.wrap it makes the wrapper function have the same name as "func", and what difference does that make? It serves to identify if the function belongs to the Worker class or not, that is, when the Worker class is created, a dictionary is created that stores the methods, attributes, etc., but when the signal invokes some_function then it returns the wrapper that has the name "wrapper" that is not in the Worker's dictionary, but in the case of using @functools.wrapper some_function is invoked then it returns to wrapper with the name "some_function" causing the Worker object to invoke it.

Related