Blender 2.91 addon dev workflow?

I’m wondering if anyone has a good workflow for addon development in blender 2.91. As far as I can tell, blender (on OSX) doesn’t support F8 to reload addons. Addons that are reloaded manually also do not recompile unless init.py changes on disk, which makes multi-file addons very tedious to edit/reload.
e.g.

  1. change code in ExampleClass.py
  2. “save” init.py (no edits are necessary, but it must write to disk)
  3. toggle ExampleAddon in addon manager OR execute “bpy.ops.preferences.addon_enable(module=‘example_addon’)” in the console

Edit: found out that F8 is now F3 > Reload Scripts but would love to know how to assign this to a hotkey. Atleast it doesn’t suffer from having to save init.py to disk constantly

If any further improvements are possible to an iterative dev workflow of an addon, please share!

Edit: I’m very grateful for all the useful feedback shared on this thread.
I just posted this WIP addon, to help speed up addon development by quickening reloading.

1 Like

Some suggestions that you may find helpful
https://developer.blender.org/T67387 how to make add-on development more efficent (e.g. reloading)
https://developer.blender.org/T71486 Support for keeping preferences of disabled add-ons

Thank you ankitm,

That post is kindof funny but extremely helpful. The only plot twist for me is that running “bpy.ops.script.reload()” crashes blender after the second or third time.

Still this is much better than before. Hopefully it attracts more tips!
Thanks again!

If that happens with official add-ons, please report a bug using Help menu > Report a bug.
Also attach the crashed thread’s text as shown here.

Linking to the bug report here, just in case:
https://developer.blender.org/T80694

1 Like

I usually have this after imports in my __init__.py

if locals().get('loaded'):
    loaded = False
    from importlib import reload
    from sys import modules

    modules[__name__] = reload(modules[__name__])
    for name, module in modules.items():
        if name.startswith(f"{__package__}."):
            globals()[name] = reload(module)
    del reload, modules

And at the bottom of the file:

loaded = True

It’s not order-aware reload, which I think is fine because it lets me resolve circular dependency issues immediately as they arise instead of the usual house of cards setup.

I then use this operator to reload a single module, which can be hotkeyed:

class SCRIPT_OT_reload_addon(Operator):
    bl_idname = "script.reload_addon_module"
    bl_label = "Reload Addon Module"

    module: bpy.props.StringProperty(default="")

    def execute(self, context):
        import addon_utils
        default, state = addon_utils.check(self.module)

        if default and state:
            modules = bpy.utils._sys.modules
            m = modules.get(self.module)

            if m is not None:
                from importlib import reload
                m.unregister()

                m = modules[self.module] = reload(m)
                print("reloading", self.module)

                for n, sub in modules.items():
                    if n.startswith("%s." % self.module):
                        modules[n] = reload(sub)
                        print("reload submodule", n)

                m.register()

        return {'CANCELLED'}

It’s a much more direct reload of a single addon.

Other than that, not sure what there is for osx, but I use ConEmu terminal emulator with a macro set to ctrl + 1 which kills and restarts blender. For those times when I really just want to hit a reset switch.

3 Likes

I just found that the reload scripts operator is hidden here, so the shortcut can be assigned once again manually.
2020-09-12 00_06_12-Blender

However what I did for many days that worked nicely was to hit refresh and check-uncheck. But this was a workaround because I thought that developers removed the reload mechanism. :slight_smile:
2020-09-12 00_07_13-Blender Preferences

1 Like

@kaio

That looks very helpful. I was thinking of making a module that “watches” the addon folder for changes and just runs bpy.ops.script.reload() (on focus change, ofcourse) but that might be overkill.

@cconst

I wish I saw that menu item sooner! The method of disable/enable actually would only work for me if I wrote __init__.py to disk.

If you develop with VSCode, you should try the extension https://github.com/JacquesLucke/blender_vscode. It offers addon reloading on file change (check its setting, not enabled by default). With this I almost never reload manually which is a huge time saver.

3 Likes

i made a little helper addon that puts some buttons in blenders top bar, including a button for script reloading maybe you find that useful.

I always develop my addons in directories (rather than plain files), so __init__.py is always there. Thanks for clarifying that.

@bunnybones If you in some part of your development need do some work in the Blender Text Editor you can find a lot of add-ons to ease the process here: https://blenderartists.org/t/essential-blender-text-editor-add-ons/1163857

I’m not sure how relatable this is to blender, but I have quite allot of experience developing for other DCCs and have taken some time to try and streamline this process

Separate API / UI

Firstly this is simply good practice, Secondly the objects registered with blender will in turn reference the modules of your standalone API, which are trivially reloaded.

Use small simple PURE functions

In computer programming, a pure function is a function that has the following properties: Its return value is the same for the same arguments. Its evaluation has no side effects. Thus a pure function is a computational analogue of a mathematical function.

Try to write your API so its functions do only one simple thing, and have no side effects. This allows you to easily test your code outside of the addon and hugely increases your iteration speed.

keep things simple.

I see people trying to make everything a class these days and it tends to lead to very rigid code which doesn’t scale well ( nothing against classes but the can be tricky to use right )

If it’s of any use, I got tired of that annoying routine (disable->remove->install->enable) and bundled it into a single button.

I’ve been using it only with single file scripts. I’m sure there’s a clever way to turn it into a more sentient reload-butler-robot, but it’s not in my immediate plans to do so.

4 Likes

This is good advice, especially relevant with how blender registers operators, though it is kindof funny that my little suite of tools could cleanly span 5 seperate addons. Maybe down the road.

One remaining addon-dev-workflow question is regarding version control.
My current approach is to just make a symlink from myAddon repo project folder, to /Users/me/Library/Application\ Support/Blender/2.91/scripts/addons/myAddon (OSX path)
It feels a bit hacky, but it seems to work alright (just ignore those __pycache__ files!).
Anyone have a different approach to this?

I use both, directory junctions (same as symlinks in windows) + setting a filepath.

https://en.wikipedia.org/wiki/NTFS_links
https://docs.blender.org/manual/en/latest/editors/preferences/file_paths.html

As for example my directories are:

  • D\programming\blenderaddons: this directory is mostly a warehouse with all of the WIP, from time to time I pick one addon to work on rather than having all of them enabled at the same time.

  • D:\graphics\blenderaddons29 any third party script is installed here, when I need some of my own scripts installed here (from blenderaddons) I just place a symlink

P.S. Though one really great idea for Blender improvement here is that if File Paths > Scripts setting can hold multiple paths separated with ; it will completely remove any need for using directory junctions.

I use a dev utils addon that appends a specific folder (personal github repo) to sys.path on startup.

Blender does this to everything in addons and addons_contrib folders (see addon_utils.py), and it’s how python is able to find and import addons.

path_ = r"path_to_scripts"
bpy.utils._sys_path_ensure_append(path_)

which is equivalent to:

import sys
path_ = r"path_to_scripts"

if path_ not in sys.path:
    sys.path.append(path_)

Edit:
And you might wonder, “how do you get the dev utils into blender?”

Well… So far I’ve been using symlinks :blush:

1 Like