Walking the Property (tree)

I’ve been working on a script to find all Properties of a particular type (class) within Blender’s Property system. It seems to work, but since I’m relatively new to Python and Blender, I suspect there are better ways to do some of it. So I’m looking for suggestions on how to improve it.

The basic idea is to accept an object and then recursively search it and its “branches” for objects of the requested type (currently hard-coded to look for objects of class “Var_Group”). All the objects of the requested type are then returned in a list.

The logic is simple. It starts with an object argument and an empty list, and it performs these tests:

  • Is this object of the requested type? If so, add it to the list and return the list.
  • Is this object a Property Group? If so, recursively call this function with each Property in the Group.
  • Is this object a Collection Property? If so, recursively call this function with each Property in the Collection.

Of course, the devil is in the details.

For Property Groups, I’ve had trouble getting a list of all the Properties in the Group. I originally searched obj.keys(), but some Properties didn’t show up there until they’d been displayed in the GUI. So I switched to using dir(obj), but that produces a lot of non-property objects. That’s OK, but the “bl_rna” Property ended up causing infinite recursion, so I had to explicitly filter it out (see code below). Is there a better way to get a list of the Property objects in a Property Group?

For Collection Properties, I could not figure a way to use “isinstance” to find if a given object was a Collection Property. So I fudged it by testing if str(type(obj)) == “<class ‘bpy_prop_collection_idprop’>”. This is a pretty ugly way to do a simple thing, and I’m hoping someone can post a better way.

Here’s the code that walks the property tree starting with “obj” and an empty list (“plist”). It returns a list of the objects of type “bpy.types.Var_Group” found in the tree:

def get_props ( obj, plist ):
    """ Recursive routine that builds a list of Var_Group properties """

    if isinstance(obj,(bpy.types.Var_Group)):
        # This is what we're looking for so add it to the list
        plist.append ( obj )

    elif isinstance(obj,bpy.types.PropertyGroup):
        # This is a property group, so walk through all of its property keys
        for objkey in dir(obj):
            if (str(objkey) == 'bl_rna'):
                # Processing the bl_rna causes infinite recursion
                pass
            else:
                try:
                    plist = get_props(getattr(obj,objkey), plist)
                except Exception as e:
                    # This seems to happen with properties in a .blend file
                    #   that are no longer in the code or have been renamed?
                    print("Error in get_props("+str(obj)+","+str(plist)+")=&gt;"+str(e))

    elif str(type(obj)) == "&lt;class 'bpy_prop_collection_idprop'&gt;":
        # This is a collection, so step through the elements as an array
        for index in range(len(obj)):
            plist = get_props(obj[index],plist)

    else:
        # This could be anything else ... like &lt;'int'&gt; or &lt;'str'&gt;
        pass

    return plist

Here’s the output from the resulting list for a test case Property Group containing nested Properties, Property Groups, and Property Collections:

----- Application Properties -----
  There are 31 properties defined
    bpy.data.scenes['Scene'].app.a = "Count" = 3.0
    bpy.data.scenes['Scene'].app.b = "ABC" = 0.0
    bpy.data.scenes['Scene'].app.g_list[0].ssg_list[0].p = "ssgp0" = 10.0
    bpy.data.scenes['Scene'].app.g_list[0].ssg_list[0].q = "ssgq0" = 99.0
    bpy.data.scenes['Scene'].app.g_list[0].ssg_list[1].p = "ssgp1" = 100.0
    bpy.data.scenes['Scene'].app.g_list[0].ssg_list[1].q = "ssgq1" = 999.0
    bpy.data.scenes['Scene'].app.g_list[0].x = "String 1_2" = 4.0
    bpy.data.scenes['Scene'].app.g_list[0].y = "String 1_3" = 6.0
    bpy.data.scenes['Scene'].app.g_list[1].x = "String 1_4" = 8.0
    bpy.data.scenes['Scene'].app.g_list[1].y = "String 1_5" = 10.0
    bpy.data.scenes['Scene'].app.g_list[2].ssg_list[0].p = "ssgp0" = 10.0
    bpy.data.scenes['Scene'].app.g_list[2].ssg_list[0].q = "ssgq0" = 99.0
    bpy.data.scenes['Scene'].app.g_list[2].ssg_list[1].p = "ssgp1" = 100.0
    bpy.data.scenes['Scene'].app.g_list[2].ssg_list[1].q = "ssgq1" = 999.0
    bpy.data.scenes['Scene'].app.g_list[2].x = "String 2_2" = 8.0
    bpy.data.scenes['Scene'].app.g_list[2].y = "String 2_3" = 12.0
    bpy.data.scenes['Scene'].app.g_list[3].x = "String 2_4" = 16.0
    bpy.data.scenes['Scene'].app.g_list[3].y = "String 2_5" = 20.0
    bpy.data.scenes['Scene'].app.g_list[4].ssg_list[0].p = "ssgp0" = 10.0
    bpy.data.scenes['Scene'].app.g_list[4].ssg_list[0].q = "ssgq0" = 99.0
    bpy.data.scenes['Scene'].app.g_list[4].ssg_list[1].p = "ssgp1" = 100.0
    bpy.data.scenes['Scene'].app.g_list[4].ssg_list[1].q = "ssgq1" = 999.0
    bpy.data.scenes['Scene'].app.g_list[4].x = "String 3_2" = 12.0
    bpy.data.scenes['Scene'].app.g_list[4].y = "String 3_3" = 18.0
    bpy.data.scenes['Scene'].app.g_list[5].x = "String 3_4" = 24.0
    bpy.data.scenes['Scene'].app.g_list[5].y = "String 3_5" = 30.0
    bpy.data.scenes['Scene'].app.s_list[0] = "String 1" = 2.0
    bpy.data.scenes['Scene'].app.s_list[1] = "String 2" = 4.0
    bpy.data.scenes['Scene'].app.s_list[2] = "String 3" = 6.0
    bpy.data.scenes['Scene'].app.sg.x = "ABC" = 0.0
    bpy.data.scenes['Scene'].app.sg.y = "ABC" = 0.0

I believe the output is correct, but I’d like to improve the script to eliminate the kludges.

Maybe use bl_rna of the parent type instance (scene)?

bpy.types.Scene.p = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)

>>> C.scene.bl_rna.properties[‘p’].type
‘COLLECTION’

Thanks (once again!!) CoDEmanX for your help.

I’m sorry that I may not have explained it clearly enough.

I want to make three tests on the object “obj” to see if it is a Var_Group, PropertyGroup, or CollectionProperty. Here are the three tests (other code replaced with “pass”):

    if isinstance(obj,(bpy.types.Var_Group)):
        # This is what we're looking for so add it to the list
        pass

    elif isinstance(obj,bpy.types.PropertyGroup):
        # This is a property group, so walk through all of its property keys
        pass

    elif str(type(obj)) == "&lt;class 'bpy_prop_collection_idprop'&gt;":
        # This is a collection, so step through elements as an array
        pass

The first two tests use “isinstance()” which seems to be the right thing to do. But in the third case, I couldn’t figure out how to tell if “obj” is a “Collection Property”. In other words, I couldn’t find a “type” to test it against using “isinstance()” as I did with “bpy.types.Var_Group” and “bpy.types.PropertyGroup”.

I can test to see if it’s a “bpy.types.CollectionProperty”, but that test fails even when it is a CollectionProperty.

&gt;&gt;&gt; type(obj)
&lt;class 'bpy_prop_collection_idprop'&gt;

&gt;&gt;&gt; isinstance(obj,bpy.types.CollectionProperty)
False

That’s why I’ve had to resort to converting the type to a string and comparing the string.

Any ideas?

I did a little more experimenting and I must be misunderstanding something.

I created a property group containing a StringProperty, FloatProperty, PointerProperty to my own PropertyGroup subclass, and a CollectionProperty of my own PropertyGroup subclass:

class Var_Group(bpy.types.PropertyGroup):
    b = BoolProperty(name="b")

class AppPropertyGroup(bpy.types.PropertyGroup):
    s = StringProperty(name="s",default="ABC")
    f = FloatProperty(name="f",default=0.1)
    v = PointerProperty(name="v", type=Var_Group)
    c = CollectionProperty(name="c", type=Var_Group)

I used Python’s “isinstance()” function on each of them, and I was surprised by the results:

app   is PropGroup:  <b>True</b>
app   is AppGroup:   <b>True</b>
app.s is StringProp: <i>False</i>
app.f is FloatProp:  <i>False</i>
app.v is PropGroup:  <b>True</b>
app.v is Var_Group:  <b>True</b>
app.c is Collection: <i>False</i>

In other words, isinstance() worked for my own PropertyGroup subclasses, but it didn’t work for Blender’s built in types (at least as I was testing them).

I found a discussion from 2008 at:

http://blenderartists.org/forum/archive/index.php/t-118003.html

A fellow named “Martin” wrote:

Blender.Mathutils.Vector isn’t a class definition, it’s a factory method.

Is that what’s going on with Properties? Doesn’t the definition:

s = StringProperty(name=“s”,default=“ABC”)

create an object “s” of type “StringProperty”???

Here’s the full source of my test addon for completeness:

"""
This program tests Blender's Property Types
"""

bl_info = {
  "version": "0.1",
  "name": "Collection Test",
  'author': 'BlenderHawk',
  "location": "Properties &gt; Scene",
  "category": "Blender Experiments"
  }

import bpy
from bpy.props import *


class Var_Group(bpy.types.PropertyGroup):
    b = BoolProperty(name="b")

class AppPropertyGroup(bpy.types.PropertyGroup):
    s = StringProperty(name="s",default="ABC")
    f = FloatProperty(name="f",default=0.1)
    v = PointerProperty(name="v", type=Var_Group)
    c = CollectionProperty(name="c", type=Var_Group)


class APP_OT_print_props(bpy.types.Operator):
    bl_idname = "app.print_props"
    bl_label = "Print Properties"
    bl_description = "Print results of testing various properties"
    bl_options = {'REGISTER'}

    def execute(self, context):
        print ( "----- Properties -----" )
        app = context.scene.app
        print ( "app   is PropGroup:  " + str ( isinstance ( app,   bpy.types.PropertyGroup ) ) )
        print ( "app   is AppGroup:   " + str ( isinstance ( app,   bpy.types.AppPropertyGroup ) ) )
        print ( "app.s is StringProp: " + str ( isinstance ( app.s, bpy.types.StringProperty ) ) )
        print ( "app.f is FloatProp:  " + str ( isinstance ( app.f, bpy.types.FloatProperty ) ) )
        print ( "app.v is PropGroup:  " + str ( isinstance ( app.v, bpy.types.PropertyGroup ) ) )
        print ( "app.v is Var_Group:  " + str ( isinstance ( app.v, bpy.types.Var_Group ) ) )
        print ( "app.c is Collection: " + str ( isinstance ( app.c, bpy.types.CollectionProperty ) ) )
        return {'FINISHED'}


class APP_PT_Explore_Props(bpy.types.Panel):
    bl_label = "Explore Application Properties"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "scene"
    def draw(self, context):
        app = context.scene.app
        row = self.layout.row()
        row.operator("app.print_props", text="Print Properties")


def register():
    print ("Registering ", __name__)
    bpy.utils.register_module(__name__)
    bpy.types.Scene.app = bpy.props.PointerProperty(type=AppPropertyGroup)

def unregister():
    print ("Unregistering ", __name__)
    del bpy.types.Scene.app
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

Hi Bob

Rather than isinstance use rna_type of the property


&gt;&gt;&gt; C.scene.app.bl_rna.properties['s'].rna_type
&lt;bpy_struct, Struct("StringProperty")&gt;


&gt;&gt;&gt; C.scene.app.bl_rna.properties['s'].rna_type.identifier
'StringProperty'


&gt;&gt;&gt; 

Pointer props and Collection props will have a fixed_type attribute, with the declaration type.


&gt;&gt;&gt; v = C.scene.app.bl_rna.properties.get('v')
&gt;&gt;&gt; v.fixed_type
&lt;bpy_struct, Struct("Var_Group")&gt;


&gt;&gt;&gt; v.fixed_type.identifier
'Var_Group'

Now you will be able to change your code accordingly to determine the types.


import bpy
context = bpy.context
app = context.scene.app


for k, p in app.bl_rna.properties.items():
    print("property %s : type %s" % (k, p.rna_type.identifier))
    if hasattr(p, 'fixed_type'):
        print("	%s" % p.fixed_type.identifier)

Found this in
blender\release\scripts\startup\bl_operators\presets.py:

elif type(value).name == “bpy_prop_collection_idprop”: # could use nicer method

CoDEmanX and batFINGER - what a great team! Thanks for your time and help.

CoDEmanX, your line from presets.py will probably solve my immediate problem of how to identify a collection. I’ve changed:

elif str(type(obj)) == "&lt;class 'bpy_prop_collection_idprop'&gt;":

to:

elif type(obj).__name__ == 'bpy_prop_collection_idprop':       # could use nicer method 

That’s still ugly, but slightly less ugly. :slight_smile: I agree with the comment in presets.py … they certainly could use a nicer method!!

BatFINGER, your exploration into bl_rna has opened up another whole range of possibilities. But I have a question…

I’ve looked at my “app” PropertyGroup through 3 different lenses (dir(), .keys(), and now .bl_rna.properties.keys()):

&gt;&gt;&gt; <b>dir(C.scene.app)</b>

    ['__dict__', '__doc__', '__module__', '__qualname__', '__weakref__', 'a',
      'add_item', 'b', 'bl_rna', 'g_list', 'name', 'rna_type', 's_list', 'sg', 'show_more']

&gt;&gt;&gt; <b>C.scene.app.keys()</b>

   ['a', 'b', 'sg', 's_list', 'g_list']

&gt;&gt;&gt; <b>C.scene.app.bl_rna.properties.keys()</b>

   ['rna_type', 'name', 'sg', 'a', 's_list', 'g_list', 'show_more', 'b']

They all give different results. I’m relatively new to Python, so I’d like to know what the difference is and which method of introspection is most appropriate. For example, I’ve noticed that the plain “.keys()” method (C.scene.app.keys()) seems to give different results depending on whether the property has actually been displayed in the user interface (or maybe if it’s actually been used?). I’ve also noticed that the “dir()” option produces the most “stuff” (and some of that stuff is circularly linked!).

So any illumination of when to use which methods of introspection (and any related warnings) would be great.

Thanks again to both of you for taking the time to slog through these long code segments and questions.

dir

http://docs.python.org/3/library/functions.html#dir

Personally for blender I’d try and avoid using dir() and use the bl_rna rna_type constructs instead.

Properties http://wiki.blender.org/index.php/Doc:2.6/Manual/Extensions/Python/Properties

obj.keys() will give a list of the names of id properties, ie ob[key]

obj.bl_rna.properties.keys() will give a list of names of custom properties defined on that object ie ob.key, whether they are initialised or not

custom properties can also be addressed like ID properties once they are set, this is how the value is saved in the blender file. For instance your app.f property will return its default when addressed as app.f but will throw a bug when addressed as app[‘f’] until set. Blender only needs to save non default values (makes sense).

You could use


is_set = k in ob.keys()

to determine if a custom property has been initialised.

btw for comprehensive code on walking the rna tree have a look at rna_info.py rna_xml.py etc in the scripts/modules folder.

Thanks to everyone for your help. I think this topic can be marked “SOLVED” (I don’t know how to do that myself or I would).

Here’s the final script that I ended up with (improved via helpful comments from CoDEmanX and batFINGER):

def get_props ( obj, plist ):
    """ Recursive routine that builds and returns a list of Var_Group properties """

    if isinstance(obj,(bpy.types.Var_Group)):
        # This is what we're looking for so add it to the list
        plist.append ( obj )

    elif isinstance(obj,bpy.types.PropertyGroup):
        # This is a property group, so walk through all of its property keys

        for objkey in obj.bl_rna.properties.keys():    # This is somewhat ugly, but works best!!
            try:
                plist = get_props(getattr(obj,objkey), plist)  # &lt;-- Recursive call !!
            except Exception as e:
                print("ERROR: get_props("+str(obj)+","+str(plist)+")=&gt;"+str(e))

    elif type(obj).__name__ == 'bpy_prop_collection_idprop': # could use nicer method 
        # This is a collection, so step through elements as an array

        for index in range(len(obj)):
            plist = get_props(obj[index],plist)  # &lt;-- Recursive call !!

    else:
        # This could be anything else ... like &lt;'int'&gt; or &lt;'str'&gt;
        pass

    return plist

That script takes an object and a list. It finds all instances of the PropertyGroup “Var_Group” within the object tree and appends them to the list. It does this recursively through Collections and PropertyGroups. It uses obj.bl_rna.properties.keys() as suggested by batFINGER (thanks!) and the collection checking from CoDEmanX (thanks as well!).

The Var_Group can represent any class (an easy extension would pass the class type sought to the function). For my testing, I used this simple class definition:

class Var_Group(bpy.types.PropertyGroup):
    """ Property Group representing a variable with a name, value, and other potential attributes """
    var_name = StringProperty(name="n",default="Var")
    var_value = FloatProperty(name="v",default=1.0)

Here’s a formatted example of the list it returns for an artificial (and somewhat tortured) Property Tree:

  There are 21 properties defined
    bpy.data.scenes['Scene'].app.var_group_list[0] = "Var" = 12.0
    bpy.data.scenes['Scene'].app.sub_group.y = "Var" = 10.0
    bpy.data.scenes['Scene'].app.sub_group.z = "Var" = 8.0
    bpy.data.scenes['Scene'].app.sub_group.x = "Var" = 5.5
    bpy.data.scenes['Scene'].app.sub_group_list[0].y = "Var" = 17.5
    bpy.data.scenes['Scene'].app.sub_group_list[0].z = "Var" = 14.0
    bpy.data.scenes['Scene'].app.sub_group_list[0].x = "Var" = 5.0
    bpy.data.scenes['Scene'].app.sub_group_list[1].y = "Var" = 15.0
    bpy.data.scenes['Scene'].app.sub_group_list[1].z = "Var" = 8.5
    bpy.data.scenes['Scene'].app.sub_group_list[1].x = "Var" = 7.0
    bpy.data.scenes['Scene'].app.sub_group_list[1].sub_sub_group_list[0].q = "Var" = 5.0
    bpy.data.scenes['Scene'].app.sub_group_list[1].sub_sub_group_list[0].p = "Var" = 11.5
    bpy.data.scenes['Scene'].app.sub_group_list[1].sub_sub_group_list[1].q = "Var" = 20.0
    bpy.data.scenes['Scene'].app.sub_group_list[1].sub_sub_group_list[1].p = "Var" = 7.5
    bpy.data.scenes['Scene'].app.sub_group_list[1].sub_sub_group_list[2].q = "Var" = 20.0
    bpy.data.scenes['Scene'].app.sub_group_list[1].sub_sub_group_list[2].p = "Var" = 11.0
    bpy.data.scenes['Scene'].app.sub_group_list[2].y = "Var" = 14.5
    bpy.data.scenes['Scene'].app.sub_group_list[2].z = "Var" = 6.0
    bpy.data.scenes['Scene'].app.sub_group_list[2].x = "Var" = 12.5
    bpy.data.scenes['Scene'].app.a = "Count" = 14.0
    bpy.data.scenes['Scene'].app.b = "b" = 13.5

Thanks again to everyone (as well as the forum owners/administrators) for your help!!