napari Plugin Engine
napari-plugin-engine is a Python library providing a generic plugin management system, forked from the widely used pluggy project. It enables applications like napari to discover, load, and execute plugins by defining clear hook specifications and allowing multiple plugin implementations. As of version 0.2.1, it provides a stable API for extending applications. Its release cadence is tied to its development within the napari ecosystem.
Common errors
-
AttributeError: 'HookRelay' object has no attribute 'my_hook'
cause The PluginManager's hook relay cannot find the specified hook. This usually means the hook specification (with `@hookspec`) was not added to the PluginManager, or the `specname_prefix` did not match.fixEnsure you have called `pm.add_hookspecs(MyHooksClass)` with the class containing your `@hookspec` definitions. Also, check that the `specname_prefix` used when adding hookspecs matches any prefix you might be expecting for the hook name. -
TypeError: my_hook() missing 1 required positional argument: 'arg1'
cause A hook implementation's signature does not match the hook specification or the arguments provided during the hook call.fixReview both the `@hookspec` definition and the `@hookimpl` definition for `my_hook`. Ensure that the `hookimpl` accepts all arguments defined in the `hookspec` and that all required arguments are passed when calling `pm.hook.my_hook(...)`. -
ModuleNotFoundError: No module named 'napari_plugin_engine'
cause The `napari-plugin-engine` package is not installed in your current Python environment.fixInstall the package using pip: `pip install napari-plugin-engine`.
Warnings
- gotcha Each PluginManager instance requires a unique `project_name` string during initialization and for its `HookspecMarker` and `HookimplMarker`. Reusing project names across different logical plugin systems or in different parts of an application can lead to unexpected plugin interactions or discovery issues.
- gotcha Hook specification (hookspec) and hook implementation (hookimpl) signatures must match, or the hookimpl must be compatible (e.g., accepting `**kwargs` for extra arguments). Mismatches will result in `TypeError` when the hook is called, or the hookimpl might not be recognized.
- gotcha When using `firstresult=True` on a hookspec, only the first hook implementation that returns a non-`None` value will have its result used. If all registered hookimpls return `None`, the hook call itself will return `None` (or the default set in the hookspec).
Install
-
pip install napari-plugin-engine
Imports
- PluginManager
from napari_plugin_engine import PluginManager
- HookspecMarker
from napari_plugin_engine import HookspecMarker
- HookimplMarker
from napari_plugin_engine import HookimplMarker
Quickstart
from napari_plugin_engine import PluginManager, HookspecMarker, HookimplMarker
# 1. Define unique markers for your project
project_name = "my_app_plugins"
hookspec = HookspecMarker(project_name)
hookimpl = HookimplMarker(project_name)
# 2. Define your hook specifications
class MyAppHooks:
@hookspec(firstresult=True)
def get_data_source(self, name: str) -> str:
"""Get data from a named source."""
pass
@hookspec
def process_data(self, data: str) -> str:
"""Process input data and return modified data."""
pass
# 3. Implement a plugin
class MyFileSystemPlugin:
@hookimpl
def get_data_source(self, name: str) -> str:
if name == "filesystem":
return "Data from filesystem"
return None # Crucial for firstresult hooks to allow other plugins
@hookimpl
def process_data(self, data: str) -> str:
return f"Processed: {data.upper()}"
class MyNetworkPlugin:
@hookimpl
def get_data_source(self, name: str) -> str:
if name == "network":
return "Data from network"
return None
# 4. Initialize the PluginManager
pm = PluginManager(project_name)
# Add hook specifications. Optional prefix can differentiate similar hook names.
pm.add_hookspecs(MyAppHooks, specname_prefix=f'{project_name}_')
# 5. Register plugins
pm.register(MyFileSystemPlugin())
pm.register(MyNetworkPlugin())
# 6. Call hooks
print(f"--- Hook Calls ---")
data_fs = pm.hook.get_data_source(name="filesystem")
print(f"get_data_source('filesystem'): {data_fs}")
data_net = pm.hook.get_data_source(name="network")
print(f"get_data_source('network'): {data_net}")
processed_data_fs = pm.hook.process_data(data=data_fs)
print(f"process_data(data_fs): {processed_data_fs}")
processed_data_net = pm.hook.process_data(data=data_net)
print(f"process_data(data_net): {processed_data_net}")