19
$\begingroup$

I am trying to create a script that will allow me to generate a list and store it in the blend file. The list starts out blank and then strings get added to the list, resulting in:

["test1","test2","test3"]

I create a PropertyGroup Class for the enum, define a new scene property as a CollectionProperty type with access to that enum Class, before appending a new item into the enum:

import bpy

class item_set(bpy.types.PropertyGroup):
    myItems = []
    enums = bpy.props.EnumProperty(name="My Items", items=myItems)
bpy.utils.register_class(item_set)

bpy.types.Scene.myItemSet = bpy.props.CollectionProperty(type=item_set)    

newItem = bpy.context.scene.myItemSet.add()
newItem.myItems.append("test")

I need to be able to add items to the list, save the blend file, re-open the file, run the script again and retrieve the list from the scene.

However, the documentation states:

One thing to keep in mind is that after saving and reloading your blend-file, your new property will have disappeared...

...To make the values you assigned in a previous session available again, you need to redefine the property. So after reopening the blend-file you need to rerun:

bpy.types.Object.myProperty = bpy.props.StringProperty()

I can do the appending into and retrieval of the property values absolutely fine upon an initial run, but as the documentation states, the property is unavailable when trying to view it's contents on a reload of the file (after having run the script, saved and re-opened the file), but I am unsure how to redefine it without clearing all it's values.

If I redefined the scene property, it would presumably also need the enum Class to be available, which would currently set myItems back to [] (empty).

Do I need to change the way I am initially setting the items in the class or am I on entirely the wrong track with all of this? All I need to do is store a list in the blend file, so if there is a better way please let me know.

Thanks.

(I would hope, eventually, to be able to store sub lists [["a","b"],["c","d"],["e","f"]], I'm just trying to get normal lists to work first.)

$\endgroup$
2
  • $\begingroup$ Can you elaborate what Items you want to add or what the script should do, just for testing purposes and to make sure I understand it correctly... ? $\endgroup$
    – p2or
    Feb 5, 2015 at 15:14
  • $\begingroup$ I'll simply be adding small strings like the example shows to result in ["test1","test2","test3"]. And hopefully, if I got that working, I would then be able to store sub lists ie. [["test1","test2"],["test1","test2"],["test1","test2"]] $\endgroup$ Feb 5, 2015 at 15:57

4 Answers 4

17
$\begingroup$

Note: The question I originally asked (and answered) was for 2.79. I have now updated this code to work with 2.8+. For 2.79, when defining properties on a class, you use = instead of :, so the custom string line would be customString = bpy.props.StringProperty() for 2.79.


Part of the confusion with my question came from the fact I presumed that defining an EnumProperty was the correct, and only, way to store a list.

What I didn't quite understand was that the CollectionProperty itself would act like a list. I can have much the same code as I had before, minus the enum:

import bpy

class customPropertiesGroup(bpy.types.PropertyGroup):
    customString : bpy.props.StringProperty()

bpy.utils.register_class(customPropertiesGroup)

bpy.types.Scene.customLists = bpy.props.CollectionProperty(type=customPropertiesGroup)

And every time I want to add a new item I do:

newCustomItem = bpy.context.scene.customLists.add()
newCustomItem.customString = "test1"
newCustomItem = bpy.context.scene.customLists.add()
newCustomItem.customString = "test2"
etc..

I can then access each custom item with:

bpy.context.scene.customLists[0].customString
bpy.context.scene.customLists[1].customString

If I want nested lists I can simply have the PropertyGroup Class' property as another CollectionProperty:

import bpy

class subcustomPropertiesGroup(bpy.types.PropertyGroup):
    customString : bpy.props.StringProperty()

bpy.utils.register_class(subcustomPropertiesGroup)

class customPropertiesGroup(bpy.types.PropertyGroup):
    subLists : bpy.props.CollectionProperty(type=subcustomPropertiesGroup)    

bpy.utils.register_class(customPropertiesGroup)

bpy.types.Scene.customLists = bpy.props.CollectionProperty(type=customPropertiesGroup)

newcustomList = bpy.context.scene.customLists.add()
newcustomList.name= "List 1"
newSubList = bpy.context.scene.customLists["List 1"].subLists.add()
newSubList.name = "Sub List 1"
newSubList.customString = "String 1"

This allows me to have an arrangement where I can access lists and sublists like so:

bpy.context.scene.customLists['List 1'].subList['Sub List 1'].customString

I can add or remove items (with customLists.remove(index)) and print them out like I would a normal python list:

for customList in bpy.context.scene.customLists:

    print("List: "+customList.name)

    for subList in customList.subLists:
    
        print("Sub List: "+subList.name)
        print("Custom property: "+subList.customString)

And because I am not clearing the list each time I define it (as I was in the question) the properties become available again after reloading the file and re-running the code.

$\endgroup$
10
$\begingroup$

I had trouble getting the other answer for the nested lists to work without the complete code. Here is an example of the answer that should work directly pasted into the text editor:

import bpy

class subcustomPropertiesGroup(bpy.types.PropertyGroup):
    customString = bpy.props.StringProperty()
bpy.utils.register_class(subcustomPropertiesGroup)

class customPropertiesGroup(bpy.types.PropertyGroup):
    subLists = bpy.props.CollectionProperty(type=subcustomPropertiesGroup)    
bpy.utils.register_class(customPropertiesGroup)

bpy.types.Scene.customLists = bpy.props.CollectionProperty(type=customPropertiesGroup)
bpy.types.Scene.subLists = bpy.props.CollectionProperty(type=subcustomPropertiesGroup)

bpy.context.scene.customLists.clear()
bpy.context.scene.subLists.clear()

bpy.context.scene.customLists.add().name = 'list1'
bpy.context.scene.customLists.add().name = 'list2'

newCustomItem = bpy.context.scene.customLists['list1'].subLists.add()
newCustomItem.customString = "test1"
newCustomItem = bpy.context.scene.customLists['list1'].subLists.add()
newCustomItem.customString = "test2"
newCustomItem = bpy.context.scene.customLists['list2'].subLists.add()
newCustomItem.customString = "test3"
newCustomItem = bpy.context.scene.customLists['list2'].subLists.add()
newCustomItem.customString = "test4"

print('_______________')
for customList in bpy.context.scene.customLists:

    print("List: "+customList.name)

    for subList in customList.subLists:

        print("Property: "+subList.customString)

Hope this helps other beginners.

$\endgroup$
1
  • $\begingroup$ This no longer works. I get line 20, in <module> AttributeError: '_PropertyDeferred' object has no attribute 'add' Error: Python script failed, check the message in the system console $\endgroup$ Jul 30, 2021 at 21:43
3
$\begingroup$

Both methods below only work on dicts that can be stringified, so they can't contain Objects.

json

A general Python solution to this involving JSON writing and reading. JSON can easily be constructed from a dictionary and written to bpy.data.texts as a string.

Imagine you have some dict of information, call it my_dict. To write that dict as a JSON to the .blend file you do:

import bpy
import json

my_dict = {
  'key1': 'some string storage',
  'key2': ['some', 'list', 'storage'],
  'key3': 5
}

# dump string
m = json.dumps(my_dict, sort_keys=True, indent=2)
text_block = bpy.data.texts.new('my_storage.json')
text_block.from_string(m)

bpy.data.texts['my_storage.json'] would then contain:

{
  "key1": "some string storage",
  "key2": [
    "some",
    "list",
    "storage"
  ],
  "key3": 5
}

to read this back in at a later stage:

import bpy
import json

text_obj = bpy.data.texts['my_storage.json']
text_str = text_obj.as_string()

my_json = json.loads(text_str)
print(my_json['key1'])

dict and ast.literal_eval

import bpy
import ast

mydict = dict(deb=["two", "three"], far="booo", foo=34)

# write the string representation of the dict to a text datablock
# this assumes you know how to create text datablocks, it is shown in the other method anyway.
d = bpy.data.texts['dict_storage']
d.from_string(str(mydict))

# read from the datablock
d = bpy.data.texts['dict_storage']
stringified_dict = d.as_string()
my_read_dict = ast.literal_eval(stringified_dict)

for k, v in my_read_dict.items():
    print(k, v)
$\endgroup$
3
$\begingroup$

There have much more better solution, if you want use you own property and make it store to blender file every time even "Undo" and "Redo", you can use Custom Properties which is bpy.context.object["YouOwnProperty"] = {}

if you use you own CollectionProperty like what you create, you will find out every time you "Undo" or "Redo",the Property you create will not change to the value in the corresponding time

$\endgroup$
2
  • $\begingroup$ I don't think this works for saving and reloading $\endgroup$ Jan 7, 2020 at 18:34
  • $\begingroup$ This answer is pure gold! Just wanted to add: if one wants to store some data "at blend file level" the best place is probably bpy.context.scene["YourProperty"]. Also it is possible to store whole lists etc. For example: bpy.context.scene["YourProperty"] = [1,2, ..., 1000000] $\endgroup$ May 24, 2020 at 19:26

You must log in to answer this question.

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