Calling conventions¶
Once plugins are registered, calling a hook runs a small dispatch engine inside
HookCaller. This page explains the rules that engine follows; each rule has a
dedicated mechanism page with a runnable demo.
Typed calls: pm.caller¶
pm.caller(spec) returns a caller whose result type is derived from the spec's
dispatch mode and checked by mypy and pyright - no hand annotations:
ingredients = pm.caller(Specs.add_ingredients)(base=["banana"]) # list[list[str]]
cup = pm.caller(Specs.choose_cup)(size="small") # str | None
pm.hook.<name>(...) works too and is more concise, but it is untyped (returns
Any). Use pm.hook for quick scripts and pm.caller when you want the type
checker's help; both share one PluginManager, so you never need a manager per spec.
Arguments¶
Calls are typically made with keyword arguments, though positional ones bind to the spec's parameters in order (matching the typed caller's signature):
pm.caller(Specs.add_ingredients)(base=["banana"]) # keyword (clearest)
pm.caller(Specs.add_ingredients)(["banana"]) # positional also works
Keyword form is what lets each implementation declare only the arguments it cares
about. The caller passes the full set of kwargs; each implementation receives
the subset matching its own signature, computed once at registration with
inspect.signature.
@extension
def add_ingredients(self): # ignores base entirely
return ["honey"]
@extension
def add_ingredients(self, base): # receives base
return [] if "berry" in base else ["blueberry"]
Collecting vs first result¶
By default a hook is collecting: every implementation runs and the non-None
results are returned as a list.
pm.caller(Specs.add_ingredients)(base=["banana"])
# [['blueberry', 'strawberry'], ['spinach', 'kale']] # typed list[list[str]]
A spec marked firstresult stops at the first implementation that returns a
non-None value and returns that value directly - not a list (typed R | None):
None results are skipped in both modes, so an implementation can abstain simply
by returning None.
A third mode, pipeline, threads the result of each
implementation into the next and returns the final value - a fold/middleware
chain.
Order¶
Within a hook, implementations run in this order:
- everything marked
tryfirst, - then normal implementations,
- then everything marked
trylast.
Inside each of those three buckets, order is registration order (first registered runs first). See Ordering for the full story and the difference from pluggy.
Wrappers¶
An implementation marked wrapper=True is a generator that wraps the call: code
before its yield runs first, the value sent back to the yield is the result of
the inner (non-wrapper) implementations, and whatever the generator returns
replaces that result. Wrappers also observe exceptions. See
Wrappers.
Historic calls¶
A historic hook is called through call_historic instead of a plain call. The
caller remembers the call and replays it to any plugin registered afterwards, so
late-loading plugins still see one-off startup events. See
Historic hooks.