Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Refactoring an Existing Add-on into a Host

So far, we have stayed inside the generated ecosystem.

Now we step outside it.

Suppose you already have a working Blender add-on.

It was written manually. It grew over time. It works. But it feels heavier than it should.

We will refactor it into a proper host.


The Starting Point

Consider a simplified existing add-on:

class AUDIO_OT_encode(bpy.types.Operator):
    bl_idname = "audio.encode"
    bl_label = "Encode Audio"

    def execute(self, context):
        path = context.scene.audio_path
        bitrate = context.scene.bitrate
        print("Encoding:", path, bitrate)
        return {'FINISHED'}


class AUDIO_OT_render(bpy.types.Operator):
    bl_idname = "audio.render"
    bl_label = "Render Audio"

    def execute(self, context):
        path = context.scene.audio_path
        print("Rendering:", path)
        return {'FINISHED'}


class AUDIO_PT_panel(bpy.types.Panel):
    bl_label = "Audio Tools"
    bl_space_type = 'SEQUENCE_EDITOR'
    bl_region_type = 'UI'
    bl_category = "Audio"

    def draw(self, context):
        layout = self.layout
        layout.prop(context.scene, "audio_path")
        layout.prop(context.scene, "bitrate")
        layout.operator("audio.encode")
        layout.operator("audio.render")

This works.

But notice:

  • Shared data is accessed directly from context.scene.
  • Operators assume a specific storage location.
  • Logic and UI are tightly coupled.
  • Extending this structure requires repetition.

There is no clear ownership boundary.


Step 1 – Separate Ownership

First, formalize storage ownership.

Define a property group:

class AudioHostProps(bpy.types.PropertyGroup):
    audio_path: bpy.props.StringProperty(subtype="FILE_PATH")
    bitrate: bpy.props.IntProperty(default=192)

Register it:

bpy.types.Scene.audio_host = bpy.props.PointerProperty(
    type=AudioHostProps
)

Now storage is explicit.

Instead of scattering values across context.scene, we centralize them.


Step 2 – Extract Tool Logic

Instead of embedding logic directly inside operators, extract the functional core:

def encode_audio(audio_path, bitrate):
    print("Encoding:", audio_path, bitrate)


def render_audio(audio_path):
    print("Rendering:", audio_path)

Now logic is reusable and independent of Blender.

Operators become thin wrappers.


Step 3 – Convert Tools into Plugins

Now these functions can become QuickAddon plugins.

They declare shared keys:

@op(
    shared={
        "audio_path": "project.audio_path",
        "bitrate": "project.bitrate",
    }
)
def encode_audio(audio_path: Path, bitrate: int):
    ...

And:

@op(
    shared={
        "audio_path": "project.audio_path",
    }
)
def render_audio(audio_path: Path):
    ...

Important contract:

  • audio_path: Path means the generated wrapper passes a real Path object to plugin code
  • if a host/plugin author wants Blender filepath UI but needs a plain string, they should use audio_path: str plus param_subtypes={"audio_path": "FILE_PATH"}

Generate them as plugins.

These plugins:

  • Do not know where storage lives.
  • Do not assume context.scene.
  • Declare only their shared contracts.

Step 4 – Turn the Existing Add-on into a Host

Your original add-on becomes:

  • UI owner
  • Property owner
  • Orchestrator

It embeds the generated plugins.

It defines:

  • KEYMAP
  • HostAPI
  • Optional orchestration logic

The structure becomes:

audio_host/
├── __init__.py
├── encode_plugin/
└── render_plugin/

The host controls routing.

The plugins remain modular.


What Changed?

Before:

  • Operators directly accessed scene properties.
  • Storage was implicit.
  • Logic and UI were tightly coupled.

After:

  • Host owns storage.
  • Plugins declare shared keys.
  • Routing is centralized.
  • Logic is modular.
  • The system becomes composable.

What Did Not Change?

  • The core encode logic.
  • The core render logic.
  • The UI intent.
  • The user workflow.

We changed structure, not behavior.


The Result

Your existing add-on:

  • Becomes cleaner.
  • Gains composability.
  • Gains embeddable plugins.
  • Gains routing discipline.
  • Gains orchestration capability.

And it did not require rewriting everything from scratch.

In the next chapter, we go the other direction:

Refactoring an existing add-on into a reusable plugin.