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

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

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:

def init(self, app_config: dict, plugin_config: dict) -> None:
    self.app_config = app_config
    self.config = plugin_config

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

activate()

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)

deactivate()

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

def deactivate(self) -> None:
    if self.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.).

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.

Optional Extension Methods

get_routes()

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]

get_frontend_manifest()

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.

health()

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.

get_migrations_dir()

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

Config Schema Validation

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.

Application Identity (v0.7.0)

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.0
Plugin 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.

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)

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)

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