Passing python functions and storing via message handlers in Perspective

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']()

I’ve tried this:

Button to send message
onClick Script:

def fn():
	system.perspective.print('Hello!')
payload={'function': fn}
system.perspective.sendMessage('mh', payload=payload, scope='view')

Button with message handler to call function
image
onClick Script:

self.onConfirm()

Message Handler ‘mh’:

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 :slight_smile:

I got it, I can usesystem.util.getGlobals() to store the function :slight_smile:

Although it’s a little bit dodgy in that the function(s) remain after closing the popup… I’ll need to clear them out upon close

Don’t do this… thanks @pturmel for the caution

1 Like

I think there is a simpler way… I extended this resource from the exchange (Ignition Exchange | Inductive Automation)

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.

image

image

	# 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))
3 Likes

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

def timer_function(func, args, kwargs):
    start_time = time.time()
    result = func(*args, **kwargs)
    end_time = time.time()
    print end_time - start_time

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()
1 Like

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…)

I like @ryanjmclaughlin’s approach.

3 Likes

A question for better understanding:
How would I “destroy” a function or code once i’m done with it if it was stored inside getGlobals?

That being said, I’m going to go with @ryanjmclaughlin’s way, much cleaner! Thanks

To be fair, most of the idea is originally @ray resource on the exchange. I just added the payload stuff :stuck_out_tongue:

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))
1 Like

I wondered about that function, cheers!

Basic python del statement as used with any dictionary:

gg = system.util.getGlobals()

del gg['someKey']

Really neat addition to the component, thanks for sharing! I’ll look into adding this to the exchange resource itself as a V2 improvement.