Hardening notes¶
pluginkit started as a teaching reimplementation. This page records what was done to move it past a toy, and why each item matters in a real plugin host.
Wrapper exception safety¶
The single most important fix. A wrapper holds control across its yield, so if
an inner implementation raises, the wrapper must still be resumed - otherwise any
try/finally or with it holds would leak.
The dispatch loop runs the inner implementations inside a try, then unwinds the
started wrappers in reverse, throwing the exception into each one. A wrapper
may clean up and re-raise, or swallow the exception by returning a value. Only
after every wrapper has been resumed is the exception re-raised (if still
pending). See HookCaller._teardown.
Covered by test_wrapper_cleanup_runs_when_inner_impl_raises and
test_wrapper_can_suppress_inner_exception.
Two malformed-wrapper edges are handled the same way. A wrapper that returns
before yielding raises the documented RuntimeError rather than leaking a bare
StopIteration. A wrapper that yields twice is closed, and if its own cleanup
raises, that error carries the contract violation as its cause and the remaining
wrappers are still unwound explicitly instead of being left to garbage collection.
Fail-fast validation¶
Registration and typed caller resolution turn silent misbehaviour into loud errors:
- unknown hook - an implementation whose name matches no spec raises
PluginValidationError(unless markedoptional). - unknown argument - an implementation that declares a parameter the spec does
not have raises
PluginValidationError. Without this, a typo likedef greet(self, nam)would silently never receive its value, because the kwarg filter would simply drop it. - contradictory order - an implementation cannot be both
tryfirstandtrylast. - typed spec identity -
caller(spec)accepts only the exact extension point registered with that manager, not a foreign same-named function.
Plugin lifecycle¶
A real host needs to remove and disable plugins, not just add them:
unregister(name)drops a plugin and all its implementations, including when a plugin does so from inside its own historic replay during registration;set_blocked(name)removes it and refuses future registration (including via entry-point discovery);is_registered,get_plugin,get_name,get_hookcallers, andplugin_namescover inspection.
Resilient discovery¶
load_entrypoints isolates each plugin load. A broken third-party plugin raises a
PluginValidationError that names it, or is skipped entirely with
ignore_errors=True, so one bad package cannot block all discovery.
load_entrypoints_report provides resilient discovery with structured loaded,
already-loaded, blocked, and failed outcomes while retaining original exceptions.
Thread-safe mutation¶
Registry mutations are guarded by a re-entrant lock, so plugins can be loaded from multiple threads. Hook calls are intentionally lock-free; see Lifecycle and validation for the rationale.
Spec-combination validation¶
A historic spec cannot also be firstresult (replay has no single winner); the
manager rejects the combination when specs are added, rather than failing
mysteriously at call time.
Packaging quality¶
- zero runtime dependencies - the library is standard-library only;
- ships a
py.typedmarker so downstreams get the types; - a curated public API in
pluginkit/__init__.pywith__version__; - strict
ruff+mypy+pyright, and a test suite covering the happy paths, the error paths, and the examples.
What is deliberately still out of scope¶
See Differences from pluggy for the features pluginkit does not implement and when you should reach for pluggy instead.