diff --git a/src/app/main.cpp b/src/app/main.cpp index d64a6aeef29b..53c1bd644258 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1634,23 +1634,14 @@ int main( int argc, char *argv[] ) { if ( !pythonfile.isEmpty() ) { -#ifdef Q_OS_WIN - //replace backslashes with forward slashes - pythonfile.replace( '\\', '/' ); -#endif pythonArgs.prepend( pythonfile ); } - - QgsPythonRunner::run( QStringLiteral( "sys.argv = ['%1']" ).arg( pythonArgs.replaceInStrings( QChar( '\'' ), QStringLiteral( "\\'" ) ).join( "','" ) ) ); + QgsPythonRunner::setArgv( pythonArgs ); } if ( !pythonfile.isEmpty() ) { -#ifdef Q_OS_WIN - //replace backslashes with forward slashes - pythonfile.replace( '\\', '/' ); -#endif - QgsPythonRunner::run( QStringLiteral( "with open('%1','r') as f: exec(f.read())" ).arg( pythonfile ) ); + QgsPythonRunner::runFile( pythonfile ); } /////////////////////////////////`//////////////////////////////////// diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 0d2bfecfc44a..16471e28c503 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -12101,6 +12101,20 @@ class QgsPythonRunnerImpl : public QgsPythonRunner return false; } + bool runFile( const QString &filename, const QString &messageOnError = QString() ) override + { +#ifdef WITH_BINDINGS + if ( mPythonUtils && mPythonUtils->isEnabled() ) + { + return mPythonUtils->runFile( filename, messageOnError ); + } +#else + Q_UNUSED( filename ) + Q_UNUSED( messageOnError ) +#endif + return false; + } + bool evalCommand( QString command, QString &result ) override { #ifdef WITH_BINDINGS @@ -12115,6 +12129,20 @@ class QgsPythonRunnerImpl : public QgsPythonRunner return false; } + bool setArgv( const QStringList &arguments, const QString &messageOnError = QString() ) override + { +#ifdef WITH_BINDINGS + if ( mPythonUtils && mPythonUtils->isEnabled() ) + { + return mPythonUtils->setArgv( arguments, messageOnError ); + } +#else + Q_UNUSED( arguments ) + Q_UNUSED( messageOnError ) +#endif + return false; + } + protected: QgsPythonUtils *mPythonUtils = nullptr; }; diff --git a/src/core/qgspythonrunner.cpp b/src/core/qgspythonrunner.cpp index 8dd7dab37ed1..91efbc620b39 100644 --- a/src/core/qgspythonrunner.cpp +++ b/src/core/qgspythonrunner.cpp @@ -39,6 +39,20 @@ bool QgsPythonRunner::run( const QString &command, const QString &messageOnError } } +bool QgsPythonRunner::runFile( const QString &filename, const QString &messageOnError ) +{ + if ( sInstance ) + { + QgsDebugMsgLevel( "Running " + filename, 3 ); + return sInstance->runFile( filename, messageOnError ); + } + else + { + QgsDebugError( QStringLiteral( "Unable to run Python command: runner not available!" ) ); + return false; + } +} + bool QgsPythonRunner::eval( const QString &command, QString &result ) { if ( sInstance ) @@ -52,9 +66,21 @@ bool QgsPythonRunner::eval( const QString &command, QString &result ) } } +bool QgsPythonRunner::setArgv( const QStringList &arguments, const QString &messageOnError ) +{ + if ( sInstance ) + { + return sInstance->setArgv( arguments, messageOnError ); + } + else + { + QgsDebugError( QStringLiteral( "Unable to run Python command: runner not available!" ) ); + return false; + } +} + void QgsPythonRunner::setInstance( QgsPythonRunner *runner ) { delete sInstance; sInstance = runner; } - diff --git a/src/core/qgspythonrunner.h b/src/core/qgspythonrunner.h index 30920de902b8..560264fe69bf 100644 --- a/src/core/qgspythonrunner.h +++ b/src/core/qgspythonrunner.h @@ -32,7 +32,6 @@ class CORE_EXPORT QgsPythonRunner { public: - /** * Returns TRUE if the runner has an instance * (and thus is able to run commands) @@ -42,9 +41,15 @@ class CORE_EXPORT QgsPythonRunner //! Execute a Python statement static bool run( const QString &command, const QString &messageOnError = QString() ); + //! Execute a Python file + static bool runFile( const QString &filename, const QString &messageOnError = QString() ); + //! Eval a Python statement static bool eval( const QString &command, QString &result SIP_OUT ); + //! Set sys.argv + static bool setArgv( const QStringList &arguments, const QString &messageOnError = QString() ); + /** * Assign an instance of Python runner so that run() can be used. * This method should be called during app initialization. @@ -59,8 +64,12 @@ class CORE_EXPORT QgsPythonRunner virtual bool runCommand( QString command, QString messageOnError = QString() ) = 0; + virtual bool runFile( const QString &filename, const QString &messageOnError = QString() ) = 0; + virtual bool evalCommand( QString command, QString &result ) = 0; + virtual bool setArgv( const QStringList &arguments, const QString &messageOnError = QString() ) = 0; + static QgsPythonRunner *sInstance; }; diff --git a/src/python/qgspythonutils.h b/src/python/qgspythonutils.h index 68bb9fc1490d..4f22592c4414 100644 --- a/src/python/qgspythonutils.h +++ b/src/python/qgspythonutils.h @@ -99,11 +99,23 @@ class PYTHON_EXPORT QgsPythonUtils */ virtual QString runStringUnsafe( const QString &command, bool single = true ) = 0; + /** + * Runs a Python \a filename, showing an error message if one occurred. + * \returns TRUE if no error occurred + */ + virtual bool runFile( const QString &filename, QString msgOnError = QString(), bool single = true ) = 0; + /** * Evaluates a Python \a command and stores the result in a the \a result string. */ virtual bool evalString( const QString &command, QString &result ) = 0; + /** + * Sets sys.argv to the given Python \a arguments, showing an error message if one occurred. + * \returns TRUE if no error occurred + */ + virtual bool setArgs( const QStringList &arguments, QString msgOnError = QString(), bool single = true ) = 0; + /** * Gets information about error to the supplied arguments * \returns FALSE if there was no Python error diff --git a/src/python/qgspythonutilsimpl.cpp b/src/python/qgspythonutilsimpl.cpp index 14facb8ea425..01b89318b17b 100644 --- a/src/python/qgspythonutilsimpl.cpp +++ b/src/python/qgspythonutilsimpl.cpp @@ -454,6 +454,132 @@ bool QgsPythonUtilsImpl::runString( const QString &command, QString msgOnError, return res; } +QString QgsPythonUtilsImpl::runFileUnsafe( const QString &filename ) +{ + // acquire global interpreter lock to ensure we are in a consistent state + PyGILState_STATE gstate; + gstate = PyGILState_Ensure(); + QString ret; + + QFile file( filename ); + if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + ret = "Cannot open file"; + goto error; + } + + PyObject *obj = PyRun_String( file.readAll().constData(), Py_file_input, mMainDict, mMainDict ); + PyObject *errobj = PyErr_Occurred(); + if ( nullptr != errobj ) + { + ret = getTraceback(); + } + Py_XDECREF( obj ); + +error: + // we are done calling python API, release global interpreter lock + PyGILState_Release( gstate ); + + return ret; +} + +bool QgsPythonUtilsImpl::runFile( const QString &filename, const QString &messageOnError = QString() ) +{ + const QString traceback = runFileUnsafe( filename ); + if ( traceback.isEmpty() ) + return true; + + if ( msgOnError.isEmpty() ) + { + // use some default message if custom hasn't been specified + msgOnError = QObject::tr( "An error occurred during execution of following file:" ) + "\n" + filename + ""; + } + + QString path, version; + evalString( QStringLiteral( "str(sys.path)" ), path ); + evalString( QStringLiteral( "sys.version" ), version ); + + QString str = "" + msgOnError + "
\n" + traceback + "\n
" + + QObject::tr( "Python version:" ) + "
" + version + "

" + + QObject::tr( "QGIS version:" ) + "
" + QStringLiteral( "%1 '%2', %3" ).arg( Qgis::version(), Qgis::releaseName(), Qgis::devVersion() ) + "

" + + QObject::tr( "Python path:" ) + "
" + path; + str.replace( '\n', QLatin1String( "
" ) ).replace( QLatin1String( " " ), QLatin1String( "  " ) ); + + qDebug() << str; + QgsMessageOutput *msg = QgsMessageOutput::createMessageOutput(); + msg->setTitle( QObject::tr( "Python error" ) ); + msg->setMessage( str, QgsMessageOutput::MessageHtml ); + msg->showMessage(); + + return false; +} + +QString QgsPythonUtilsImpl::setArgvUnsafe( const QStringList &arguments ) +{ + // acquire global interpreter lock to ensure we are in a consistent state + PyGILState_STATE gstate; + gstate = PyGILState_Ensure(); + QString ret; + + PyObject *sysobj = nullptr, *argsobj = nullptr; + bool success = false; + sysobj = PyRun_String( "sys", Py_single_input, mMainDict, mMainDict ); + if ( !sysobj ) + goto error; + PyObject *argsobj = PyList_New( arguments.size() ); + if ( !argsobj ) + goto error; + for ( size_t i = 0; i != arguments.size(); ++i ) + { + if ( PyList_SET_ITEM( argsobj, i, PyUnicode_FromString( arguments[i].toUtf8().constData() ) ) != 0 ) + goto error; + } + if ( PyObject_SetAttrString( sysobj, "argv", argsobj ) != 0 ) + goto error; + success = true; +error: + Py_XDECREF( argsobj ); + Py_XDECREF( sysobj ); + if ( !success ) + ret = "Cannot set sys.argv"; + + // we are done calling python API, release global interpreter lock + PyGILState_Release( gstate ); + + return ret; +} + +bool QgsPythonUtilsImpl::setArgv( const QStringList &arguments, const QString &messageOnError = QString() ) +{ + const QString traceback = setArgvUnsafe( arguments ); + if ( traceback.isEmpty() ) + return true; + + if ( msgOnError.isEmpty() ) + { + // use some default message if custom hasn't been specified + msgOnError = QObject::tr( "An error occurred while setting sys.argv from following list:" ) + "\n" + arguments.join( ',' ) + ""; + } + + QString path, version; + evalString( QStringLiteral( "str(sys.path)" ), path ); + evalString( QStringLiteral( "sys.version" ), version ); + + QString str = "" + msgOnError + "
\n" + traceback + "\n
" + + QObject::tr( "Python version:" ) + "
" + version + "

" + + QObject::tr( "QGIS version:" ) + "
" + QStringLiteral( "%1 '%2', %3" ).arg( Qgis::version(), Qgis::releaseName(), Qgis::devVersion() ) + "

" + + QObject::tr( "Python path:" ) + "
" + path; + str.replace( '\n', QLatin1String( "
" ) ).replace( QLatin1String( " " ), QLatin1String( "  " ) ); + + qDebug() << str; + QgsMessageOutput *msg = QgsMessageOutput::createMessageOutput(); + msg->setTitle( QObject::tr( "Python error" ) ); + msg->setMessage( str, QgsMessageOutput::MessageHtml ); + msg->showMessage(); + + return false; +} + QString QgsPythonUtilsImpl::getTraceback() { diff --git a/src/python/qgspythonutilsimpl.h b/src/python/qgspythonutilsimpl.h index d900f1b55693..26ec410926e8 100644 --- a/src/python/qgspythonutilsimpl.h +++ b/src/python/qgspythonutilsimpl.h @@ -44,7 +44,11 @@ class QgsPythonUtilsImpl : public QgsPythonUtils bool isEnabled() final; bool runString( const QString &command, QString msgOnError = QString(), bool single = true ) final; QString runStringUnsafe( const QString &command, bool single = true ) final; // returns error traceback on failure, empty QString on success + bool runFile( const QString &filename, const QString &messageOnError = QString() ) final; + QString runFileUnsafe( const QString &filename ) final; // returns error traceback on failure, empty QString on success bool evalString( const QString &command, QString &result ) final; + QString setArgvUnsafe( const QStringList &arguments ) final; // returns error traceback on failure, empty QString on success + bool setArgv( const QStringList &arguments, const QString &messageOnError = QString() ) final; bool getError( QString &errorClassName, QString &errorText ) final; /**