diff --git a/python/PyQt6/core/auto_additions/qgspythonrunner.py b/python/PyQt6/core/auto_additions/qgspythonrunner.py index 78e70ef42d23..140aee65a9d5 100644 --- a/python/PyQt6/core/auto_additions/qgspythonrunner.py +++ b/python/PyQt6/core/auto_additions/qgspythonrunner.py @@ -2,7 +2,9 @@ try: QgsPythonRunner.isValid = staticmethod(QgsPythonRunner.isValid) QgsPythonRunner.run = staticmethod(QgsPythonRunner.run) + QgsPythonRunner.runFile = staticmethod(QgsPythonRunner.runFile) QgsPythonRunner.eval = staticmethod(QgsPythonRunner.eval) + QgsPythonRunner.setArgv = staticmethod(QgsPythonRunner.setArgv) QgsPythonRunner.setInstance = staticmethod(QgsPythonRunner.setInstance) except (NameError, AttributeError): pass diff --git a/python/PyQt6/core/auto_generated/qgspythonrunner.sip.in b/python/PyQt6/core/auto_generated/qgspythonrunner.sip.in index d085b259b212..6e3c7b5aa71f 100644 --- a/python/PyQt6/core/auto_generated/qgspythonrunner.sip.in +++ b/python/PyQt6/core/auto_generated/qgspythonrunner.sip.in @@ -33,11 +33,21 @@ Returns ``True`` if the runner has an instance static bool run( const QString &command, const QString &messageOnError = QString() ); %Docstring Execute a Python statement +%End + + static bool runFile( const QString &filename, const QString &messageOnError = QString() ); +%Docstring +Execute a Python file %End static bool eval( const QString &command, QString &result /Out/ ); %Docstring Eval a Python statement +%End + + static bool setArgv( const QStringList &arguments, const QString &messageOnError = QString() ); +%Docstring +Set sys.argv %End static void setInstance( QgsPythonRunner *runner /Transfer/ ); @@ -56,8 +66,12 @@ Protected constructor: can be instantiated only from children virtual bool runCommand( QString command, QString messageOnError = QString() ) = 0; + virtual bool runFileCommand( const QString &filename, const QString &messageOnError = QString() ) = 0; + virtual bool evalCommand( QString command, QString &result ) = 0; + virtual bool setArgvCommand( const QStringList &arguments, const QString &messageOnError = QString() ) = 0; + }; /************************************************************************ diff --git a/python/core/auto_additions/qgspythonrunner.py b/python/core/auto_additions/qgspythonrunner.py index 78e70ef42d23..140aee65a9d5 100644 --- a/python/core/auto_additions/qgspythonrunner.py +++ b/python/core/auto_additions/qgspythonrunner.py @@ -2,7 +2,9 @@ try: QgsPythonRunner.isValid = staticmethod(QgsPythonRunner.isValid) QgsPythonRunner.run = staticmethod(QgsPythonRunner.run) + QgsPythonRunner.runFile = staticmethod(QgsPythonRunner.runFile) QgsPythonRunner.eval = staticmethod(QgsPythonRunner.eval) + QgsPythonRunner.setArgv = staticmethod(QgsPythonRunner.setArgv) QgsPythonRunner.setInstance = staticmethod(QgsPythonRunner.setInstance) except (NameError, AttributeError): pass diff --git a/python/core/auto_generated/qgspythonrunner.sip.in b/python/core/auto_generated/qgspythonrunner.sip.in index d085b259b212..6e3c7b5aa71f 100644 --- a/python/core/auto_generated/qgspythonrunner.sip.in +++ b/python/core/auto_generated/qgspythonrunner.sip.in @@ -33,11 +33,21 @@ Returns ``True`` if the runner has an instance static bool run( const QString &command, const QString &messageOnError = QString() ); %Docstring Execute a Python statement +%End + + static bool runFile( const QString &filename, const QString &messageOnError = QString() ); +%Docstring +Execute a Python file %End static bool eval( const QString &command, QString &result /Out/ ); %Docstring Eval a Python statement +%End + + static bool setArgv( const QStringList &arguments, const QString &messageOnError = QString() ); +%Docstring +Set sys.argv %End static void setInstance( QgsPythonRunner *runner /Transfer/ ); @@ -56,8 +66,12 @@ Protected constructor: can be instantiated only from children virtual bool runCommand( QString command, QString messageOnError = QString() ) = 0; + virtual bool runFileCommand( const QString &filename, const QString &messageOnError = QString() ) = 0; + virtual bool evalCommand( QString command, QString &result ) = 0; + virtual bool setArgvCommand( const QStringList &arguments, const QString &messageOnError = QString() ) = 0; + }; /************************************************************************ diff --git a/src/app/main.cpp b/src/app/main.cpp index afaaa23eedf8..9e50b8b775a6 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 bc5c5eea0504..646b20115484 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -12246,6 +12246,20 @@ class QgsPythonRunnerImpl : public QgsPythonRunner return false; } + bool runFileCommand( 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 @@ -12260,6 +12274,20 @@ class QgsPythonRunnerImpl : public QgsPythonRunner return false; } + bool setArgvCommand( 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..70518cc7de22 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->runFileCommand( 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,6 +66,19 @@ bool QgsPythonRunner::eval( const QString &command, QString &result ) } } +bool QgsPythonRunner::setArgv( const QStringList &arguments, const QString &messageOnError ) +{ + if ( sInstance ) + { + return sInstance->setArgvCommand( arguments, messageOnError ); + } + else + { + QgsDebugError( QStringLiteral( "Unable to run Python command: runner not available!" ) ); + return false; + } +} + void QgsPythonRunner::setInstance( QgsPythonRunner *runner ) { delete sInstance; diff --git a/src/core/qgspythonrunner.h b/src/core/qgspythonrunner.h index 30920de902b8..bff33f448ca4 100644 --- a/src/core/qgspythonrunner.h +++ b/src/core/qgspythonrunner.h @@ -42,9 +42,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 +65,12 @@ class CORE_EXPORT QgsPythonRunner virtual bool runCommand( QString command, QString messageOnError = QString() ) = 0; + virtual bool runFileCommand( const QString &filename, const QString &messageOnError = QString() ) = 0; + virtual bool evalCommand( QString command, QString &result ) = 0; + virtual bool setArgvCommand( 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..9f1786d921b1 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, const QString &messageOnError = QString() ) = 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 setArgv( const QStringList &arguments, const QString &messageOnError = QString() ) = 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..0ba1185fcd3e 100644 --- a/src/python/qgspythonutilsimpl.cpp +++ b/src/python/qgspythonutilsimpl.cpp @@ -454,6 +454,134 @@ 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; + + PyObject *obj, *errobj; + + QFile file( filename ); + if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + ret = "Cannot open file"; + goto error; + } + + obj = PyRun_String( file.readAll().constData(), Py_file_input, mMainDict, mMainDict ); + 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 ) +{ + const QString &traceback = runFileUnsafe( filename ); + if ( traceback.isEmpty() ) + return true; + + // use some default message if custom hasn't been specified + const QString &errMsg = !messageOnError.isEmpty() ? messageOnError : 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 = "" + errMsg + "
\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, *errobj = nullptr, *argsobj = nullptr; + sysobj = PyImport_ImportModule( "sys" ); + if ( !sysobj ) + { + errobj = PyErr_Occurred(); + if ( errobj ) + ret = QString( "SetArgvTraceback" ) + getTraceback(); + else + ret = "Error occurred in PyImport_ImportModule"; + goto error; + } + argsobj = PyList_New( arguments.size() ); + if ( !argsobj ) + { + ret = "Error occurred in PyList_New"; + goto error; + } + for ( int i = 0; i != arguments.size(); ++i ) + PyList_SET_ITEM( argsobj, i, PyUnicode_FromString( arguments[i].toUtf8().constData() ) ); + if ( PyObject_SetAttrString( sysobj, "argv", argsobj ) != 0 ) + { + ret = "Error occurred in PyObject_SetAttrString"; + goto error; + } +error: + Py_XDECREF( argsobj ); + Py_XDECREF( sysobj ); + + // we are done calling python API, release global interpreter lock + PyGILState_Release( gstate ); + + return ret; +} + +bool QgsPythonUtilsImpl::setArgv( const QStringList &arguments, const QString &messageOnError ) +{ + const QString &traceback = setArgvUnsafe( arguments ); + if ( traceback.isEmpty() ) + return true; + + // use some default message if custom hasn't been specified + const QString &errMsg = !messageOnError.isEmpty() ? messageOnError : 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 = "" + errMsg + "
\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..6177b226cd1d 100644 --- a/src/python/qgspythonutilsimpl.h +++ b/src/python/qgspythonutilsimpl.h @@ -44,9 +44,15 @@ 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; bool evalString( const QString &command, QString &result ) final; + bool setArgv( const QStringList &arguments, const QString &messageOnError = QString() ) final; bool getError( QString &errorClassName, QString &errorText ) final; + private: + QString runFileUnsafe( const QString &filename ); // returns error traceback on failure, empty QString on success + QString setArgvUnsafe( const QStringList &arguments ); // returns error traceback on failure, empty QString on success + public: /** * Returns the path where QGIS Python related files are located. */ diff --git a/tests/code_layout/acceptable_missing_doc.py b/tests/code_layout/acceptable_missing_doc.py index 58537a4c59bb..5fcb571ecf99 100644 --- a/tests/code_layout/acceptable_missing_doc.py +++ b/tests/code_layout/acceptable_missing_doc.py @@ -235,7 +235,9 @@ "QgsScopeLogger": ["QgsScopeLogger(const char *file, const char *func, int line)"], "QgsPythonRunner": [ "evalCommand(QString command, QString &result)=0", + "runFileCommand(const QString &filename, const QString &messageOnError=QString())=0", "runCommand(QString command, QString messageOnError=QString())=0", + "setArgvCommand(const QStringList &arguments, const QString &messageOnError=QString())=0", ], "QgsAttributeActionDialog": [ "init(const QgsActionManager &action, const QgsAttributeTableConfig &attributeTableConfig)",