Is it possible to pass python functions via message handlers?
I’d like to be able to pass ad-hoc functions to a popup which would then be called on buttons in the popup. I used to be able to do this in Vision for example for custom confirmation popups where you might want to do a number of things when the user presses “confirm” or “cancel”. To make it generic, I made it so that you could pass functions with specific names to the popup which the buttons called (there were defaults in case none were passed in).
I’ve confirmed that I am able to send the functions, but I just can’t store them anywhere on the component, so i’m unable to call it again when I need it. I can however call it straight away: payload['onConfirmFunction']()
self.onConfirm = payload['function'] ### <--- Line 3
However when I try to send the message, I get the error:
com.inductiveautomation.ignition.common.GenericTransferrableException: Traceback (most recent call last):
File "<function:onMessageReceived>", line 3, in onMessageReceived
AttributeError: 'com.inductiveautomation.perspective.gateway.script' object has no attribute 'onConfirm'
I can call the function right in the message handler script using:
payload['function']()
I have also tried writing the function to a custom prop, but it just writes the memory address of the function into it
The buttons on the view are set to fire the corresponding message handler tied to the view params. I either have a listener somewhere on the page or in the session to match to the events. Didn’t have to deal with passing around functions, although some of the message handlers just turn around and call project scripts.
Turned out to be an extremely flexible and reusable popup that I used all over the place especially to replicate the systyem.gui.* popup functions from vision.
# Set the message and payload
messageType = self.view.params.btnActionPrimary
payload = perspective.toDict(self.view.params.btnPayloadPrimary)
# Append the ID of the popup
payload['id'] = self.view.params.id
# Send the message to perform cool actions!
system.perspective.sendMessage(messageType, scope = "session", payload = payload)
system.perspective.print('Send Message | %s | %s' % (messageType, payload))
Generally speaking functions are first class in python and yes you can pass them. It’s what allows you to make decorators in general. You can do stuff like
and then call this on any generic function to time it.
You can store functions in a list and then evaluate them in a certain order and store a list of results
my_funcs = [someModule.foo, module.bar, foo.bar]
results = [func() for func in my_funcs]
You can assign them to a variable (though use case wise I don’t have anything for this)
x = someModule.foo
x()
You most likely don’t want to just be able to send back any function via message handler to be run on the gateway as if someone is able to spoof a client and knows just a little ignition or say some of your functions that relate to deleting/updating your database, then you could be in a world of hurt.
Heres a post I made about it a while back, but my method was passing a string of the function name and then importing it with importlib. However to make sure I was safe I would need to white list what functions would be acceptable for a client to call and I was able to do that with string comparisons. If you’re passing a function object back directly I am not sure how you would have a whitelist.
I use this for my large number of json manipulation functions that do various things parsing json, where most of the time they will need to call themselves recursively. Instead of having to call the function name itself every time, at the top I just set a variable to the function's name and then call that instead. It means I can copy and paste any single function that has a similar structure to what I need for a new function, and just mod the top variable to set it to the new name.
E.g.
def foo():
this_function = foo
for i in range(999999):
this_function()
Perspective properties are limited to the documented types in order to yield something javascript compatible for the front end. Which is why your attempt to store a function on the view’s properties stringified it instead of retaining its true nature. You theoretically could cache items in getGlobals(), but that requires strict control over lifetimes, especially since you want to store code in there. Even the slightest error in lifetime management will yield huge memory leaks. (The code will hold entire old interpreter environments in memory after script restarts…)
To be fair, most of the idea is originally @ray resource on the exchange. I just added the payload stuff
BTW… in the button code, here is the code to the perspective.toDict() function
def toDict(perspective_object):
"""Formats a perspective object as Dict
Adapted from http://forum.inductiveautomation.com/t/ideal-way-to-display-objects-on-a-screen/41575
Args:
perspective_object: A perspective property object
"""
from com.inductiveautomation.ignition.common import TypeUtilities
# Convert the Perspective property object to Gson and back to a Py Object
return TypeUtilities.gsonToPy(TypeUtilities.pyToGson(perspective_object))