Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw c++ exception when a Python exception occurs while running a algorithm #4764

Merged
merged 13 commits into from
Jun 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cmake_templates/Doxyfile.in
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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