diff --git a/.gitignore b/.gitignore index 31c8f9267358..1caba388c306 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ ms-windows/osgeo4w/untgz/ ms-windows/packages/ ms-windows/progs/ ms-windows/untgz/ +python/expressions/ python/plugins/grassprovider/description/algorithms.json python/plugins/grassprovider/tests/testdata/directions.tif.aux.xml python/plugins/processing/tests/testdata/*.aux.xml diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 1e9ffc3a0cdc..12bfb4672efc 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -620,34 +620,37 @@ Qgis.VectorLayerTypeFlags.baseClass = Qgis VectorLayerTypeFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum -Qgis.Never = Qgis.PythonMacroMode.Never -Qgis.Never.is_monkey_patched = True -Qgis.Never.__doc__ = "Macros are never run" -Qgis.Ask = Qgis.PythonMacroMode.Ask -Qgis.Ask.is_monkey_patched = True -Qgis.Ask.__doc__ = "User is prompt before running" -Qgis.SessionOnly = Qgis.PythonMacroMode.SessionOnly -Qgis.SessionOnly.is_monkey_patched = True -Qgis.SessionOnly.__doc__ = "Only during this session" -Qgis.Always = Qgis.PythonMacroMode.Always -Qgis.Always.is_monkey_patched = True -Qgis.Always.__doc__ = "Macros are always run" -Qgis.NotForThisSession = Qgis.PythonMacroMode.NotForThisSession -Qgis.NotForThisSession.is_monkey_patched = True -Qgis.NotForThisSession.__doc__ = "Macros will not be run for this session" -Qgis.PythonMacroMode.__doc__ = """Authorisation to run Python Macros - -.. versionadded:: 3.10 - -* ``Never``: Macros are never run +Qgis.PythonEmbeddedMode.Never.__doc__ = "Python embedded never run" +Qgis.PythonEmbeddedMode.Ask.__doc__ = "User is prompt before running" +Qgis.PythonEmbeddedMode.SessionOnly.__doc__ = "Only during this session" +Qgis.PythonEmbeddedMode.Always.__doc__ = "Python embedded is always run" +Qgis.PythonEmbeddedMode.NotForThisSession.__doc__ = "Python embedded will not be run for this session" +Qgis.PythonEmbeddedMode.__doc__ = """Authorisation to run Python Embedded in projects + +.. versionadded:: 3.40 + +* ``Never``: Python embedded never run * ``Ask``: User is prompt before running * ``SessionOnly``: Only during this session -* ``Always``: Macros are always run -* ``NotForThisSession``: Macros will not be run for this session +* ``Always``: Python embedded is always run +* ``NotForThisSession``: Python embedded will not be run for this session + +""" +# -- +Qgis.PythonEmbeddedMode.baseClass = Qgis +# monkey patching scoped based enum +Qgis.PythonEmbeddedType.Macro.__doc__ = "" +Qgis.PythonEmbeddedType.ExpressionFunction.__doc__ = "" +Qgis.PythonEmbeddedType.__doc__ = """Type of Python Embedded in projects + +.. versionadded:: 3.40 + +* ``Macro``: +* ``ExpressionFunction``: """ # -- -Qgis.PythonMacroMode.baseClass = Qgis +Qgis.PythonEmbeddedType.baseClass = Qgis QgsDataProvider.ReadFlag = Qgis.DataProviderReadFlag # monkey patching scoped based enum QgsDataProvider.FlagTrustDataSource = Qgis.DataProviderReadFlag.TrustDataSource diff --git a/python/PyQt6/core/auto_generated/expression/qgsexpression.sip.in b/python/PyQt6/core/auto_generated/expression/qgsexpression.sip.in index 8a244756dc97..1141236cfb16 100644 --- a/python/PyQt6/core/auto_generated/expression/qgsexpression.sip.in +++ b/python/PyQt6/core/auto_generated/expression/qgsexpression.sip.in @@ -519,8 +519,6 @@ Returns the number of functions defined in the parser :return: The number of function defined in the parser. %End - - static QString quotedColumnRef( QString name ); %Docstring Returns a quoted column reference (in double quotes) diff --git a/python/PyQt6/core/auto_generated/project/qgsproject.sip.in b/python/PyQt6/core/auto_generated/project/qgsproject.sip.in index 7dec7c97a28c..80f1a9bbdc87 100644 --- a/python/PyQt6/core/auto_generated/project/qgsproject.sip.in +++ b/python/PyQt6/core/auto_generated/project/qgsproject.sip.in @@ -1657,6 +1657,9 @@ Sets the elevation shading renderer used for global map shading .. versionadded:: 3.30 %End + + + SIP_PYOBJECT __repr__(); %MethodCode QString str = QStringLiteral( "" ).arg( sipCpp->fileName(), diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 3906b737d0cf..0cc524d93a5a 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -224,8 +224,8 @@ The development version typedef QFlags VectorLayerTypeFlags; - enum class PythonMacroMode /BaseType=IntEnum/ - { + enum class PythonEmbeddedMode /BaseType=IntEnum/ + { Never, Ask, SessionOnly, @@ -233,6 +233,12 @@ The development version NotForThisSession, }; + enum class PythonEmbeddedType /BaseType=IntEnum/ + { + Macro, + ExpressionFunction, + }; + enum class DataProviderReadFlag /BaseType=IntFlag/ { TrustDataSource, diff --git a/python/PyQt6/gui/auto_generated/qgsgui.sip.in b/python/PyQt6/gui/auto_generated/qgsgui.sip.in index 8fa7cf80046f..cb5e4013bccc 100644 --- a/python/PyQt6/gui/auto_generated/qgsgui.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsgui.sip.in @@ -251,7 +251,6 @@ Returns the screen at the given global ``point`` (pixel). - signals: void optionsChanged(); diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index ca1231fcf2fb..26e8ecc3644a 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -612,34 +612,37 @@ Qgis.VectorLayerTypeFlags.baseClass = Qgis VectorLayerTypeFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum -Qgis.Never = Qgis.PythonMacroMode.Never -Qgis.Never.is_monkey_patched = True -Qgis.Never.__doc__ = "Macros are never run" -Qgis.Ask = Qgis.PythonMacroMode.Ask -Qgis.Ask.is_monkey_patched = True -Qgis.Ask.__doc__ = "User is prompt before running" -Qgis.SessionOnly = Qgis.PythonMacroMode.SessionOnly -Qgis.SessionOnly.is_monkey_patched = True -Qgis.SessionOnly.__doc__ = "Only during this session" -Qgis.Always = Qgis.PythonMacroMode.Always -Qgis.Always.is_monkey_patched = True -Qgis.Always.__doc__ = "Macros are always run" -Qgis.NotForThisSession = Qgis.PythonMacroMode.NotForThisSession -Qgis.NotForThisSession.is_monkey_patched = True -Qgis.NotForThisSession.__doc__ = "Macros will not be run for this session" -Qgis.PythonMacroMode.__doc__ = """Authorisation to run Python Macros - -.. versionadded:: 3.10 - -* ``Never``: Macros are never run +Qgis.PythonEmbeddedMode.Never.__doc__ = "Python embedded never run" +Qgis.PythonEmbeddedMode.Ask.__doc__ = "User is prompt before running" +Qgis.PythonEmbeddedMode.SessionOnly.__doc__ = "Only during this session" +Qgis.PythonEmbeddedMode.Always.__doc__ = "Python embedded is always run" +Qgis.PythonEmbeddedMode.NotForThisSession.__doc__ = "Python embedded will not be run for this session" +Qgis.PythonEmbeddedMode.__doc__ = """Authorisation to run Python Embedded in projects + +.. versionadded:: 3.40 + +* ``Never``: Python embedded never run * ``Ask``: User is prompt before running * ``SessionOnly``: Only during this session -* ``Always``: Macros are always run -* ``NotForThisSession``: Macros will not be run for this session +* ``Always``: Python embedded is always run +* ``NotForThisSession``: Python embedded will not be run for this session + +""" +# -- +Qgis.PythonEmbeddedMode.baseClass = Qgis +# monkey patching scoped based enum +Qgis.PythonEmbeddedType.Macro.__doc__ = "" +Qgis.PythonEmbeddedType.ExpressionFunction.__doc__ = "" +Qgis.PythonEmbeddedType.__doc__ = """Type of Python Embedded in projects + +.. versionadded:: 3.40 + +* ``Macro``: +* ``ExpressionFunction``: """ # -- -Qgis.PythonMacroMode.baseClass = Qgis +Qgis.PythonEmbeddedType.baseClass = Qgis QgsDataProvider.ReadFlag = Qgis.DataProviderReadFlag # monkey patching scoped based enum QgsDataProvider.FlagTrustDataSource = Qgis.DataProviderReadFlag.TrustDataSource diff --git a/python/core/auto_generated/expression/qgsexpression.sip.in b/python/core/auto_generated/expression/qgsexpression.sip.in index 961ff9ac3ef1..d6bff3e67ddf 100644 --- a/python/core/auto_generated/expression/qgsexpression.sip.in +++ b/python/core/auto_generated/expression/qgsexpression.sip.in @@ -519,8 +519,6 @@ Returns the number of functions defined in the parser :return: The number of function defined in the parser. %End - - static QString quotedColumnRef( QString name ); %Docstring Returns a quoted column reference (in double quotes) diff --git a/python/core/auto_generated/project/qgsproject.sip.in b/python/core/auto_generated/project/qgsproject.sip.in index a482a2a80d2f..c14571fd6147 100644 --- a/python/core/auto_generated/project/qgsproject.sip.in +++ b/python/core/auto_generated/project/qgsproject.sip.in @@ -1657,6 +1657,9 @@ Sets the elevation shading renderer used for global map shading .. versionadded:: 3.30 %End + + + SIP_PYOBJECT __repr__(); %MethodCode QString str = QStringLiteral( "" ).arg( sipCpp->fileName(), diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index effc72c91ebd..eec0e4bfc52b 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -224,8 +224,8 @@ The development version typedef QFlags VectorLayerTypeFlags; - enum class PythonMacroMode - { + enum class PythonEmbeddedMode + { Never, Ask, SessionOnly, @@ -233,6 +233,12 @@ The development version NotForThisSession, }; + enum class PythonEmbeddedType + { + Macro, + ExpressionFunction, + }; + enum class DataProviderReadFlag { TrustDataSource, diff --git a/python/gui/auto_generated/qgsgui.sip.in b/python/gui/auto_generated/qgsgui.sip.in index 267030ad8212..e2160e896465 100644 --- a/python/gui/auto_generated/qgsgui.sip.in +++ b/python/gui/auto_generated/qgsgui.sip.in @@ -251,7 +251,6 @@ Returns the screen at the given global ``point`` (pixel). - signals: void optionsChanged(); diff --git a/src/app/options/qgsoptions.cpp b/src/app/options/qgsoptions.cpp index 2b7afc7e04b2..74c64ef3f808 100644 --- a/src/app/options/qgsoptions.cpp +++ b/src/app/options/qgsoptions.cpp @@ -230,11 +230,11 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl, const QListsetText( QStringLiteral( "%1 (%2)" ).arg( lblUITheme->text(), tr( "QGIS restart required" ) ) ); - mEnableMacrosComboBox->addItem( tr( "Never" ), QVariant::fromValue( Qgis::PythonMacroMode::Never ) ); - mEnableMacrosComboBox->addItem( tr( "Ask" ), QVariant::fromValue( Qgis::PythonMacroMode::Ask ) ); - mEnableMacrosComboBox->addItem( tr( "For This Session Only" ), QVariant::fromValue( Qgis::PythonMacroMode::SessionOnly ) ); - mEnableMacrosComboBox->addItem( tr( "Not During This Session" ), QVariant::fromValue( Qgis::PythonMacroMode::NotForThisSession ) ); - mEnableMacrosComboBox->addItem( tr( "Always (Not Recommended)" ), QVariant::fromValue( Qgis::PythonMacroMode::Always ) ); + mEnableMacrosComboBox->addItem( tr( "Never" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::Never ) ); + mEnableMacrosComboBox->addItem( tr( "Ask" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::Ask ) ); + mEnableMacrosComboBox->addItem( tr( "For This Session Only" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::SessionOnly ) ); + mEnableMacrosComboBox->addItem( tr( "Not During This Session" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::NotForThisSession ) ); + mEnableMacrosComboBox->addItem( tr( "Always (Not Recommended)" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::Always ) ); mIdentifyHighlightColorButton->setColorDialogTitle( tr( "Identify Highlight Color" ) ); mIdentifyHighlightColorButton->setAllowOpacity( true ); @@ -823,8 +823,8 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl, const QListsetChecked( mSettings->value( QStringLiteral( "qgis/askToSaveProjectChanges" ), QVariant( true ) ).toBool() ); mLayerDeleteConfirmationChkBx->setChecked( mSettings->value( QStringLiteral( "qgis/askToDeleteLayers" ), true ).toBool() ); chbWarnOldProjectVersion->setChecked( mSettings->value( QStringLiteral( "/qgis/warnOldProjectVersion" ), QVariant( true ) ).toBool() ); - Qgis::PythonMacroMode pyMacroMode = mSettings->enumValue( QStringLiteral( "/qgis/enableMacros" ), Qgis::PythonMacroMode::Ask ); - mEnableMacrosComboBox->setCurrentIndex( mEnableMacrosComboBox->findData( QVariant::fromValue( pyMacroMode ) ) ); + Qgis::PythonEmbeddedMode pyEmbeddedMode = mSettings->enumValue( QStringLiteral( "/qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask ); + mEnableMacrosComboBox->setCurrentIndex( mEnableMacrosComboBox->findData( QVariant::fromValue( pyEmbeddedMode ) ) ); mDefaultPathsComboBox->addItem( tr( "Absolute" ), static_cast< int >( Qgis::FilePathType::Absolute ) ); mDefaultPathsComboBox->addItem( tr( "Relative" ), static_cast< int >( Qgis::FilePathType::Relative ) ); @@ -1655,7 +1655,7 @@ void QgsOptions::saveOptions() mSettings->setValue( QStringLiteral( "/qgis/projectTemplateDir" ), leTemplateFolder->text() ); QgisApp::instance()->updateProjectFromTemplates(); } - mSettings->setEnumValue( QStringLiteral( "/qgis/enableMacros" ), mEnableMacrosComboBox->currentData().value() ); + mSettings->setEnumValue( QStringLiteral( "/qgis/enablePythonEmbedded" ), mEnableMacrosComboBox->currentData().value() ); mSettings->setValue( QStringLiteral( "/qgis/defaultProjectPathsRelative" ), static_cast< Qgis::FilePathType >( mDefaultPathsComboBox->currentData().toInt() ) == Qgis::FilePathType::Relative ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index c8f58dec27f6..44be66a7982f 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -2889,17 +2889,17 @@ void QgisApp::readSettings() readRecentProjects(); // this is a new session, reset enable macros value when they are set for session - Qgis::PythonMacroMode macroMode = settings.enumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask ); - switch ( macroMode ) + Qgis::PythonEmbeddedMode pythonEmbeddedMode = settings.enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask ); + switch ( pythonEmbeddedMode ) { - case Qgis::PythonMacroMode::NotForThisSession: - case Qgis::PythonMacroMode::SessionOnly: - settings.setEnumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask ); + case Qgis::PythonEmbeddedMode::NotForThisSession: + case Qgis::PythonEmbeddedMode::SessionOnly: + settings.setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask ); break; - case Qgis::PythonMacroMode::Always: - case Qgis::PythonMacroMode::Never: - case Qgis::PythonMacroMode::Ask: + case Qgis::PythonEmbeddedMode::Always: + case Qgis::PythonEmbeddedMode::Never: + case Qgis::PythonEmbeddedMode::Ask: break; } } @@ -6575,14 +6575,14 @@ bool QgisApp::addProject( const QString &projectFile ) if ( !QgsProject::instance()->readEntry( QStringLiteral( "Macros" ), QStringLiteral( "/pythonCode" ), QString() ).isEmpty() ) { auto lambda = []() {QgisApp::instance()->enableProjectMacros();}; - QgsGui::pythonMacroAllowed( lambda, mInfoBar ); + QgsGui::pythonEmbeddedInProjectAllowed( lambda, mInfoBar, Qgis::PythonEmbeddedType::Macro ); } // does the project have expression functions? const QString projectFunctions = QgsProject::instance()->readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() ); if ( !projectFunctions.isEmpty() ) { - QgsGui::pythonExpressionFromProjectAllowed( mInfoBar ); + QgsGui::pythonEmbeddedInProjectAllowed( nullptr, mInfoBar, Qgis::PythonEmbeddedType::ExpressionFunction ); } } #endif @@ -13861,15 +13861,6 @@ void QgisApp::closeProject() } mPythonMacrosEnabled = false; -#ifdef WITH_BINDINGS - // unload the project expression functions and reload user expressions - const QString projectFunctions = QgsProject::instance()->readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() ); - if ( !projectFunctions.isEmpty() ) - { - QgsExpression::cleanFunctionsFromProject(); - } -#endif - mLegendExpressionFilterButton->setExpressionText( QString() ); mLegendExpressionFilterButton->setChecked( false ); mFilterLegendByMapContentAction->setChecked( false ); diff --git a/src/core/expression/qgsexpression.h b/src/core/expression/qgsexpression.h index f8c5de9f8655..726704c3900b 100644 --- a/src/core/expression/qgsexpression.h +++ b/src/core/expression/qgsexpression.h @@ -630,24 +630,6 @@ class CORE_EXPORT QgsExpression */ static int functionCount(); - /** - * Loads python expression functions stored in the currrent project - * \returns Whether the project functions where loaded or not. - * - * \note not available in Python bindings - * \since QGIS 3.40 - */ - static bool loadFunctionsFromProject() SIP_SKIP; - - /** - * Unloads python expression functions stored in the current project - * and reloads local functions from the user profile. - * - * \note not available in Python bindings - * \since QGIS 3.40 - */ - static void cleanFunctionsFromProject() SIP_SKIP; - /** * Returns a quoted column reference (in double quotes) * \see quotedString() diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 92a6fb1a8e43..b1c483d3e3b6 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -64,7 +64,6 @@ #include "qgsunittypes.h" #include "qgsspatialindex.h" #include "qgscolorrampimpl.h" -#include "qgspythonrunner.h" #include #include @@ -9436,22 +9435,6 @@ const QStringList &QgsExpression::BuiltinFunctions() return *sBuiltinFunctions(); } -bool QgsExpression::loadFunctionsFromProject() -{ - const QString projectFunctions = QgsProject::instance()->readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() ); - if ( !projectFunctions.isEmpty() ) - { - QgsPythonRunner::run( projectFunctions ); - return true; - } - return false; -} - -void QgsExpression::cleanFunctionsFromProject() -{ - QgsPythonRunner::run( "qgis.utils.clean_project_expression_functions()" ); -} - QgsArrayForeachExpressionFunction::QgsArrayForeachExpressionFunction() : QgsExpressionFunction( QStringLiteral( "array_foreach" ), QgsExpressionFunction::ParameterList() // skip-keyword-check << QgsExpressionFunction::Parameter( QStringLiteral( "array" ) ) diff --git a/src/core/project/qgsproject.cpp b/src/core/project/qgsproject.cpp index ca7e5e15c5e6..f79659566d26 100644 --- a/src/core/project/qgsproject.cpp +++ b/src/core/project/qgsproject.cpp @@ -74,6 +74,7 @@ #include "qgsrunnableprovidercreator.h" #include "qgssettingsregistrycore.h" #include "qgspluginlayer.h" +#include "qgspythonrunner.h" #include #include @@ -1167,6 +1168,15 @@ void QgsProject::clear() emit aboutToBeCleared(); + if ( !mIsBeingDeleted ) + { + // Unregister expression functions stored in the project. + // If we clean on destruction we may end-up with a non-valid + // mPythonUtils, so be safe and only clean when not destroying. + // This should be called before calling mProperties.clearKeys(). + cleanFunctionsFromProject(); + } + mProjectScope.reset(); mFile.setFileName( QString() ); mProperties.clearKeys(); @@ -2284,6 +2294,11 @@ bool QgsProject::readProjectFile( const QString &filename, Qgis::ProjectReadFlag QgsMessageLog::logMessage( tr( "Project Variables Invalid" ), tr( "The project contains invalid variable settings." ) ); } + // Register expression functions stored in the project. + // They might be using project variables and might be + // in turn being used by other components (e.g., layouts). + loadFunctionsFromProject(); + QDomElement element = doc->documentElement().firstChildElement( QStringLiteral( "projectMetadata" ) ); if ( !element.isNull() ) @@ -5266,6 +5281,33 @@ void QgsProject::loadProjectFlags( const QDomDocument *doc ) setFlags( flags ); } +bool QgsProject::loadFunctionsFromProject( bool force ) +{ + if ( QgsPythonRunner::isValid() ) + { + const Qgis::PythonEmbeddedMode pythonEmbeddedMode = QgsSettings().enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask ); + + if ( force || pythonEmbeddedMode == Qgis::PythonEmbeddedMode::SessionOnly || pythonEmbeddedMode == Qgis::PythonEmbeddedMode::Always ) + { + const QString projectFunctions = readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() ); + if ( !projectFunctions.isEmpty() ) + { + QgsPythonRunner::run( projectFunctions ); + return true; + } + } + } + return false; +} + +void QgsProject::cleanFunctionsFromProject() +{ + if ( QgsPythonRunner::isValid() ) + { + QgsPythonRunner::run( "qgis.utils.clean_project_expression_functions()" ); + } +} + /// @cond PRIVATE GetNamedProjectColor::GetNamedProjectColor( const QgsProject *project ) : QgsScopedExpressionFunction( QStringLiteral( "project_color" ), 1, QStringLiteral( "Color" ) ) diff --git a/src/core/project/qgsproject.h b/src/core/project/qgsproject.h index 982bdc44841a..32c72eddea97 100644 --- a/src/core/project/qgsproject.h +++ b/src/core/project/qgsproject.h @@ -1698,6 +1698,26 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera */ void setElevationShadingRenderer( const QgsElevationShadingRenderer &elevationShadingRenderer ); + /** + * Loads python expression functions stored in the currrent project + * \param force Whether to check enablePythonEmbedded setting (default) or not. + * \returns Whether the project functions were loaded or not. + * + * \note not available in Python bindings + * \since QGIS 3.40 + */ + bool loadFunctionsFromProject( bool force = false ) SIP_SKIP; + + /** + * Unloads python expression functions stored in the current project + * and reloads local functions from the user profile. + * + * \note not available in Python bindings + * \since QGIS 3.40 + */ + void cleanFunctionsFromProject() SIP_SKIP; + + #ifdef SIP_RUN SIP_PYOBJECT __repr__(); % MethodCode diff --git a/src/core/qgis.h b/src/core/qgis.h index a5231089182a..aeb374a2491b 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -333,18 +333,29 @@ class CORE_EXPORT Qgis Q_FLAG( VectorLayerTypeFlags ) /** - * Authorisation to run Python Macros - * \since QGIS 3.10 + * Authorisation to run Python Embedded in projects + * \since QGIS 3.40 */ - enum class PythonMacroMode SIP_MONKEYPATCH_SCOPEENUM_UNNEST( Qgis, PythonMacroMode ) : int - { - Never = 0, //!< Macros are never run + enum class PythonEmbeddedMode : int + { + Never = 0, //!< Python embedded never run Ask = 1, //!< User is prompt before running SessionOnly = 2, //!< Only during this session - Always = 3, //!< Macros are always run - NotForThisSession, //!< Macros will not be run for this session + Always = 3, //!< Python embedded is always run + NotForThisSession, //!< Python embedded will not be run for this session + }; + Q_ENUM( PythonEmbeddedMode ) + + /** + * Type of Python Embedded in projects + * \since QGIS 3.40 + */ + enum class PythonEmbeddedType : int + { + Macro = 0, + ExpressionFunction = 1, }; - Q_ENUM( PythonMacroMode ) + Q_ENUM( PythonEmbeddedType ) /** * Flags which control data provider construction. diff --git a/src/gui/qgsattributeform.cpp b/src/gui/qgsattributeform.cpp index 8ddf9c776aa2..4c81d689dc7b 100644 --- a/src/gui/qgsattributeform.cpp +++ b/src/gui/qgsattributeform.cpp @@ -2356,7 +2356,7 @@ void QgsAttributeForm::initPython() // If we have a function code, run it if ( !initCode.isEmpty() ) { - if ( QgsGui::pythonMacroAllowed() ) + if ( QgsGui::pythonEmbeddedInProjectAllowed( nullptr, nullptr, Qgis::PythonEmbeddedType::Macro ) ) QgsPythonRunner::run( initCode ); else mMessageBar->pushMessage( QString(), diff --git a/src/gui/qgsexpressionaddfunctionfiledialog.cpp b/src/gui/qgsexpressionaddfunctionfiledialog.cpp index fe2179c9fec4..65bdf10e107f 100644 --- a/src/gui/qgsexpressionaddfunctionfiledialog.cpp +++ b/src/gui/qgsexpressionaddfunctionfiledialog.cpp @@ -13,9 +13,10 @@ * * ***************************************************************************/ +#include "qgsexpressionaddfunctionfiledialog.h" + #include #include -#include "qgsexpressionaddfunctionfiledialog.h" QgsExpressionAddFunctionFileDialog::QgsExpressionAddFunctionFileDialog( bool enableProjectFunctions, QWidget *parent ) : QDialog( parent ) diff --git a/src/gui/qgsgui.cpp b/src/gui/qgsgui.cpp index 9a313f4db6c3..63b82192e3ed 100644 --- a/src/gui/qgsgui.cpp +++ b/src/gui/qgsgui.cpp @@ -370,71 +370,121 @@ QgsGui::QgsGui() qRegisterMetaType< QgsHistoryEntry >( "QgsHistoryEntry" ); } -bool QgsGui::pythonMacroAllowed( void ( *lambda )(), QgsMessageBar *messageBar ) +bool QgsGui::pythonEmbeddedInProjectAllowed( void ( *lambda )(), QgsMessageBar *messageBar, Qgis::PythonEmbeddedType embeddedType ) { - const Qgis::PythonMacroMode macroMode = QgsSettings().enumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask ); + const Qgis::PythonEmbeddedMode pythonEmbeddedMode = QgsSettings().enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask ); - switch ( macroMode ) + switch ( pythonEmbeddedMode ) { - case Qgis::PythonMacroMode::SessionOnly: - case Qgis::PythonMacroMode::Always: - if ( lambda ) - lambda(); + case Qgis::PythonEmbeddedMode::SessionOnly: + case Qgis::PythonEmbeddedMode::Always: + if ( embeddedType == Qgis::PythonEmbeddedType::Macro ) + { + if ( lambda ) + lambda(); + } + // If this is the case, expression functions + // are loaded directly by the QGIS project. return true; - case Qgis::PythonMacroMode::Never: - case Qgis::PythonMacroMode::NotForThisSession: + case Qgis::PythonEmbeddedMode::Never: + case Qgis::PythonEmbeddedMode::NotForThisSession: if ( messageBar ) { - messageBar->pushMessage( tr( "Python Macros" ), - tr( "Python macros are currently disabled and will not be run" ), - Qgis::MessageLevel::Warning ); + switch ( embeddedType ) + { + case Qgis::PythonEmbeddedType::Macro: + messageBar->pushMessage( tr( "Python Macros" ), + tr( "Python macros are currently disabled and will not be run" ), + Qgis::MessageLevel::Warning ); + break; + case Qgis::PythonEmbeddedType::ExpressionFunction: + messageBar->pushMessage( tr( "Python Expressions" ), + tr( "Python expressions from project are currently disabled and will not be loaded" ), + Qgis::MessageLevel::Warning ); + break; + } } return false; - case Qgis::PythonMacroMode::Ask: - if ( !lambda ) + case Qgis::PythonEmbeddedMode::Ask: + if ( embeddedType == Qgis::PythonEmbeddedType::Macro ) { - QMessageBox msgBox( QMessageBox::Information, tr( "Python Macros" ), - tr( "Python macros are currently disabled. Do you allow this macro to run?" ) ); - QAbstractButton *stopSessionButton = msgBox.addButton( tr( "Disable for this Session" ), QMessageBox::DestructiveRole ); - msgBox.addButton( tr( "No" ), QMessageBox::NoRole ); - QAbstractButton *yesButton = msgBox.addButton( tr( "Yes" ), QMessageBox::YesRole ); - msgBox.exec(); - - QAbstractButton *clicked = msgBox.clickedButton(); - if ( clicked == stopSessionButton ) + if ( !lambda ) + { + QMessageBox msgBox( QMessageBox::Information, tr( "Python Macros" ), + tr( "Python macros are currently disabled. Do you allow this macro to run?" ) ); + QAbstractButton *stopSessionButton = msgBox.addButton( tr( "Disable for this Session" ), QMessageBox::DestructiveRole ); + msgBox.addButton( tr( "No" ), QMessageBox::NoRole ); + QAbstractButton *yesButton = msgBox.addButton( tr( "Yes" ), QMessageBox::YesRole ); + msgBox.exec(); + + QAbstractButton *clicked = msgBox.clickedButton(); + if ( clicked == stopSessionButton ) + { + QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::NotForThisSession ); + } + return clicked == yesButton; + } + else { - QgsSettings().setEnumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::NotForThisSession ); + // create the notification widget for macros + Q_ASSERT( messageBar ); + if ( messageBar ) + { + QToolButton *btnEnableMacros = new QToolButton(); + btnEnableMacros->setText( tr( "Enable Macros" ) ); + btnEnableMacros->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) ); + btnEnableMacros->setCursor( Qt::PointingHandCursor ); + btnEnableMacros->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred ); + + QgsMessageBarItem *macroMsg = new QgsMessageBarItem( + tr( "Security warning" ), + tr( "Python macros cannot currently be run." ), + btnEnableMacros, + Qgis::MessageLevel::Warning, + 0, + messageBar ); + + connect( btnEnableMacros, &QToolButton::clicked, messageBar, [ = ]() + { + lambda(); + messageBar->popWidget( macroMsg ); + } ); + + // display the macros notification widget + messageBar->pushItem( macroMsg ); + } + + return false; } - return clicked == yesButton; } - else + else if ( embeddedType == Qgis::PythonEmbeddedType::ExpressionFunction ) { - // create the notification widget for macros + // create the notification widget for expressions from project Q_ASSERT( messageBar ); if ( messageBar ) { - QToolButton *btnEnableMacros = new QToolButton(); - btnEnableMacros->setText( tr( "Enable Macros" ) ); - btnEnableMacros->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) ); - btnEnableMacros->setCursor( Qt::PointingHandCursor ); - btnEnableMacros->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred ); + QToolButton *btnEnableExpressionsFromProject = new QToolButton(); + btnEnableExpressionsFromProject->setText( tr( "Enable python expressions from project" ) ); + btnEnableExpressionsFromProject->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) ); + btnEnableExpressionsFromProject->setCursor( Qt::PointingHandCursor ); + btnEnableExpressionsFromProject->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred ); - QgsMessageBarItem *macroMsg = new QgsMessageBarItem( + QgsMessageBarItem *expressionFromProjectMsg = new QgsMessageBarItem( tr( "Security warning" ), - tr( "Python macros cannot currently be run." ), - btnEnableMacros, + tr( "Python expressions from project cannot currently be loaded." ), + btnEnableExpressionsFromProject, Qgis::MessageLevel::Warning, 0, messageBar ); - connect( btnEnableMacros, &QToolButton::clicked, messageBar, [ = ]() + connect( btnEnableExpressionsFromProject, &QToolButton::clicked, messageBar, [ = ]() { - lambda(); - messageBar->popWidget( macroMsg ); + QgsProject::instance()->loadFunctionsFromProject( true ); + messageBar->popWidget( expressionFromProjectMsg ); } ); - // display the macros notification widget - messageBar->pushItem( macroMsg ); + // display the notification widget + messageBar->pushItem( expressionFromProjectMsg ); } return false; @@ -443,59 +493,6 @@ bool QgsGui::pythonMacroAllowed( void ( *lambda )(), QgsMessageBar *messageBar ) return false; } -bool QgsGui::pythonExpressionFromProjectAllowed( QgsMessageBar *messageBar ) -{ - const Qgis::PythonMacroMode pythonMode = QgsSettings().enumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask ); - - switch ( pythonMode ) - { - case Qgis::PythonMacroMode::SessionOnly: - case Qgis::PythonMacroMode::Always: - QgsExpression::loadFunctionsFromProject(); - return true; - case Qgis::PythonMacroMode::Never: - case Qgis::PythonMacroMode::NotForThisSession: - if ( messageBar ) - { - messageBar->pushMessage( tr( "Python Expressions" ), - tr( "Python expressions from project are currently disabled and will not be loaded" ), - Qgis::MessageLevel::Warning ); - } - return false; - case Qgis::PythonMacroMode::Ask: - // create the notification widget for expressions from project - Q_ASSERT( messageBar ); - if ( messageBar ) - { - QToolButton *btnEnableExpressionsFromProject = new QToolButton(); - btnEnableExpressionsFromProject->setText( tr( "Enable python expressions from project" ) ); - btnEnableExpressionsFromProject->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) ); - btnEnableExpressionsFromProject->setCursor( Qt::PointingHandCursor ); - btnEnableExpressionsFromProject->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred ); - - QgsMessageBarItem *expressionFromProjectMsg = new QgsMessageBarItem( - tr( "Security warning" ), - tr( "Python expressions from project cannot currently be loaded." ), - btnEnableExpressionsFromProject, - Qgis::MessageLevel::Warning, - 0, - messageBar ); - - connect( btnEnableExpressionsFromProject, &QToolButton::clicked, messageBar, [ = ]() - { - QgsExpression::loadFunctionsFromProject(); - messageBar->popWidget( expressionFromProjectMsg ); - } ); - - // display the notification widget - messageBar->pushItem( expressionFromProjectMsg ); - } - - return false; - } - return false; -} - void QgsGui::initCalloutWidgets() { static std::once_flag initialized; diff --git a/src/gui/qgsgui.h b/src/gui/qgsgui.h index 4c13ade24d32..61eb8a8471f5 100644 --- a/src/gui/qgsgui.h +++ b/src/gui/qgsgui.h @@ -18,6 +18,7 @@ #ifndef QGSGUI_H #define QGSGUI_H +#include "qgis.h" #include "qgis_gui.h" #include "qgis_sip.h" #include @@ -291,8 +292,9 @@ class GUI_EXPORT QgsGui : public QObject static QScreen *findScreenAt( QPoint point ); /** - * Returns TRUE if python macros are currently allowed to be run - * If the global option is to ask user, a modal dialog will be shown + * Returns TRUE if python embedded in a project is currently allowed to be loaded. + * If the global option is to ask user, a modal dialog will be shown for macros + * or a button to enable Python expressions will be shown in a message bar. * \param lambda a pointer to a lambda method. If specified, the dialog is not modal, * a message is shown with a button to enable macro. * The lambda will be run either if macros are currently allowed or if the user accepts the message. @@ -300,19 +302,9 @@ class GUI_EXPORT QgsGui : public QObject * \param messageBar the message bar must be provided if a lambda method is used. * * \note Not available in Python bindings - */ - static bool pythonMacroAllowed( void ( *lambda )() = nullptr, QgsMessageBar *messageBar = nullptr ) SIP_SKIP; - - /** - * Returns TRUE if python expression functions from project are currently allowed to be loaded. - * - * If the global option is to ask user, a button to enable them will be shown in a message bar. - * \param messageBar Message bar to communicate with the user. - * - * \note Not available in Python bindings * \since QGIS 3.40 */ - static bool pythonExpressionFromProjectAllowed( QgsMessageBar *messageBar ) SIP_SKIP; + static bool pythonEmbeddedInProjectAllowed( void ( *lambda )() = nullptr, QgsMessageBar *messageBar = nullptr, Qgis::PythonEmbeddedType embeddedType = Qgis::PythonEmbeddedType::Macro ) SIP_SKIP; /** * Initializes callout widgets. diff --git a/src/ui/ui_qgsexpressionaddfunctionfiledialogbase.h b/src/ui/ui_qgsexpressionaddfunctionfiledialogbase.h deleted file mode 100644 index 21cbf5c4e47e..000000000000 --- a/src/ui/ui_qgsexpressionaddfunctionfiledialogbase.h +++ /dev/null @@ -1,96 +0,0 @@ -/******************************************************************************** -** Form generated from reading UI file 'qgsexpressionaddfunctionfiledialogbase.ui' -** -** Created by: Qt User Interface Compiler version 5.15.3 -** -** WARNING! All changes made in this file will be lost when recompiling UI file! -********************************************************************************/ - -#ifndef QGSEXPRESSIONADDFUNCTIONFILEDIALOGBASE_H -#define QGSEXPRESSIONADDFUNCTIONFILEDIALOGBASE_H - -#include -#include -#include -#include -#include -#include -#include -#include - -QT_BEGIN_NAMESPACE - -class Ui_QgsAddFunctionFileDialogBase -{ - public: - QGridLayout *gridLayout; - QLabel *label; - QComboBox *cboFileOptions; - QLabel *lblNewFileName; - QDialogButtonBox *buttonBox; - QLineEdit *txtNewFileName; - - void setupUi( QDialog *QgsAddFunctionFileDialogBase ) - { - if ( QgsAddFunctionFileDialogBase->objectName().isEmpty() ) - QgsAddFunctionFileDialogBase->setObjectName( QString::fromUtf8( "QgsAddFunctionFileDialogBase" ) ); - QgsAddFunctionFileDialogBase->resize( 277, 132 ); - gridLayout = new QGridLayout( QgsAddFunctionFileDialogBase ); - gridLayout->setObjectName( QString::fromUtf8( "gridLayout" ) ); - label = new QLabel( QgsAddFunctionFileDialogBase ); - label->setObjectName( QString::fromUtf8( "label" ) ); - - gridLayout->addWidget( label, 0, 0, 1, 1 ); - - cboFileOptions = new QComboBox( QgsAddFunctionFileDialogBase ); - cboFileOptions->addItem( QString() ); - cboFileOptions->addItem( QString() ); - cboFileOptions->setObjectName( QString::fromUtf8( "cboFileOptions" ) ); - - gridLayout->addWidget( cboFileOptions, 0, 1, 1, 1 ); - - lblNewFileName = new QLabel( QgsAddFunctionFileDialogBase ); - lblNewFileName->setObjectName( QString::fromUtf8( "lblNewFileName" ) ); - - gridLayout->addWidget( lblNewFileName, 1, 0, 1, 1 ); - - buttonBox = new QDialogButtonBox( QgsAddFunctionFileDialogBase ); - buttonBox->setObjectName( QString::fromUtf8( "buttonBox" ) ); - buttonBox->setOrientation( Qt::Horizontal ); - buttonBox->setStandardButtons( QDialogButtonBox::Cancel | QDialogButtonBox::Ok ); - - gridLayout->addWidget( buttonBox, 2, 0, 1, 2 ); - - txtNewFileName = new QLineEdit( QgsAddFunctionFileDialogBase ); - txtNewFileName->setObjectName( QString::fromUtf8( "txtNewFileName" ) ); - - gridLayout->addWidget( txtNewFileName, 1, 1, 1, 1 ); - - - retranslateUi( QgsAddFunctionFileDialogBase ); - QObject::connect( buttonBox, SIGNAL( accepted() ), QgsAddFunctionFileDialogBase, SLOT( accept() ) ); - QObject::connect( buttonBox, SIGNAL( rejected() ), QgsAddFunctionFileDialogBase, SLOT( reject() ) ); - - QMetaObject::connectSlotsByName( QgsAddFunctionFileDialogBase ); - } // setupUi - - void retranslateUi( QDialog *QgsAddFunctionFileDialogBase ) - { - QgsAddFunctionFileDialogBase->setWindowTitle( QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Add Function File", nullptr ) ); - label->setText( QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Create", nullptr ) ); - cboFileOptions->setItemText( 0, QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Function file", nullptr ) ); - cboFileOptions->setItemText( 1, QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Project functions", nullptr ) ); - - lblNewFileName->setText( QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "File name", nullptr ) ); - } // retranslateUi - -}; - -namespace Ui -{ - class QgsAddFunctionFileDialogBase: public Ui_QgsAddFunctionFileDialogBase {}; -} // namespace Ui - -QT_END_NAMESPACE - -#endif // QGSEXPRESSIONADDFUNCTIONFILEDIALOGBASE_H diff --git a/tests/src/app/testqgsprojectexpressions.cpp b/tests/src/app/testqgsprojectexpressions.cpp index 24fd093392f8..d2a59635c933 100644 --- a/tests/src/app/testqgsprojectexpressions.cpp +++ b/tests/src/app/testqgsprojectexpressions.cpp @@ -82,9 +82,17 @@ void TestQgsProjectExpressions::projectExpressions() // Load expressions from project // Project registers 2 functions: mychoice (overwriting it) and myprojectfunction const QByteArray projectPath = QByteArray( TEST_DATA_DIR ) + "/projects/test_project_functions.qgz"; + + const Qgis::PythonEmbeddedMode pythonEmbeddedMode = QgsSettings().enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask ); + QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Never ); QgsProject::instance()->read( projectPath ); QCOMPARE( QgsExpression::functionIndex( QStringLiteral( "myprojectfunction" ) ), -1 ); - QgsExpression::loadFunctionsFromProject(); + + // Set the global setting to accept expression functions + QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::SessionOnly ); + QgsProject::instance()->loadFunctionsFromProject(); + QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), pythonEmbeddedMode ); + QVERIFY( QgsExpression::functionIndex( QStringLiteral( "myprojectfunction" ) ) != -1 ); QVERIFY( QgsExpression::functionIndex( QStringLiteral( "mychoice" ) ) != -1 ); // Overwritten function const int count_project_loaded = QgsExpression::functionCount(); @@ -95,7 +103,7 @@ void TestQgsProjectExpressions::projectExpressions() QCOMPARE( exp.evaluate().toInt(), 2 ); // Different result because now it's from project // Unload expressions from project, reload user ones - QgsExpression::cleanFunctionsFromProject(); + QgsProject::instance()->cleanFunctionsFromProject(); const int count_project_unloaded = QgsExpression::functionCount(); QCOMPARE( count_before_project, count_project_unloaded ); // myprojectfunction is gone diff --git a/tests/src/python/test_qgseditformconfig.py b/tests/src/python/test_qgseditformconfig.py index cfaafce9ab69..46cf2d4a9933 100644 --- a/tests/src/python/test_qgseditformconfig.py +++ b/tests/src/python/test_qgseditformconfig.py @@ -133,7 +133,7 @@ def testFormPy(self): pyUrl = 'http://localhost:' + \ str(self.port) + '/qgis_local_server/layer_attribute_form.py' - QgsSettings().setEnumValue('qgis/enableMacros', Qgis.Always) + QgsSettings().setEnumValue('qgis/enablePythonEmbedded', Qgis.Always) config.setInitFilePath(pyUrl) config.setInitFunction('formOpen')