Skip to content

Commit

Permalink
[expressions] Unload expression functions from project on project clo…
Browse files Browse the repository at this point in the history
…se and reload user expressions (from profile) to avoid any potential overwrite from unloaded project functions
  • Loading branch information
gacarrillor committed Aug 23, 2024
1 parent e898fc2 commit c5dc0d9
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ Returns the number of functions defined in the parser
%End



static QString quotedColumnRef( QString name );
%Docstring
Returns a quoted column reference (in double quotes)
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_generated/expression/qgsexpression.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ Returns the number of functions defined in the parser
%End



static QString quotedColumnRef( QString name );
%Docstring
Returns a quoted column reference (in double quotes)
Expand Down
38 changes: 37 additions & 1 deletion python/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import glob
import traceback

from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtCore import QCoreApplication, qDebug
from qgis.core import Qgis, QgsApplication, QgsMessageLog


Expand All @@ -51,6 +51,41 @@ def load_user_expressions(path):
QgsMessageLog.logMessage(msg + "\n" + error, msgtitle, Qgis.MessageLevel.Warning)


def reload_user_expressions(path):
"""
Reload all user expressions from the given path
"""
# First unload expression modules, looping all
# py files and remove them from sys.modules
modules = glob.glob(path + "/*.py")
names = [os.path.basename(f)[:-3] for f in modules]
for name in names:
if name == "__init__":
continue

mod = "expressions.{0}".format(name)
if mod not in sys.modules:
continue

# try removing path
if hasattr(sys.modules[mod], '__path__'):
for path in sys.modules[mod].__path__:
try:
sys.path.remove(path)
except ValueError:
# Discard if path is not there
pass

# try to remove the module from python
try:
del sys.modules[mod]
except:
qDebug("Error when removing module:\n%s" % traceback.format_exc())

# Finally, load again the users expressions from the given path
load_user_expressions(path)


userpythonhome = os.path.join(QgsApplication.qgisSettingsDirPath(), "python")
expressionspath = os.path.join(userpythonhome, "expressions")

Expand Down Expand Up @@ -87,6 +122,7 @@ def my_sum(value1, value2):
expressions.load = load_user_expressions
expressions.load(expressionspath)
expressions.template = template
expressions.reload = reload_user_expressions
except ImportError:
# We get a import error and crash for some reason even if we make the expressions package
# TODO Fix the crash on first load with no expressions folder
Expand Down
43 changes: 43 additions & 0 deletions python/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,49 @@ def closeProjectMacro():
mod.closeProject()


#######################

def _list_project_expression_functions():
""" Get a list of expression functions stored in the current project """
import ast
from qgis.core import QgsProject

functions = []
project_functions, ok = QgsProject.instance().readEntry("ExpressionFunctions", "/pythonCode")
if ok and project_functions:
code = ast.parse(project_functions)

for e in code.body:
if isinstance(e, ast.FunctionDef):
for d in e.decorator_list:
if d.func.id == "qgsfunction":
functions.append(e.name)

return functions


def clean_project_expression_functions():
"""
Unload expression functions from current project
and reload user expressions from profile folder
to avoid any potential overwrite from the
unloaded project functions
"""
project_functions = _list_project_expression_functions()
if project_functions:
from qgis.core import QgsExpression
for function in project_functions:
QgsExpression.unregisterFunction(function)

# Reload user expressions
from qgis.core import QgsApplication
import expressions

userpythonhome = os.path.join(QgsApplication.qgisSettingsDirPath(), "python")
expressionspath = os.path.join(userpythonhome, "expressions")
expressions.reload(expressionspath)


#######################
# SERVER PLUGINS
#
Expand Down
10 changes: 9 additions & 1 deletion src/app/qgisapp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13883,9 +13883,17 @@ void QgisApp::closeProject()
{
QgsPythonRunner::run( QStringLiteral( "qgis.utils.unloadProjectMacros();" ) );
}

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 );
Expand Down
9 changes: 9 additions & 0 deletions src/core/expression/qgsexpression.h
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,15 @@ class CORE_EXPORT QgsExpression
*/
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()
Expand Down
4 changes: 4 additions & 0 deletions src/core/expression/qgsexpressionfunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9337,6 +9337,10 @@ bool QgsExpression::loadFunctionsFromProject()
return false;
}

void QgsExpression::cleanFunctionsFromProject()
{
QgsPythonRunner::run( "qgis.utils.clean_project_expression_functions()" );
}

QgsArrayForeachExpressionFunction::QgsArrayForeachExpressionFunction()
: QgsExpressionFunction( QStringLiteral( "array_foreach" ), QgsExpressionFunction::ParameterList() // skip-keyword-check
Expand Down

0 comments on commit c5dc0d9

Please sign in to comment.