Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #4764 from nyalldawson/processing_exception
Throw c++ exception when a Python exception occurs while running a algorithm
  • Loading branch information
nyalldawson committed Jun 23, 2017
2 parents 2906d1f + c3e24b7 commit 19dd097
Show file tree
Hide file tree
Showing 94 changed files with 399 additions and 213 deletions.
1 change: 1 addition & 0 deletions cmake_templates/Doxyfile.in
Expand Up @@ -2073,6 +2073,7 @@ EXPAND_AS_DEFINED = "SIP_ABSTRACT" \
"SIP_TRANSFER" \
"SIP_TRANSFERBACK" \
"SIP_TRANSFERTHIS" \
"SIP_VIRTUALERRORHANDLER" \
"SIP_WHEN_FEATURE"

# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
Expand Down
91 changes: 91 additions & 0 deletions python/core/core.sip
@@ -1,6 +1,93 @@
%Module(name=qgis._core,
keyword_arguments="Optional")

%ModuleCode

#include "qgsexception.h"

QString getTraceback()
{
#define TRACEBACK_FETCH_ERROR(what) {errMsg = what; goto done;}

// acquire global interpreter lock to ensure we are in a consistent state
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

QString errMsg;
QString result;

PyObject *modStringIO = nullptr;
PyObject *modTB = nullptr;
PyObject *obStringIO = nullptr;
PyObject *obResult = nullptr;

PyObject *type, *value, *traceback;

PyErr_Fetch( &type, &value, &traceback );
PyErr_NormalizeException( &type, &value, &traceback );

const char *iomod = "io";

modStringIO = PyImport_ImportModule( iomod );
if ( !modStringIO )
TRACEBACK_FETCH_ERROR( QString( "can't import %1" ).arg( iomod ) );

obStringIO = PyObject_CallMethod( modStringIO, ( char * ) "StringIO", nullptr );

/* Construct a cStringIO object */
if ( !obStringIO )
TRACEBACK_FETCH_ERROR( "cStringIO.StringIO() failed" );

modTB = PyImport_ImportModule( "traceback" );
if ( !modTB )
TRACEBACK_FETCH_ERROR( "can't import traceback" );

obResult = PyObject_CallMethod( modTB, ( char * ) "print_exception",
( char * ) "OOOOO",
type, value ? value : Py_None,
traceback ? traceback : Py_None,
Py_None,
obStringIO );

if ( !obResult )
TRACEBACK_FETCH_ERROR( "traceback.print_exception() failed" );

Py_DECREF( obResult );

obResult = PyObject_CallMethod( obStringIO, ( char * ) "getvalue", nullptr );
if ( !obResult )
TRACEBACK_FETCH_ERROR( "getvalue() failed." );

/* And it should be a string all ready to go - duplicate it. */
if ( !PyUnicode_Check( obResult ) )
TRACEBACK_FETCH_ERROR( "getvalue() did not return a string" );

result = QString::fromUtf8( PyUnicode_AsUTF8( obResult ) );

done:

// All finished - first see if we encountered an error
if ( result.isEmpty() && !errMsg.isEmpty() )
{
result = errMsg;
}

Py_XDECREF( modStringIO );
Py_XDECREF( modTB );
Py_XDECREF( obStringIO );
Py_XDECREF( obResult );
Py_XDECREF( value );
Py_XDECREF( traceback );
Py_XDECREF( type );

// we are done calling python API, release global interpreter lock
PyGILState_Release( gstate );

return result;
}

%End

%Import QtXml/QtXmlmod.sip
%Import QtNetwork/QtNetworkmod.sip
%Import QtSql/QtSqlmod.sip
Expand Down Expand Up @@ -413,3 +500,7 @@
%Include expression/qgsexpressionnodeimpl.sip
%Include expression/qgsexpressionnode.sip
%Include expression/qgsexpressionfunction.sip

%VirtualErrorHandler processing_exception_handler
throw QgsProcessingException( getTraceback() );
%End
6 changes: 4 additions & 2 deletions python/core/processing/qgsprocessingalgorithm.sip
Expand Up @@ -215,14 +215,16 @@ class QgsProcessingAlgorithm
%End

QVariantMap run( const QVariantMap &parameters,
QgsProcessingContext &context, QgsProcessingFeedback *feedback ) const;
QgsProcessingContext &context, QgsProcessingFeedback *feedback, bool *ok /Out/ = 0 ) const;
%Docstring
Executes the algorithm using the specified ``parameters``.

The ``context`` argument specifies the context in which the algorithm is being run.

Algorithm progress should be reported using the supplied ``feedback`` object.

If specified, ``ok`` will be set to true if algorithm was successfully run.

:return: A map of algorithm outputs. These may be output layer references, or calculated
values such as statistical calculations.
:rtype: QVariantMap
Expand Down Expand Up @@ -292,7 +294,7 @@ class QgsProcessingAlgorithm
%End

virtual QVariantMap processAlgorithm( const QVariantMap &parameters,
QgsProcessingContext &context, QgsProcessingFeedback *feedback ) const = 0;
QgsProcessingContext &context, QgsProcessingFeedback *feedback ) const = 0 /VirtualErrorHandler=processing_exception_handler/;
%Docstring
Runs the algorithm using the specified ``parameters``. Algorithms should implement
their custom processing logic here.
Expand Down
2 changes: 2 additions & 0 deletions python/core/processing/qgsprocessingcontext.sip
Expand Up @@ -137,6 +137,8 @@ Destination project
void setInvalidGeometryCheck( const QgsFeatureRequest::InvalidGeometryCheck &check );
%Docstring
Sets the behavior used for checking invalid geometries in input layers.
Settings this to anything but QgsFeatureRequest.GeometryNoCheck will also
reset the invalidGeometryCallback() to a default implementation.
.. seealso:: invalidGeometryCheck()
%End

Expand Down
14 changes: 13 additions & 1 deletion python/core/qgsexception.sip
@@ -1,7 +1,7 @@
%Exception QgsCsException(SIP_Exception) /PyName=QgsCsException/
{
%TypeHeaderCode
#include <qgscsexception.h>
#include <qgsexception.h>
%End
%RaiseCode
SIP_BLOCK_THREADS
Expand All @@ -10,6 +10,18 @@
%End
};

%Exception QgsProcessingException(SIP_Exception) /PyName=QgsProcessingException/
{
%TypeHeaderCode
#include <qgsexception.h>
%End
%RaiseCode
SIP_BLOCK_THREADS
PyErr_SetString(sipException_QgsProcessingException, sipExceptionRef.what().toUtf8().constData() );
SIP_UNBLOCK_THREADS
%End
};

%Exception QgsException(SIP_Exception) /PyName=QgsException/
{
%TypeHeaderCode
Expand Down
2 changes: 1 addition & 1 deletion python/core/qgsfeaturerequest.sip
Expand Up @@ -364,7 +364,7 @@ Get feature IDs that should be fetched.
QgsFeatureRequest &setInvalidGeometryCallback( SIP_PYCALLABLE / AllowNone / );
%Docstring
Sets a callback function to use when encountering an invalid geometry and
invalidGeometryCheck() is set to GeometryAbortOnInvalid. This function will be
invalidGeometryCheck() is set to GeometryAbortOnInvalid or GeometrySkipInvalid. This function will be
called using the feature with invalid geometry as a parameter.
.. versionadded:: 3.0
.. seealso:: invalidGeometryCallback()
Expand Down
41 changes: 13 additions & 28 deletions python/plugins/processing/core/GeoAlgorithm.py
Expand Up @@ -113,7 +113,7 @@ def execute(self, parameters, context=None, feedback=None, model=None):
if feedback is None:
feedback = QgsProcessingFeedback()
if context is None:
context = dataobjects.createContext()
context = dataobjects.createContext(feedback)

self.model = model
try:
Expand Down Expand Up @@ -327,35 +327,20 @@ def executeAlgorithm(alg, parameters, context=None, feedback=None, model=None):
if feedback is None:
feedback = QgsProcessingFeedback()
if context is None:
context = dataobjects.createContext()
context = dataobjects.createContext(feedback)

#self.model = model
try:
#self.setOutputCRS()
#self.resolveOutputs()
#self.evaluateParameterValues()
#self.runPreExecutionScript(feedback)
result = alg.run(parameters, context, feedback)
#self.processAlgorithm(parameters, context, feedback)
feedback.setProgress(100)
return result
#self.convertUnsupportedFormats(context, feedback)
#self.runPostExecutionScript(feedback)
except GeoAlgorithmExecutionException as gaee:
lines = [self.tr('Error while executing algorithm')]
lines = []
lines.append(traceback.format_exc())
feedback.reportError(gaee.msg)
QgsMessageLog.logMessage(gaee.msg, self.tr('Processing'), QgsMessageLog.CRITICAL)
raise GeoAlgorithmExecutionException(gaee.msg, lines, gaee)
#except Exception as e:
# If something goes wrong and is not caught in the
# algorithm, we catch it here and wrap it
#lines = [self.tr('Uncaught error while executing algorithm')]
# lines = []
# lines.append(traceback.format_exc())
#QgsMessageLog.logMessage('\n'.join(lines), self.tr('Processing'), QgsMessageLog.CRITICAL)
#raise GeoAlgorithmExecutionException(str(e) + self.tr('\nSee log for more details'), lines, e)

#self.setOutputCRS()
#self.resolveOutputs()
#self.evaluateParameterValues()
#self.runPreExecutionScript(feedback)
result, ok = alg.run(parameters, context, feedback)
#self.processAlgorithm(parameters, context, feedback)
feedback.setProgress(100)
return result, ok
#self.convertUnsupportedFormats(context, feedback)
#self.runPostExecutionScript(feedback)

def helpUrl(self):
return QgsHelp.helpUrl("processing_algs/{}/{}".format(
Expand Down
14 changes: 7 additions & 7 deletions python/plugins/processing/core/Processing.py
Expand Up @@ -193,11 +193,17 @@ def runAlgorithm(algOrName, onFinish, *args, **kwargs):
return
i = i + 1

feedback = None
if kwargs is not None and "feedback" in list(kwargs.keys()):
feedback = kwargs["feedback"]
elif iface is not None:
feedback = MessageBarProgress(alg.displayName())

context = None
if kwargs is not None and 'context' in list(kwargs.keys()):
context = kwargs["context"]
else:
context = dataobjects.createContext()
context = dataobjects.createContext(feedback)

ok, msg = alg.checkParameterValues(parameters, context)
if not ok:
Expand Down Expand Up @@ -226,12 +232,6 @@ def runAlgorithm(algOrName, onFinish, *args, **kwargs):
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
overrideCursor = True

feedback = None
if kwargs is not None and "feedback" in list(kwargs.keys()):
feedback = kwargs["feedback"]
elif iface is not None:
feedback = MessageBarProgress(alg.displayName())

ret, results = execute(alg, parameters, context, feedback)
if ret:
if onFinish is not None:
Expand Down
17 changes: 10 additions & 7 deletions python/plugins/processing/gui/AlgorithmDialog.py
Expand Up @@ -164,12 +164,11 @@ def checkExtentCRS(self):
def accept(self):
super(AlgorithmDialog, self)._saveGeometry()

context = dataobjects.createContext()
feedback = self.createFeedback()
context = dataobjects.createContext(feedback)

checkCRS = ProcessingConfig.getSetting(ProcessingConfig.WARN_UNMATCHING_CRS)
try:
feedback = self.createFeedback()

parameters = self.getParamValues()

if checkCRS and not self.alg.validateInputCrs(parameters, context):
Expand Down Expand Up @@ -244,10 +243,14 @@ def accept(self):
if command:
ProcessingLog.addToLog(command)
self.buttonCancel.setEnabled(self.alg.flags() & QgsProcessingAlgorithm.FlagCanCancel)
result = executeAlgorithm(self.alg, parameters, context, feedback)
feedback.pushInfo(self.tr('Execution completed in {0:0.2f} seconds'.format(time.time() - start_time)))
feedback.pushInfo(self.tr('Results:'))
feedback.pushCommandInfo(pformat(result))
result, ok = executeAlgorithm(self.alg, parameters, context, feedback)
if ok:
feedback.pushInfo(self.tr('Execution completed in {0:0.2f} seconds'.format(time.time() - start_time)))
feedback.pushInfo(self.tr('Results:'))
feedback.pushCommandInfo(pformat(result))
else:
feedback.reportError(
self.tr('Execution failed after {0:0.2f} seconds'.format(time.time() - start_time)))
feedback.pushInfo('')

self.buttonCancel.setEnabled(False)
Expand Down
6 changes: 3 additions & 3 deletions python/plugins/processing/gui/AlgorithmExecutor.py
Expand Up @@ -56,11 +56,11 @@ def execute(alg, parameters, context=None, feedback=None):
if feedback is None:
feedback = QgsProcessingFeedback()
if context is None:
context = dataobjects.createContext()
context = dataobjects.createContext(feedback)

try:
results = alg.run(parameters, context, feedback)
return True, results
results, ok = alg.run(parameters, context, feedback)
return ok, results
except GeoAlgorithmExecutionException as e:
QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', QgsMessageLog.CRITICAL)
if feedback is not None:
Expand Down
2 changes: 1 addition & 1 deletion python/plugins/processing/gui/BatchAlgorithmDialog.py
Expand Up @@ -78,8 +78,8 @@ def accept(self):
alg_parameters = []
load = []

context = dataobjects.createContext()
feedback = self.createFeedback()
context = dataobjects.createContext(feedback)

for row in range(self.mainWidget.tblParameters.rowCount()):
col = 0
Expand Down
2 changes: 1 addition & 1 deletion python/plugins/processing/gui/ProcessingToolbox.py
Expand Up @@ -253,7 +253,6 @@ def executeAlgorithmAsBatchProcess(self):
def executeAlgorithm(self):
item = self.algorithmTree.currentItem()
if isinstance(item, TreeAlgorithmItem):
context = dataobjects.createContext()
alg = QgsApplication.processingRegistry().algorithmById(item.alg.id())
ok, message = alg.canExecute()
if not ok:
Expand Down Expand Up @@ -287,6 +286,7 @@ def executeAlgorithm(self):
self.addRecentAlgorithms(True)
else:
feedback = MessageBarProgress()
context = dataobjects.createContext(feedback)
parameters = {}
ret, results = execute(alg, parameters, context, feedback)
handleAlgorithmResults(alg, parameters, context, feedback)
Expand Down
2 changes: 1 addition & 1 deletion python/plugins/processing/gui/menus.py
Expand Up @@ -203,7 +203,6 @@ def _executeAlgorithm(alg):
dlg.exec_()
return

context = dataobjects.createContext()
if (alg.countVisibleParameters()) > 0:
dlg = alg.createCustomParametersWidget(None)
if not dlg:
Expand All @@ -220,6 +219,7 @@ def _executeAlgorithm(alg):
canvas.setMapTool(prevMapTool)
else:
feedback = MessageBarProgress()
context = dataobjects.createContext(feedback)
parameters = {}
ret, results = execute(alg, parameters, context, feedback)
handleAlgorithmResults(alg, context, feedback)
Expand Down

0 comments on commit 19dd097

Please sign in to comment.