Skip to content

BasePlugin

Asterios Raptis edited this page May 21, 2026 · 5 revisions

BasePlugin

BasePlugin is the abstract base class that all PluginForge plugins must inherit from.

Class Attributes

AttributeTypeDefaultDescription
namestr(required)Unique plugin identifier
versionstr"0.1.0"Plugin version
api_versionstr"1"Hook spec compatibility version
min_app_versionstr | NoneNoneMinimum host application version required (v0.6.0). Enforced only when the host sets app_version on the PluginManager.
target_applicationstr | NoneNoneIdentifier 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.
descriptionstr""Human-readable description
authorstr""Plugin author
depends_onlist[str][]Plugin names this plugin depends on
app_configdict{}Global app config (populated by init(), refreshed by refresh_config() since v0.6.0)
configdict{}Plugin-specific config (populated by init())
config_schemadict[str, type] | NoneNoneOptional config type validation schema

Lifecycle Methods

init(app_config, plugin_config)

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_config

Override 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")

activate()

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)

deactivate()

Called when the plugin is being shut down. Release resources here.

defdeactivate(self) ->None:
ifself.connection:
self.connection.close()

on_config_changed(old, new)(v0.6.0)

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.

Optional Extension Methods

get_routes()

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]

get_frontend_manifest()

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.

health()

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.

get_migrations_dir()

Return the path to Alembic migration scripts. Only relevant when using Alembic integration.

defget_migrations_dir(self) ->str|None:
returnstr(Path(__file__).parent/"migrations")

Config Schema Validation

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.

Application Identity (v0.7.0)

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.0
Plugin target_applicationHost app_idOutcome (v0.9.0)
"bibliogon""bibliogon"Activate
"bibliogon""adaptivlearner"Filter with wrong_application
"bibliogon"not declaredActivate (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 declarednot declaredActivate (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.

Inspecting a plugin at runtime

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.

Complete Example (v0.7.0 best practice)

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)

Design Note: BasePlugin + pluggy

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.

Clone this wiki locally