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))
And that component in recurse(component)
would or at least could be a container right? Love recursive solutions, they’re so recursive.
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.
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.
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.
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?
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))
}
The filter function goes first
edit: I'm not sure what to call the filtering function actually
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.
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
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.
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))
}
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
}
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 !
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