2
$\begingroup$

I'm trying to add buttons to install/uninstall various dependencies for my add-on.

Preferences Screenshot

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)

$\endgroup$
5
  • 1
    $\begingroup$ You need to use class annotations as attributes e.g. attr: Property not attr = 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$ Commented Jul 24, 2024 at 21:42
  • $\begingroup$ If the operator is going to be used for UI where you set the properties yourself in the code, do you really need to check if it's None?.. I mean checking for all possible errors is good, but trust yourself a little... :D $\endgroup$ Commented Jul 25, 2024 at 5:22
  • $\begingroup$ @MartynasŽiemys Most of the None checks are because not all of the dependencies have a package or global name that's different than the module name - but you're right that I could probably simplify the code a lot more by just typing the module name a few more times when defining the Dependency. $\endgroup$ Commented Jul 25, 2024 at 13:28
  • $\begingroup$ @Jakemoyo Switching to 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 an identifier, name, description, icon, and number - not custom values like 3 different strings. $\endgroup$ Commented Jul 25, 2024 at 13:40
  • $\begingroup$ Yeah, idk maybe I'm confused about how you're using that object in the script. $\endgroup$ Commented Jul 25, 2024 at 15:10

1 Answer 1

1
$\begingroup$

Here is an example:

import bpy


class CyanicDependancy(bpy.types.Operator):
    """Description"""
    bl_idname = "cyanic.install_dependency"
    bl_label = "Install Cyanic Dependancy"
    
    dependancy: bpy.props.StringProperty(
        name="Dependancy",
        description="Install Cyanic Dependancy",
        default="",
    )
    uninstall: bpy.props.BoolProperty(
        name="Uninstall",
        description="Uninstall",
        default = False,
        options={'SKIP_SAVE'} #for the next time operator is run
    )

    def execute(self, context):
        if self.uninstall:
            self.report({'INFO'}, "Uninstall " + self.dependancy)
        else:
            self.report({'INFO'}, "Install " + self.dependancy)
        return {'FINISHED'}

class LayoutDemoPanel(bpy.types.Panel):
    """Creates a Panel in the scene context of the properties editor"""
    bl_label = "Layout Demo"
    bl_idname = "SCENE_PT_layout"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        col = self.layout.column()
        # if you need to set only one property for an operator button
        col.operator("cyanic.install_dependency", text = "Install dep" ).dependancy="dep"
        #if you have more properties
        op = col.operator("cyanic.install_dependency", text = "Uninstall dep")
        op.dependancy = "dep"
        op.uninstall = True
        

def register():
    bpy.utils.register_class(CyanicDependancy)
    bpy.utils.register_class(LayoutDemoPanel)

def unregister():
    bpy.utils.unregister_class(CyanicDependancy)
    bpy.utils.unregister_class(LayoutDemoPanel)


if __name__ == "__main__":
    register()
$\endgroup$
1
  • $\begingroup$ I'm going to mark this as accepted because the dependency: bpy.props.StringProperty('') part was what I was missing. With that piece of information I was able to rewrite my code to work. $\endgroup$ Commented Jul 29, 2024 at 4:43

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.