Get which enum property was updated in a PropertyGroup

Hey all, I have a dynamic property group here that gets built. Works just fine however i want to identify which item has been clicked when update function is called.

# update function
def update(instance, context):
    print('how do i know which enum property was selected?..')
# registering elsewhere in code...
        for key, item_values in data.items():
            items = []
            for each in item_values:
                items.append((each, each, ''))
            annotations[key] = bpy.props.EnumProperty(
                name=key,
                items=items,
                update=update
            )
        MyProps= type(
            # Class name
            "MyProps",
            # Base class
            (bpy.types.PropertyGroup,),
            {"__annotations__": annotations},
        )
        bpy.utils.register_class(MyProps)
        bpy.types.Scene.my_props= bpy.props.PointerProperty(type=MyProps)`

I thought turning my update property into a lambda might work

name=key,
items=items,
update=lambda inst, ctx: update(inst, ctx, key)

But unfortunately the last key in the loop is the one passed. I’ve currently got a hacky workaround which is i run through every key in the PropertyGroup and update accordingly everytime update def is called.

You can use the get/set function argument, when you declare bpy.props.PointerProperty
https://docs.blender.org/api/current/bpy.props.html#getter-setter-example

1 Like

This is a very common problem with such tasks of automation using python lambdas (common in other languages, too). The thing is, the parameters of the lambda are evaluated at runtime, not when the lambda is assigned.

This prevents among other things the update from firing when the item registers.

But at runtime the variable key can only hold one value, the last one it has been assigned.

Usually you would solve it by using an initialized argument (I don’t know the exact term, you can look it up if interested)

update = lambda self, context, k=key: update(self, context, k),

But alas, Blender’s python API will not allow registering the property if the update function signature does not contain exactly two arguments : self and context (you can name them otherwise but they must be exactly two). So this is not a solution. See the docs : https://docs.blender.org/api/current/bpy.props.html#bpy.props.EnumProperty

You can however use another less common method which allows passing arbitrary data to a layout element.

bpy.types.UILayout.context_pointer_set

See What is context_pointer_set? - #6 by 1_conscience_0_dimen and https://blender.stackexchange.com/questions/203442/how-to-pass-a-bpy-data-objects-bpy-data-materials-etc-to-an-operator-from-th/203443#203443 for how-to use it.

In your case, here’s a stripped down example.

import bpy

def update(instance, context):
    print(context.enum)


class VIEW3D_PT_Test(bpy.types.Panel):
    bl_label ="Test Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        for key in ("a", "b", "c"):
            row = self.layout.row()
            row.context_pointer_set("enum", context.scene.bl_rna.properties[f"my_enum_{key}"])
            row.prop(context.scene, f"my_enum_{key}")


def register():
    for key in ("a", "b", "c"):
        setattr(
            bpy.types.Scene, 
            f"my_enum_{key}",
            bpy.props.EnumProperty(
                items=(
                    ("1",) * 3,
                    ("2",) * 3,
                    ("3",) * 3,
                ),
                update=update,
            )
        )
    bpy.utils.register_class(VIEW3D_PT_Test)


if __name__ == "__main__":
    register()

Result :

Animation3

3 Likes

Hello, stumbled across your thread while looking for something else, tho a little old, can offer another method to accompany @Gorgious for others.

Using Gorgious’s example:
change the update method to

def update(info_passed):
    def inner(self, context):
        print(info_passed)
        another_method_if_you_want(info_passed)
    return inner

and the EnumProperty to…
update=update(key),
or
update=update('info to pass - ' + key),

then remove the pointer from the ‘draw’ method.

in full:

import bpy

def update(info_passed):
    def inner(self, context):
        print(info_passed)
    return inner


class VIEW3D_PT_Test(bpy.types.Panel):
    bl_label ="Test Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        for key in ("a", "b", "c"):
            row = self.layout.row()
            row.prop(context.scene, f"my_enum_{key}")


def register():
    for key in ("a", "b", "c"):
        setattr(
            bpy.types.Scene, 
            f"my_enum_{key}",
            bpy.props.EnumProperty(
                items=(
                    ("1",) * 3,
                    ("2",) * 3,
                    ("3",) * 3,
                ),
                update=update(key),
            )
        )
    bpy.utils.register_class(VIEW3D_PT_Test)


if __name__ == "__main__":
    register()

With that you only get what you pass to the update() method, not the whole pointer (for good or bad).

1 Like