diff --git a/gui/wxpython/core/settings.py b/gui/wxpython/core/settings.py index 6316f37a75d..df2d6eebb8e 100644 --- a/gui/wxpython/core/settings.py +++ b/gui/wxpython/core/settings.py @@ -733,6 +733,7 @@ def _defaultSettings(self): "height": 100, }, }, + "grassAPI": {"selection": 0}, # script package }, "mapswipe": { "cursor": { @@ -875,6 +876,11 @@ def _internalSettings(self): _("circle"), ) + self.internalSettings["modeler"]["grassAPI"]["choices"] = ( + _("Script package"), + _("PyGRASS"), + ) + def ReadSettingsFile(self, settings=None): """Reads settings file (mapset, location, gisdbase)""" if settings is None: diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index a3aa2ca2399..d906aed1853 100644 --- a/gui/wxpython/gmodeler/frame.py +++ b/gui/wxpython/gmodeler/frame.py @@ -2103,7 +2103,13 @@ def RefreshScript(self): return False fd = tempfile.TemporaryFile(mode="r+") - self.write_object(fd, self.parent.GetModel()) + grassAPI = UserSettings.Get(group="modeler", key="grassAPI", subkey="selection") + self.write_object( + fd, + self.parent.GetModel(), + grassAPI="script" if grassAPI == 0 else "pygrass", + ) + fd.seek(0) self.body.SetText(fd.read()) fd.close() diff --git a/gui/wxpython/gmodeler/g.gui.gmodeler.html b/gui/wxpython/gmodeler/g.gui.gmodeler.html index c7f8be58a22..a6067c19aa8 100644 --- a/gui/wxpython/gmodeler/g.gui.gmodeler.html +++ b/gui/wxpython/gmodeler/g.gui.gmodeler.html @@ -372,6 +372,12 @@

Using the Python editor

Figure: Python editor in the wxGUI Graphical Modeler - set to PyWPS. +

+By default GRASS script package API is used +(grass.script.core.run_command()). This can be changed in the +settings. Alternatively also PyGRASS API is supported +(grass.pygrass.modules.Module). +

Defining loops

In the example below the MODIS MOD13Q1 (NDVI) satellite data products are used in a loop. The original data are diff --git a/gui/wxpython/gmodeler/model.py b/gui/wxpython/gmodeler/model.py index fb191ebe2b2..4789ba156a6 100644 --- a/gui/wxpython/gmodeler/model.py +++ b/gui/wxpython/gmodeler/model.py @@ -19,7 +19,7 @@ - model::WritePythonFile - model::ModelParamDialog -(C) 2010-2018 by the GRASS Development Team +(C) 2010-2024 by the GRASS Development Team This program is free software under the GNU General Public License (>=v2). Read the file COPYING that comes with GRASS for details. @@ -2664,11 +2664,12 @@ def _getItemFlags(self, item, opts, variables): class WritePyWPSFile(WriteScriptFile): """Class for exporting model to PyWPS script.""" - def __init__(self, fd, model): + def __init__(self, fd, model, grassAPI="script"): """Class for exporting model to PyWPS script.""" self.fd = fd self.model = model self.indent = 8 + self.grassAPI = grassAPI self._writePyWPS() @@ -2683,8 +2684,15 @@ def _writePyWPS(self): import os import atexit import tempfile -from grass.script import run_command -from pywps import Process, LiteralInput, ComplexInput, ComplexOutput, Format +""" + ) + if self.grassAPI == "script": + self.fd.write("from grass.script import run_command\n") + else: + self.fd.write("from grass.pygrass.modules import Module\n") + + self.fd.write( + r"""from pywps import Process, LiteralInput, ComplexInput, ComplexOutput, Format class Model(Process): @@ -2868,7 +2876,10 @@ def _writeHandler(self): def _writePythonAction(self, item, variables={}, intermediates=None): """Write model action to Python file""" task = GUI(show=None).ParseCommand(cmd=item.GetLog(string=False)) - strcmd = "\n%srun_command(" % (" " * self.indent) + strcmd = "\n%s%s(" % ( + " " * self.indent, + "run_command" if self.grassAPI == "script" else "Module", + ) self.fd.write( strcmd + self._getPythonActionCmd(item, task, len(strcmd) - 1, variables) ) @@ -2917,6 +2928,7 @@ def _writePythonAction(self, item, variables={}, intermediates=None): else: overwrite_string = "" + strcmd_len = len(strcmd.strip()) self.fd.write( """ {run_command}"{cmd}", @@ -2928,14 +2940,14 @@ def _writePythonAction(self, item, variables={}, intermediates=None): """.format( run_command=strcmd, cmd=command, - indent1=" " * (self.indent + 12), + indent1=" " * (self.indent + strcmd_len), input=param_request, - indent2=" " * (self.indent + 12), - indent3=" " * (self.indent + 16), - indent4=" " * (self.indent + 16), + indent2=" " * (self.indent + strcmd_len), + indent3=" " * (self.indent * 2 + strcmd_len), + indent4=" " * (self.indent * 2 + strcmd_len), out=param_request, format_ext=extension, - indent5=" " * (self.indent + 12), + indent5=" " * (self.indent + strcmd_len), format=format, overwrite_string=overwrite_string, ) @@ -3061,14 +3073,17 @@ def _getSupportedFormats(prompt): class WritePythonFile(WriteScriptFile): - def __init__(self, fd, model): + def __init__(self, fd, model, grassAPI="script"): """Class for exporting model to Python script :param fd: file descriptor + :param model: model to translate + :param grassAPI: script or pygrass """ self.fd = fd self.model = model self.indent = 4 + self.grassAPI = grassAPI self._writePython() @@ -3188,9 +3203,13 @@ def _writePython(self): import os import atexit -from grass.script import parser, run_command +from grass.script import parser """ ) + if self.grassAPI == "script": + self.fd.write("from grass.script import run_command\n") + else: + self.fd.write("from grass.pygrass.modules import Module\n") # cleanup() rast, vect, rast3d, msg = self.model.GetIntermediateData() @@ -3199,26 +3218,27 @@ def _writePython(self): def cleanup(): """ ) + run_command = "run_command" if self.grassAPI == "script" else "Module" if rast: self.fd.write( - r""" run_command("g.remove", flags="f", type="raster", + r""" %s("g.remove", flags="f", type="raster", name=%s) """ - % ",".join(map(lambda x: '"' + x + '"', rast)) + % (run_command, ",".join(map(lambda x: '"' + x + '"', rast))) ) if vect: self.fd.write( - r""" run_command("g.remove", flags="f", type="vector", + r""" %s("g.remove", flags="f", type="vector", name=%s) """ - % ",".join(map(lambda x: '"' + x + '"', vect)) + % (run_command, ",".join(map(lambda x: '"' + x + '"', vect))) ) if rast3d: self.fd.write( - r""" run_command("g.remove", flags="f", type="raster_3d", + r""" %s("g.remove", flags="f", type="raster_3d", name=%s) """ - % ",".join(map(lambda x: '"' + x + '"', rast3d)) + % (run_command, ",".join(map(lambda x: '"' + x + '"', rast3d))) ) if not rast and not vect and not rast3d: self.fd.write(" pass\n") @@ -3260,7 +3280,10 @@ def getParameterizedFlags(paramFlags, itemFlags): def _writePythonAction(self, item, variables={}, intermediates=None): """Write model action to Python file""" task = GUI(show=None).ParseCommand(cmd=item.GetLog(string=False)) - strcmd = "%srun_command(" % (" " * self.indent) + strcmd = "%s%s(" % ( + " " * self.indent, + "run_command" if self.grassAPI == "script" else "Module", + ) self.fd.write( strcmd + self._getPythonActionCmd(item, task, len(strcmd), variables) + "\n" ) @@ -3278,16 +3301,31 @@ def _getPythonActionCmd(self, item, task, cmdIndent, variables={}): for p in opts["params"]: name = p.get("name", None) value = p.get("value", None) + ptype = p.get("type", "string") + + if ( + self.grassAPI == "pygrass" + and (p.get("multiple", False) is True or len(p.get("key_desc", [])) > 1) + and "," in value + ): + value = value.split(",") + if ptype == "integer": + value = list(map(int, value)) + elif ptype == "float": + value = list(map(float, value)) if (name and value) or (name in parameterizedParams): - ptype = p.get("type", "string") foundVar = False if name in parameterizedParams: foundVar = True value = 'options["{}"]'.format(self._getParamName(name, item)) - if foundVar or ptype != "string": + if ( + foundVar + or isinstance(value, list) + or (ptype != "string" and len(p.get("key_desc", [])) < 2) + ): params.append("{}={}".format(name, value)) else: params.append('{}="{}"'.format(name, value)) diff --git a/gui/wxpython/gmodeler/preferences.py b/gui/wxpython/gmodeler/preferences.py index 8a8638931a7..750960d94de 100644 --- a/gui/wxpython/gmodeler/preferences.py +++ b/gui/wxpython/gmodeler/preferences.py @@ -7,7 +7,7 @@ - preferences::PreferencesDialog - preferences::PropertiesDialog -(C) 2010-2013 by the GRASS Development Team +(C) 2010-2024 by the GRASS Development Team This program is free software under the GNU General Public License (>=v2). Read the file COPYING that comes with GRASS for details. @@ -48,9 +48,9 @@ def _createGeneralPage(self, notebook): """Create notebook page for action settings""" panel = wx.Panel(parent=notebook, id=wx.ID_ANY) notebook.AddPage(page=panel, text=_("General")) + border = wx.BoxSizer(wx.VERTICAL) # colors - border = wx.BoxSizer(wx.VERTICAL) box = StaticBox(parent=panel, id=wx.ID_ANY, label=" %s " % _("Item properties")) sizer = wx.StaticBoxSizer(box, wx.VERTICAL) @@ -84,6 +84,48 @@ def _createGeneralPage(self, notebook): border=3, ) + # Python editor + box = StaticBox(parent=panel, id=wx.ID_ANY, label=" %s " % _("Python editor")) + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + + gridSizer = wx.GridBagSizer(hgap=3, vgap=3) + + row = 0 + gridSizer.Add( + StaticText(parent=panel, id=wx.ID_ANY, label=_("GRASS API:")), + flag=wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL, + pos=(row, 0), + ) + grassAPI = wx.Choice( + parent=panel, + id=wx.ID_ANY, + size=(150, -1), + choices=self.settings.Get( + group="modeler", + key="grassAPI", + subkey="choices", + settings_type="internal", + ), + name="GetSelection", + ) + grassAPI.SetSelection( + self.settings.Get(group="modeler", key="grassAPI", subkey="selection") + ) + self.winId["modeler:grassAPI:selection"] = grassAPI.GetId() + + gridSizer.Add( + grassAPI, flag=wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, pos=(row, 1) + ) + + gridSizer.AddGrowableCol(0) + sizer.Add(gridSizer, proportion=1, flag=wx.ALL | wx.EXPAND, border=5) + border.Add( + sizer, + proportion=0, + flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, + border=3, + ) + panel.SetSizer(border) return panel