Can bpy.props be used for dynamic lists?

More details can be found on my blender.stackexchange post, but basically I’m trying initialize an empty, globally available list that I can later populate with object.location tuples. Anyone handle this kind of thing before?

Pseudocode

# __init__.py
bpy.types.Scene.prop_name = bpy.props.list(
    items = []
)

# some_operator.py
bpy.types.Scene.prop_name.append(obj.location)

I think the usual way of doing things is to make a bpy.props.PropertyGroup that belongs to the scene. You can use PointerProperty to create links to the previous/next object. Take a look at this: https://blender.stackexchange.com/questions/6975/is-it-possible-to-use-bpy-props-pointerproperty-to-store-a-pointer-to-an-object

1 Like

For such lists, I would suggest using an EnumProperty. If you want you can make the items parameter a function, in which case you can dynamically return items to your heart’s content. The items can’t contain arbitrary Python objects, though, so you’ll have to make your own mapping from the value in the EnumProperty to the object locations.

PS: Please don’t use ‘here’ as a link text, as it makes it hard for people to understand what the link is about.

2 Likes

Here is my solution in agreement with @sybren :

In the register() function:

Scene.pdt_lib_objects = EnumProperty(
    items=enumlist_objects, name="Objects", description=PDT_DES_LIBOBS
)

Populating function:

        def enumlist_objects(self, context):
            """Populate Objects List from Parts Library.

            Creates list of objects that optionally have search string contained in them
            to populate variable pdt_lib_objects enumerator.

            Args:
                context: Current Blender bpy.context

            Returns:
                list of Object Names.
            """

            scene = context.scene
            path =  os.path.join(bpy.utils.user_resource("SCRIPTS", "addons"), "clockworxpdt", "parts_library.blend")
            with bpy.data.libraries.load(path) as (data_from, data_to):
                if len(scene.pdt_obsearch) == 0:
                    object_names = [ob for ob in data_from.objects]
                else:
                    object_names = [ob for ob in data_from.objects if scene.pdt_obsearch in ob]
            items = []
            for ob in object_names:
                items.append((ob, ob, ""))
            return items

Cheers, Clock.

1 Like

This code has the potential of crashing Blender:

items = []
for ob in object_names:
    items.append((ob, ob, ""))
return items

As per the warning in the documentation, you have to keep a reference to the returned strings from within Python itself. Making items a module-global variable would help in this regard (and of course it would require a rename to make sense then).

About naming variables, I would always keep ob or obj to refer to actual objects. Just use for object_name in object_names instead, that avoids confusion.

3 Likes

Oops, sorry, I did not know that, is this better:

        global pdt_obj_items
        pdt_obj_items = []
        for object_name in object_names:
            pdt_obj_items.append((object_name, object_name, ""))
        return pdt_obj_items

Or have I not understood you properly?

I think @sybren meant something more like

some_module.py

list_of_items = []

some_operator.py

from .some_module import list_of_items
for object_name in list_of_items:
    list_of_items.append((object_name, object_name, ""))
return list_of_items

Hmm, I have an issue here, because this is in the __init__.py file of an add-on, so the enumerator is declared in the register() function of the __init__.py file and is populated in a separate function in the __init__.py file, that reads the object library file. So I am not sure where I can declare the items list in this case? I am confused, because what I did definitely works and what I did before has not crashed Blender yet, but I accept it might…

Should I just have a separate items_list.py file with the three of these I require and then import them into the __init__.py file?

Sorry to seem thick, but this is a bit new to me…

EDIT:

If it helps here is the declaration of the EnumProperty:

    Scene.pdt_lib_objects = EnumProperty(
        items=enumlist_objects, name="Objects", description=PDT_DES_LIBOBS
    )

And here is the menu in the UI:

54

You certainly don’t sound thick to me. I’ve probably misunderstood @sybren. Let’s see what he has to say

1 Like

A module-global name is just one that’s declared at the module level (so not inside a function). Doing that in one file or another file (like the proposed some_module.py) has no effect on this.

The global keyword is only necessary inside a function, if you want to assign something. Python is quite a simple language; if a function contains any line like x = Y, it assumes the name x is local to that function. If you wanted to assign to the global x, you have to use the global keyword. If you look at this from the other side: if you can avoid the assignment, you can also remove the global.

This would work:

_enum_items = []

def enum_items(self, context):
    _enum_items.clear()
    for ob in object_names:
        _enum_items.append((ob, ob, ""))
    return _enum_items

Because there is no assignment to _enum_items inside the function, it’s clear to Python what you mean, and you don’t need global.

2 Likes

Thank you so much for your help here, now I understand! I am very new to making add-ons and new to Python, it is so different to the languages I used in my career all those years ago…

Here is my revised code showing just one of the three functions to populate the three EnumProperties:

# Declare enum items variables
#
_pdt_obj_items = []
_pdt_col_items = []
_pdt_mat_items = []


def enumlist_objects(self, context):
    """Populate Objects List from Parts Library.

    Creates list of objects that optionally have search string contained in them
    to populate variable pdt_lib_objects enumerator.

    Args:
        context: Current Blender bpy.context

    Returns:
        list of Object Names.
    """

    scene = context.scene
    path = os.path.join(str(Path(__file__).parents[0]), "parts_library.blend")
    _pdt_obj_items.clear()

    if Path(path).is_file():
        with bpy.data.libraries.load(path) as (data_from, data_to):
            if len(scene.pdt_obsearch) == 0:
                object_names = [ob for ob in data_from.objects]
            else:
                object_names = [ob for ob in data_from.objects if scene.pdt_obsearch in ob]
        for object_name in object_names:
            _pdt_obj_items.append((object_name, object_name, ""))
    else:
        _pdt_obj_items.append(("MISSING","Library is Missing",""))
    return _pdt_obj_items

Thanks also to @Zollie :grin:

Cheers, Clock.

Ok, that’s overly complex. If you’re using pathlib.Path (which I really can recommend because it makes life easy), you can avoid using os.path. This does pretty much the same:

path = Path(__file__).parent / "parts_library.blend"

The only difference that here path is still a Path object. Probably easier to work with, and you can always convert it to a string with str(path) when you need to pass it to some code that doesn’t understand pathlib.Path objects. However, later you do Path(path) anyway, so having a Path is handy.

1 Like

Thank you once again, I changed it to this:

path = Path(__file__).parent / "parts_library.blend"
    _pdt_obj_items.clear()

    if path.is_file():
        with bpy.data.libraries.load(str(path)) as (data_from, data_to):

So much to learn still…

Cheers, Clock.