creating a napari plugin¶
This document explains how to extend napari’s functionality by writing a plugin
that can be installed with pip
and autodetected by napari. For more
information on how plugins are implemented internally in napari, see
napari plugin architecture.
Overview¶
napari
supports plugin development through hooks:
specific places in the napari
codebase where functionality can be extended.
For example, when a user tries to open a filepath in napari, we
might want to enable plugins to extend the file formats that can be handled. A
hook, then, is the place within napari where we
“promise” to call functions created by external developers & installed by the user.
1. Hook Specifications: For each supported hook, we have created “hook specifications”, which are well-documented function signatures that define the API (or “contract”) that a plugin developer must adhere to when writing their function that we promise to call somewhere in the napari codebase. See Step 1: Choose a hook specification to implement.
2. Hook Implementations: To make a plugin, plugin developers then write functions (“hook implementations”) and mark that function as meeting the requirements of a specific hook specification offered by napari. See Step 2: Write your hook implementation.
3. Plugin discovery: Plugins that are installed in the same python
environment as napari can make themselves known to napari. napari
will then
scan plugin modules for hook implementations that will then be called at the
appropriate time place during the execution of napari
.
See Step 3: Make your plugin discoverable.
4. Plugin sharing: When you are ready to share your plugin, tag your repo with napari-plugin, push a release to pypi, and announce it on Image.sc. See Step 4: Share your plugin with the world.
Step 1: Choose a hook specification to implement¶
The functionality of plugins, as currently designed and implemented in
napari
, is rather specific in scope: They are not just independent code
blocks with their own GUIs that show up next to the main napari window. Rather,
plugin developers must decide which of the current hook specifications
defined by napari that they would like to implement.
For a complete list of hook specifications that developers can implement, see the napari hook specification reference.
A single plugin package may implement more than one hook specification, but may not declare more the one hook implementation for any given specification.
Note
One of the primary ways that we will extend the functionality of napari over time is by identifying new ideas for hook specifications that developers can implement. If you have a plugin idea that requires napari to create a new hook specification, we’d love to hear about it! Please think about what the signature of your proposed hook specification would look like, and where within the napari codebase you’d like your hook implementation to be called, and open a feature request in the napari issue tracker with your proposal.
Let’s take the napari_get_reader()
hook (our primary “reader plugin” hook) as an example. It is defined as:
LayerData = Union[Tuple[Any], Tuple[Any, Dict], Tuple[Any, Dict, str]]
ReaderFunction = Callable[[str], List[LayerData]]
@napari_hook_specification(firstresult=True)
def napari_get_reader(
path: Union[str, List[str]]
) -> Optional[ReaderFunction]:
...
Note that it takes a str
or a list
of str
and either returns
None
or a function. From the docstring
of the hook
specification, we see that the implementation should return None
if the
path is of an unrecognized format, otherwise it should return a
ReaderFunction
, which is a function that takes a str
(the filepath to
read) and returns a list
of LayerData
, where LayerData
is any one
of (data,)
, (data, meta)
, or (data, meta, layer_type)
.
That seems like a bit of a mouthful! But it’s a precise (though flexible) contract that you can follow, and know that napari will handle the rest.
Step 2: Write your hook implementation¶
Once you have identified the hook specification that you want to implement, you have to create a hook implementation: a function that accepts the arguments specified by the hook specification signature and returns a value with the expected return type.
Here’s an example hook implementation for
napari_get_reader()
that enables
napari to open a numpy binary file with a .npy
extension (previously saved
with numpy.save()
)
import numpy as np
from napari_plugin_engine import napari_hook_implementation
def npy_file_reader(path):
array = np.load(path)
# return it as a list of LayerData tuples,
# here with no optional metadata
return [(array,)]
# this line is explained below in "Decorating your function..."
@napari_hook_implementation
def napari_get_reader(path):
# remember, path can be a list, so we check it's type first...
# (this example plugin doesn't handle lists)
if isinstance(path, str) and path.endswith(".npy"):
# If we recognize the format, we return the actual reader function
return npy_file_reader
# otherwise we return None.
return None
Decorating your function with HookImplementationMarker
¶
In order to let napari
know that one of your functions satisfies the API of
one of the napari hook specifications, you must decorate your function with
an instance of HookImplementationMarker
,
initialized with the name "napari"
. As a convenience, napari provides this
decorator at napari_plugin_engine.napari_hook_implementation
as shown in
the example above.
However, it’s not required to import from or depend on napari at all when
writing a plugin. You can import a napari_hook_implementation
decorator
directly from napari_plugin_engine
(a very lightweight dependency that uses
only standard lib python).
from napari_plugin_engine import napari_hook_implementation
Matching hook implementations to specifications¶
By default, napari
matches your implementation to one of our hook
specifications by looking at the name of your decorated function. So in the
example above, because hook implementation was literally
named napari_get_reader
, it gets interpreted as an implementation for the
hook specification of the same name.
@napari_hook_implementation
def napari_get_reader(path: str):
...
However, you may also mark any function as satisfying a particular napari
hook specification (regardless of the function’s name) by providing the name of
the target hook specification to the specname
argument in your
implementation decorator:
@napari_hook_implementation(specname="napari_get_reader")
def whatever_name_you_want(path: str):
...
This allows you to specify multiple hook implementations of the same hook specification in the same module or class, without needing a separate entry point.
Step 3: Make your plugin discoverable¶
Packages and modules installed in the same environment as napari
may make
themselves “discoverable” to napari using package metadata, as outlined in the
Python Packaging Authority guide.
By providing an entry_points
argument with the key napari.plugin
to
setup()
in setup.py
, plugins can register themselves for discovery.
For example if you have a package named mypackage
with a submodule
napari_plugin
where you have decorated one or more napari hook
implementations, then if you include in setup.py
:
# setup.py
setup(
...
entry_points={'napari.plugin': 'plugin_name = mypackage.napari_plugin'},
...
)
… then napari will search the mypackage.napari_plugin
module for
functions decorated with the HookImplementationMarker("napari")
decorator
and register them the plugin name "plugin_name"
.
A user would then be able to use napari
, extended with your package’s
functionality by simply installing your package along with napari:
pip install napari mypackage
Cookiecutter template¶
To quickly generate a new napari plugin project, you may wish to use the cookiecutter-napari-plugin template. This uses the cookiecutter command line utility, which will ask you a few questions about your project and get you started with a ready-to-go package layout where you can begin implementing your plugin.
Install cookiecutter and use the template as follows:
pip install cookiecutter
cookiecutter https://github.com/napari/cookiecutter-napari-plugin
Example Plugins¶
For a minimal working plugin example, see the napari-dv plugin, which allows napari
to
read the Priism/MRC/Deltavision image file format.
For a more thorough plugin see napari-aicsimageio, one of the first community plugins developed for napari. This plugin takes advantage of entry_point discovery to offer multiple readers for both in-memory and lazy-loading of image files.
More examples of plugins can be found on the “napari-plugin” Github topic.
Help¶
If you run into trouble creating your plugin, please don’t hesitate to reach out for help in the Image.sc Forum. Alternatively, if you find a bug or have a specific feature request for plugin support, please open an issue at our github issue tracker.