Skip to content

Commit

Permalink
Allow QgsTask subclasses to defined a finished function, which is
Browse files Browse the repository at this point in the history
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 6d4392a
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 22 deletions.
27 changes: 22 additions & 5 deletions python/core/__init__.py
Expand Up @@ -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)
12 changes: 12 additions & 0 deletions python/core/qgstaskmanager.sip
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions python/utils.py
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/core/qgstaskmanager.cpp
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/core/qgstaskmanager.h
Expand Up @@ -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
Expand Down Expand Up @@ -249,6 +261,8 @@ class CORE_EXPORT QgsTask : public QObject
double mProgress;
bool mShouldTerminate;

friend class QgsTaskManager;

};

Q_DECLARE_OPERATORS_FOR_FLAGS( QgsTask::Flags )
Expand Down
100 changes: 92 additions & 8 deletions tests/src/core/testqgstaskmanager.cpp
Expand Up @@ -70,7 +70,7 @@ class TestTerminationTask : public TestTask
}
};

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

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

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

Expand All @@ -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;
}
};


Expand All @@ -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();
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 );
Expand All @@ -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 );
Expand Down Expand Up @@ -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();
Expand All @@ -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 );
}

Expand Down

0 comments on commit 6d4392a

Please sign in to comment.