Appending addon preferences from modules

Hi, I’m making a multiple files addon, one of those “multiple tools and tweaks” kind, and I’m adding features here and there, now and then. I’ making this primarily for my in-house workflow, but I think that some tool might be useful for other artists too.
So I’m willing to share single independent addons, but I want also to get them all bundled together. So to say many single addons, into one big container addon.
I stumbled upon the “Autoload” tool by @Jacques_Lucke and it looks like the perfect fit for what I’m doing. I was able to rewrite the modules and see them loaded smootly.
But…

I have a question: I’d like to have some addon settings (as in bpy.types.AddonPreferences) for some feature, but I’d like them to be bound to each module, for sharing purposes.
So, what I wasn’t able to do is to see all the preferences together in the addon settings, once I put them into each single file.
What should I do?

And also, how could I have some preference in the “container” addon that affect the whole tools set, but without touching each single file too much? (e.g I have some icons that I might want to show up or not, or to be changed)

Thanks in advance

I’m not sure what you mean by having the preferences “bound to each module”.
As in, each module has its own preferences class?

Yes, one in each module, so that they can be used as single-file addon with their own preferences.
But when loaded in a multi-file addon, I’d like to have all of the singular preferences together.
If I use __name__ as the class bl_idname it doesn’t work at all.
If I use __package__ as the class bl_idname it seems that the last loaded module overwrites any other. But also it wouldn’t work when using the module as a standalone addon.
The ideal would be if all __name__ in each module, could be added in a main __package__ bl_idname.
Is that possible?

I think what you want to use is inheritance…

The basic idea is that in each module, you define some addon preferences with the bl_idname set to the __name__ variable, and then in the full addon, you import all of those subpreferences, and use them as parent classes for a full addon preferences class, which is very minimal, and just defines bl_idname as being __package__

for example:
Submodule A:

class ModuleAPrefs(AddonPreferences):
    bl_idname = __name__
    # Put your module A preferences here

Submodule B:

class ModuleBPrefs(AddonPreferences):
    bl_idname = __name__
    # Put your module B preferences here

Multi file addon module:

class AllPrefs(ModuleAPrefs, ModuleBPrefs):
    bl_idname = __package__
   # All of the setting from modules A and B will automatically be available here.

This means that if you give someone only the Module A file, the preferences will only have the settings defined in that file, likewise for B, and if you give someone the entire addon, all the preferences will be added to the main preferences.

Some caveats:

  • If each module has it’s own draw function, that will just get overwritten by the inheritance, so in the main preferences you probably want to use the super() function, to get the draw function of each of the parent classes individually and then call them in a new draw function.
  • the settings for each of the submodules must have unique names, or they will also be overwritten.

There’s obviously quite a few details that I’ve glossed over here, but I hope this is a good starting point to getting what you want :slight_smile:

1 Like

A big thank you for now. I’ll try to make something that works from this, and I’ll report back here!

1 Like

Ok, I’ve tried a bit but here’s my issue:
how do I list the prefs-classes from each module in the main “Allprefs” class?
The script should be able to recognize the AddonPreferences classes inside the modules, and then I should list them into the Allprefs brackets (with a tuple?)

The Autoload script from @jacqueslucke already loads classes from modules (although it does it in order to also register them, which I don’t want).

Well, the simplest solution would be to import each class by name and then write the inheritance into the code as I showed above. But I assume what you want is to be able to load any number of these files automatically, rather than doing it manually. For that, you need to follow these steps:

  1. Get a list of all the modules in the directory. (Have a look at the pathlib library)
  2. Import them (You can import a module from it’s name using the __import__() function)
  3. Iterate through each modules attributes, and check if any of them are subclasses of bpy.types.AddonPreferences with issubclass. (You can list the attributes with the dir function, and get their values with the getattr function).
  4. Put all of those into a list
  5. In order to inherit from all of the items in the list, use the * list unpacking operator in the class definition:
    class MyPrefs(*my_list_of_modules):

I don’t have time at the moment to make an example, but the first 4 steps should be able to be lifted directly from the auto_load.py file you linked above, and the rest can be googled, hopefully that’s enough to get you on the right track.

I’d also recommend having a look into how inheritance works/the syntax of it, as it’s always good to know how it works under the surface.

Sorry to bother again… (I’m such a newbie…)

My buggy code here
import os
import bpy
import sys
import typing
import inspect
import pkgutil
import importlib
from pathlib import Path

__all__ = (
    "init",
    "register",
    "unregister",
)

blender_version = bpy.app.version

modules = None
ordered_classes = None
pref_classes = []

def init():
    global modules
    global ordered_classes
    global pref_classes

    modules = get_all_submodules(Path(__file__).parent)
    ordered_classes = get_ordered_classes_to_register(modules)
    for cls in ordered_classes:
        pref_classes.append(cls)

class addon_prefs(*pref_classes):
    bl_idname = __package__

    def draw(self, context):
        super().draw(self,context)

def register():
    # for cls in ordered_classes:
    #     bpy.utils.register_class(cls)
    bpy.utils.register_class(addon_prefs)

    for module in modules:
        if module.__name__ == __name__:
            continue
        if hasattr(module, "register"):
            module.register()

def unregister():
    # for cls in reversed(ordered_classes):
    #     bpy.utils.unregister_class(cls)
    bpy.utils.unregister_class(addon_prefs)

    for module in modules:
        if module.__name__ == __name__:
            continue
        if hasattr(module, "unregister"):
            module.unregister()


# Import modules
#################################################

def get_all_submodules(directory):
    return list(iter_submodules(directory, directory.name))

def iter_submodules(path, package_name):
    for name in sorted(iter_submodule_names(path)):
        if name == "auto_load": continue
        yield importlib.import_module("." + name, package_name)

def iter_submodule_names(path, root=""):
    for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
        if is_package:
            sub_path = path / module_name
            sub_root = root + module_name + "."
            yield from iter_submodule_names(sub_path, sub_root)
        else:
            yield root + module_name


# Find classes to register
#################################################

def get_ordered_classes_to_register(modules):
    return toposort(get_register_deps_dict(modules))

def get_register_deps_dict(modules):
    my_classes = set(iter_my_classes(modules))
    my_classes_by_idname = {cls.bl_idname : cls for cls in my_classes if hasattr(cls, "bl_idname")}

    deps_dict = {}
    for cls in my_classes:
        deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname))
    return deps_dict

def iter_my_register_deps(cls, my_classes, my_classes_by_idname):
    yield from iter_my_deps_from_annotations(cls, my_classes)
    yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname)

def iter_my_deps_from_annotations(cls, my_classes):
    for value in typing.get_type_hints(cls, {}, {}).values():
        dependency = get_dependency_from_annotation(value)
        if dependency is not None:
            if dependency in my_classes:
                yield dependency

def get_dependency_from_annotation(value):
    if blender_version >= (2, 93):
        if isinstance(value, bpy.props._PropertyDeferred):
            return value.keywords.get("type")
    else:
        if isinstance(value, tuple) and len(value) == 2:
            if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
                return value[1]["type"]
    return None

def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
    if bpy.types.Panel in cls.__bases__:
        parent_idname = getattr(cls, "bl_parent_id", None)
        if parent_idname is not None:
            parent_cls = my_classes_by_idname.get(parent_idname)
            if parent_cls is not None:
                yield parent_cls

def iter_my_classes(modules):
    base_types = get_register_base_types()
    for cls in get_classes_in_modules(modules):
        if any(base in base_types for base in cls.__bases__):
            if not getattr(cls, "is_registered", False):                
                yield cls

def get_classes_in_modules(modules):
    classes = set()
    for module in modules:
        for cls in iter_classes_in_module(module):
            classes.add(cls)
    return classes

def iter_classes_in_module(module):
    for value in module.__dict__.values():
        if inspect.isclass(value):
            yield value

def get_register_base_types():
    return set(getattr(bpy.types, name) for name in [
        "AddonPreferences",
    ])


# Find order to register to solve dependencies
#################################################

def toposort(deps_dict):
    sorted_list = []
    sorted_values = set()
    while len(deps_dict) > 0:
        unsorted = []
        for value, deps in deps_dict.items():
            if len(deps) == 0:
                sorted_list.append(value)
                sorted_values.add(value)
            else:
                unsorted.append(value)
        deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted}
    return sorted_list

So here what I did to the original autoload code (and it doesn’t work):

  • added a prefs_classes list and stored in it all the ordered_classes items
  • filtered out all the classes type except "AddonPreferences" type
  • avoid loading (and registering) auto_load module (sounded redundant to me)
  • created and registered a new class that should inherit those *pref_classes
  • messed with super()
for completeness, this is an example of how the preferences classes look in the modules
class bake_for_PBR_prefs(bpy.types.AddonPreferences):

    bl_idname = __name__

    filepath: StringProperty(name="Export File Path",subtype='FILE_PATH',)

    def draw(self, context):

        layout = self.layout

        box = layout.box()

        box.label(text="Bake for PBR")

        box.prop(self, "filepath")

The returned error is encountered the moment Blender tries to register the addon_prefs class:
RuntimeError: register_class(...):, missing bl_rna attribute from 'type' instance (may not be registered)

I take it as it is a problem with the list I’m feeding, isn’ it?
Did I post enough infos?
Thanks in advance

Ok, here’s how I would do it:

import importlib
import inspect
from pathlib import Path
import bpy
from bpy.props import BoolProperty
from bpy.types import AddonPreferences

bl_info = {
    "name": "auto_load_prefs",
    "author": "Andrew Stevenson",
    "description": "",
    "blender": (2, 80, 0),
    "version": (0, 0, 1),
    "location": "",
    "warning": "",
    "category": "Generic"
}


def get_preferences_classes():
    """Return a list of all addon preferences classes defined in files in the current directory."""
    prefs_classes = []
    # Iterate through all files in the current directory
    directory = Path(__file__).parent
    for file in directory.iterdir():
        # Don't include this file, or non python files
        if file == Path(__file__) or file.suffix != ".py":
            continue

        # Import the module, the same syntax as 'import .module'
        module = importlib.import_module("." + file.stem, package=__package__)

        # Iterate through all of the attributes in that module, check if they are addon preferences, and if so, append them.
        for attr in dir(module):
            attr = getattr(module, attr)
            if inspect.isclass(attr) and issubclass(attr, AddonPreferences):
                prefs_classes.append(attr)
    return prefs_classes


prefs_classes = get_preferences_classes()


# Inherit from all of the gathered classes into a single preferences class, combining all of their attributes.
# This will error if there are no prefs classes, so you might want to account for that.
class AllPrefs(*prefs_classes):
    bl_idname = __package__

    all_prefs_prop: BoolProperty(name="This property is only in all preferences")

    def draw(self, context):
        # Iterate through the classes and draw them
        # Turns out you don't need super here, you can just use the list of classes
        for cls in prefs_classes:
            if hasattr(cls, "draw"):
                cls.draw(self, context)


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


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

And in another module with it’s own preferences:

import bpy
from bpy.props import StringProperty


class PrefsA(bpy.types.AddonPreferences):
    bl_idname = __name__

    prop_a: StringProperty(name="Prefs A",)

    def draw(self, context):
        self.layout.label(text="PrefsA")


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


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

I’m not sure for certain what’s going wrong with your version, but It looks like it’s probably something to do with the fact that the class is being defined before the list of addon preferences has been populated by the init function, and so it’s not technically a preferences class itself when it gets registered.

Hope that helps!

image

Ok Andrew, now I need a photo of your face and your height, so that I can forge a real size bronze statue of you! :sweat_smile:

It works! I had to do a mix of your code with that of the auto_load script, but in the end it worked!
Once I have polished it I’ll post here.
Thank you thousand times!!!

Polished code for anyone, thanks to everyone
import bpy

import inspect

import pkgutil

import importlib

from pathlib import Path

from bpy.types import AddonPreferences

__all__ = (

    "init",

    "register",

    "unregister",

)

blender_version = bpy.app.version

modules = None

def init():

    global modules

    modules = get_all_submodules(Path(__file__).parent)

# Import modules

#################################################

def get_all_submodules(directory):

    return list(iter_submodules(directory, directory.name))

def iter_submodules(path, package_name):

    for name in sorted(iter_submodule_names(path)):

        if name == "auto_load": continue

        yield importlib.import_module("." + name, package_name)

def iter_submodule_names(path, root=""):

    for _, module_name, is_package in pkgutil.iter_modules([str(path)]):

        if is_package:

            sub_path = path / module_name

            sub_root = root + module_name + "."

            yield from iter_submodule_names(sub_path, sub_root)

        else:

            yield root + module_name

# Find classes to register

#################################################

def get_preferences_classes():

    """Return a list of all addon preferences classes defined in files in the current directory."""

    prefs_classes = []

    # Iterate through all files in the current directory

    directory = Path(__file__).parent

    for file in directory.iterdir():

        # Don't include this file, or non python files

        if file == Path(__file__) or file.suffix != ".py":

            continue

        # Import the module, the same syntax as 'import .module'

        module = importlib.import_module("." + file.stem, package=__package__)

        # Iterate through all of the attributes in that module, check if they are addon preferences, and if so, append them.

        for attr in dir(module):

            attr = getattr(module, attr)

            if inspect.isclass(attr) and issubclass(attr, AddonPreferences):

                prefs_classes.append(attr)

    return prefs_classes

#################################################

pref_classes = get_preferences_classes()

class addon_prefs(*pref_classes):

    bl_idname = __package__

    def draw(self, context):

        layout = self.layout

        for cls in pref_classes:

            if hasattr(cls, "draw"):

                cls.draw(self, context)

            layout.separator(factor = 1)


def register():

    bpy.utils.register_class(addon_prefs)

    for module in modules:

        if module.__name__ == __name__:

            continue

        if hasattr(module, "register"):

            module.register()

def unregister():

    bpy.utils.unregister_class(addon_prefs)

    for module in modules:

        if module.__name__ == __name__:

            continue

        if hasattr(module, "unregister"):

            module.unregister()
1 Like

Great, glad it’s working :smile: