Skip to content
Asterios Raptis edited this page Mar 27, 2026 · 1 revision

Hooks

PluginForge uses pluggy for its hook system. Pluggy is the same system used by pytest, tox, and datasette.

Concepts

  • Hook Spec - Defines the interface (what hooks exist and their signatures)
  • Hook Impl - A plugin's implementation of a hook
  • Hook Caller - The application calling a hook on all registered plugins

Defining Hook Specs

Create a module with @hookspec-decorated functions:

# myapp/hookspecs.py
import pluggy

hookspec = pluggy.HookspecMarker("myapp")

class MyAppHookSpec:
    @hookspec
    def on_startup(self) -> None:
        """Called when the application starts."""

    @hookspec
    def on_document_save(self, document: dict) -> dict | None:
        """Called when a document is saved. Can modify the document."""

    @hookspec(firstresult=True)
    def get_renderer(self, format: str) -> object | None:
        """Return a renderer for the given format. First non-None result wins."""

Register the specs with PluginManager:

from myapp.hookspecs import MyAppHookSpec

pm = PluginManager("config/app.yaml")
pm.register_hookspecs(MyAppHookSpec)

Implementing Hooks

Plugins implement hooks using @hookimpl:

import pluggy
from pluginforge import BasePlugin

hookimpl = pluggy.HookimplMarker("myapp")

class ExportPlugin(BasePlugin):
    name = "export"

    @hookimpl
    def on_startup(self) -> None:
        print("ExportPlugin ready")

    @hookimpl
    def on_document_save(self, document: dict) -> dict | None:
        document["exported"] = True
        return document

The marker name ("myapp") must match between HookspecMarker and HookimplMarker.

Calling Hooks

# Call all implementations
results = pm.call_hook("on_startup")

# Call with arguments
results = pm.call_hook("on_document_save", document={"title": "Hello"})

call_hook() returns a list of results from all implementations. If the hook is not found, it logs a warning and returns an empty list.

Hook Wrappers

Since pluggy >= 1.1.0, hook wrappers require explicit wrapper=True:

@hookimpl(wrapper=True)
def on_document_save(self, document: dict) -> dict | None:
    # Code before all other implementations
    print("Before save")

    # Yield to run other implementations
    result = yield

    # Code after all other implementations
    print("After save")
    return result

pluggy vs. PluginForge

Concern Managed by
Hook specs and implementations pluggy
Hook calling and result collection pluggy
Plugin discovery (entry points) PluginForge
Plugin lifecycle (init/activate/deactivate) PluginForge
YAML configuration PluginForge
Dependency resolution PluginForge
Enable/disable PluginForge

They are complementary layers, not competing.

Clone this wiki locally