How to make work multiple scripts in a folder for an addon?

Hi,

I’m trying to make an addon and I would like to put all related scripts in a folder.

The problem is that I can’t import the modules properly from the __init __ and they aren’t taken into account by Blender. It works fine when I run all the scripts from the Text Editor but not when starting Blender and activating it from the Addon preferences (Note: I activate it without errors but the modules aren’t taken into account, only the __init __ script is).

I tried copying the already ported addons, I tried googling the errors I was getting, but couldn’t find a solution…

So the question is how do I properly include other modules?

It would be great if someone could look into it and write the lines of code I’m missing to make it work.

Here is the __init __ script:

bl_info = {
    "name": "Pie Menu: Toolbox Edit Mode (Metaball)",
    "blender": (2, 80, 0),
    "description": "Pie Menu Toolbox for 3D VIEW Edit Mode (Metaball).",
    "category": "Custom"
}

import bpy
from bpy.types import Menu

# import TestModule

#--------------------------------------------------------------------------------------
# F U N C T I O N A L I T I E S
#--------------------------------------------------------------------------------------

# EMPTY CLASS
class EmptyClass(bpy.types.Operator):
    bl_idname = "empty.class"
    bl_label = "Empty Class"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        
        return {'FINISHED'}

#--------------------------------------------------------------------------------------
# P I E   M E N U
#--------------------------------------------------------------------------------------

class VIEW3D_PIE_EditModeToolbox_Metaball(Menu):
    bl_label = "EDIT MODE TOOLBOX (METABALL)"

    def draw(self, context):
        layout = self.layout

        pie = layout.menu_pie()
        
        pie.operator("mball.delete_metaelems", text="Delete", icon="X")
        
        pie.operator("test.class", text="Test Pie Button", icon="X")
        
#--------------------------------------------------------------------------------------
# R E G I S T R Y
#--------------------------------------------------------------------------------------

classes = (
    EmptyClass,
    VIEW3D_PIE_EditModeToolbox_Metaball
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    wm = bpy.context.window_manager

    if wm.keyconfigs.addon:
        km = wm.keyconfigs.addon.keymaps.new(name='Metaball')
        kmi = km.keymap_items.new('wm.call_menu_pie', 'RIGHTMOUSE', 'PRESS', shift=True)
        kmi.properties.name = 'VIEW3D_PIE_EditModeToolbox_Metaball'

def unregister():
    from bpy.utils import unregister_class
    for cls in classes:
        unregister_class(cls)
    
    addon_keymaps = []
    
    wm = bpy.context.window_manager

    if wm.keyconfigs.addon:
        for km in addon_keymaps:
            for kmi in km.keymap_items:
                km.keymap_items.remove(kmi)

            wm.keyconfigs.addon.keymaps.remove(km)

    addon_keymaps.clear()


if __name__ == "__main__":
    register()

And here is the TestModule script:

import bpy
from bpy.props import *
from bpy.types import WindowManager

#--------------------------------------------------------------------------------------
# F U N C T I O N A L I T I E S
#--------------------------------------------------------------------------------------

# EMPTY CLASS
class EmptyClass(bpy.types.Operator):
    bl_idname = 'empty.class'
    bl_label = 'Empty Class'
    bl_description = "Empty Class"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self,context):

        return{'FINISHED'}

#--------------------------------------------------------------------------------------
# O P E R A T O R
#--------------------------------------------------------------------------------------

class TestClass(bpy.types.Operator):
    bl_label = "Test Class"
    bl_idname = "test.class"

    def draw(self, context):
        layout = self.layout
        
        box = layout.column()
        box.label(text="TEST")
        
    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_popup(self)

    def execute(self, context):
        self.report({'INFO'}, self.my_enum)
        return {'FINISHED'}

#--------------------------------------------------------------------------------------
# R E G I S T R Y
#--------------------------------------------------------------------------------------

classes = (
    EmptyClass,
    TestClass
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

def unregister():
    from bpy.utils import unregister_class
    for cls in classes:
        unregister_class(cls)

if __name__ == "__main__":
    register()

from . import TestModule is the way to go.
I think the problem here is that you execute the __init__.py file inside of Blenders Text Editor.
Maybe it is a good idea to use another text editor for addon development.

Also you have to call the register and unregister method of TestModule manually inside the same functions in __init__.py.

Oh, and you defined the empty.class operator twice. So one will overwrite the other.

I only execute the __init__ from the Text Editor to be sure it doesn’t make errors.
But otherwise, I activate the addon, save preferences, close Blender, reopen Blender and expect it to work.

The empty.class is actually a placeholder for future functionalities. And because I need at least 2 classes in the same script to register them with the ‘for’ loop so I don’t have to put the classes names twice (once in register and once in unregister).

I corrected from . import TestModule in my script.

Now, I understand that these errors were rising from running the script from the Text Editor.
So the next problem is how to register / unregister the other module from the __init__ script.

Do I have to register the module I want taken into account (TestModule)?
Or the classes from TestModule in the __init__ (which are TestClass and EmptyClass2, renamed to avoid conflicts)?

In either case, could you write the correct python formulation?

Thanks.

As I see it you have three main options when it comes to registering all your classes.

  1. After you did from . import TestModule in __init__.py you can call TestModule.register() and TestModule.unregister() in the register/unregister function in __init__.py
  2. You can import all the classes into __init__.py, put them into one classes tuple and then register them the way you are doing it already. from . TestModule import EmptyClass, TestClass can be used to import these classes.
  3. Use my auto_load.py that can do all the class registration for you. However it would be good if you could get this working without it just for the purpose of learning how it works. You can find it here. Make sure to also read the limitations of that script.

I’m so glad, it finally works!! Thank you very much for your support Jacques! I spent so much time figuring this one out…

Here’s is what I changed if it helps others:

Thats how I do:

modules = [
    "module1",
    "module2",
    "module3"
]

imported_modules = {}

for module_name in modules:
    if module_name in locals():
        print(f"Reloading {module_name}")
        importlib.reload(locals()[module_name])
        imported_modules[module_name] = locals()[module_name]
    else:
        exec("from . import {}".format(module_name))
        imported_modules[module_name] = locals()[module_name]

classes = []
for module in imported_modules.values():
    for thing in module.__dict__.values():
        if hasattr(thing, "TAG_REGISTER"):
            classes.append(thing)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)


def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)