Creating multifile add-on for Blender

In the development of complex add-ons with large code volume storing all the code in a single file is inappropriate. When a single file contains logically unrelated classes, functions, and datasets, it is difficult to read, debug, find the necessary code pieces, reuse code. Such code layout is considered as very bad programming tone.

Blender Python supports modular system that allows subdividing logical code parts of the add-on into different files, and then connect them to use. Even if you have never thought about modules, creating scripts or add-ons, you have already used them – any code stored in the *.py file is a separate independent module. Just your addon consists of only one module. Complex add-ons may consist of several tens of modules.

Multifile add-on
Multifile add-on

So the module is the separate file with the *.py extension, contains Python executable code.

In order to make the whole add-on from several modules, they are combined to package. A package is just a directory (folder), which collected the necessary add-ons modules. The hallmark of a package is the presence of a file named __init__.py inside it. To correctly install such add-on to Blender, entire package (the directory, not only files from it) needs to be archived to *.zip and during installation – Install From File – this archive needs to be specified.

Let’s consider a simple creating multifile add-on example:

  1. Create a package:
    1. In your favorite file manager create a directory with sample name. For example – d:/Python/TestMultifile/
  2. It is more convenient to use an external IDE for coding, but you can write code with the built-in Blender Text Editor.
    1. Create a new PyCharm project.
    2. Specify the created package as the working directory.
  3. Create the first module:
    1. Create new file.
    2. Name it addCube.py

Module (file) names should not be exactly the same as the package (directory) name, not to make troubles with their further importing within the add-on.

Let’s write the simplest operator code which adds to the scene the default cube, nothing without it ).

This creates a custom operator, by wrapping the system operator bpy.ops.mesh.primitive_cube_add (), that adds a cube to the scene, to user-defined class.

Another difference from a single file add-on – setting the add-on description dictionary bl_info in each module is not required, only executable code.

  1. Create the second module:
    1. Create new file.
    2. Name it addCubePanel.py

In this module let’s create an interface to call the operator from the first module. Now we follow the MVC (Model-View-Controller) concept, which prescribes the separation of the executable code (model) and the code responsible for the interface creation (view).

Let’s create a simple “Add Cube” tab in the T-bar with a single button to call the operator from the first module.

  1. And now let’s create an index file for the combining modules from the package:
    1. Create new file.
    2. Name it __init__.py

Only this file from the whole package is executed at the time of the add-on activation. Therefore, add-on description dictionary bl_info should be placed here:

And it needs to make the import of all other modules exactly in this file.

Index file __init__.py is used only for modules import and registration and therefore it should not contain any executable code related to a specific addon. This file should be universal as much as possible.

Append the names of all the modules that must be imported to the list:

Module name – is the file name without the extension “.py”.

This string is the one changeable string in the file. To make an index file for any other add-on the above code can be used, only with specifying the necessary module names in the modulesNames list.

Modules, imported from the package, in Python environment are available by the ID: package_name.module_name. Let’s extend module names in the list to full format:

Now the modulesFullNames dictionary contains the full module names used in our add-on.

Let’s import all the modules from this dictionary with their full names using the importlib utility:

All imported modules are stored in sys.modules. Reloading already imported module (importlib.reload) is required when the user presses the refresh button f8 in add-ons installing window (User Preferences – Add-ons).

To speed up the Blender add-ons performance, modules compiled and then stored in the add-ons cache directory, from which they are taken for execution. If the module code changes, the Blender will still use the old, already compiled version of the module, as long as the add-on will not be reinstalled or reloaded by pressing f8 key. Module reloading required to recompile the changed code and make it available for use.

importlib.import_module doesn’t create a global variable pointed to imported module. To further refer imported module by name it needs to put in the globals() that variable.

Attribute “modulesNames” creates in every imported module to have an ability to access all other imported modules. Now from any imported module we can access another imported module by its name. For example to access addCube module from addCubePanel module we need to use the instruction:

After importing all of the necessary modules we must register their classes. The register() function is executed automatically at the time of add-on activation only in the index file. So we need to call the register functions for all add-on modules inside it.

The same needs to be done with unregistering functions.

Completed __init__.py code:

This is the finished (release) version of the index __init__.py file. It can be used for any multifile addon. It is only necessary to specify the desired module names in modulesNames list.

  1. Our TestMultifile directory now contains three files:
    1. __init__.py
    2. addCube.py
    3. addCubePanel.py

It remains to pack it (the directory, not only files) to *.zip archive, and install a test multifile add-on to Blender.

Sample multifile add-on
Sample multifile add-on

Bonus: debugging multifile add-on from Blender internal Text Editor

If develop multifile add-on with external IDE, existing startup script in Blender internal editor don’t work. With existing script Blender can’t understand what is called – the package index file or the single file add-on. To debug a multifile add-on we need to change the startup Blender script.

First of all, we need to specify the location of our add-on – the path to the directory d:/Python/TestMultifile/. Python search paths to the imported modules in the sys.path list. Let’s append our package directory path to sys.path. The name of the index file let’s store to the separate variable for convenient use.

Debug add-on launch is different from running add-on already installed in Blender. In order to understand what type of execution is required in __init__.py, add to the list of input parameters sys.argv additional parameter “DEBUG_MODE”.

Remove this parameter after script finish.

Full script code that runs the multifile add-on from internal Blender Text Editor in debug mode now looks like this:

This script code is universal. For the development of other multifile add-ons, you only need to change the modules location path in the filesDir variable.

Executing from the Blender Text Editor by pressing Run Script button __init__.py file considered a separate module, not the package index file. Therefore, all add-on modules should not be imported as package parts. They must be imported as the separate individual modules. So the full module name will contain only the name of the module, omitting the package name.

Use the “DEBUG_MODE” input parameter to modify __init__.py file. Make the condition for the debugging run that allows correctly modules importing. To do this we just need to correctly form full modules names in the modulesFullNames dictionary:

Finished __init__.py file for running in the debug and release modes:

To publish add-on release you can use both the __init__.py file, above, and modified.

0 0 votes
Article Rating
Subscribe
Notify of
guest

12 Comment
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
adrian
adrian
1 year ago

hi i hv a question. i see the files uses the same regsiter() function while doing their own thing, so i assumed functions are not shared accross different files?

what if i hv a function needed to be shared across?

NikitaD
Admin
1 year ago
Reply to  adrian

The register() and the unregister() functions will be called automatically from the __init__.py
To register classes in all other modules we need to call this functions from modules inside this function from the init module.
You can also call them any time you need. But it is more convenient to register all we need in modules one time in the beginning.

1conscience0dimension
1conscience0dimension
2 years ago

correction debug= 1 is ON

1conscience0dimension
1conscience0dimension
2 years ago

very interresting, but I found this a little to difficult. so I did another solution
I have a very simple multifile with an operator to resize an init file and a panel file :
debug is at 0 so I can do modifications and reinstalling the addon UI is changed without restarting blender… the main difference the reload is in the register. but later of course when sharing the addon the debug must be at 1 (OFF) or any value…

I did a github page
https://github.com/1C0D/example-multifiles-blender-addon-to-use-with-advanced-addon-installer-or-not
I’m using my addon advanced addon installer this is doing this https://youtu.be/2tcAjbeFSd8

NikitaD
Admin
2 years ago

Not bad too!

1conscience0dimension
1conscience0dimension
2 years ago
Reply to  Nikita

finally I will use this notation for the format: exec(f”from . import {mod}”)
it’s more readable.
but I wonder too if it’s necessary to handle errors (this is the same question I have in my addon advance addons installer). by default Blender is dealing with errors. for example in the blender code around installing addons in blender when opening files you find some try except everywhere, in a python script. but even there I wonder it was really necessary. I’m reading this topic around this https://blenderartists.org/t/error-handling-and-exceptions/686503/3
and if my intuition is confirmed I won’t even not put some try except in the code above. if you have an opinion around this, I’m listening to it

Last edited 2 years ago by 1conscience0dimension
1conscience0dimension
1conscience0dimension
2 years ago
Reply to  Nikita

yes but was it necessary to add these try except? or just let the error happening. I put Exception more to know what possible error could occur. but I think now, no try except was necessary, because this is not changing the behaviour of the program. right?

NikitaD
Admin
2 years ago

If the error could occur – it must be processed. The “try-except” is the roughest method but it is better than nothing.

1conscience0dimension
1conscience0dimension
2 years ago

ok I understood why try except was useless. my addon is sending already an error. but with a normal installation, the error become silent (importing a wrong module name). the zip is just installed but the addon can’t be enabled… just printing “addon not found” in the console. lol
I thought I was very confused there. but now I realize not really. imagine million of user dealing with this…

Last edited 2 years ago by 1conscience0dimension
1conscience0dimension
1conscience0dimension
2 years ago

“To me this is what try-except’s are about: keeping your code running when odd things are happening. ” what I understand too

wizardOfRobots
wizardOfRobots
2 years ago

Thank you so much. This was helpful!