north-road / qgis-processing-r

QGIS Processing R Provider Plugin

Home Page:https://north-road.github.io/qgis-processing-r/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Global and project variables in R.

gavg712 opened this issue · comments

This is my official request about global and project variables as objects available in R. I am not sure how important it would be to have these variables. But here are some ideas I have been working on.

Taking a bit from #37 and #31 and my own needs, I have thought that a new parser could be generated for a header line that would allow to have the QGIS variables we need. Use cases:

  • Change the working directory to the project path.
  • Customize graph and log outputs using global or project variables.
  • Save intermediate results as resources or backups for other projects.

My progress allows me to have most variables available, except for the project variables "layers" and "_project_transform_context"

Here an example:
image

And the R session behavior
image

@nyalldawson, @JanCaha, Guys, I think you have already seen this on twitter. But I wanted to ask you directly, do you think it is a good idea to include it in the plugin?

Guys, I think you have already seen this on twitter. But I wanted to ask you directly, do you think it is a good idea to include it in the plugin?

It sounds useful to me. I can definitely see use cases where you'd need to vary the behaviour depending on the qgis version its being running from!

I'm wondering though if we should take a step back and instead allow evaluation of ANY type of QGIS expression for storage in an r variable. Eg.

##my_r_variable=expression 1+2+3
##qgis_version=expression @qgis_version_no
##some_other_var=expression layer_property('lines layer', ... )

It could address the same use case, but in a much more powerful way..

That might be even better and it covers much wider use cases!

@nyalldawson If you can point out how expressions can be evaluated in the context of the current project, I will try to give it a shot.

I'm wondering though if we should take a step back and instead allow evaluation of ANY type of QGIS expression for storage in an r variable. Eg.

I read it some where. And of course I agree! I will re-think in this approach and give it a shot too.

@JanCaha This is not the nicer example, just as starting point, to evaluate a expression using a context stack of global and project context:

# Build a Context Stack
context = QgsExpressionContext()
context.appendScopes([
    QgsExpressionContextUtils.globalScope(), 
    QgsExpressionContextUtils.projectScope(QgsProject.instance())
    ])

# to evaluate any expression from Project or Global scopes, except 'layers' and ''
exp = QgsExpression("@project_home")
print(exp.evaluate(context))
exp = QgsExpression("@qgis_version")
print(exp.evaluate(context))
exp = QgsExpression("@user_full_name")
print(exp.evaluate(context))
try:
    exp = QgsExpression("@layers")
    print(exp.evaluate(context))
except TypeError:
    pass
else:
    print("expression '@layers' does not work!")

try:
    exp = QgsExpression("1 + 1")
    print(exp.evaluate(context))
except TypeError:
    pass
else:
    print("expression '1 + 1' does not work in this context!")

Actually instead of this:

context = QgsExpressionContext()
context.appendScopes([
    QgsExpressionContextUtils.globalScope(), 
    QgsExpressionContextUtils.projectScope(QgsProject.instance())
    ])

it's better to take the expression context directly from the processing context available to algorithms:

    # somewhere in the algorithm's "processAlgorithm" implementation, where "context" is the QgsProcessingContext passed to that function
    exp_context = context.createExpressionContext(parameters, context)

That way things will all work just as expected, regardless of where the algorithm is run (through a model, via qgis_process, etc)

try:
    exp = QgsExpression("1 + 1")
    print(exp.evaluate(context))
except TypeError:
    pass
else:
    print("expression '1 + 1' does not work in this context!")

I'm curious where this example is taken from. I can't see any way a TypeError would be raised here. The correct approach should be:

    exp = QgsExpression("1 + 1")

    # if the expression will be evaluated multiple times, e.g. if it's something like "area($geometry)" and you'll be evaluating that same expression against multiple features:
    if not exp.prepare(exp_context):
        # will happen for a malformed expression , e,g "(1 + 2 ) * ( 3" 
        print(" bad expression: {}".format(exp.parserErrorString() )

    res = exp.evaluate(exp_context)
    if exp.hasEvalError():
        # will happen when an otherwise valid expression cannot be calculated, e.g. something like
        # "centroid('i am not a geometry')"
        print("error evaluating expression: {}".format( exp.evalErrorString() )

I'm curious where this example is taken from. I can't see any way a TypeError would be raised here. The correct approach should be:

My mistake. I was typing some things in the python console. However, I caught your approach

@nyalldawson thanks a lot.

Ok, the quick demo seems to be working. Now comes the hard part - I have to prepare for all possible return types from the Expression and process them as needed.

@JanCaha I'd say the priorities would be:

  • str
  • ints/floats
  • lists
  • dates/times/datetimes (these will be QDate/QTime/QDateTime values)
  • geometries (lower priority)

The others types are significantly less important (at least in the context of single values calculated at the start of a script), and are much rarer (eg interval types, dicts)

The first try is available in my fork. Seems to be working rather fine. I need to find some time to write unit tests to be sure ;-)

If you want to take a look, this commit handles most of the important stuff.

@JanCaha does that expose them as expression ** inputs ** to the user though? (I don't think we want that, right?)

It does expose them. To be honest I did not really think about that...I was focused on the functionality. You are right that it is probably not desirable.

It seems to be solved in #102. Then I will close it!