invokeLater Python wrapper with parameters and retries

The other day I needed to perform some work after a screen loaded, but the blocking resource was lagging unpredictably. Now, I could just make the delay longer, but that doesn’t feel reliable. But I don’t want to retry forever, either, since that’s a great way to create zombie threads.

In short, sometimes you want VBA’s on resume next, but persistent. :smiling_imp:

So here’s a handy Python script. Pass it a function and a dictionary of parameters (similar to how you’d talk to a window).
[ul]
[li]A default delay of 250 ms and no retries makes this act similarly to system.util.invokeLater(…).[/li]
[li]If the function fails, it will re-do invokeLater until retry decrements to zero.[/li]
[li]If you only want to retry on specific errors (and you should), then pass those exceptions in as a tuple to exceptOn[/li]
[li]Set initialDelay to False in case you want to try first and delay only if needed.[/li][/ul]
If the pattern looks familiar, that’s simply because it’s a wrapper for that process of invokeLater: build a function with defaults and call it from system.util.invokeLater(…). In this case though, we can simply build our function like normal and pass in a parameter list. I think this feels a bit more natural.

def invokeLater(function, functionArguments={}, delay = 250, retry = 0, exceptOn = (), initialDelay=True):
	"""Invokes the given function with the (unpacked) dictionary functionArguments later.
	
	Always tries at least once.
	
	Defaults to 0 retries, each separated by 250 milliseconds.  This is sort of a pass-thru to invokeLater.
		Max retries is 20, to prevent infinite retries
		Max delay of 86400000 (about a day... because come on...)
	Retries on all excpetions, unless a specific tuple of exceptions are provided for exceptOn
	
	NOTE: if the functionArguments dict contains the arguments in invokeLater and they are None,
		then these will be added to functionArguments!
	"""	
	def overrideArguments(functionArguments, argList, argValList):
		"""Scan over the the functionArguments and gather which args need to be overridden with val."""
		overrides = {}
		if delay > 86400000: #consider making this smaller. Say 10000.
			delay = 86400000
		for arg, val in zip(argList, argValList):
			if arg in functionArguments and functionArguments[arg] is None:
				overrides[arg] = val
		
		return overrides
				
	def toBeInvoked(function=function, functionArguments=functionArguments, delay=delay, retry=retry, exceptOn=exceptOn):

		try:
			overrides = overrideArguments(functionArguments, ['function', 'functionArguments', 'delay', 'retry', 'exceptOn'], 
															 [ function,   functionArguments,   delay,   retry,   exceptOn])
			function(**dict(functionArguments.items() + overrides.items()))
		
		# Only retry on the given exceptions (defaults to any, though)
		except exceptOn, e:
			if retry <= 0 or retry > 20: #Hard limits to retries to prevent a zombie thread apocalypse
				return
			else:
				retry -= 1
				
			from system.util import invokeLater
			
			def retryInvocation(retry=retry):
				toBeInvoked(retry=retry)
			
			invokeLater(retryInvocation, delay)

	if initialDelay:
		from system.util import invokeLater
		invokeLater (toBeInvoked, delay)
	else:
		toBeInvoked()

For example, let’s say you’ve got a message box that has to pop up 3 and 5 seconds from the initial call to invokeLater (but bail out early with 2 seconds left). Try this in the playground:

from shared.meta import invokeLater

def mb(message, retry):
	# Stop retrying early
	if retry < 2:
		return

	print 'On retry %d' % retry
				
	# pop a message box on 3 or 5
	if retry in [3,5]:
		system.gui.messageBox(message % retry, 'invokeLater Test')
	
	assert False # Force retry
	
functionArgs = {'message':'invokeLater: On retry %d', 
		 'retry': None}

invokeLater(mb,functionArgs , retry=7, delay=1000, exceptOn=(AssertionError))

Prolly worth noting this example isn’t a really useful thing to do - it just demos how it works. For example, the message box is modal, so it blocks the retry thread; as long as the box is open, the counter is paused. Also note that I’m passing in a None for ‘retry’: this signals to overrideArguments that I’d like my parameter to be overridden to what invokeLater’s using.

Now, this is a pretty dangerous script: a person can really go crazy with it, so don’t use it without safeguards like an explicit exceptOn. Always except exceptionally!

But if you want something done as soon as possible, but don’t worry if it’s not right now, then this might be for you.

Cheers!

Nice!

OT: Do you have a preference for Andrew? Andy? Drew? I do my best to give a personal touch where I can! :mrgreen:

Normally Andrew, but frankly I’ll take anything that doesn’t require grawlixes :slight_smile:

Andrew it is, then! :laughing:

I wrote a series of similar functions that I posted some weeks ago. You might like them:
https://www.inductiveautomation.com/forum/viewtopic.php?f=70&t=13562

Literally just this morning I was wondering if there was an effective way to do stuff only once the window’s finished running events as well as a lightweight assign-later method.

Apropos and I appreciate it!