I'm trying to add buttons to install/uninstall various dependencies for my add-on.
I don't want to make a new operator for each button, so I need a way to pass in the dependency.
Looking at How display and use operator properties in a Python Blender UI panel? and How to pass multiple operator properties via UI layout? the answer seems to be adding a variable to the class that extends bpy.types.operator and then calling my_op = layout.operator(my_op.bl_idname) and my_op.my_variable = 'something cool', but that's not working for me.
I'm getting AttributeError: 'CYANIC_OT_uninstall_single_dependency' object has no attribute 'dependency' when I try to run this code, and I'm not seeing any reason for it. I'm developing on Blender 4.1, but I doubt that would break this. I'm at a loss at what to try next to achieve this result.
(This isn't the entire code, just trying to include the relevant parts)
import bpy
from collections import namedtuple
Dependency = namedtuple("Dependency", ["module", "package", "name"])
dependencies = (
Dependency(module="numpy", package="numpy==1.26.4", name='np'), # numpy 2.0 had just released, and wasn't compatible with mediapipe yet
Dependency(module="skimage", package="scikit-image", name=None),
Dependency(module="cv2", package="opencv-python", name=None),
Dependency(module="mediapipe", package=None, name=None),
)
class CYANIC_OT_install_single_dependency(bpy.types.Operator):
bl_idname = "cyanic.install_single_dependency"
bl_label = "Install"
bl_description = "Install a single dependency"
bl_options = {"REGISTER", "INTERNAL"} # IDK if this is needed or not
dependency = None
def execute(self, context):
if self.dependency is None:
return {'CANCELLED'}
try:
install_and_import_module(module_name=self.dependency.module,
package_name=self.dependency.package,
global_name=self.dependency.name)
except (subprocess.CalledProcessError, ImportError) as err:
self.report({'ERROR'}, str(err))
return {"CANCELLED"}
return {'FINISHED'}
class CYANIC_OT_uninstall_single_dependency(bpy.types.Operator):
bl_idname = "cyanic.uninstall_single_dependency"
bl_label = "Uninstall"
bl_description = "Uninstall a single dependency"
bl_options = {"REGISTER", "INTERNAL"} # IDK if this is needed or not
dependency = None
def execute(self, context):
if self.dependency is None:
return {'CANCELLED'}
try:
uninstall_module(module_name=self.dependency.module,
package_name=self.dependency.package)
except (subprocess.CalledProcessError, ImportError) as err:
self.report({'ERROR'}, str(err))
return {"CANCELLED"}
return {'FINISHED'}
class CYANIC_preferences(bpy.types.AddonPreferences):
# The preferences in Preferences > Add-ons > Cyanic Toolbox
bl_idname = __name__
header_names = ["Module", "Version", ""]
def draw_dependency(self, dependency, dependency_box):
# module name, version, install/remove button
# Check if installed
installed = False
global_name = dependency.module
if dependency.name is not None:
global_name = dependency.name
if global_name in globals():
installed = True
_d_box = dependency_box.box()
box_split = _d_box.split()
cols = [box_split.column(align=False) for _ in range(len(CYANIC_preferences.header_names))]
# Module Name
cols[0].label(text=dependency.module)
# Version
if installed:
cols[1].label(text=globals()[global_name].__version__)
else:
cols[1].label(text='Not Installed')
#-----
# This is where the error is being triggered
# Install/Remove button
if installed:
operator = cols[-1].operator(CYANIC_OT_uninstall_single_dependency.bl_idname)
operator.dependency = dependency
else:
operator = cols[-1].operator(CYANIC_OT_install_single_dependency.bl_idname)
operator.dependency = dependency
#-----
def draw(self, context):
layout = self.layout
layout.operator(CYANIC_OT_install_dependencies.bl_idname, icon="CONSOLE")
# dependency box
dependency_box = layout.box()
dependency_box.label(text="Add-on Dependencies")
headers = dependency_box.split()
for name in CYANIC_preferences.header_names:
col = headers.column()
col.label(text=name)
for dependency in dependencies:
self.draw_dependency(dependency, dependency_box)

attr: Propertynotattr = Property. You should also be using a built-in EnumProperty not a namedtuple (they're basically the same, the latter is just designed for Blender operators). Give this page a good lookthrough and I think you'll find what you need. $\endgroup$module_name : bpy.props.StringProperty('')looks like it's working for me. I had tried using StringProperty before, but I think I used=instead of:. I'm still struggling to see how I'd convert the namedtuple to an EnumProperty. It looks to me like an EnumProperty can only have anidentifier,name,description,icon, andnumber- not custom values like 3 different strings. $\endgroup$