Fix qgsfunction decorator args handling

This commit is contained in:
Yoann Quenach de Quivillic 2023-04-29 14:25:11 +02:00
parent d3d8ba1a30
commit 947931ad2b

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
"""
***************************************************************************
qgsfunction.py
@ -26,30 +24,31 @@ from qgis.PyQt.QtCore import QCoreApplication
from qgis._core import QgsExpressionFunction, QgsExpression, QgsMessageLog, QgsFeatureRequest, Qgis
def register_function(function, arg_count, group, usesgeometry=False,
referenced_columns=[QgsFeatureRequest.ALL_ATTRIBUTES], handlesnull=False, **kwargs):
def register_function(
function,
group,
usesgeometry=False,
referenced_columns=[QgsFeatureRequest.ALL_ATTRIBUTES],
handlesnull=False,
**kwargs,
):
"""
Register a Python function to be used as a expression function.
Functions should take (values, feature, parent) as args:
The function signature may contains special parameters:
- context: the QgsExpressionContext-related to the current evaluation
- feature: the QgsFeature-related to the current evaluation
- parent: the QgsExpressionFunction parent
Example:
def myfunc(values, feature, parent):
pass
If those parameters are present in the function signature, they will be automatically passed to the function,
without the need to specify them in the expression.
They can also shortcut naming feature and parent args by using *args
if they are not needed in the function.
Example:
def myfunc(values, *args):
pass
If the only other parameter in the signature is called "values", parameters will be passed as a list.
Otherwise, parameters will be expanded in the parameter list.
Functions should return a value compatible with QVariant
Eval errors can be raised using parent.setEvalErrorString("Error message")
:param function:
:param arg_count:
:param group:
:param usesgeometry:
:param handlesnull: Needs to be set to True if this function does not always return NULL if any parameter is NULL. Default False.
@ -57,12 +56,20 @@ def register_function(function, arg_count, group, usesgeometry=False,
"""
class QgsPyExpressionFunction(QgsExpressionFunction):
def __init__(self, func, name, args, group, helptext='', usesGeometry=True,
referencedColumns=QgsFeatureRequest.ALL_ATTRIBUTES, expandargs=False, handlesNull=False):
QgsExpressionFunction.__init__(self, name, args, group, helptext)
def __init__(
self,
func,
name,
group,
helptext="",
usesGeometry=True,
referencedColumns=QgsFeatureRequest.ALL_ATTRIBUTES,
handlesNull=False,
paramsAsList=False,
):
QgsExpressionFunction.__init__(self, name, -1, group, helptext)
self.function = func
self.expandargs = expandargs
self.params_as_list = paramsAsList
self.uses_geometry = usesGeometry
self.referenced_columns = referencedColumns
self.handles_null = handlesNull
@ -71,21 +78,26 @@ def register_function(function, arg_count, group, usesgeometry=False,
feature = None
if context:
feature = context.feature()
try:
if self.expandargs:
values.append(feature)
values.append(parent)
if inspect.getfullargspec(self.function).args[-1] == 'context':
values.append(context)
return self.function(*values)
else:
if inspect.getfullargspec(self.function).args[-1] == 'context':
self.function(values, feature, parent, context)
return self.function(values, feature, parent)
parameters = inspect.signature(self.function).parameters
kwvalues = {}
# Handle special parameters
# those will not be inserted in the parameter list
# if they are present in the function signature
if "context" in parameters:
kwvalues["context"] = context
if "feature" in parameters:
kwvalues["feature"] = feature
if "parent" in parameters:
kwvalues["parent"] = parent
if self.params_as_list:
return self.function(values, **kwvalues)
return self.function(*values, **kwvalues)
except Exception as ex:
tb = traceback.format_exception(None, ex, ex.__traceback__)
formatted_traceback = ''.join(tb)
formatted_traceback = "".join(tb)
formatted_exception = f"{ex}:<pre>{formatted_traceback}</pre>"
parent.setEvalErrorString(formatted_exception)
return None
@ -99,53 +111,42 @@ def register_function(function, arg_count, group, usesgeometry=False,
def handlesNull(self):
return self.handles_null
helptemplate = string.Template("""<h3>$name function</h3><br>$doc""")
name = kwargs.get('name', function.__name__)
helptext = kwargs.get('helpText') or function.__doc__ or ''
helptemplate = string.Template("<h3>$name function</h3><br>$doc")
name = kwargs.get("name", function.__name__)
helptext = kwargs.get("helpText") or function.__doc__ or ""
helptext = helptext.strip()
expandargs = False
if arg_count == "auto":
# Work out the number of args we need.
# Number of function args - 2. The last two args are always feature, parent.
args = inspect.getfullargspec(function).args
number = len(args)
arg_count = number - 2
if args[-1] == 'context':
arg_count -= 1
expandargs = True
register = kwargs.get('register', True)
register = kwargs.get("register", True)
if register and QgsExpression.isFunctionName(name):
if not QgsExpression.unregisterFunction(name):
msgtitle = QCoreApplication.translate("UserExpressions", "User expressions")
msg = QCoreApplication.translate("UserExpressions",
"The user expression {0} already exists and could not be unregistered.").format(
name)
msg = QCoreApplication.translate(
"UserExpressions", "The user expression {0} already exists and could not be unregistered."
).format(name)
QgsMessageLog.logMessage(msg + "\n", msgtitle, Qgis.Warning)
return None
function.__name__ = name
helptext = helptemplate.safe_substitute(name=name, doc=helptext)
f = QgsPyExpressionFunction(function, name, arg_count, group, helptext, usesgeometry, referenced_columns,
expandargs, handlesnull)
# This doesn't really make any sense here but does when used from a decorator context
# so it can stay.
# Legacy: if args was not 'auto', parameters were passed as a list
params_as_list = kwargs.get("params_as_list", kwargs.get("args", "auto") != "auto")
f = QgsPyExpressionFunction(
function, name, group, helptext, usesgeometry, referenced_columns, handlesnull, params_as_list
)
if register:
QgsExpression.registerFunction(f)
return f
def qgsfunction(args='auto', group='custom', **kwargs):
def qgsfunction(args="auto", group="custom", **kwargs):
r"""
Decorator function used to define a user expression function.
:param args: Number of parameters, set to 'auto' to accept a variable length of parameters.
:param args: DEPRECATED since QGIS 3.32. Use the "params_as_list" keyword argument instead if you want to pass parameters as a list.
:param group: The expression group to which this expression should be added.
:param \**kwargs:
See below
:Keyword Arguments:
* *referenced_columns* (``list``) --
An array of field names on which this expression works. Can be set to ``[QgsFeatureRequest.ALL_ATTRIBUTES]``. By default empty.
@ -153,24 +154,36 @@ def qgsfunction(args='auto', group='custom', **kwargs):
Defines if this expression requires the geometry. By default False.
* *handlesnull* (``bool``) --
Defines if this expression has custom handling for NULL values. If False, the result will always be NULL as soon as any parameter is NULL. False by default.
* *params_as_list* (``bool``) \since QGIS 3.32 --
Defines if the parameters are passed to the function as a list, or if they are expanded. Default to False.
Example:
@qgsfunction(2, 'test'):
def add(values, feature, parent):
pass
Examples:
Will create and register a function in QgsExpression called 'add' in the
'test' group that takes two arguments.
@qgsfunction(group="custom")
def myfunc(values, feature, parent):
return values + [feature.id()]
or not using feature and parent:
This register a function called "myfunc" in the "custom" group. It can then be called with any number of parameters
which will be passed as a list to the function. From the function, it is possible to access the feature and the parent
Example:
@qgsfunction(2, 'test'):
def add(values, *args):
pass
>>> myfunc("a", "b", "c")
["a", "b", "c", 1]
@qgsfunction(group="custom")
def myfunc2(val1, val2, context):
return val1 + val2
This register a function called "myfunc2" in the "custom" group. It expects exactly two parameters, val1 and val2
From the function, it is possible to access the feature and the parent
>>> myfunc2(40, 2)
42
"""
kwargs["args"] = args
def wrapper(func):
return register_function(func, args, group, **kwargs)
return register_function(func, group, **kwargs)
return wrapper