# What's the correct way of implementing dynamic dropdown menus in Python?

First time bpy user here.. working on a small automated pipeline. Most of it is already in place, including a database and a PHP framework that provides data for use by the Python code below.

My intent in this particular part is to download JSON lists of projects, asset types, assets, tasks and task statuses, and to load them into dropdown lists inside Blender. Selecting the appropriate values in these dropdowns then saves the file into the correct directory.

The code I'm posting here is a mixture of a variety of answers to questions much like this one. However, none of it seems to work as my dropdown lists simply don't show up. There are no errors, but also no dropdowns. From what I've seen, there seem to be quite a few ways to do this, but I can't figure out what exactly I'm doing wrong or what is the generally accepted way of accomplishing this.

# Imports
import bpy, requests
from bpy.types import Panel, Operator
from bpy.props import EnumProperty

base_url = "http://my.fancy.base.url/"

bl_info = {
"name": "Pipeline",
"description": "Asset management for Blender",
"author": "Sam",
"version": (1, 0),
"blender": (2, 75, 0),
"location": "File Browser",
"category": "System"
}

class FILEBROWSER_PT_Pipeline(Panel):

bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOLS'
bl_label = "Asset management"
bl_category = "Pipeline tools"

def draw(self, context):
l = self.layout
# Draw project dropdown
r = l.row(align = True)
r.prop(context.scene, "PL_Project")
# Draw type dropdown
r = l.row(align = True)
r.prop(context.scene, "PL_Type")
# Draw asset dropdown
r = l.row(align = True)
r.prop(context.scene, "PL_Asset")
r = l.row(align = True)
# Draw status dropdown
r = l.row(align = True)
r.prop(context.scene, "PL_Status")

# Callbacks for dropdown changes

def change_project_list(self, context):
return [(p['p_project'], p['project_reference'], p['project_name']) for p in bpy.data.scenes["Scene"].PL_Project_List]

def change_type_list(self, context):
return [(ty['p_type'], ty['type_reference'], ty['type_name']) for ty in bpy.data.scenes["Scene"].PL_Type_List]

def change_asset_list(self, context):
return [(a['p_asset'], a['asset_reference'], a['asset_name']) for a in bpy.data.scenes["Scene"].PL_Asset_List]

def change_status_list(self, context):
return [(s['p_status'], s['status_reference'], s['status_name']) for s in bpy.data.scenes["Scene"].PL_Status_List]

# Registration

def register():
bpy.utils.register_module(__name__)

def unregister():
bpy.utils.unregister_module(__name__)

# Let's go

if __name__ == "__main__":
register()
bpy.data.scenes["Scene"].PL_Project_List = requests.get(base_url + "tool/get-projects").json()
bpy.data.scenes["Scene"].PL_Type_List = requests.get(base_url + "tool/get-types/" + bpy.data.scenes["Scene"].PL_Project).json()
bpy.data.scenes["Scene"].PL_Asset_List = requests.get(base_url + "tool/get-assets/" + bpy.data.scenes["Scene"].PL_Project + "/" + bpy.types.Scene.PL_Type).json()
bpy.data.scenes["Scene"].PL_Status_List = requests.get(base_url + "tool/get-statuses").json()
# Create dropdowns with update callbacks
bpy.types.Scene.PL_Project = EnumProperty(items = change_project_list, name = "Project")
bpy.types.Scene.PL_Type = EnumProperty(items = change_type_list, name = "Type")
bpy.types.Scene.PL_Asset = EnumProperty(items = change_asset_list, name = "Asset")
bpy.types.Scene.PL_Status = EnumProperty(items = change_status_list, name = "Status")

• If this was a proper addon register() would get called before blender's bpy.data is fully ready. you might get a _RestrictData error in that case. -- but as you are running this from TextEditor and bpy.data is already present, something else is going on. I'd do a print statement to see the value of .PL_Status_List at various points , it might not be filled with what you think.. – zeffii Aug 7 '15 at 6:57
• That's an interesting answer, as I did get a _RestrictData error until I put all the downloading and list generating code in the if block behind the register() function. I'm not running this from the text editor, I reinstall the script every time I want to test it. But I tried it, and the Text Editor says "Python script fail, look in the console for now..." - the console doesn't say anything, but when I try to create PL_Project_List myself there, all I get is "'Scene' object has no attribute 'PL_Project_List'" .. – sam_vh Aug 7 '15 at 7:19
• – p2or Aug 7 '15 at 7:42

The two main problems with your script are

• you are assigning to bpy.data.* in the register function, that won't be allowed when the add-on's register() is called on startup

• the content of bpy.data.scenes["Scene"].PL_Project_List isn't what you expect. See the python console.. (if you can't assign something via console, it's not likely to work in a script)

code:

>>> bpy.data.scenes["Scene"].PL_Project_List = 'yello'
Traceback (most recent call last):
File "<blender_console>", line 1, in <module>
AttributeError: 'Scene' object has no attribute 'PL_Project_List'

# this would work (called an ID property) but it has a limited
# range of acceptable data types (it must be stringifyable)
# _and_ you still can't do this in register + startup
>>> bpy.data.scenes["Scene"]['PL_Project_List'] = 'yello'
>>> bpy.data.scenes["Scene"]['PL_Project_List']
'yello'


RNA props vs ID props

I've reduced this to what I think is the core of the problem. If it doesn't work for one, it won't work for any. There's nothing stopping you from using Python dicts as Cache objects.

# Imports
import bpy
import requests
from bpy.types import Panel, Operator
from bpy.props import EnumProperty

def fake_request():
return [{'p_project': str(i), 'project_reference': str(i**2), 'project_name': str((i**2 / 2))} for i in range(3)]

# think of it as a cache dict / list
PL_Project_List = {}

class FILEBROWSER_PT_Pipeline(Panel):

bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOLS'
bl_label = "Asset management"
bl_category = "Pipeline tools"

def draw(self, context):
l = self.layout
r = l.row(align=True)
r.prop(context.scene, "PL_Project")

def change_project_list(self, context):
return [(p['p_project'], p['project_reference'], p['project_name']) for p in PL_Project_List['projects']]

def register():
PL_Project_List['projects'] = fake_request()
bpy.types.Scene.PL_Project = EnumProperty(items=change_project_list, name="Project")
bpy.utils.register_module(__name__)

def unregister():
bpy.utils.unregister_module(__name__)
del bpy.types.Scene.PL_Project

if __name__ == "__main__":
register()


This part is only really useful for the development stage, and running the addon from TextEditor after hitting F8 a few times to clear the Python added stuff.

if __name__ == "__main__":
register()
...


If it's running as an addon, but you aren't testing from TextEditor it's still not a good idea to rely on this construct of if __name__ == "__main__" to populate the data structures.

### But how do I populate my lists?

• Either have a button on the layout to 'populate lists', or 'update lists' (people like updates!)
• Or populate an object like a Dictionary as a cache object (shown above, this PL_Project_List is a dict, so maybe that's a bad name.. I'd settle for PL_Project_Cache -- this object is local to this file only, unless the file is imported by some other file, it's not strictly a 'global' )
• Or investigate how to store the info in a CollectionProperty / PropertyGroup ( good reference is here : How can I store and retrieve a custom list in a blend file? )
• Or if you are comfortable stringifying your incoming data (i expect the database information to be lightweight.. not 100mb of lists..)

code:

>>> bpy.types.Scene.PL_Project_List = bpy.props.StringProperty()
>>> bpy.types.Scene.PL_Project_List = 'yello'
>>> bpy.types.Scene.PL_Project_List
'yello'
`
• This is looking promising, thanks very much. When I get home tonight I'm definitely trying this out. – sam_vh Aug 7 '15 at 7:28
• have updated for completeness, i'm almost certain people will have additional views on the matter. – zeffii Aug 7 '15 at 7:34
• this is working like a charm, thanks very much! looking into that update button thingy for populating my data now, thanks again! – sam_vh Aug 8 '15 at 4:17