Skip to content
Permalink
Browse files
Allow QgsTask subclasses to defined a finished function, which is
called when the task has completed (successfully or otherwise).

This allows for simpler task design when the signal/slot
based approach is not required. Just implement run() with your
heavy lifting, and 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
here.

Python based tasks using the simplified QgsTask.fromFunction
approach can now set a on_finished argument to a function
to call when the task is complete.

eg:

def calculate(task):
    # pretend this is some complex maths and stuff we want
    # to run in the background
    return 5*6

def calculation_finished(result, value=None):
    if result == QgsTask.ResultSuccess:
	iface.messageBar().pushMessage(
            'the magic number is {}'.format(value))
    elif result == QgsTask.ResultFail:
        iface.messageBar().pushMessage(
            'couldn\'t work it out, sorry')

task = QgsTask.fromFunction('my task', calculate,
		on_finished=calculation_finished)
QgsTaskManager.instance().addTask(task)

Multiple values can also be returned, eg:

def calculate(task):
    return (4, 8, 15)

def calculation_finished(result, count=None, max=None, sum=None):
    # here:
    # count = 4
    # max = 8
    # sum = 15

task = QgsTask.fromFunction('my task', calculate,
		on_finished=calculation_finished)
QgsTaskManager.instance().addTask(task)
  • Loading branch information
nyalldawson committed Dec 5, 2016
1 parent 252f2e1 commit 6d4392a0f71456ff96fed0f6cdf4524d9f69318c
Showing with 236 additions and 22 deletions.
  1. +22 −5 python/core/__init__.py
  2. +12 −0 python/core/qgstaskmanager.sip
  3. +2 −2 python/utils.py
  4. +7 −0 src/core/qgstaskmanager.cpp
  5. +14 −0 src/core/qgstaskmanager.h
  6. +92 −8 tests/src/core/testqgstaskmanager.cpp
  7. +87 −7 tests/src/python/test_qgstaskmanager.py
@@ -187,26 +187,43 @@ def __exit__(self, ex_type, ex_value, traceback):

class QgsTaskWrapper(QgsTask):

def __init__(self, description, function, *args, **kwargs):
def __init__(self, description, function, on_finished, *args, **kwargs):
QgsTask.__init__(self, description)
self.args = args
self.kwargs = kwargs
self.function = function
self.result = None
self.on_finished = on_finished
self.returned_values = None
self.exception = None

def run(self):
try:
self.result = self.function(self, *self.args, **self.kwargs)
self.returned_values = self.function(self, *self.args, **self.kwargs)
except Exception as ex:
# report error
self.exception = ex
return QgsTask.ResultFail

return QgsTask.ResultSuccess

def finished(self, result):
if not self.on_finished:
return

def fromFunction(cls, description, function, *args, **kwargs):
return QgsTaskWrapper(description, function, *args, **kwargs)
try:
if self.returned_values:
# we want to support singular returned values which are not iterable
if hasattr(self.returned_values, '__iter__'):
self.on_finished(result, *self.returned_values)
else:
self.on_finished(result, self.returned_values)
else:
self.on_finished(result)
except Exception as ex:
self.exception = ex


def fromFunction(cls, description, function, *args, on_finished=None, **kwargs):
return QgsTaskWrapper(description, function, on_finished, *args, **kwargs)

QgsTask.fromFunction = classmethod(fromFunction)
@@ -185,6 +185,18 @@ class QgsTask : QObject
*/
virtual TaskResult run() = 0;

/**
* If the task is managed by a QgsTaskManager, this will be called after the
* task has finished (whether through successful completion or via early
* termination). The result argument reflects whether
* the task was successfully completed or not. This method is always called
* from the main thread, so it is safe to create widgets and perform other
* operations which require the main thread. However, the GUI will be blocked
* for the duration of this method so tasks should avoid performing any
* lengthy operations here.
*/
virtual void finished( TaskResult result );

/**
* Will return true if task should terminate ASAP. If the task reports the CanCancel
* flag, then derived classes' run() methods should periodically check this and
@@ -32,9 +32,9 @@
"""

from qgis.PyQt.QtCore import QCoreApplication, QLocale
from qgis.PyQt.QtCore import QCoreApplication, QLocale, QThread
from qgis.PyQt.QtWidgets import QPushButton, QApplication
from qgis.core import Qgis, QgsExpression, QgsMessageLog, qgsfunction, QgsMessageOutput, QgsWkbTypes
from qgis.core import Qgis, QgsExpression, QgsMessageLog, qgsfunction, QgsMessageOutput, QgsWkbTypes, QgsApplication
from qgis.gui import QgsMessageBar

import sys
@@ -365,6 +365,13 @@ void QgsTaskManager::taskStatusChanged( int status )
if ( id < 0 )
return;

if ( status == QgsTask::Terminated || status == QgsTask::Complete )
{
QgsTask::TaskResult result = status == QgsTask::Complete ? QgsTask::ResultSuccess
: QgsTask::ResultFail;
task->finished( result );
}

if ( status == QgsTask::Terminated )
{
//recursively cancel dependant tasks
@@ -207,6 +207,18 @@ class CORE_EXPORT QgsTask : public QObject
*/
virtual TaskResult run() = 0;

/**
* If the task is managed by a QgsTaskManager, this will be called after the
* task has finished (whether through successful completion or via early
* termination). The result argument reflects whether
* the task was successfully completed or not. This method is always called
* from the main thread, so it is safe to create widgets and perform other
* operations which require the main thread. However, the GUI will be blocked
* for the duration of this method so tasks should avoid performing any
* lengthy operations here.
*/
virtual void finished( TaskResult result ) { Q_UNUSED( result ); }

/**
* Will return true if task should terminate ASAP. If the task reports the CanCancel
* flag, then derived classes' run() methods should periodically check this and
@@ -249,6 +261,8 @@ class CORE_EXPORT QgsTask : public QObject
double mProgress;
bool mShouldTerminate;

friend class QgsTaskManager;

};

Q_DECLARE_OPERATORS_FOR_FLAGS( QgsTask::Flags )
@@ -70,7 +70,7 @@ class TestTerminationTask : public TestTask
}
};

class SuccessTask : public TestTask
class SuccessTask : public QgsTask
{
Q_OBJECT

@@ -82,7 +82,7 @@ class SuccessTask : public TestTask
}
};

class FailTask : public TestTask
class FailTask : public QgsTask
{
Q_OBJECT

@@ -92,6 +92,35 @@ class FailTask : public TestTask
{
return ResultFail;
}

};

class FinishTask : public QgsTask
{
Q_OBJECT

public:

FinishTask()
: desiredResult( QgsTask::ResultPending )
, resultObtained( QgsTask::ResultPending )
{}

TaskResult desiredResult;
TaskResult resultObtained;

protected:

TaskResult run() override
{
return desiredResult;
}

void finished( TaskResult result ) override
{
Q_ASSERT( QApplication::instance()->thread() == QThread::currentThread() );
resultObtained = result;
}
};


@@ -106,6 +135,7 @@ class TestQgsTaskManager : public QObject
void cleanup();// will be called after every testfunction.
void task();
void taskResult();
void taskFinished();
void createInstance();
void addTask();
void deleteTask();
@@ -206,7 +236,7 @@ void TestQgsTaskManager::task()

void TestQgsTaskManager::taskResult()
{
QScopedPointer< TestTask > task( new SuccessTask() );
QScopedPointer< QgsTask > task( new SuccessTask() );
QCOMPARE( task->status(), QgsTask::Queued );
QSignalSpy statusSpy( task.data(), &QgsTask::statusChanged );

@@ -330,6 +360,37 @@ void TestQgsTaskManager::taskTerminationBeforeDelete()
}
#endif


void TestQgsTaskManager::taskFinished()
{
// test that finished is called and passed correct result, and that it is called
// from main thread
QgsTaskManager manager;

FinishTask* task = new FinishTask();
task->desiredResult = QgsTask::ResultSuccess;
manager.addTask( task );
while ( task->status() == QgsTask::Running
|| task->status() == QgsTask::Queued ) { }
while ( manager.countActiveTasks() > 0 )
{
QCoreApplication::processEvents();
}
QCOMPARE( task->resultObtained, QgsTask::ResultSuccess );

task = new FinishTask();
task->desiredResult = QgsTask::ResultFail;
manager.addTask( task );

while ( task->status() == QgsTask::Running
|| task->status() == QgsTask::Queued ) { }
while ( manager.countActiveTasks() > 0 )
{
QCoreApplication::processEvents();
}
QCOMPARE( task->resultObtained, QgsTask::ResultFail );
}

void TestQgsTaskManager::taskId()
{
//test finding task IDs
@@ -380,6 +441,10 @@ void TestQgsTaskManager::progressChanged()
QCOMPARE( spy2.count(), 0 );

task->emitTaskCompleted();
while ( manager.countActiveTasks() > 1 )
{
QCoreApplication::processEvents();
}
task2->emitProgressChanged( 80.0 );
//single running task, so progressChanged(double) should be emitted
QCOMPARE( spy2.count(), 1 );
@@ -392,6 +457,10 @@ void TestQgsTaskManager::progressChanged()
QCOMPARE( spy2.count(), 1 );

task2->emitTaskStopped();
while ( manager.countActiveTasks() > 1 )
{
QCoreApplication::processEvents();
}
task3->emitProgressChanged( 30.0 );
//single running task, so progressChanged(double) should be emitted
QCOMPARE( spy2.count(), 2 );
@@ -438,10 +507,16 @@ void TestQgsTaskManager::allTasksFinished()
QSignalSpy spy( &manager, &QgsTaskManager::allTasksFinished );

task->emitTaskStopped();
while ( task->status() == QgsTask::Running ) { }
while ( manager.countActiveTasks() > 1 )
{
QCoreApplication::processEvents();
}
QCOMPARE( spy.count(), 0 );
task2->emitTaskCompleted();
while ( task2->status() == QgsTask::Running ) { }
while ( manager.countActiveTasks() > 0 )
{
QCoreApplication::processEvents();
}
QCOMPARE( spy.count(), 1 );

TestTask* task3 = new TestTask();
@@ -451,16 +526,25 @@ void TestQgsTaskManager::allTasksFinished()
manager.addTask( task4 );
while ( task4->status() != QgsTask::Running ) { }
task3->emitTaskStopped();
while ( task3->status() == QgsTask::Running ) { }
while ( manager.countActiveTasks() > 1 )
{
QCoreApplication::processEvents();
}
QCOMPARE( spy.count(), 1 );
TestTask* task5 = new TestTask();
manager.addTask( task5 );
while ( task5->status() != QgsTask::Running ) { }
task4->emitTaskStopped();
while ( task4->status() == QgsTask::Running ) { }
while ( manager.countActiveTasks() > 1 )
{
QCoreApplication::processEvents();
}
QCOMPARE( spy.count(), 1 );
task5->emitTaskStopped();
while ( task5->status() == QgsTask::Running ) { }
while ( manager.countActiveTasks() > 0 )
{
QCoreApplication::processEvents();
}
QCOMPARE( spy.count(), 2 );
}

Loading

0 comments on commit 6d4392a

Please sign in to comment.