-
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:
def init(self, app_config: dict, plugin_config: dict) -> None:
self.app_config = app_config
self.config = plugin_configOverride to perform custom initialization:
def init(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.
def activate(self) -> None:
self.connection = create_connection(self.db_url)Called when the plugin is being shut down. Release resources here.
def deactivate(self) -> None:
if self.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.).
def on_config_changed(self, old: dict, new: dict) -> None:
new_log_level = new.get("logging", {}).get("level")
if new_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.
from fastapi import APIRouter
def get_routes(self) -> list:
router = APIRouter()
@router.get("/hello")
def hello():
return {"message": self.config.get("greeting", "Hello")}
return [router]Return a dict describing frontend UI components. Useful for apps with plugin-driven UIs.
def get_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.).
def health(self) -> dict:
try:
self.api_client.ping()
return {"status": "ok", "latency_ms": 12}
except Exception as e:
return {"status": "error", "error": str(e)}Returns {"status": "ok"} by default.
Return the path to Alembic migration scripts. Only relevant when using Alembic integration.
def get_migrations_dir(self) -> str | None:
return str(Path(__file__).parent / "migrations")Plugins can declare expected config types via config_schema. Validation happens automatically during init():
class ExportPlugin(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:
class BibliogonPlugin(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.
import pluggy
from pluginforge import BasePlugin
hookimpl = pluggy.HookimplMarker("myapp")
class ExportPlugin(BasePlugin):
name = "export"
version = "1.0.0"
target_application = "myapp" # v0.7.0: declare which host this targets
min_app_version = "1.2.0" # v0.6.0: minimum host version
description = "Export documents to various formats"
author = "Asterios Raptis"
depends_on = ["storage"]
config_schema = {"formats": list, "pandoc_path": str}
def init(self, app_config: dict, plugin_config: dict) -> None:
super().init(app_config, plugin_config)
self.formats = self.config.get("formats", ["pdf"])
def activate(self) -> None:
self.engine = self._create_engine()
def deactivate(self) -> None:
self.engine = None
def on_config_changed(self, old: dict, new: dict) -> None:
# v0.6.0: react to live config changes
new_formats = new.get("export", {}).get("formats")
if new_formats and new_formats != old.get("export", {}).get("formats"):
self.formats = new_formats
def health(self) -> dict:
return {"status": "ok", "formats": self.formats}
@hookimpl
def on_document_save(self, document: dict) -> None:
for fmt in self.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.