Cobertos

Importing and Reloading Python Modules in Blender

Using: blenderv2.7 pythonv3.5

Python in Blender can be tiring. A simple problem becomes an arduous trek through docs, examples, and sometimes the C API to find the Blender way to write given Python code. This is due to the many quirks of Blender's own internal Python environment.

Importing is one of those arduous tasks. Python provides a lot of functionality to import all different kinds of source and data files but Blender's implementation makes design decisions that create issues. This is my deep dive into Blender's Python import integration where at the end I provide a small module to make your future Blender importing life easier.

Importing

In a nutshell, Python's module importing/loading comes bundled in the from ... import ... as ... syntax. This syntax ultimately compiles to various forms of the __import__() function. Instead of interfacing with this directly, Python provides importlib in Python 3.4+ (previously imp in Python < 3.4) to make interfacing with __import__() much easier. Blender decides to forgo this setup for its own implementation with the intention of making their own integration work better with the way Blender handles its data. This allows it to support nifty features like code files being text datablocks and inline python snippets in other parts of the application.

Unfortunately this override seems to break some functionality including relative imports and simply makes other tasks like how to structure a multi-file project confusing. This is a real bummer for large Blender applications. The above StackOverflow answer recommends appending to sys.path which works just fine but leaves a more comprehensive system to be desired. Things like module reloading for easy development and the ability to register and unregister large applications at will to support the little checkbox in the User Preferences window.

blenderUserPreferences
This little checkbox is the bane of my existence

NOTE: If you go very deep into the implementation, you'll find the above monkey patch of __import__() internally redirects to PyImport_ImportModuleLevel() which according to the CPython source uses __package__ and __spec__ or __name__ and __path__ to find the parent package to import from. From here, I assume Blender doesn't set these globals to standard values which causes issues traversing the package heirarchy and this is the cause of one such issue in the import system.

Reloading

Reloading Python modules is usually not a common task but is crucial in Blender. Contrast from normal Python development, the Python environment in Blender stays for as long as the application is open. It takes an application restart to clean the state of the interpretter and update any add-on changes. Module reloading alone will only take you so far because of complications from nested child modules, Blender register() functions, and other necessities.

In the amount of I was developing on a single add-on for Blender, I found it useful to make my own importer that handles specific tasks I would otherwise type in manually. Below is a small class that will handle module loading and registering for you. See bl_register(self, moduleNames) and bl_unregister(self).

"""
Utility that allow a system of registering and unregistering of different modules
within a framework/add-on to be loaded into Blender

A module imported with this can have a bl_register and a bl_unregister at the
top level to add what it needs to Blender upon add-on load and unload

Note that this will handle recursively reloading of child modules, so module loops
will definitely cause headaches. Be wary of how you use this
"""

import importlib
import sys
from types import ModuleType

def rreload(module):
    '''Recursively reload modules.'''
    importlib.reload(module) 
    for childModule in [v for v in module.__dict__.values() if v is ModuleType]:
        rreload(childModule)

class BLModuleLoader:
    '''
    A module loader that plays nice with Blenders reloading system
    '''
    def __init__(self):
        self._registeredModules = {}

    def _register(self, name):
        '''
        Imports and registers a single module where name is the "."
        separated list for the package, absolute or relative
        '''
        importEquivalentStr = "import " + name
        try:
            if name in sys.modules:
                print("Reloading import \"" + importEquivalentStr + "\"")
                rreload(sys.modules[name])
            else:
                print("Importing for first time \"" + importEquivalentStr + "\"")
                sys.modules[name] = importlib.import_module(name)
            imported = sys.modules[name]
        except ImportError as e:
            print("Importing error " + name + ": " + str(e))
            return None
        
        if "bl_register" in dir(imported) and name not in self._registeredModules:
            try:
                imported.bl_register()
            except ValueError as e:
                print("Registration error " + name + ": " + str(e))
                return None
        self._registeredModules[name] = imported
        return imported

    def _unregister(self, name):
        '''
        Unregisters a previously registered module
        '''
        module = self._registeredModules[name]
        if "bl_unregister" in dir(module):
            try:
                module.bl_unregister()
            except ValueError as e:
                print("Unregistration error " + name + ": " + str(e))
                return None
        del self._registeredModules[name]

    def bl_register(self, moduleNames):
        '''
        Registers all the passed modules, returning a dict of all
        the loaded modules, key'd by their passed names
        '''
        return { k : self._register(k) for k in moduleNames }

    def bl_unregister(self):
        '''
        Unregisters all the modules that were previously imported
        '''
        for name in list(self._registeredModules.keys()):
            self._unregister(name)

This code is backed by 5 unit tests that handle loading and unloading modules, both nested and top level, inside of a Python environment run alongside headless Blender (though creating these unit tests to dynamically create and unload modules in Python was even itself a challenge).

Usage looks like as follows:

#__init__.py on top level of module
from BLModuleUtils import BLModuleLoader

#Create the loader
ml = BLModuleLoader
def register():
    #Register your modules
    loaded = ml.bl_register(["myModuleOnPythonPath",
        "myOtherModuleOnPythonPath"])
        
    #loaded holds the loaded modules if they worked
    #at the corresponding key
    #e.g. loaded["myModuleOnPythonPath"]
    #otherwise it will be None if an error occured
    #during the load and registration process

def unregister():
    #Unregister your modules
    ml.bl_unregister()
    
#myModuleOnPythonPath.py in the same folder, or wherever if you use a . path
def bl_register():
    bpy.utils.register_class(Your_RNA_Meta_Class)
def bl_unregister():
    bpy.utils.unregister_class(Your_RNA_Meta_Class)

Toggling and untoggling the checkbox will call unregister() and register() which will reload your modules and reregister all your new class bytecode. Just make sure you watch out for things that stick around like draw call handlers from bpy.types.SpaceView3D.draw_handler_add.

A lot of this was possible thanks to some great research from those around the Internet that have made similar plunges into Python's module loading system:

Cubecus for Blender is here!

Cubecus is here! After a long time coming (4 years on and off), I've gotten my Blender level design…

It's the small things

Just a little loading screen I made. Getting the text to not clip with the cube was a challenge…