handsPlugin Handlers

Handlers

What is a Handler?

A Handler is the bridge between the Plugin Manager and your Plugins. It defines how a category of plugins should behave—what services they receive and what happens when they're initialized.

Think of it this way:

  • The Manager orchestrates everything

  • The Handler defines the rules for a plugin category

  • The Plugins follow those rules and implement functionality

Every plugin must be attached to a handler. The handler is responsible for:

  1. Injecting services — Providing dependencies your plugins need

  2. Initialization logic — Running setup code when a plugin is registered


Anatomy of a Handler

To create a handler, extend PluginHandler and implement two required class methods:

from gl_plugin.plugin.handler import PluginHandler
from typing import Any, Dict

class MyHandler(PluginHandler):
    def __init__(self) -> None:
        super().__init__()

    @classmethod
    def create_injections(cls, instance: Any) -> Dict[str, Any]:
        # Define what services to inject into plugins
        return {}

    @classmethod
    def initialize_plugin(cls, instance: Any, plugin: Any) -> None:
        # Run setup logic after a plugin is instantiated
        pass

create_injections(cls, instance)

This method returns a dictionary of services that will be injected into every plugin attached to this handler. The injection happens automatically—plugins receive these services as instance attributes.

Parameters:

  • instance — The handler instance (useful for accessing handler properties)

Returns:

  • A dictionary mapping types to service instances or factories

initialize_plugin(cls, instance, plugin)

This method runs after a plugin is instantiated and its services are injected. Use it for any setup logic that requires the fully-constructed plugin.

Parameters:

  • instance — The handler instance

  • plugin — The newly created plugin instance


Handler Properties

Handlers can define their own properties to configure behavior. These properties are accessible in both create_injections() and initialize_plugin() via the instance parameter.


Example: Building a Calculator Handler

Let's build a handler that provides a CalculatorService to its plugins.

1

Define the Service

First, create the service that will be injected:

2

Create the Handler

Now create a handler that injects this service:

What's happening here:

  • create_injections() returns a dictionary with CalculatorService as the key and an instance as the value

  • Since CalculatorService is stateless, all plugins can safely share the same instance

  • initialize_plugin() passes the handler's precision config to each plugin

3

Create an Abstract Plugin for the Handler

First, create an abstract base plugin that defines the contract. This is to ensure that all plugins implementing the CalculatorHandler has calculate (you don't have to, but this is highly recommended to ensure functional integrity):

4

Create the Concrete Implementations

Now let's create the concrete implementations. For the purposes of this example, we shall only create the ones defined within the CalculatorService; you are definitely allowed and welcome to create your own implementation and flows:

Notice that:

  • MathPlugin defines the abstract calculate() method and receives the injected CalculatorService

  • Concrete plugins inherit both the service injection and the contract

  • Each implementation uses the same calculator service differently

5

Wire It Up

This pattern allows you to:

  • Define a consistent interface via the base plugin

  • Extend with new operations without modifying existing code

  • Swap implementations at runtime

  • Retrieve all plugins for a handler type with get_plugins()

6

The Completed Code

You can now simply paste this code and then run uv run main.py to execute it!

The expected output should be:

Instance vs Factory Injection

In create_injections(), you can return either a direct instance or a factory function. The choice depends on whether plugins need isolated or shared services.

Shared Instance (Direct)

The service is instantiated once when create_injections() is called. All plugins receive the same instance.

Use when:

  • The service is stateless (like our CalculatorService)

  • You want shared state across plugins (e.g., a shared cache)

  • The service doesn't hold plugin-specific data

Per-Plugin Instance (Factory)

The factory function is called each time a plugin is instantiated. Every plugin receives its own instance.

Use when:

  • The service holds plugin-specific state

  • Plugins would conflict if sharing (e.g., routers with overlapping routes)

  • Each plugin needs isolation

Example: Why Routers Need Factories

Consider an HTTP handler where plugins define their own routes:

If we used Router: Router() instead, both plugins share the same Router instance:

  • GithubPlugin registers POST /webhook → handler: github_webhook_handler

  • GooglePlugin registers POST /webhook → handler: google_webhook_handler

  • The second registration overwrites the first in the shared Router

Even if the HttpHandler correctly prefixes the paths (/github/webhook and /google/webhook), both routes now point to google_webhook_handler because the handler function was overwritten in the shared Router.

With lambda: Router(), each plugin gets its own Router instance:

  • GithubPlugin → its own Router → POST /webhookgithub_webhook_handler

  • GooglePlugin → its own Router → POST /webhookgoogle_webhook_handler

  • Each route correctly maps to its own handler function


Quick Reference

circle-exclamation
Syntax
Instantiation
Plugins Share?
Use Case

Service: Service()

Once

Yes

Stateless utilities, shared caches

Service: lambda: Service()

Per plugin

No

Routers, plugin-specific state


Summary

Method
Purpose
When It Runs

__init__

Configure handler properties

When handler is created

create_injections()

Define services to inject

During plugin instantiation

initialize_plugin()

Post-injection setup

After plugin is fully created

Last updated