diff --git a/python/PyQt6/core/auto_additions/qgspythonrunner.py b/python/PyQt6/core/auto_additions/qgspythonrunner.py index 70223f5b1a3..eea3f842be6 100644 --- a/python/PyQt6/core/auto_additions/qgspythonrunner.py +++ b/python/PyQt6/core/auto_additions/qgspythonrunner.py @@ -2,8 +2,10 @@ 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) - QgsPythonRunner.__abstract_methods__ = ['runCommand', 'evalCommand'] + QgsPythonRunner.__abstract_methods__ = ['runCommand', 'runFileCommand', 'evalCommand', 'setArgvCommand'] 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 03154aeecc3..4d047a3dd9c 100644 --- a/python/PyQt6/core/auto_generated/qgspythonrunner.sip.in +++ b/python/PyQt6/core/auto_generated/qgspythonrunner.sip.in @@ -33,11 +33,23 @@ commands) 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 ``filename``, showing an error message if one occurred. + +:return: true if no error occurred %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 +68,24 @@ Protected constructor: can be instantiated only from children virtual ~QgsPythonRunner(); virtual bool runCommand( QString command, QString messageOnError = QString() ) = 0; +%Docstring +Runs the given statement. +%End + + virtual bool runFileCommand( const QString &filename, const QString &messageOnError = QString() ) = 0; +%Docstring +Runs the code from the given file. +%End virtual bool evalCommand( QString command, QString &result ) = 0; +%Docstring +Evaluates the given expression, producing a result. +%End + + virtual bool setArgvCommand( const QStringList &arguments, const QString &messageOnError = QString() ) = 0; +%Docstring +Sets sys.argv to the given arguments. +%End }; diff --git a/python/PyQt6/core/class_map.yaml b/python/PyQt6/core/class_map.yaml index 400c3ebcb6e..738a005338b 100644 --- a/python/PyQt6/core/class_map.yaml +++ b/python/PyQt6/core/class_map.yaml @@ -14013,12 +14013,16 @@ QgsProxyProgressTask.finalize: src/core/qgsproxyprogresstask.h#L57 QgsProxyProgressTask.run: src/core/qgsproxyprogresstask.h#L59 QgsProxyProgressTask.setProxyProgress: src/core/qgsproxyprogresstask.h#L66 QgsProxyProgressTask: src/core/qgsproxyprogresstask.h#L37 -QgsPythonRunner.eval: src/core/qgspythonrunner.h#L46 -QgsPythonRunner.evalCommand: src/core/qgspythonrunner.h#L62 +QgsPythonRunner.eval: src/core/qgspythonrunner.h#L52 +QgsPythonRunner.evalCommand: src/core/qgspythonrunner.h#L76 QgsPythonRunner.isValid: src/core/qgspythonrunner.h#L40 QgsPythonRunner.run: src/core/qgspythonrunner.h#L43 -QgsPythonRunner.runCommand: src/core/qgspythonrunner.h#L60 -QgsPythonRunner.setInstance: src/core/qgspythonrunner.h#L53 +QgsPythonRunner.runCommand: src/core/qgspythonrunner.h#L70 +QgsPythonRunner.runFile: src/core/qgspythonrunner.h#L49 +QgsPythonRunner.runFileCommand: src/core/qgspythonrunner.h#L73 +QgsPythonRunner.setArgv: src/core/qgspythonrunner.h#L55 +QgsPythonRunner.setArgvCommand: src/core/qgspythonrunner.h#L79 +QgsPythonRunner.setInstance: src/core/qgspythonrunner.h#L62 QgsPythonRunner: src/core/qgspythonrunner.h#L32 QgsQtLocationConnection.broadcastConnectionAvailable: src/core/gps/qgsqtlocationconnection.h#L45 QgsQtLocationConnection.parseData: src/core/gps/qgsqtlocationconnection.h#L48 diff --git a/python/core/auto_additions/qgspythonrunner.py b/python/core/auto_additions/qgspythonrunner.py index 70223f5b1a3..eea3f842be6 100644 --- a/python/core/auto_additions/qgspythonrunner.py +++ b/python/core/auto_additions/qgspythonrunner.py @@ -2,8 +2,10 @@ 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) - QgsPythonRunner.__abstract_methods__ = ['runCommand', 'evalCommand'] + QgsPythonRunner.__abstract_methods__ = ['runCommand', 'runFileCommand', 'evalCommand', 'setArgvCommand'] except (NameError, AttributeError): pass diff --git a/python/core/auto_generated/qgspythonrunner.sip.in b/python/core/auto_generated/qgspythonrunner.sip.in index 03154aeecc3..4d047a3dd9c 100644 --- a/python/core/auto_generated/qgspythonrunner.sip.in +++ b/python/core/auto_generated/qgspythonrunner.sip.in @@ -33,11 +33,23 @@ commands) 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 ``filename``, showing an error message if one occurred. + +:return: true if no error occurred %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 +68,24 @@ Protected constructor: can be instantiated only from children virtual ~QgsPythonRunner(); virtual bool runCommand( QString command, QString messageOnError = QString() ) = 0; +%Docstring +Runs the given statement. +%End + + virtual bool runFileCommand( const QString &filename, const QString &messageOnError = QString() ) = 0; +%Docstring +Runs the code from the given file. +%End virtual bool evalCommand( QString command, QString &result ) = 0; +%Docstring +Evaluates the given expression, producing a result. +%End + + virtual bool setArgvCommand( const QStringList &arguments, const QString &messageOnError = QString() ) = 0; +%Docstring +Sets sys.argv to the given arguments. +%End }; diff --git a/python/core/class_map.yaml b/python/core/class_map.yaml index eb6c1df61fa..25f5d0d7d85 100644 --- a/python/core/class_map.yaml +++ b/python/core/class_map.yaml @@ -14013,12 +14013,16 @@ QgsProxyProgressTask.finalize: src/core/qgsproxyprogresstask.h#L57 QgsProxyProgressTask.run: src/core/qgsproxyprogresstask.h#L59 QgsProxyProgressTask.setProxyProgress: src/core/qgsproxyprogresstask.h#L66 QgsProxyProgressTask: src/core/qgsproxyprogresstask.h#L37 -QgsPythonRunner.eval: src/core/qgspythonrunner.h#L46 -QgsPythonRunner.evalCommand: src/core/qgspythonrunner.h#L62 +QgsPythonRunner.eval: src/core/qgspythonrunner.h#L52 +QgsPythonRunner.evalCommand: src/core/qgspythonrunner.h#L76 QgsPythonRunner.isValid: src/core/qgspythonrunner.h#L40 QgsPythonRunner.run: src/core/qgspythonrunner.h#L43 -QgsPythonRunner.runCommand: src/core/qgspythonrunner.h#L60 -QgsPythonRunner.setInstance: src/core/qgspythonrunner.h#L53 +QgsPythonRunner.runCommand: src/core/qgspythonrunner.h#L70 +QgsPythonRunner.runFile: src/core/qgspythonrunner.h#L49 +QgsPythonRunner.runFileCommand: src/core/qgspythonrunner.h#L73 +QgsPythonRunner.setArgv: src/core/qgspythonrunner.h#L55 +QgsPythonRunner.setArgvCommand: src/core/qgspythonrunner.h#L79 +QgsPythonRunner.setInstance: src/core/qgspythonrunner.h#L62 QgsPythonRunner: src/core/qgspythonrunner.h#L32 QgsQtLocationConnection.broadcastConnectionAvailable: src/core/gps/qgsqtlocationconnection.h#L45 QgsQtLocationConnection.parseData: src/core/gps/qgsqtlocationconnection.h#L48 diff --git a/src/app/main.cpp b/src/app/main.cpp index fef82c85263..6f2804d6604 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1637,23 +1637,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 d7c4f790c97..bcc8fd2844c 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -12169,6 +12169,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 @@ -12183,6 +12197,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 8dd7dab37ed..70518cc7de2 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 30920de902b..a9a2db56510 100644 --- a/src/core/qgspythonrunner.h +++ b/src/core/qgspythonrunner.h @@ -42,9 +42,18 @@ class CORE_EXPORT QgsPythonRunner //! Execute a Python statement static bool run( const QString &command, const QString &messageOnError = QString() ); + /** + * Execute a Python \a filename, showing an error message if one occurred. + * \returns true if no error occurred + */ + 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. @@ -57,10 +66,18 @@ class CORE_EXPORT QgsPythonRunner QgsPythonRunner() = default; virtual ~QgsPythonRunner() = default; + //! Runs the given statement. virtual bool runCommand( QString command, QString messageOnError = QString() ) = 0; + //! Runs the code from the given file. + virtual bool runFileCommand( const QString &filename, const QString &messageOnError = QString() ) = 0; + + //! Evaluates the given expression, producing a result. virtual bool evalCommand( QString command, QString &result ) = 0; + //! Sets sys.argv to the given arguments. + 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 68bb9fc1490..9f1786d921b 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 14facb8ea42..0d5854b2475 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; + + PyObject *obj, *errobj; + + QFile file( filename ); + if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + ret = QStringLiteral( "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( "  " ) ); + + 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( "  " ) ); + + 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 d900f1b5569..6177b226cd1 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 0d05a24f1e7..12d6431a538 100644 --- a/tests/code_layout/acceptable_missing_doc.py +++ b/tests/code_layout/acceptable_missing_doc.py @@ -233,10 +233,6 @@ ACCEPTABLE_MISSING_DOCS = { "top() const", ], "QgsScopeLogger": ["QgsScopeLogger(const char *file, const char *func, int line)"], - "QgsPythonRunner": [ - "evalCommand(QString command, QString &result)=0", - "runCommand(QString command, QString messageOnError=QString())=0", - ], "QgsAttributeActionDialog": [ "init(const QgsActionManager &action, const QgsAttributeTableConfig &attributeTableConfig)", "QgsAttributeActionDialog(const QgsActionManager &actions, QWidget *parent=nullptr)",