Multi-version add-ons, is this too crazy?

When Blender is loading add-ons, it first parses the bl_info block (addon_utils.py) of your script entry point (either __init__ or the single .py file), then later on it checks if the add-on has a compatible version (space_userpref.py).

In order to make your add-on more convenient to download, with a single archive (without having to distribute separate 2.79, 2.83, 2.90 etc. downloads), would it be too crazy to do something like this:

# Notice the super-high major version.
bl_info = {
    'name': 'My Awesome Add-on',
    'author': 'John Doe',
    'description': 'Description',
    'version': (1, 0, 0),
    'blender': (999,),
    'location': 'Menu > Foo > Bar',
    'warning': '',
    'category': '3D View',
}

import bpy

def register():
    major, minor = bpy.app.version[:2]
    if major == 2:
        if minor == 83:
            # Import your 2.83 package.
            from _283 import _register
            _register()
        elif minor == 79:
            # Import your 2.79 package.            
            from _279 import _register
            _register()
        else:
            pass
    elif major == 3:
        (...)

I tested and it works. The benefit of this would be that you can distribute a single ZIP with all the scripts inside, and the add-on only registers the classes needed based on the Blender version registering the add-on.

Doesn’t seem crazy to me at all in principle. It would be a bit weird to hardcode the versions to only 2.79 and 2.83 like that but I suppose you’re just doing that for example. And I’d probably throw some sort of warning / error instead of just “passing”.

1 Like

@bsavery thanks for the perspective! I think the fallback for when the Blender version isn’t supported could be to register a dummy AddonPreferences class that draws some labels explaining this, and where to go to get support.
If I understood what you meant about selecting the package to import, it’s safer to use bigger-than / less-than comparisons than equality: (minor >= 80 and minor < 90) for the 2.8x series, for example. And from 2.9x forward, add new ranges whenever there’s some new API changes that affect your add-on.

Yes this is what I mean.

Not a silly idea, there are different ways to mix in compatibility adjustments, from using a simple if to using class and function decorations.

The main question is, “What blender versions are your addon users using?”, no need to add compatibility if no one will use it.

For reference, and a start on what you need to adjust, a couple of addons that already have some compatibility :-

Blender Cloud
Magic-UV

1 Like

Hi Shane, good points, and thanks a lot for the links as there’s some useful techniques in there, like using those functions and class decorators for testing compatibility (and even replacing class properties with annotations).

Hm, I can see in the Magic-UV add-on that they do matrix multiplications through a function, so the appropriate operator is selected:

def matmul(m1, m2):
    if check_version(2, 80, 0) < 0:
        return m1 * m2

    return m1 @ m2

(From https://github.com/nutti/Magic-UV/blob/master/src/magic_uv/utils/compatibility.py#L75)

This involves a function call, which can be slow depending on how it’s used. Python lets you compile code on the fly, so I thought of this:

import bpy
from mathutils import Matrix

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    
    # The source should have no initial indent.
    MY_MATRIX_FUNC_SOURCE = '''    
def _myMatrixFunc(self, x, y):
    print('Result:', x {MATMUL} y)
'''      

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        # Calling this compiled method like normal.
        self.myMatrixFunc(
            context.object.matrix_world,
            Matrix.Identity(4))
        )
        return {'FINISHED'}

IS_BLENDER_280PLUS = bpy.app.version >= (2, 80)
codeObject = compile(
    SimpleOperator.MY_MATRIX_FUNC_SOURCE.replace('{MATMUL}', ('@' if IS_BLENDER_280PLUS else '*')),
    '<string>',
    'exec'
)
exec(codeObject)
SimpleOperator.myMatrixFunc = _myMatrixFunc
    

def register():
    bpy.utils.register_class(SimpleOperator)


def unregister():
    bpy.utils.unregister_class(SimpleOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.simple_operator()

Edit: In case it’s not clear what this is supposed to do, you would take all functions that have compatibility problems, have them as multiline strings in your script, then replace parts of the string depending on what Blender version is being used, and compile() + exec() that modified string and then use these created functions like normal. So the Python code changes depending on the Blender version running it.

PS: There’s also this great article by Artem Golubin (https://rushter.com/blog/python-bytecode-patch/), showing how you could patch the Python bytecode of a function to replace operations and such, but that would be too low-level for me.