QGIS version 3.4.2
If you define a task and add it to the task manager within a function scope, it will cause the task to either not execute properly or, at worst, crash QGIS. Consider the following code.
import random
from time import sleep
from qgis.core import QgsApplication, QgsTask, QgsMessageLog, Qgis
MESSAGE_CATEGORY = 'Wasting time'
def do_task(task, wait_time):
"""
Raises an exception to abort the task.
Returns a result if success.
The result will be passed together with the exception (None in
the case of success) to the on_finished method
"""
QgsMessageLog.logMessage('Started task {}'.format(task.description()),
MESSAGE_CATEGORY, Qgis.Info)
wait_time = wait_time / 100
total = 0
iterations = 0
for i in range(100):
sleep(wait_time)
# use task.setProgress to report progress
task.setProgress(i)
arandominteger = random.randint(0, 500)
total += arandominteger
iterations += 1
# check task.isCanceled() to handle cancellation
if task.isCanceled():
stopped(task)
return None
# raise an exception to abort the task
if arandominteger == 42:
raise Exception('bad value!')
return {'total': total, 'iterations': iterations,
'task': task.description()}
def stopped(task):
QgsMessageLog.logMessage(
'Task "{name}" was canceled'.format(
name=task.description()),
MESSAGE_CATEGORY, Qgis.Info)
def completed(exception, result=None):
"""This is called when do_task is finished.
Exception is not None if do_task raises an exception.
Result is the return value of do_task."""
if exception is None:
if result is None:
QgsMessageLog.logMessage(
'Completed with no exception and no result '\
'(probably manually canceled by the user)',
MESSAGE_CATEGORY, Qgis.Warning)
else:
QgsMessageLog.logMessage(
'Task {name} completed\n'
'Total: {total} ( with {iterations} '
'iterations)'.format(
name=result['task'],
total=result['total'],
iterations=result['iterations']),
MESSAGE_CATEGORY, Qgis.Info)
else:
QgsMessageLog.logMessage("Exception: {}".format(exception),
MESSAGE_CATEGORY, Qgis.Critical)
raise exception
# Create and execute a few tasks
task1 = QgsTask.fromFunction(u'Waste cpu 1', do_task,
on_finished=completed, wait_time=4)
task2 = QgsTask.fromFunction(u'Waste cpu 2', do_task,
on_finished=completed, wait_time=3)
QgsApplication.taskManager().addTask(task1)
QgsApplication.taskManager().addTask(task2)
If you put this in the script editor in QGIS and execute it, it will run just fine. The user can cancel. The exceptions are raised, occasionally. All is well. Now if you replace the last block of the code to be within a function scope, like so:
def task_create_and_execute():
# Create and execute a few tasks
task1 = QgsTask.fromFunction(u'Waste cpu 1', do_task,
on_finished=completed, wait_time=4)
task2 = QgsTask.fromFunction(u'Waste cpu 2', do_task,
on_finished=completed, wait_time=3)
QgsApplication.taskManager().addTask(task1)
QgsApplication.taskManager().addTask(task2)
task_create_and_execute()
Now when you run the script you will find that either task1 will appear to run, but the completed() function will never be reached (task2 will NOT run) or QGIS will crash. If you run it twice, QGIS will most surely crash. This will also happen when using the class method, as in OP's code. This is likely a bug and I may link to this answer and report it. A possible workaround would be to take the task creation and execution out of scope, as in my first example, and run it from the script editor.
Update: I have reported this issue to QGIS, however it may not be a bug and just a matter of scope. If you have the following code, in a module named task_example.py, you will find it to work correctly for both the subclassed and fromFunction methods:
import random
from time import sleep
from qgis.core import QgsApplication, QgsTask, QgsMessageLog, Qgis
MESSAGE_CATEGORY = 'Task Subclass'
class TaskSubclass(QgsTask):
"""This shows how to subclass QgsTask"""
def __init__(self, description, duration):
super().__init__(description, QgsTask.CanCancel)
self.duration = duration
self.total = 0
self.iterations = 0
self.exception = None
def run(self):
"""Here you implement your heavy lifting.
Should periodically test for isCanceled() to gracefully
abort.
This method MUST return True or False.
Raising exceptions will crash QGIS, so we handle them
internally and raise them in self.finished
"""
QgsMessageLog.logMessage('Started task "{}"'.format(
self.description()),
MESSAGE_CATEGORY, Qgis.Info)
wait_time = self.duration / 100
for i in range(100):
sleep(wait_time)
# use setProgress to report progress
self.setProgress(i)
arandominteger = random.randint(0, 500)
self.total += arandominteger
self.iterations += 1
# check isCanceled() to handle cancellation
if self.isCanceled():
return False
# # simulate exceptions to show how to abort task
# if arandominteger == 42:
# # DO NOT raise Exception('bad value!')
# # this would crash QGIS
# self.exception = Exception('bad value!')
# return False
return True
def finished(self, result):
"""
This function is automatically called when the task has
completed (successfully or not).
You implement finished() to do whatever follow-up stuff
should happen after the task is complete.
finished is always called from the main thread, so it's safe
to do GUI operations and raise Python exceptions here.
result is the return value from self.run.
"""
if result:
QgsMessageLog.logMessage(
'Task "{name}" completed\n' \
'Total: {total} (with {iterations} '\
'iterations)'.format(
name=self.description(),
total=self.total,
iterations=self.iterations),
MESSAGE_CATEGORY, Qgis.Success)
else:
if self.exception is None:
QgsMessageLog.logMessage(
'Task "{name}" not successful but without '\
'exception (probably the task was manually '\
'canceled by the user)'.format(
name=self.description()),
MESSAGE_CATEGORY, Qgis.Warning)
else:
QgsMessageLog.logMessage(
'Task "{name}" Exception: {exception}'.format(
name=self.description(),
exception=self.exception),
MESSAGE_CATEGORY, Qgis.Critical)
raise self.exception
def cancel(self):
QgsMessageLog.logMessage(
'Task "{name}" was canceled'.format(
name=self.description()),
MESSAGE_CATEGORY, Qgis.Info)
super().cancel()
class DoStuff:
def __init__(self):
"""Example of the class that needs to get tasks done. """
"""Subclassed examples. """
self.longtask = TaskSubclass('class method long', 20)
self.shorttask = TaskSubclass('class method short', 10)
self.minitask = TaskSubclass('class method mini', 5)
# Subtasks to do.
self.shortsubtask = TaskSubclass('class method subtask short', 5)
self.longsubtask = TaskSubclass('class method subtask long', 10)
self.shortestsubtask = TaskSubclass('class method subtask shortest', 4)
# Add a subtask (shortsubtask) to shorttask that must run after
# minitask and longtask has finished
self.shorttask.addSubTask(self.shortsubtask,
[self.minitask, self.longtask])
# Add a subtask (longsubtask) to longtask that must be run
# before the parent task
self.longtask.addSubTask(self.longsubtask, [],
TaskSubclass.ParentDependsOnSubTask)
# Add a subtask (shortestsubtask) to longtask
self.longtask.addSubTask(self.shortestsubtask)
"""From function examples. """
self.MESSAGE_CATEGORY = 'Task from Function'
self.task1 = QgsTask.fromFunction(u'function task', self.internal_task,
on_finished=self.completed, wait_time=4)
self.task2 = QgsTask.fromFunction(u'function task 2', self.internal_task,
on_finished=self.completed, wait_time=3)
def do_subclassed_tasks(self):
"""Do tasks using QgsTask subclass. """
QgsApplication.taskManager().addTask(self.longtask)
QgsApplication.taskManager().addTask(self.shorttask)
QgsApplication.taskManager().addTask(self.minitask)
def do_from_function_tasks(self):
"""Do tasks from a function. """
QgsApplication.taskManager().addTask(self.task1)
QgsApplication.taskManager().addTask(self.task2)
def internal_task(self, task, wait_time):
"""
Raises an exception to abort the task.
Returns a result if success.
The result will be passed together with the exception (None in
the case of success) to the on_finished method
"""
QgsMessageLog.logMessage('Started task {}'.format(task.description()),
self.MESSAGE_CATEGORY, Qgis.Info)
wait_time = wait_time / 100
total = 0
iterations = 0
for i in range(100):
sleep(wait_time)
# use task.setProgress to report progress
task.setProgress(i)
arandominteger = random.randint(0, 500)
total += arandominteger
iterations += 1
# check task.isCanceled() to handle cancellation
if task.isCanceled():
stopped(task)
return None
# # raise an exception to abort the task
# if arandominteger == 42:
# raise Exception('bad value!')
return {'total': total, 'iterations': iterations,
'task': task.description()}
def stopped(self, task):
QgsMessageLog.logMessage(
'Task "{name}" was canceled'.format(
name=task.description()),
self.MESSAGE_CATEGORY, Qgis.Info)
def completed(self, exception, result=None):
"""This is called when do_task is finished.
Exception is not None if do_task raises an exception.
Result is the return value of do_task."""
if exception is None:
if result is None:
QgsMessageLog.logMessage(
'Completed with no exception and no result '\
'(probably manually canceled by the user)',
self.MESSAGE_CATEGORY, Qgis.Warning)
else:
QgsMessageLog.logMessage(
'Task {name} completed\n'
'Total: {total} ( with {iterations} '
'iterations)'.format(
name=result['task'],
total=result['total'],
iterations=result['iterations']),
self.MESSAGE_CATEGORY, Qgis.Info)
else:
QgsMessageLog.logMessage("Exception: {}".format(exception),
MESSAGE_CATEGORY, Qgis.Critical)
raise exception
You can now, in the python console (or another py file), make the following executions:
import task_example
okay = task_example.DoStuff()
okay.do_subclassed_tasks()
okay.do_from_function_tasks()
Please note that I commented out the parts of the code that cause an exception when the random integer is 42. I was tired of the answer to the ultimate question of life, the universe, and everything throwing exceptions at me.
i found the same question on the right sidebar. (QGIS QgsTaks finished and completed never called)
It says that QGIS tasks in plugins are broken, because the finished()
and completed()
functions are never executed, but there is a way to achieve the same with Qt Signals.
I tried this and it worked:
I added a pyqtSignal
to my task class:
class GeocodeTask(QgsTask):
finished_signal = pyqtSignal(bool, str, int, object)
def __init__(self, only_missing: bool, config):
if only_missing:
super().__init__('Fehlende Adressen bestimmen', QgsTask.CanCancel)
else:
super().__init__('Alle Adressen bestimmen', QgsTask.CanCancel)
self.exception = None
self.only_missing = only_missing
self.config = config
# Counters for progress display
self.total_features = 0
self.finished_features = 0
self.failed_features = 0
Then i added every time, my run function could exit, this lines:
self.finished_signal.emit(False, self.description(), self.failed_features, self.exception)
return False
The first parameter shows if the task completed successfully, the second and third parameters are needed for a message and the last parameter contains an exception (if aviable) to trow it in the main thread.
As last, I moved the finished function to the main plugin class and registered it to the signal in the task:
def geocode_finished(self, state: bool, description: str, failed_features: int, exception: object):
if state:
message_bar('Die Aufgabe "{}" wurde erfolgreich beendet. Es wurden {} Adressen nicht gefunden'.format(
description, str(failed_features)), level='success')
else:
if exception is None:
log_message(
'Die Aufgabe "{}" wurde nicht erfolgreich, aber ohne Fehlermeldung beendet.\n\
(Wahrscheinlich vom Benutzer abgebrochen)'.format(
description), level='warning')
message_bar(
'Die Aufgabe "{}" wurde wahrscheinlich vom Benutzer abgebrochen.'.format(description),
level='warning')
else:
message_bar(
'Die Aufgabe "{}" wurde mit einem Fehler beendet: {}'.format(description, exception),
level='critical')
raise exception
self.task_running = False
# ...
def geocode_all(self):
self.task_running = True
task = GeocodeTask(False, self.config)
task.finished_signal.connect(self.geocode_finished)
QgsApplication.taskManager().addTask(task)
message_bar('Task gestartet: Alle Adressen bestimmen', level='info')
Best Answer
There's a typo in that script - it should be isCanceled() (one l, not two)
You should also add your task to the global task manager instead of creating a new manager:
One thing to be very careful about is never to create widgets or alter gui in a task. This is a strict Qt guideline - gui must never be altered outside of the main thread. So your progress dialog must operate on the main thread, connecting to the progress report signals from the task which operates in the background thread.