1

I have a Python function that I simply use as a label expression in a print layout.
Its objective is to render elements from a json object stored as a field in a vector layer.
This vector layer is coming from a rest API and is having several attributes, many if not all of them are json fields (strings).

The expression only deals with on of these fields, which contains a json object structured as:

{
  "bar1": {
    "foo1": "val_1_1",
    "foo2": "val_1_2",
    "foo3": "val_1_3",
    ....
    "fooN": "val_1_N",
    },
  ...
  "barN": {
    "foo1": "val_N_1",
    "foo2": "val_N_2",
    "foo3": "val_N_3",
    ....
    "fooN": "val_N_N",
    }
}

There could be one, two, or more "barX" level 1 features, their number is not fixed.
There are always 10 level 2 "fooX" features, but sometime, some of them are empty.

I need to render them all dynamically in a print layout, hence the usage of the following Python expression (this is the most advanced simplification I can make of an actually more complex situation, but it should capture its essence):

from qgis.core import *
from qgis.gui import *
import json

@qgsfunction(args='auto', group='Custom')
def get_attribute_table_bar_features(feature, parent, print_keys=True): # <-- 'print_keys' is my custom named arg
    """
    Function to get a string output from a list of features
    stored as 'attribute_table_bar_feature' in the attribute 
    table of the vector layer.
    """
    # print_keys = True # <-- if that variable is hard coded
    field_names = [field.name() for field in feature.fields()]
    d = dict(zip(field_names, feature.attributes())) # <-- pack them all
    attribute_table_bar_features = json.loads(d['attribute_table_bar_feature'])
    results = ''
    for k, v in "attribute_table_bar_features".items():
        if print_keys: # <-- use of the custom named argument here
            res_str = f"{k} {v}\n"
        else:
            res_str = f"{v}\n"

        result += res_str  

    return results

I simply call it inside a label in my print layout:

[% get_attribute_table_bar_features(print_keys=True) %]
  1. Without the custom named arg in the function definition and without the if statement inside the function, everything goes fine, it the only prints "Hello World!" at the end of the string.
  2. If I set that custom named arg as a hard coded variable inside the function, and keeping the if statement, everything is also fine, but then, I am not able to use this function dynamically; i.e. I have to change the print_keys boolean value manually each time I need to switch. Hence, building two almost identical functions could be the way to go but it's definitely not pythonic.
  3. And if I set up the custom named arg in the function definition as presented here, I got an error when calling it:

      a) without the argument:

get_attribute_table_bar_features()

Parser Errors:
get_attribute_table_bar_features function is called with wrong number of arguments

      b) with the argument:

get_attribute_table_bar_features(print_keys=True)

Eval Error: Field 'print_keys' not found

      c) if arg is placed on front of get_attribute_table_bar_features(print_keys=False, feature, parent), it's invalid: SyntaxError: non-default argument follows default argument

Hence my question: how could I get a custom argument to work within a Python function used in the QGIS expression builder? (Notice I may need to use more than one custom argument and each may need a default value).

QGIS version: 3.22.0 Python: 3.9.5

1
  • Answer: As you did in 3.c., but without the default value, since custom expression functions do not support default values. Commented Nov 22, 2021 at 22:21

1 Answer 1

1

You cannot put your print_keys argument in the @qgsfunction decorator, because it only accept the args, group, handlesnull, usesgeometry and referenced_columns arguments as stated in the documentation and in the Function Editor Help section.

You need to put it in the custom function definition:

def get_attribute_table_bar_features(print_keys, feature, parent):

Anyway Python doesn't allow that a non-default arguments follow a default argument.

If you want to make it possible for the custom function to be used without the need to specify the argument value when it has a default value, you need to put a logic in the custom function code to set a default value for the argument and to pass the @qgsfunction decorator the args = -1 argument so the custom function will accept any (including 0) number of arguments.

For example, the following function:

    @qgsfunction(args=-1, group='Custom')
    def custom_function(your_arg, feature, parent):

        # this is the logic to set a default value
        if not your_arg:
            your_arg = 'default value'
        else:
            your_arg = your_arg[0]
    
        return your_arg

will behave like a function with an optional your_arg parameter which defaults to the default value value.

So, custom_function() will return the 'default value' value, while custom_function('another value') will return the 'another value' value.

2
  • Sorry, I was wrong with my copy/paste; I actually tried to add the named argument to the function definition and not to the decorator. I edited the main post to fix that. Commented Dec 5, 2021 at 21:19
  • @swiss_knight OK. Anyway I think my answer responds to your question. Commented Dec 6, 2021 at 7:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.