Iterate through all components programatically in Perspective?

I was wondering if there's a way to iterate through all components on screen for a perspective view? I was trying to use getRootContainer and getChildren but it doesn't seem as straight forward as I thought/as it was for Vision. Any know how this is done?

You’ll have to recurse down the children; eg:

def recurse(component):
		return {child.name: recurse(child) for child in component.children}
	
system.perspective.print(recurse(self.view.rootContainer))
5 Likes

And that component in recurse(component) would or at least could be a container right? Love recursive solutions, they’re so recursive.

2 Likes

Right, just start the function with whatever component you want to build your tree from.

Been a while but related to this - I want to go through each component, and if it has a custom property called outValue I want to stuff that into a dictionary that is returned. Any idea how I could modify the above to do that?

The end goal - go through all my components, if it has an outValue custom property, put that in a dictionary like {componentName: outValue}.

I was able to do this in Vision like so -

def parseContainer(rc, recursive=False, dataDictionary=None):
	"""
	Creates a dictionary that is to be used in conjunction of named queries.
	Key values are the database column names, and the values are the database column values.
	Looks to root container and grabs any custom properties that have a "_out_" prefix, strips that off an uses remained as the key name.
	Iterates through all components and checks if there is an outValue.  If so, the name of the component will be used as the key value,
	and value of outValue as the value.
	Args:
		rc: root container of the window.
		recursive: bool, if True, runs this same thing on any child containers down
		dataDictionary: dictionary, key/values are added to this used in the case of recursive (though guess a coder could prepopultae a dictionary with other stuff
		and feed it in if they so chose to but no real reason to)
	Returns:
		dictionary {"column_name_1":column_value_1, "columne_name_2":column_value_2, ...}
	"""
	if dataDictionary is None:
		dataDictionary = {}
	for component in rc.components:
		# Recursive step for inner containers
		if isinstance(component, com.inductiveautomation.factorypmi.application.components.BasicContainer) and recursive:
			parseContainer(component, dataDictionary=dataDictionary)
		for prop in component.getProperties():
			if str(prop.getName()) == 'outValue':
				dataDictionary[component.getName()] = prop.getValue()		
	return dataDictionary

But the structure of these perspective components and view I am having trouble making this work. Any assistance would be greatly appreciated.

1 Like

Perspective doesn't expose any of its hierarchy (or bindings). At best, you could reprocess the on-disk JSON of the view to identify what components are present and the properties thereof (cache that) and use that to explicitly obtain runtime values.

Perspective simply isn't as geek-friendly as Vision. :man_shrugging:

Edit: Hmmm. No, Perspective does expose a view's .rootContainer component and components expose their .children, so you can recursively identify all components in a view. You should do some introspection to see if you can enumerate properties.

If you are only looking for one named property, consider just accessing it on everything, wrapped in a try:, and discarding any nulls.

1 Like

Unfortunate but glad I have an answer. Thanks Phil.

	dataDictionary = {}
	def recurse(component):
		for child in component.children:
			for c in child.custom:
				if "outValue" in c:
					dataDictionary[c] = child.custom[c]
			recurse(child)
	recurse(self.view.rootContainer)	
	system.perspective.print(dataDictionary)

not sure if this is what you were looking for?

4 Likes

Will be testing it soon, I think I just want this line to be dataDictionary[child.name] = child.custom[c] but otherwise I think this is what I am looking for. I want the component name (as those line up directly to columns in my named query) as my keys.

You can write a simple recursive generator function that can be combined with other functional operations pretty easily:

def getComponents(element):
	def walkComponents(component):
		for child in component.children:
			yield child
			walkComponents(child)
	
	return walkComponents(element)

hasOutProperty = lambda element: "outValue" in element.custom

parameters = {}
for component in filter(hasOutProperty, getComponents(self.view.rootContainer)):
	parameters[component.name] = component.custom["outValue"]

# or, for fun:
parameters = {
	c.name: c.custom["outValue"]
	for c in filter(hasOutProperty, getComponents(self.view.rootContainer))
}
3 Likes

The filter function goes first

edit: I'm not sure what to call the filtering function actually

1 Like

How can I put this into a project library script so that I can call it on any container?

Put the getComponents function in your lib, let's say you put it in lib/utils, then you can call lib.utils.getComponents(your_component)

note that lib here is just a folder (package) named "lib", and utils is a script.

2 Likes

same goes for the lambda function.

lib.utils.hasOutProperty

Although might make more since to do something like:

def hasProperty(element, property):
    return lambda element: property in element.custom
1 Like

If you make it a function, you need to get rid of the lambda:

def hasProperty(element, property):
    return property in element.custom

otherwise you're returning the lambda. Which is okay, if you actually pass a CALL to hasProperty(), and not hasProperty itself. But that's a weird thing to do.

edit: actually, a function like this won't work with filter, it's only expecting one parameter.
You can get around that with tuples, or calling a function in a lambda, but... Things are getting weird.
Just use a lambda like Paul did. That's what they're for.

2 Likes

Yes, that lambda if fine, and perhaps in @bkarabinchak.psi's case works great, but it requires the property be named exclusively 'outValue' or whatever property you type in. In most cases you would want it to be more generic so you can pass in the property that you are using.

hmmm, I'll need to mess around with this.

functools.partial is the solution:

from functools import partial

def hasProperty(element, property, scope = 'custom'):
    return lambda element: property in element[scope]

parameters = {
	c.name: c.custom["outValue"]
	for c in filter(partial(hasProperty, property="outValue"), getComponents(self.view.rootContainer))
}
3 Likes

Or, of course, the less-meta-programmy, probably more Pythonic solution:

parameters = {
	c.name: c.custom["outValue"]
	for c in getComponents(self.view.rootContainer) if "outValue" in c.custom
}
2 Likes

Well lambdas are not meant to be generic.

You could define a generic function, and pass it your property name with functools.partial, or think of other convoluted ways of doing this... but in the end, this:

filter(lambda el: 'foo' in el), some_iterator)

is the simplest and most pythonic way of doing it. At least with filter. Some will argue that

[el for el in some_iterator if 'foo' in el]

is the way to go. To each his own.

edit:
Hey, it seems I'm on the same wavelength as Paul on this !

1 Like

You could also use currying probably:

elementhasproperty = lambda property: lambda element: property in element.custom

... filter(elementhasproperty("outvalue"), getComponents(self.view.rootContainer)) ...

Tested in script console:

>>> xiny = lambda x: lambda y: x in y
>>> abiny = xiny("ab")
>>> abiny("B")
False
>>> abiny("abstract")
True
>>> fooiny = xiny("foo")
>>> fooiny("abstract")
False
>>> fooiny("football")
True
>>> xiny("baz")("foobazbar")
True
>>> filter(xiny("foo"), ["football", "baseball", "foolish", "wise"])
['football', 'foolish']

Edit: fixed typo, actual test case with filter