Skip to content

Lifecycle

Asterios Raptis edited this page Mar 27, 2026 · 3 revisions

Lifecycle

PluginForge manages plugin lifecycle through three phases: init, activate, and deactivate.

State Machine

                                       pre_activate()
                                            |
[Discovered] --> init() --> [Initialized] --+--> activate() --> [Active]
                  |                         |                      |
             config_schema              (rejected)          deactivate()
              validation                    |                      |
                                      [Skipped]             [Deactivated]
                                                                   |
                                                          reload_plugin()
                                                                   |
                                                            [Re-Active]

Phases

1. Init

plugin.init(app_config, plugin_config) is called first. The plugin receives both the global app config and its own plugin-specific config.

What happens at this point:

  • self.app_config is set to the global app configuration
  • self.config is set to the plugin-specific config from config/plugins/{name}.yaml
  • If config_schema is defined, config values are type-checked
  • The plugin is tracked as "initialized" but not yet "active"
  • No hooks are registered yet - the plugin cannot receive hook calls

If init() fails: The plugin is skipped entirely. The error is logged and reported in get_load_errors(). Other plugins are not affected.

If config_schema validation fails: Same as init failure. Example: toc_depth: "abc" when config_schema = {"toc_depth": int}.

2. Pre-Activate Check

If a pre_activate callback is configured on the PluginManager, it is called with (plugin, plugin_config) before activation. If it returns False, the plugin is skipped.

What you can check here:

  • License validation (is this premium plugin licensed?)
  • Permission checks (does the user have access to this plugin?)
  • Environment checks (are required external services available?)
def check_license(plugin, config):
    if plugin.name in premium_plugins:
        return validate_license(plugin.name)
    return True

pm = PluginManager("config/app.yaml", pre_activate=check_license)

If rejected: Plugin is not activated, not registered with pluggy, and the rejection is reported in get_load_errors().

3. Activate

plugin.activate() is called after successful init and pre-activate check.

What happens at this point:

  • The plugin is registered with pluggy - its @hookimpl methods become callable
  • The plugin is tracked as "active"
  • self.config and self.app_config are available
  • This is where plugins should start services, open connections, or register resources
def activate(self) -> None:
    self.connection = create_connection(self.db_url)
    self.cache = {}

If activate() fails: The plugin is unregistered from pluggy, error logged, reported in get_load_errors().

4. Deactivate

plugin.deactivate() is called during shutdown or when explicitly deactivating a plugin.

What happens:

  • deactivate() is called on the plugin
  • The plugin is unregistered from pluggy - its @hookimpl methods are no longer called
  • The plugin is removed from the active list
def deactivate(self) -> None:
    if self.connection:
        self.connection.close()
    self.cache.clear()

If deactivate() fails: The error is logged, but deactivation continues. The plugin is considered deactivated even if deactivate() threw an exception. This prevents one broken plugin from blocking shutdown of the entire application.

Reverse order: deactivate_all() processes plugins in reverse activation order (LIFO). If plugin B depends on plugin A, B is deactivated before A. This ensures dependencies are still available when a plugin's deactivate() runs.

Deactivation Failure Scenarios

Scenario Behavior
deactivate() raises exception Error logged, plugin still removed from active list
Plugin has dependent plugins Dependents are deactivated first (LIFO order)
deactivate() hangs No timeout mechanism - plugin blocks shutdown
Plugin was never activated deactivate_plugin() returns False, no error
Multiple failures during deactivate_all() Each error is logged, all plugins are processed

Hot-Reload

reload_plugin(name) performs a full cycle:

  1. deactivate() the running plugin
  2. Unregister from pluggy
  3. Re-import the plugin's Python module from disk
  4. Re-instantiate the plugin class
  5. init() with current config
  6. Run pre_activate check (if configured)
  7. Register with pluggy
  8. activate() the new instance
pm.reload_plugin("export")

Important: The old plugin instance is discarded. Any state held in the old instance is lost. If you need persistent state across reloads, store it externally (database, file, etc.).

Error Handling Summary

Each lifecycle phase is wrapped in error handling:

Phase On Error
init() Plugin skipped, error logged, reported in get_load_errors()
config_schema Plugin skipped, error logged, reported in get_load_errors()
pre_activate Plugin skipped, reported in get_load_errors()
activate() Plugin unregistered from pluggy, error logged, reported in get_load_errors()
deactivate() Error logged, continues with remaining plugins
Hook execution Error logged, other hooks still execute (see Graceful Degradation)

Errors in one plugin never prevent other plugins from being processed.

Graceful Degradation

When calling hooks, a single plugin's hook implementation can fail without affecting others:

# Standard call - if any implementation throws, returns []
results = pm.call_hook("on_save", document=doc)

# Safe call - calls each implementation individually, skips failures
results = pm.call_hook_safe("on_save", document=doc)

call_hook_safe() is recommended for non-critical hooks where partial results are acceptable (notifications, analytics, etc.). Use call_hook() for critical hooks where all-or-nothing semantics are needed.

PluginLifecycle Class

The PluginLifecycle class tracks state internally:

lifecycle = PluginLifecycle()

# Init
lifecycle.init_plugin(plugin, app_config, plugin_config)  # -> bool

# Activate
lifecycle.activate_plugin(plugin)   # -> bool

# Query
lifecycle.is_active("export")       # -> bool
lifecycle.get_plugin("export")      # -> BasePlugin | None
lifecycle.get_active_plugins()      # -> list[BasePlugin]

# Deactivate
lifecycle.deactivate_plugin(plugin) # -> bool
lifecycle.deactivate_all()          # reverse order

# Remove (used during hot-reload)
lifecycle.remove_plugin("export")   # clears all tracking

All methods return bool to indicate success or failure.

Typical Flow

pm = PluginManager("config/app.yaml")
pm.discover_plugins()       # init + activate all discovered plugins

# Check what failed
for name, error in pm.get_load_errors().items():
    print(f"Plugin '{name}': {error}")

# ... application runs ...

pm.reload_plugin("export")  # hot-reload during development

# ... application continues ...

pm.deactivate_all()         # deactivate in reverse order

Introspection

Query which hooks a plugin implements or which hook specs are registered:

# Which hooks does the "export" plugin implement?
hooks = pm.get_plugin_hooks("export")
# ["export_execute", "on_document_save"]

# Which hook specs are registered?
all_hooks = pm.get_all_hook_names()
# ["on_startup", "on_shutdown", "on_document_save", "export_execute"]

Clone this wiki locally