Skip to content

Commit

Permalink
Merge pull request qgis#58354 from gacarrillor/save_functions_in_project
Browse files Browse the repository at this point in the history
[feature] Allow users to save expression functions in QGIS project file
  • Loading branch information
3nids committed Sep 13, 2024
2 parents e25bc20 + c5286e4 commit 2817262
Show file tree
Hide file tree
Showing 36 changed files with 995 additions and 198 deletions.
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ desktop.ini
doc/INSTALL.tex
i18n/*.qm
ms-windows/*.exe*
ms-windows/Installer-Files/postinstall.bat
ms-windows/Installer-Files/preremove.bat
ms-windows/nsis/
ms-windows/osgeo4w/addons/
ms-windows/osgeo4w/binary-*
ms-windows/osgeo4w/build-*
ms-windows/osgeo4w/nsis/
ms-windows/osgeo4w/packages-x86/
ms-windows/osgeo4w/packages-x86_64/
ms-windows/osgeo4w/unpacked/
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
Expand Down Expand Up @@ -91,6 +106,8 @@ tests/testdata/raster/band3_float32_noct_epsg4326.tif.aux.xml
tests/testdata/raster/band3_int16_noct_epsg4326.tif.aux.xml
tests/testdata/tenbytenraster.asc.aux.xml
tests/testdata/test_plugin_path/plugin_started.txt
tests/testdata/test_qgis_config_path/profiles/default/*
!tests/testdata/test_qgis_config_path/profiles/default/python
tests/testdata/widget_config.qlr
tests/testdata/zip/testtar.tgz.properties
venv
49 changes: 26 additions & 23 deletions python/PyQt6/core/auto_additions/qgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,34 +728,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
Expand Down
3 changes: 3 additions & 0 deletions python/PyQt6/core/auto_generated/project/qgsproject.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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( "<QgsProject: '%1'%2>" ).arg( sipCpp->fileName(),
Expand Down
10 changes: 8 additions & 2 deletions python/PyQt6/core/auto_generated/qgis.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,21 @@ The development version
typedef QFlags<Qgis::VectorLayerTypeFlag> VectorLayerTypeFlags;


enum class PythonMacroMode /BaseType=IntEnum/
{
enum class PythonEmbeddedMode /BaseType=IntEnum/
{
Never,
Ask,
SessionOnly,
Always,
NotForThisSession,
};

enum class PythonEmbeddedType /BaseType=IntEnum/
{
Macro,
ExpressionFunction,
};

enum class DataProviderReadFlag /BaseType=IntFlag/
{
TrustDataSource,
Expand Down
15 changes: 15 additions & 0 deletions python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@




class QgsExpressionBuilderWidget : QWidget
{
%Docstring(signature="appended")
Expand Down Expand Up @@ -270,6 +271,13 @@ Saves the current function editor text to the given file.
void loadCodeFromFile( QString path );
%Docstring
Loads code from the given file into the function editor
%End

void loadCodeFromProjectFunctions();
%Docstring
Loads code from the project into the function editor

.. versionadded:: 3.40
%End

void loadFunctionCode( const QString &code );
Expand All @@ -280,6 +288,13 @@ Loads code into the function editor
void updateFunctionFileList( const QString &path );
%Docstring
Updates the list of function files found at the given path
%End

void saveProjectFunctionsEntry();
%Docstring
Saves the current function editor text to a project entry.

.. versionadded:: 3.40
%End

QStandardItemModel *model() /Deprecated/;
Expand Down
49 changes: 26 additions & 23 deletions python/core/auto_additions/qgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,34 +719,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
Expand Down
3 changes: 3 additions & 0 deletions python/core/auto_generated/project/qgsproject.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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( "<QgsProject: '%1'%2>" ).arg( sipCpp->fileName(),
Expand Down
10 changes: 8 additions & 2 deletions python/core/auto_generated/qgis.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,21 @@ The development version
typedef QFlags<Qgis::VectorLayerTypeFlag> VectorLayerTypeFlags;


enum class PythonMacroMode
{
enum class PythonEmbeddedMode
{
Never,
Ask,
SessionOnly,
Always,
NotForThisSession,
};

enum class PythonEmbeddedType
{
Macro,
ExpressionFunction,
};

enum class DataProviderReadFlag
{
TrustDataSource,
Expand Down
15 changes: 15 additions & 0 deletions python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@




class QgsExpressionBuilderWidget : QWidget
{
%Docstring(signature="appended")
Expand Down Expand Up @@ -270,6 +271,13 @@ Saves the current function editor text to the given file.
void loadCodeFromFile( QString path );
%Docstring
Loads code from the given file into the function editor
%End

void loadCodeFromProjectFunctions();
%Docstring
Loads code from the project into the function editor

.. versionadded:: 3.40
%End

void loadFunctionCode( const QString &code );
Expand All @@ -280,6 +288,13 @@ Loads code into the function editor
void updateFunctionFileList( const QString &path );
%Docstring
Updates the list of function files found at the given path
%End

void saveProjectFunctionsEntry();
%Docstring
Saves the current function editor text to a project entry.

.. versionadded:: 3.40
%End

QStandardItemModel *model() /Deprecated/;
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
Loading

0 comments on commit 2817262

Please sign in to comment.