Creating and accessing PropertyGroup instances

I’m trying to wrap my head around why I’m getting different types of objects when I do this:

    import bpy
from bpy.types import PropertyGroup
from bpy.props import *
from bpy.utils import register_class, unregister_class


class TOOL_Settings(PropertyGroup):
    
    my_property: IntProperty(
        name = "my_property",
        description = "my_property",
        min = 6,
        max = 26,
        default = 12,
        )

classes = (
    TOOL_Settings,
)

instance_classes = []
max_count = 4
bpy.types.Scene.instancetools = [x for x in range(max_count)]

for index in range(max_count):
    id = f"TOOL_Settings_{index}"
    propgroup = type(id,
                    (TOOL_Settings, PropertyGroup, ),
                    {"bl_idname" : id,
                    "count" : index
                    }
                    )
                    
    instance_classes.append(propgroup)


def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.Scene.singletools = PointerProperty(type=TOOL_Settings) 
    
    for index in range(max_count):
        register_class(instance_classes[index])
        bpy.types.Scene.instancetools[index] = PointerProperty(type=instance_classes[index])
        

def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)
    del bpy.types.Scene.singletools
    
if __name__ == "__main__":
    register()
    print(type(bpy.context.scene.singletools), bpy.context.scene.singletools)
    print(type(bpy.context.scene.instancetools[0]), bpy.context.scene.instancetools[0])
    print(type(bpy.context.scene.instancetools[0][0]), bpy.context.scene.instancetools[0][0])
    print(type(bpy.context.scene.instancetools[0][1]), bpy.context.scene.instancetools[0][1])

But what I get spat back in the console is this:

<class 'bpy.types.TOOL_Settings'> <bpy_struct, TOOL_Settings("")>
<class 'tuple'> (<built-in function PointerProperty>, {'type': <class '__main__.TOOL_Settings_0'>})
<class 'builtin_function_or_method'> <built-in function PointerProperty>
<class 'dict'> {'type': <class '__main__.TOOL_Settings_0'>}

Maybe I’m doing it totally wrong, but what I want to do is have distinct instances of a property group, so each instance can have unique values. Eventually I want to display these instance properties on unique sub-panels, but I’m falling at the first hurdle.

I get the expected bpy_struct object for the single instance of Tool_Settings, but I don’t get a bpy_struct object for the multi-instances.

Could anyone help?

What is bpy.types.Scene.instancetools? This doesn’t look valid to me.

bpy.types.Scene.instancetools = [x for x in range(max_count)]

If you want to add a property to the scene, that property must be a bpy.props class, it can’t be a regular Python object. See this example: (https://docs.blender.org/api/current/bpy.props.html#propertygroup-example)

Also, I think you’re doing some things in a weird or roundabout way, creating a list with some numbers that correspond to its indices, and then using the entries in the list as indices to another list… why don’t you just use a dict?

Anyways, you can’t have a Python list as a property of a Blender object (then it’s only available to Python and Blender doesn’t know about it). You should try to make a property group of TOOL_Settings property groups. Take a look at this example: (https://docs.blender.org/api/current/bpy.props.html#collection-example)

I also don’t understand why you’re trying to use a pointer property. If I recall correctly, the pointer property is a way of holding a pointer to a Blender ID (that is, any subclass of bpy.types.ID). And there’s some outstanding bugs that keep this feature from really working, so most of the time people use workarounds for this (again, IIRC). But really, I don’t think there’s any reason for you to use a pointer property here.

Also, take a look here: (https://docs.blender.org/api/current/bpy.types.PropertyGroup.html#bpy.types.PropertyGroup)

Hopefully this is some help. I’ve never tried to do what you’re doing here, so it’s possible I’m wrong about some things.

Thanks for the response Joseph.

So maybe I should explain my end goal here. What I’m trying to do is have a panel class, a property group class, and an operator class. I want to tie them all together into a single panel that I can create unique instances of, without having to actually copy+paste classes all over the place.

So if you imagine something like this (pseudo code):

Object 1 - Panel + PropertyGroup + “Build Me an object” Operator
Object 2 - Panel + PropertyGroup + “Build Me an object” Operator
Object 3 - Panel + PropertyGroup + “Build Me an object” Operator
Object 4 - Panel + PropertyGroup + “Build Me an object” Operator
Object 5 - Panel + PropertyGroup + “Build Me an object” Operator

Where object 1-5 are the objects created by the operator. The panel, propertygroup, and operator however all use template classes.

The end result should be multiple panels like this:

Without any shared property objects/classes or operator objects/classes.

Does that make any sense? I guess I’m revealing my ignorance of the API, but it doesn’t work like I am used to in Python.


To answer your questions:

bpy.types.Scene.instancetools = [x for x in range(max_count)]

That’s a temporary list object with the correct number of temporary entries that then get written over when I do the append later on, inside the register function:

        bpy.types.Scene.instancetools[index] = PointerProperty(type=instance_classes[index])

My idea was to collect all of the pointer objects together so I could pick them up inside an operator and panel later on.

If it helps, here is my full code so far (what I posted before was a prototype stripped back idea of where to go next.)

import bpy, sys, importlib, pandas as pd
from bpy.types import Operator, Panel, PropertyGroup, Menu
from bpy.props import *
from bpy.utils import register_class, unregister_class
from pathlib import Path
path = Path(bpy.data.filepath)
sys.path.append(str(path.parent.parent.parent.parent.parent) + "\\Blender Scripts")
import functions
importlib.reload(functions)


C = bpy.context
D = bpy.data
O = bpy.ops
P = bpy.props
T = bpy.types

class DRUMTOOL_Settings(PropertyGroup):
    
    csv_path = sys.path[-1] + "\\model_references.csv"
    csv_read = pd.read_csv(csv_path)
    csv_manufacturers = set(csv_read['Manufacturer'].tolist())
    manufacturer_list = [(manufacturer, manufacturer, "") for manufacturer in csv_manufacturers]

    manufacturer: EnumProperty(
        name = "Manufacturer",
        description = "Manufacturer",
        items = manufacturer_list
        )
        
    drum_class: EnumProperty(
        name = "Drum Class",
        description = "Drum Class",
        items = [('Kick','Kick','Kick'),
                ('Snare','Snare','Snare'),
                ('Tom','Tom','Tom')
                ],
        options = {'ENUM_FLAG'},
        )

    sub_class: EnumProperty(
        name = "Sub Class",
        description = "Sub Class",
        items = [('None','None','None'),
                ('Rack','Rack','Rack'),
                ('Floor','Floor','Floor')
                ],
        options = {'ENUM_FLAG'}
        )
    
    diameter: IntProperty(
        name = "Diameter",
        description = "Diameter",
        min = 6,
        max = 26,
        default = 12,
        )

    depth: FloatProperty(
        name = "Depth",
        description = "Depth",
        min = 3.5,
        max = 26,
        default = 9.0,
        step = 50
        )

    lug_count: IntProperty(
        name = "Lug Count",
        description = "Lug Count",
        min = 4,
        max = 12,
        default = 8,
        )

class DRUMTOOL_PT_main_panel(Panel):
    bl_label = "DRUM Tools"
    bl_idname = "DRUMTOOL_PT_main_panel"
    bl_space_type = "VIEW_3D"   
    bl_region_type = "UI"
    bl_category = "DRUM Tools"
    bl_context = "objectmode"

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        drumtools = scene.drumtools
        row = layout.row()
        row.prop(drumtools, "manufacturer", text="Manufacturer")
        row = layout.row()
        row.prop(drumtools, "drum_class", text="Class")
        row = layout.row()
        row.prop(drumtools, "sub_class", text="Sub Class")
        row = layout.row()
        row.prop(drumtools, "diameter", text="Diameter")
        row = layout.row()
        row.prop(drumtools, "depth", text="Depth")
        row = layout.row()
        row.prop(drumtools, "lug_count", text="Lug Count")
        row = layout.row()
        row.operator('drumtool.build', text="Build")

class DRUMTOOL_OT_build_drum(Operator):
    bl_label = "DRUM Tools"
    bl_idname = "drumtool.build"
    bl_description = "Build Drum"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        drum_class = str(C.scene.drumtools.drum_class).strip("{}'")
        sub_class = str(C.scene.drumtools.sub_class).strip("{}'")
        manufacturer = str(C.scene.drumtools.manufacturer)
        diameter = int(C.scene.drumtools.diameter)
        depth = float(C.scene.drumtools.depth)
        lug_count = int(C.scene.drumtools.lug_count)
        print(diameter)
        print(depth)
        drum_name = "Drum Name"
        drum = functions.DrumBuilder(diameter=diameter, depth=depth,
                        drum_class=drum_class, drum_subclass=sub_class,
                        lug_count=lug_count, manufacturer=manufacturer,
                        drum_name='DW Tom')
        return {'FINISHED'}

classes = (
    DRUMTOOL_OT_build_drum,
    DRUMTOOL_Settings,
    DRUMTOOL_PT_main_panel,
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)
    del bpy.types.Scene.drumtools
    
if __name__ == "__main__":
    register()

What this does is call an external build function, and constructs a model from a template. I basically want to turn this into something that can make multiple models at once, and position them in the scene.

Not sure if that helps, or just adds noise to the signal!

I have two thoughts, here:

  1. Don’t try to optimize until the code actually works! Solve one problem at a time! and
  2. “Without any shared property objects/classes or operator objects/classes.” – does this mean you don’t want to do any inheritance? Is there a reason for that? In my addon, I have an operator base-class with some rather long and annoying enums that I used to copy-and-paste, now I just inherit them. The operator itself isn’t available anywhere in the UI and it doesn’t do anything… which reminds that I haven’t completely finished all the de-duplication!

I guess I just really don’t understand why you’re doing things with a temporary list object, especially if you’re trying to make it a custom property of bpy.types.Scene (which is just not correct – now that I look at it again, it isn’t even an actual Scene object you’re doing this to, it’s the Scene class. That’s not right!). I think you should keep the temporary object temporary, that is, within the Python script. You might try adding the temporary stuff to the if __name__ = "__main__" section.

And again… why a PointerProperty?

Thanks for the additional code, maybe I’ll have time to read through it later. It’s possible that it will answer some of my questions, either now or later on :smiley:

1 Like

I’m building drum models from template models in other files. So each drum needs it’s own drum class (kick, snare, tom, etc) and sub-class (if it’s a tom then it could be rack or floor tom for example).

Then they each need their own diameter, depth, and other assorted properties too.

Basically I’m at the point where I can make a single drum, but now I want to make a “drum kit” - a collection of drums, at different positions in the scene. So I’m just trying to make the panels to create each element of the drumkit.

Does that make sense?

I guess I just really don’t understand why you’re doing things with a temporary list object, especially if you’re trying to make it a custom property of bpy.types.Scene (which is just not correct – now that I look at it again, it isn’t even an actual Scene object you’re doing this to, it’s the Scene class . That’s not right!).

Yeah so I was trying to do something like this:

bpy.types.Scene.drum1properties
bpy.types.Scene.drum2properties
bpy.types.Scene.drum3properties
bpy.types.Scene.drum4properties
bpy.types.Scene.drum5properties
etc.

Yeah, it does. I wonder if you can’t use a single model or a generator method and just load all the information into one re-usable function? But I suppose that’s an implementation detail.

If you want to add properties to the scene, I think you have to add them to the actual scene object, bpy.data.scenes["MyScene"] or maybe bpy.context.scene or whatever.

Longwinded:

For my addon, I solve this by two methods: First, with simple class inheritance (for bpy.props properties exclusively, so far), and second- I structure my addon as several different Python modules, one for each sort of function. There’s a file for operators and for the functions used by the operators. It’s a rigging addon, so I’m dealing with Bones, PoseBones, and EditBones, I also have functions for Mesh and Curve objects, lots of code for parsing names, some code for general functions, code for traversing rig hierarchies, preference code, miscelaneous operators… it’s a lot of stuff! I avoid the mess by organizing this way:

__init__.py finds the classes it needs in the ops_ files and registers them.
Each “variety of thing” has its own ops_ file,e.g. ops_editbone, ops_posebone, etc.
Each collection of related functions is in an f_ file, so f_editbone, f_posebone etc.
All of my operators are composed of structural code (if/else/for/etc.) and function calls, none of the execution code is in the execute function, it just calls functions from the correct file.

The benefit for me, of doing things this way, is that I have access to all of the functions of all of my operators, so I can use them internally without calling the operator, and I can have access to any of the pieces by simply calling from . import f_general or similar. This makes it really easy to build new operators up from the existing operators I’ve built.

What I’m trying to communicate here is this: you may be able to break this all down into generic functions that you can re-use and simply compose your operators from a bunch of re-usable functions, instead of trying to do OOP for everything.

Finally, I don’t know why I haven’t reccomended this yet, but-- GO AND READ OTHER PEOPLE’S CODE! I think the archimesh addon does something exactly like what you want to do here… you can learn a lot this way!

Cool, I’ll have a look through the archimesh addon.

BTW - here is where I got the pointerproperty thing from: https://blender.stackexchange.com/questions/57306/how-to-create-a-custom-ui

Hey @Josephbburg

So I went back to the API and poured over the overview page. I think I’ve worked something out that will be useful for me. It’s regarding PointerProperty and sub-propertygroups. Here is some test code.

import bpy
from bpy.types import PropertyGroup
from bpy.props import *
from bpy.utils import register_class, unregister_class


C = bpy.context
D = bpy.data
O = bpy.ops
P = bpy.props
T = bpy.types
U = bpy.utils

class DrumProperties(PropertyGroup):
    diameter: P.IntProperty(name='Diameter',
                            description='Drum Diameter',
                            min=1,
                            max=50,
                            default=12)
                            
    depth: P.FloatProperty(name = "Depth",
                            description = "Depth",
                            min = 3.5,
                            max = 26,
                            default = 9.0,
                            step = 50)

class KitProperties(PropertyGroup):
    kick: P.PointerProperty(type=DrumProperties)
    snare: P.PointerProperty(type=DrumProperties)
    hightom1: P.PointerProperty(type=DrumProperties)
    hightom2: P.PointerProperty(type=DrumProperties)
    midtom1: P.PointerProperty(type=DrumProperties)
    midtom2: P.PointerProperty(type=DrumProperties)
    floortom1: P.PointerProperty(type=DrumProperties)
    floortom2: P.PointerProperty(type=DrumProperties)

def register():
    register_class(DrumProperties)
    register_class(KitProperties)
    bpy.types.Scene.drumkitproperties =  P.PointerProperty(type=KitProperties)

def unregister():
    del bpy.types.Scene.bfdkitproperties
    unregister_class(DrumProperties)
    unregister_class(KitProperties)
    
if __name__ == "__main__":
    register()
    print(bpy.data.scenes[0].drumkitproperties.kick.depth)
    bpy.data.scenes[0].drumkitproperties.kick.depth = 15
    print(bpy.data.scenes[0].drumkitproperties.kick.depth)
    print(bpy.data.scenes[0].drumkitproperties.snare.depth)
    
    
# A valid data path would be bpy.data.scenes[0].drumkitproperties.kick.depth

So I’ve got a class for drum properties and a class for a drumkit. The drumkit assigns a unique instance That code gives me access to unique instances of each drums diameter and depth property.

I can access the data by doing: bpy.data.scenes[0].drumkitproperties.kick.depth
I can update each drum uniquely without affecting the others.

This is the sort of thing I was trying to do in terms of just gathering parameter values to pipe into my build operator. But as you say, it does seem quite unreliable.

Hey, whatever works! I’m still confused about why you’re using PointerProperty, though.

Does adding a Python property to the scene class persist when you save/load and enable/disable the addon?

Mainly because it’s all I’ve seen other people do. How would you approach this sort of thing?

I don’t mean to be difficult, it’s just that the official docs even suggest using a PointerProperty when using property groups: https://docs.blender.org/api/current/bpy.props.html

I guess I don’t understand how to assign a property group without using a propertypointer.

That’s good enough for me. I just thought the right way to do it was with a collection property.