Skip to content

Specs and implementations

The markers are the smallest part of the framework but they set up everything else. Source: pluginkit/markers.py.

How a marker works

A marker is a callable bound to a project name. When it decorates a function it stamps a small frozen dataclass of options onto that function under a project-namespaced attribute. Nothing else happens at decoration time - the manager reads those attributes later by introspection.

extension_point = ExtensionPoint("kitchen")   # stamps  func.kitchen_spec = ExtensionPointOpts(...)
extension = Extension("kitchen")   # stamps  func.kitchen_impl = ExtensionOpts(...)

Because the marker only sets an attribute and returns the function unchanged, a decorated function is still perfectly callable on its own - handy for testing a plugin method directly.

Bare vs called forms

Both markers support @extension and @extension(...). This is expressed with typing.overload so a type checker understands both:

@extension                       # bare form
def add_ingredients(self, base): ...

@extension(tryfirst=True)        # called form with options
def prep_step(self, steps): ...

Spec options

@extension_point accepts:

Option Meaning
firstresult Stop at the first non-None result and return it directly. See First result.
historic Remember calls and replay them to plugins registered later. See Historic hooks.

A spec cannot be both historic and firstresult; the manager rejects that combination when the specs are added.

Implementation options

@extension accepts:

Option Meaning
tryfirst Run earlier than normal implementations.
trylast Run later than normal implementations.
wrapper This implementation is a generator that wraps the others. See Wrappers.
optional Do not error if the host never declared a matching spec.
target Bind to a spec whose name differs from the method name.

Empty spec bodies

Spec bodies are intentionally empty - a docstring and nothing more. The manager never calls them; they exist only to declare a name and signature. The type checkers are told to ignore the "missing return" complaint for the specs module.

@extension_point
def choose_cup(size: str) -> str | None:
    """Pick a cup for the size; the first plugin to answer wins."""
    # no body on purpose

A structural contract with Protocol

The specs module also defines a typing.Protocol so a checker can confirm, structurally, that a plugin's method lines up with the hook it implements - no base class or registration needed:

@runtime_checkable
class IngredientProvider(Protocol):
    def add_ingredients(self, base: list[str]) -> list[str]: ...

This pairs nicely with the runtime validation the manager performs at registration: the Protocol catches signature drift in the type checker, and the manager catches it again at run time.