pluggy plugin system with hookspecs, firstresult hooks and wrappers: working versions (tested 2026-10-18)
A pluggy plugin system with hookspecs, LIFO hookimpl ordering, tryfirst/trylast, firstresult scoring and wrapper hooks, all tested offline.
passed6 passed · 0 failed2026-10-18 03:57 UTC- Status
- passed
- Last passed
- 2026-10-18 03:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
pluggy@1.7.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 39095370682
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pluggy-plugin-hooks") (install), or see them on GitHub.
requirements.txt
pluggy==1.7.0 pytest==9.1.1
hookspecs.py
import pluggy
project_name = "provenpin"
hookspec = pluggy.HookspecMarker(project_name)
hookimpl = pluggy.HookimplMarker(project_name)
class ProcessorSpec:
@hookspec
def enrich(self, record: dict) -> None:
"""Mutate the record in place. Every plugin gets a chance to run."""
@hookspec(firstresult=True)
def score(self, record: dict) -> int | None:
"""Return the first non-None score, or None when no plugin scores it."""
@hookspec
def audit(self, event: str, payload: dict) -> None:
"""Side-effect hook. All plugins and wrappers are called."""
plugins.py
from hookspecs import hookimpl
class Enricher:
def __init__(self, tag: str):
self.tag = tag
@hookimpl
def enrich(self, record: dict) -> None:
record.setdefault("order", []).append(self.tag)
class FirstEnricher:
@hookimpl(tryfirst=True)
def enrich(self, record: dict) -> None:
record.setdefault("order", []).append("first")
class LastEnricher:
@hookimpl(trylast=True)
def enrich(self, record: dict) -> None:
record.setdefault("order", []).append("last")
class PriorityScorer:
@hookimpl(tryfirst=True)
def score(self, record: dict) -> int | None:
return record.get("priority")
class FallbackScorer:
@hookimpl(trylast=True)
def score(self, record: dict) -> int | None:
return 1
class ScoreClamp:
@hookimpl(wrapper=True)
def score(self, record: dict):
raw = yield
return min(raw or 0, 50)
class Outer:
def __init__(self, log: list):
self.log = log
@hookimpl(wrapper=True, tryfirst=True)
def audit(self, event: str, payload: dict):
self.log.append("outer-before")
try:
return (yield)
finally:
self.log.append("outer-after")
class Inner:
def __init__(self, log: list):
self.log = log
@hookimpl(wrapper=True, trylast=True)
def audit(self, event: str, payload: dict):
self.log.append("inner-before")
try:
return (yield)
finally:
self.log.append("inner-after")
class AuditLog:
def __init__(self, log: list):
self.log = log
@hookimpl
def audit(self, event: str, payload: dict) -> None:
self.log.append(f"impl:{event}")
core.py
import pluggy
from hookspecs import ProcessorSpec, project_name
def build_pm() -> pluggy.PluginManager:
pm = pluggy.PluginManager(project_name)
pm.add_hookspecs(ProcessorSpec)
return pm
def process(pm: pluggy.PluginManager, record: dict) -> dict:
pm.hook.enrich(record=record)
record["score"] = pm.hook.score(record=record)
pm.hook.audit(event="processed", payload=record)
return record
tests/test_hooks.py
import pluggy
import pytest
from core import build_pm, process
from hookspecs import hookimpl
from plugins import (
AuditLog,
Enricher,
FallbackScorer,
FirstEnricher,
Inner,
LastEnricher,
Outer,
PriorityScorer,
ScoreClamp,
)
def test_all_impls_run_in_lifo_order_unless_tryfirst_or_trylast_says_otherwise():
pm = build_pm()
pm.register(Enricher("a"))
pm.register(Enricher("b"))
pm.register(FirstEnricher())
pm.register(LastEnricher())
record = process(pm, {})
assert record["order"] == ["first", "b", "a", "last"]
def test_firstresult_takes_first_non_none_and_falls_through_to_trylast():
pm = build_pm()
pm.register(PriorityScorer())
pm.register(FallbackScorer())
assert process(pm, {"priority": 7})["score"] == 7
assert process(pm, {})["score"] == 1
assert process(build_pm(), {})["score"] is None
def test_wrapper_can_transform_a_firstresult_value():
pm = build_pm()
pm.register(ScoreClamp())
pm.register(PriorityScorer())
assert process(pm, {"priority": 100})["score"] == 50
def test_tryfirst_wrapper_is_outermost_and_trylast_wrapper_is_innermost():
pm = build_pm()
log = []
pm.register(AuditLog(log))
pm.register(Inner(log))
pm.register(Outer(log))
process(pm, {})
assert log == [
"outer-before",
"inner-before",
"impl:processed",
"inner-after",
"outer-after",
]
def test_unknown_hookimpl_registers_silently_until_check_pending():
class Rogue:
@hookimpl
def does_not_exist(self):
return 1
pm = build_pm()
pm.register(Rogue())
with pytest.raises(pluggy.PluginValidationError):
pm.check_pending()
def test_optionalhook_lets_a_plugin_ahead_of_its_hookspec_register_cleanly():
class Future:
@hookimpl(optionalhook=True)
def does_not_exist_yet(self):
return 1
pm = build_pm()
pm.register(Future(), "future")
assert pm.get_plugin("future").__class__ is Future
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.