-
Notifications
You must be signed in to change notification settings - Fork 0
Lifecycle
PluginForge manages plugin lifecycle through three phases: init, activate, and deactivate.
pre_activate()
|
[Discovered] --> init() --> [Initialized] --+--> activate() --> [Active]
| | |
config_schema (rejected) deactivate()
validation | |
[Skipped] [Deactivated]
|
reload_plugin()
|
[Re-Active]
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_configis set to the global app configuration -
self.configis set to the plugin-specific config fromconfig/plugins/{name}.yaml - If
config_schemais 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}.
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().
plugin.activate() is called after successful init and pre-activate check.
What happens at this point:
- The plugin is registered with pluggy - its
@hookimplmethods become callable - The plugin is tracked as "active"
-
self.configandself.app_configare 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().
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
@hookimplmethods 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.
| 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 |
reload_plugin(name) performs a full cycle:
-
deactivate()the running plugin - Unregister from pluggy
- Re-import the plugin's Python module from disk
- Re-instantiate the plugin class
-
init()with current config - Run
pre_activatecheck (if configured) - Register with pluggy
-
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.).
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.
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.
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 trackingAll methods return bool to indicate success or failure.
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 orderQuery 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"]