4
$\begingroup$

So I was making an addon with a dynamic EnumProperty and apparently ran into a known bug when trying to use Expand = True for this property in the panel. Although everything works fine when not using Expand = True, for my addon, it is essential to have the options expanded and not collapsed in a drop-down menu. The bug can be found in the documentation:

https://docs.blender.org/api/current/bpy.props.html#bpy.props.EnumProperty

enter image description here

This is the most compact example I could think of (add a few objects to the scene to see the bug in effect):

import bpy

def getEnumStuff(self, context):
    return [((obj.name, obj.name, "")) for obj in bpy.context.selectable_objects]

class OBJECT_PT_example_panel(bpy.types.Panel):
    bl_category = "Example"
    bl_label = "Example"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = "objectmode"

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.prop(context.scene, "stuff", expand = True)
        
classes = (
    OBJECT_PT_example_panel,)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.stuff = bpy.props.EnumProperty(
        name = "stuff", 
        items = getEnumStuff,
        description = "")

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()

I tried a lot of things, such as making the list global before returning it from the getEnumStuff function and copying the name strings in order to have them point to a different address than the original object names. Nothing I tried improved the results so far.

The question is, is there even a workaround for this bug? Is there another way to have the options expanded in the panel while still being able to select them (not only printing the name)?

$\endgroup$

2 Answers 2

6
$\begingroup$

The Warning in the documentation states the EnumProperty should have a permanent handle on the items.

There is a known bug with using a callback, Python must keep a reference to the strings returned by the callback or Blender will misbehave or even crash.

One way I see, without resorting to a global variable, is to add an attribute to the getEnumStuff function. See relevant PEP 232.

By the way, if you pass the context in a function, you should not use bpy.context afterwards but rather the context parameter because there could be a mismatch between the two, resulting in an incorrect context error.

Important note :

Enum items can be generated with a python generator using (item1, item2, item3) however this won't be cached as it is generated on the fly every-time. You need to use a python list [item1, item2, item3] to correctly serialize the result.

import bpy


def getEnumStuff(self, context):
    getEnumStuff.enum_stuffs = [((obj.name, obj.name, "")) for obj in context.selectable_objects]
    return getEnumStuff.enum_stuffs
getEnumStuff.enum_stuffs = []


class OBJECT_PT_example_panel(bpy.types.Panel):
    bl_category = "Example"
    bl_label = "Example"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = "objectmode"

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.prop(context.scene, "stuff", expand = True)
        
        
classes = (
    OBJECT_PT_example_panel,)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.stuff = bpy.props.EnumProperty(
        name = "stuff", 
        items = getEnumStuff,
        description = "")


def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()

Result :

enter image description here

$\endgroup$
3
  • $\begingroup$ Note that the line getEnumStuff(context) # Update enum items before drawing them can be avoided, as this kind of "making sure" is error prone. In def getEnumStuff(context): add return getEnumStuff.enum_stuffs and in def register: change the lambda setup to items = lambda self, context: getEnumStuff(context), $\endgroup$
    – taiyo
    Commented Aug 28, 2023 at 10:06
  • $\begingroup$ @taiyo yeah I just realized by re-reading it that it was weirdly coded. Hopefully my python skills have improved a bit in the last two years >< Thank you. $\endgroup$
    – Gorgious
    Commented Aug 28, 2023 at 10:07
  • $\begingroup$ i have just seen you took care of that after i added my comment^^ +1 $\endgroup$
    – taiyo
    Commented Aug 28, 2023 at 10:08
3
$\begingroup$

For an EnumProperty on a bpy.types.Object, I had text corruption with storing the items array in a global. A global string cache worked better for me.

STRING_CACHE = {}
def intern_enum_items(items):
    def intern_string(s):
        if not isinstance(s, str):
            return s
        global STRING_CACHE
        if s not in STRING_CACHE:
            STRING_CACHE[s] = s
        return STRING_CACHE[s]
    return [tuple(intern_string(s) for s in item) for item in items]


def getEnumStuff(self, context):
    # make your items like normal here
    items = ...

    # call intern_enum_items before you return
    return intern_enum_items(items)

The downside is the strings in STRING_CACHE will never be GCed (sort of the opposite of the original problem). For most people though, I doubt the cache will grow large enough for it to become an issue.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .