Part D - Defining Custom Signals

Just like you can define a custom result type, you can also define custom signals for your worker to emit. The signals that a worker can emit are stored in the signals attribute.

By default, an AsyncWorker emits three kinds of signals:

  • started: Signifies that the worker has started performing it’s task (fired when emit_start() is called)

  • progress: Signifies that the worker has updated its progress. This usually gets connected to a function that displays progress to the user, possibly within a ProgressBarWidget

  • finished: Signifies that the worker has completed it’s task. This signal also passes along the results of the worker (see Part C)

In most cases, these signals are sufficient for the worker to fully communicate its state. However, it is sometimes useful for your worker to have additional signals on top of those described above. In this example, our worker will perform two distinct tasks sequentially, and it will fire a custom signal once the first task is completed.

First, let’s define the custom signal type. Just like custom results inherit from AsyncWorkerResults, custom signals inherit from AsyncWorkerSignals. PySide6 signals can pass along any value type, but in this example it will pass along a simple string:

1class CustomWorkerSignals(AsyncWorkerSignals):
2    part_1_complete_signal = Signal(str)

Now that the signals are defined, let’s take a look at this example’s worker:

 1class CustomAsyncWorker(AsyncWorker):
 2    def __init__(self, delay_seconds: int, cycles=4):
 3        super(CustomAsyncWorker, self).__init__()
 4        self.delay_seconds = delay_seconds
 5        self.cycles = cycles
 6        # Redefine the 'results' and 'signals' attributes
 7        self.results = CustomWorkerResults()
 8        self.signals = CustomWorkerSignals()
 9
10    def do_part_1(self):
11        progress = 5
12        self.update_progress(progress, 'Starting Task')
13        for ii in range(self.cycles):
14            time.sleep(self.delay_seconds)
15            progress += 90 / (2 * self.cycles)
16            self.update_progress(progress, f'Progress message from part 1 #{ii + 1}')
17        self.results.custom_result_1 = 'result 1'
18
19    def do_part_2(self):
20        progress = 50
21        for ii in range(self.cycles):
22            time.sleep(self.delay_seconds)
23            progress += 90 / (2 * self.cycles)
24            self.update_progress(progress, f'Progress message from part 2 #{ii + 1}')
25        self.results.custom_result_2 = 'result 2'
26
27    def run(self):
28        self.emit_start()
29        self.do_part_1()
30        self.signals.part_1_complete_signal.emit('Part 1 Complete')
31        self.do_part_2()
32        self.complete()

Within the __init__ method, the signals attribute and the results attributes are redefined (this example will use the same result type as Part C). This worker has two tasks that are separated out into their own methods. Within run(), we emit the started signal, perform the first task, emit the custom part_1_complete signal, perform the second task, and finally emit the finished signal.

Just like the progress and finished signals get connected to callbacks, the custom signal needs to be connected to a callback as well. This callback receives a string from the signal and prints it to the console:

1def custom_signal_callback(signal_value: str):
2    print(f'\nCustom Signal received! Value: {signal_value}\n')

Now that everything is defined, let’s see the full example:

 1from PySide6.QtWidgets import QApplication
 2from PySide6.QtCore import Signal
 3from PySink import AsyncManager, AsyncWorker, AsyncWorkerProgress, AsyncWorkerResults, AsyncWorkerSignals
 4import sys
 5import time
 6
 7
 8# Define a class representing your result type, storing result values as attributes
 9class CustomWorkerResults(AsyncWorkerResults):
10    custom_result_1 = None
11    custom_result_2 = None
12
13
14# Define a class representing your signal type, storing signals as attributes
15class CustomWorkerSignals(AsyncWorkerSignals):
16    part_1_complete_signal = Signal(str)
17
18
19class CustomAsyncWorker(AsyncWorker):
20    def __init__(self, delay_seconds: int, cycles=4):
21        super(CustomAsyncWorker, self).__init__()
22        self.delay_seconds = delay_seconds
23        self.cycles = cycles
24        # Redefine the 'results' and 'signals' attributes
25        self.results = CustomWorkerResults()
26        self.signals = CustomWorkerSignals()
27
28    def do_part_1(self):
29        progress = 5
30        self.update_progress(progress, 'Starting Task')
31        for ii in range(self.cycles):
32            time.sleep(self.delay_seconds)
33            progress += 90 / (2 * self.cycles)
34            self.update_progress(progress, f'Progress message from part 1 #{ii + 1}')
35        self.results.custom_result_1 = 'result 1'
36
37    def do_part_2(self):
38        progress = 50
39        for ii in range(self.cycles):
40            time.sleep(self.delay_seconds)
41            progress += 90 / (2 * self.cycles)
42            self.update_progress(progress, f'Progress message from part 2 #{ii + 1}')
43        self.results.custom_result_2 = 'result 2'
44
45    def run(self):
46        self.emit_start()
47        self.do_part_1()
48        self.signals.part_1_complete_signal.emit('Part 1 Complete')
49        self.do_part_2()
50        self.complete()
51
52
53# Function to be called when the custom signal is emitted
54def custom_signal_callback(signal_value: str):
55    print(f'\nCustom Signal received! Value: {signal_value}\n')
56
57
58# Function to be called whenever a worker's task has started
59def worker_started_callback(worker_id: str):
60    print(f'Worker with id {worker_id} has started its task\n')
61
62
63# Function to be called whenever progress is updated
64def progress_callback(progress: AsyncWorkerProgress):
65    print(f'Progress Received, value: {progress.value}, message: {progress.message}')
66
67
68# Function to be called when the worker is finished. The results are now of type CustomWorkerResults.
69def completion_callback(results: CustomWorkerResults):
70    print(f'\nWorker Complete!')
71    print(f'\tErrors: {results.errors}')
72    print(f'\tWarnings: {results.warnings}')
73    print(f'\tResult Attribute 1: {results.custom_result_1}')
74    print(f'\tResult Attribute 2: {results.custom_result_2}')
75    sys.exit()  # Exit the App event loop
76
77
78def run_main():
79    app = QApplication()
80    #   Create the Async Manager
81    manager = AsyncManager()
82    #   Create the Worker and pass in the necessary values
83    worker = CustomAsyncWorker(delay_seconds=1, cycles=3)
84    #   Connect the Worker's signals to their callbacks
85    worker.signals.part_1_complete_signal.connect(custom_signal_callback)
86    worker.signals.started.connect(worker_started_callback)
87    worker.signals.progress.connect(progress_callback)
88    worker.signals.finished.connect(completion_callback)
89    #   Start the Worker and App event loop
90    manager.start_worker(worker)
91    app.exec()
92
93
94run_main()

Running this script prints the following to the console:

 1Worker with id cd5575e0-5ec2-4dd9-b15a-daae136c3528 has started its task
 2
 3Progress Received, value: 5, message: Starting Task
 4Progress Received, value: 20.0, message: Progress message from part 1 #1
 5Progress Received, value: 35.0, message: Progress message from part 1 #2
 6Progress Received, value: 50.0, message: Progress message from part 1 #3
 7
 8Custom Signal received! Value: Part 1 Complete
 9
10Progress Received, value: 65.0, message: Progress message from part 2 #1
11Progress Received, value: 80.0, message: Progress message from part 2 #2
12Progress Received, value: 95.0, message: Progress message from part 2 #3
13
14Worker Complete!
15    Errors: []
16    Warnings: []
17    Result Attribute 1: result 1
18    Result Attribute 2: result 2