- Notifications
You must be signed in to change notification settings - Fork 0
BasePlugin
BasePlugin is the abstract base class that all PluginForge plugins must inherit from.
| Attribute | Type | Default | Description |
|---|---|---|---|
name | str | (required) | Unique plugin identifier |
version | str | "0.1.0" | Plugin version |
api_version | str | "1" | Hook spec compatibility version |
min_app_version | str | None | None | Minimum host application version required (v0.6.0). Enforced only when the host sets app_version on the PluginManager. |
target_application | str | None | None | Identifier of the host application this plugin targets (v0.7.0). Validated against the host's app_id when both are set. See "Application identity" below. |
description | str | "" | Human-readable description |
author | str | "" | Plugin author |
depends_on | list[str] | [] | Plugin names this plugin depends on |
app_config | dict | {} | Global app config (populated by init(), refreshed by refresh_config() since v0.6.0) |
config | dict | {} | Plugin-specific config (populated by init()) |
config_schema | dict[str, type] | None | None | Optional config type validation schema |
Called when the plugin is loaded. Receives both the global application config and the plugin-specific config from YAML. The default implementation stores both:
definit(self, app_config: dict, plugin_config: dict) ->None:
self.app_config=app_configself.config=plugin_configOverride to perform custom initialization:
definit(self, app_config: dict, plugin_config: dict) ->None:
super().init(app_config, plugin_config)
self.db_url=app_config.get("database", {}).get("url")
self.output_dir=self.config.get("output_dir", "/tmp")Called after init() when the plugin is activated. Use this to start services, open connections, or register resources.
defactivate(self) ->None:
self.connection=create_connection(self.db_url)Called when the plugin is being shut down. Release resources here.
defdeactivate(self) ->None:
ifself.connection:
self.connection.close()Called by PluginManager.refresh_config() when the app-config snapshot changes. Default implementation is a no-op. Override only if the plugin needs to react to config changes (cache invalidation, reconnecting external services, etc.).
defon_config_changed(self, old: dict, new: dict) ->None:
new_log_level=new.get("logging", {}).get("level")
ifnew_log_level!=old.get("logging", {}).get("level"):
self.logger.setLevel(new_log_level)The new app_config is already assigned to self.app_config before this hook fires. Stateful plugins holding in-flight work should be careful: the new config applies to future operations, not in-flight ones. See Lifecycle for the full refresh_config() flow.
Return a list of FastAPI APIRouter instances. Only relevant when using FastAPI integration.
fromfastapiimportAPIRouterdefget_routes(self) ->list:
router=APIRouter()
@router.get("/hello")defhello():
return {"message": self.config.get("greeting", "Hello")}
return [router]Return a dict describing frontend UI components. Useful for apps with plugin-driven UIs.
defget_frontend_manifest(self) ->dict|None:
return {
"components": ["ExportDialog", "FormatSelector"],
"css": "/static/plugins/export/style.css",
}Returns None by default.
Return plugin health status. Override to check external dependencies (APIs, databases, etc.).
defhealth(self) ->dict:
try:
self.api_client.ping()
return {"status": "ok", "latency_ms": 12}
exceptExceptionase:
return {"status": "error", "error": str(e)}Returns {"status": "ok"} by default.
Return the path to Alembic migration scripts. Only relevant when using Alembic integration.
defget_migrations_dir(self) ->str|None:
returnstr(Path(__file__).parent/"migrations")Plugins can declare expected config types via config_schema. Validation happens automatically during init():
classExportPlugin(BasePlugin):
name="export"config_schema= {
"pandoc_path": str,
"toc_depth": int,
"default_format": str,
}If a config value has the wrong type, the plugin is filtered with filter_reason="load_failed" and the error is surfaced in DiscoveryResult.errors (and the back-compat pm.get_load_errors()). Missing keys are not an error - only present keys with wrong types are rejected.
When the host declares app_id on its PluginManager, plugins are validated by their target_application:
classBibliogonPlugin(BasePlugin):
name="comics"target_application="bibliogon"# v0.7.0Plugin target_application | Host app_id | Outcome (v0.9.0) |
|---|---|---|
"bibliogon" | "bibliogon" | Activate |
"bibliogon" | "adaptivlearner" | Filter with wrong_application |
"bibliogon" | not declared | Activate (claim unvalidated, plugins remain loaded) |
| not declared | "bibliogon" | Filter with missing_target_application(v0.9.0; was a deprecation warning in v0.7.0/v0.8.0) |
| not declared | not declared | Activate (silent; v0.7.0 deprecation warning retired in v0.9.0) |
Hard-filter transition (shipped v0.9.0). Plugins without target_application are filtered with PluginError(phase="identity_check", severity="error") and filter_reason="missing_target_application" when the host has adopted app_id. The user-facing message references the host's app_id and tells the author what to declare:
Plugin 'X' does not declare target_application. Activation refused by host 'my-app'; declare
target_application = "my-app"in the plugin class.
Hosts that have not adopted app_id remain permissive: their plugins continue to load without identity validation, and the v0.7.0 deprecation warning has been retired in v0.9.0. This mirrors the v0.6.0 min_app_version / app_version pattern: identity protection is opt-in for the host. See docs/design/v0.7.0-application-identity.md for the original design rationale, and docs/design/v0.9.0-lifecycle-visibility.md for the as-shipped behavior.
For the full state of a plugin (lifecycle state, config snapshot, health, hooks, routes, identity), use PluginManager.inspect_plugin(name) rather than threading multiple accessors:
inspection=pm.inspect_plugin("export")
# inspection.state.activated, inspection.state.activated_at,# inspection.config, inspection.health, inspection.hooks, ...See PluginManager#inspect_plugin for the full surface.
importpluggyfrompluginforgeimportBasePluginhookimpl=pluggy.HookimplMarker("myapp")
classExportPlugin(BasePlugin):
name="export"version="1.0.0"target_application="myapp"# v0.7.0: declare which host this targetsmin_app_version="1.2.0"# v0.6.0: minimum host versiondescription="Export documents to various formats"author="Asterios Raptis"depends_on= ["storage"]
config_schema= {"formats": list, "pandoc_path": str}
definit(self, app_config: dict, plugin_config: dict) ->None:
super().init(app_config, plugin_config)
self.formats=self.config.get("formats", ["pdf"])
defactivate(self) ->None:
self.engine=self._create_engine()
defdeactivate(self) ->None:
self.engine=Nonedefon_config_changed(self, old: dict, new: dict) ->None:
# v0.6.0: react to live config changesnew_formats=new.get("export", {}).get("formats")
ifnew_formatsandnew_formats!=old.get("export", {}).get("formats"):
self.formats=new_formatsdefhealth(self) ->dict:
return {"status": "ok", "formats": self.formats}
@hookimpldefon_document_save(self, document: dict) ->None:
forfmtinself.formats:
self.engine.export(document, fmt)A plugin is both:
- A class inheriting
BasePlugin(lifecycle:init,activate,deactivate,on_config_changed) - An object with
@hookimpl-decorated methods (pluggy hook system)
PluginForge manages the lifecycle around the plugin. pluggy manages the hooks. These are complementary, not competing.