1st Gen Plugin Guide (Deprecated)#
DEPRECATED
We introduced a new plugin engine (npe2
) in December 2021.
Plugins targeting the first generation napari-plugin-engine
(described on this page) will continue to work for at least the
first half of 2022, but we recommend that new plugins use npe2
and
existing plugins consider migrating soon. See the
npe2 migration guide for details.
The content below describes the original
napari-plugin-engine
and exists for archival reference purposes during the deprecation period.
Overview#
napari
supports plugin development through hooks:
specific places in the napari codebase where functionality can be extended.
Hook specifications: The available hooks are declared as “hook specifications”: 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.
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.
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 be called at the appropriate time and place during the execution ofnapari
. See Step 3: Make your plugin discoverable.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. Your plugin will then be available for users on the napari hub. See Step 4: Deploy your plugin.
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 they would like to implement.
For a complete list of hook specifications that developers can implement, see the Hook Specification Reference.
A single plugin package may implement more than one hook specification, and each hook specification could have multiple hook implementations within a single package.
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 its 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 the 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 under 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:
python -m pip install napari mypackage
Step 4: Deploy your plugin#
See testing and deploying your plugin. (This hasn’t changed
significantly with the secod generation (npe2
) plugin engine).
Hook Specification Reference#
All napari hook specifications for pluggable functionality are defined here.
A hook specification is a function signature (with documentation) that declares an API that plugin developers must adhere to when providing hook implementations. Hook implementations provided by plugins (and internally by napari) will then be invoked in various places throughout the code base.
When implementing a hook specification, pay particular attention to the number and types of the arguments in the specification signature, as well as the expected return type.
To allow for hook specifications to evolve over the lifetime of napari, hook implementations may accept fewer arguments than defined in the specification. (This allows for extending existing hook arguments without breaking existing implementations). However, implementations must not require more arguments than defined in the spec.
For more general background on the plugin hook calling mechanism, see the napari-plugin-manager documentation.
Note
Hook specifications are a feature borrowed from pluggy. In the pluggy
documentation, hook
specification marker instances are named hookspec
by convention, and
hook implementation marker instances are named hookimpl
. The
convention in napari is to name them more explicitly:
napari_hook_specification
and napari_hook_implementation
,
respectively.
IO hooks#
- napari.plugins.hook_specifications.napari_provide_sample_data() Dict[str, str | Path | Callable[[...], Iterable[Tuple[Any] | Tuple[Any, Dict] | Tuple[Any, Dict, Literal['graph', 'image', 'labels', 'points', 'shapes', 'surface', 'tracks', 'vectors']]]] | SampleDict] [source]#
Provide sample data.
Plugins may implement this hook to provide sample data for use in napari. Sample data is accessible in the File > Open Sample menu, or programmatically, with
napari.Viewer.open_sample()
.Plugins implementing this hook specification must return a
dict
, where each key is a sample_key (the string that will appear in the Open Sample menu), and the value is either a string, or a callable that returns an iterable ofLayerData
tuples, where each tuple is a 1-, 2-, or 3-tuple of(data,)
,(data, meta)
, or(data, meta, layer_type)
(thus, an individual sample-loader may provide multiple layers). If the value is a string, it will be opened withnapari.Viewer.open()
.Examples
Here’s a minimal example of a plugin that provides three samples:
random data from numpy
a random image pulled from the internet
- random data from numpy, provided as a dict with the keys:
- ‘display_name’: a string that will show in the menu (by default,
the sample_key will be shown)
‘data’: a string or callable, as in 1/2.
import numpy as np from napari_plugin_engine import napari_hook_implementation def _generate_random_data(shape=(512, 512)): data = np.random.rand(*shape) return [(data, {'name': 'random data'})] @napari_hook_implementation def napari_provide_sample_data(): return { 'random data': _generate_random_data, 'random image': 'https://picsum.photos/1024', 'sample_key': { 'display_name': 'Some Random Data (512 x 512)' 'data': _generate_random_data, } }
- napari.plugins.hook_specifications.napari_get_reader(path: str | List[str]) Callable[[str | Sequence[str]], List[Tuple[Any] | Tuple[Any, Dict] | Tuple[Any, Dict, Literal['graph', 'image', 'labels', 'points', 'shapes', 'surface', 'tracks', 'vectors']]]] | None [source]#
Return a function capable of loading
path
into napari, orNone
.This is the primary “reader plugin” function. It accepts a path or list of paths, and returns a list of data to be added to the
Viewer
. The function may return[(None, )]
to indicate that the file was read successfully, but did not contain any data.The main place this hook is used is in
Viewer.open()
, via theread_data_with_plugins()
function.It will also be called on
File -> Open...
or when a user drops a file or folder onto the viewer. This function must execute quickly, and should returnNone
if the filepath is of an unrecognized format for this reader plugin. Ifpath
is determined to be recognized format, this function should return a new function that accepts the same filepath (or list of paths), and returns a list ofLayerData
tuples, where each tuple is a 1-, 2-, or 3-tuple of(data,)
,(data, meta)
, or(data, meta, layer_type)
.napari
will then use each tuple in the returned list to generate a new layer in the viewer using theViewer._add_layer_from_data()
method. The first, (optional) second, and (optional) third items in each tuple in the returned layer_data list, therefore correspond to thedata
,meta
, andlayer_type
arguments of theViewer._add_layer_from_data()
method, respectively.Important
path
may be either astr
or alist
ofstr
. If alist
, then each path in the list can be assumed to be one part of a larger multi-dimensional stack (for instance: a list of 2D image files that should be stacked along a third axis). Implementations should do their own checking forlist
orstr
, and handle each case as desired.- Parameters:
path (str or list of str) – Path to file, directory, or resource (like a URL), or a list of paths.
- Returns:
A function that accepts the path, and returns a list of
layer_data
, wherelayer_data
is one of(data,)
,(data, meta)
, or(data, meta, layer_type)
. If unable to read the path, must returnNone
(notFalse
!).- Return type:
Callable or None
- napari.plugins.hook_specifications.napari_get_writer(path: str, layer_types: List[str]) Callable[[str, List[Tuple[Any, Dict, Literal['graph', 'image', 'labels', 'points', 'shapes', 'surface', 'tracks', 'vectors']]]], List[str]] | None [source]#
Return function capable of writing napari layer data to
path
.This function will be called whenever the user attempts to save multiple layers (e.g. via
File -> Save Layers
, orsave_layers()
). This function must execute quickly, and should returnNone
ifpath
has an unrecognized extension for the reader plugin or the list of layer types are incompatible with what the plugin can write. Ifpath
is a recognized format, this function should return a function that accepts the samepath
, and a list of tuples containing the data for each layer being saved in the form of(Layer.data, Layer._get_state(), Layer._type_string)
. The writer function should return a list of strings (the actual filepath(s) that were written).Important
It is up to plugins to inspect and obey any extension in
path
(and returnNone
if it is an unsupported extension).An example function signature for a
WriterFunction
that might be returned by this hook specification is as follows:def writer_function( path: str, layer_data: List[Tuple[Any, Dict, str]] ) -> List[str]: ...
- Parameters:
path (str) – Path to file, directory, or resource (like a URL). Any extensions in the path should be examined and obeyed. (i.e. if the plugin is incapable of returning a requested extension, it should return
None
).layer_types (list of str) – List of layer types (e.g. “image”, “labels”) that will be provided to the writer function.
- Returns:
A function that accepts the path, a list of layer_data (where layer_data is
(data, meta, layer_type)
). If unable to write to the path or write the layer_data, must returnNone
(notFalse
).- Return type:
Callable or None
Single Layers IO#
The following hook specifications will be called when a user saves a single
layer in napari, and should save the layer to the requested format and return
the save path if the data are successfully written. Otherwise, if nothing was saved, return None
.
They each accept a path
.
It is up to plugins to inspect and obey the extension of the path (and return
False
if it is an unsupported extension). The data
argument will come
from Layer.data
, and a meta
dict that will correspond to the layer’s
_get_state()
method.
- napari.plugins.hook_specifications.napari_write_image(path: str, data: Any, meta: dict) str | None [source]#
Write image data and metadata into a path.
It is the responsibility of the implementation to check any extension on
path
and returnNone
if it is an unsupported extension. Ifpath
has no extension, implementations may append their preferred extension.- Parameters:
path (str) – Path to file, directory, or resource (like a URL).
data (array or list of array) – Image data. Can be N dimensional. If meta[‘rgb’] is
True
then the data should be interpreted as RGB or RGBA. If meta[‘multiscale’] is True, then the data should be interpreted as a multiscale image.meta (dict) – Image metadata.
- Returns:
path – If data is successfully written, return the
path
that was written. Otherwise, if nothing was done, returnNone
.- Return type:
str or None
- napari.plugins.hook_specifications.napari_write_labels(path: str, data: Any, meta: dict) str | None [source]#
Write labels data and metadata into a path.
It is the responsibility of the implementation to check any extension on
path
and returnNone
if it is an unsupported extension. Ifpath
has no extension, implementations may append their preferred extension.- Parameters:
path (str) – Path to file, directory, or resource (like a URL).
data (array or list of array) – Integer valued label data. Can be N dimensional. Every pixel contains an integer ID corresponding to the region it belongs to. The label 0 is rendered as transparent. If a list and arrays are decreasing in shape then the data is from a multiscale image.
meta (dict) – Labels metadata.
- Returns:
path – If data is successfully written, return the
path
that was written. Otherwise, if nothing was done, returnNone
.- Return type:
str or None
- napari.plugins.hook_specifications.napari_write_points(path: str, data: Any, meta: dict) str | None [source]#
Write points data and metadata into a path.
It is the responsibility of the implementation to check any extension on
path
and returnNone
if it is an unsupported extension. Ifpath
has no extension, implementations may append their preferred extension.- Parameters:
- Returns:
path – If data is successfully written, return the
path
that was written. Otherwise, if nothing was done, returnNone
.- Return type:
str or None
- napari.plugins.hook_specifications.napari_write_shapes(path: str, data: Any, meta: dict) str | None [source]#
Write shapes data and metadata into a path.
It is the responsibility of the implementation to check any extension on
path
and returnNone
if it is an unsupported extension. Ifpath
has no extension, implementations may append their preferred extension.- Parameters:
- Returns:
path – If data is successfully written, return the
path
that was written. Otherwise, if nothing was done, returnNone
.- Return type:
str or None
- napari.plugins.hook_specifications.napari_write_surface(path: str, data: Any, meta: dict) str | None [source]#
Write surface data and metadata into a path.
It is the responsibility of the implementation to check any extension on
path
and returnNone
if it is an unsupported extension. Ifpath
has no extension, implementations may append their preferred extension.- Parameters:
path (str) – Path to file, directory, or resource (like a URL).
data (3-tuple of array) – The first element of the tuple is an (N, D) array of vertices of mesh triangles. The second is an (M, 3) array of int of indices of the mesh triangles. The third element is the (K0, …, KL, N) array of values used to color vertices where the additional L dimensions are used to color the same mesh with different values.
meta (dict) – Surface metadata.
- Returns:
path – If data is successfully written, return the
path
that was written. Otherwise, if nothing was done, returnNone
.- Return type:
str or None
- napari.plugins.hook_specifications.napari_write_vectors(path: str, data: Any, meta: dict) str | None [source]#
Write vectors data and metadata into a path.
It is the responsibility of the implementation to check any extension on
path
and returnNone
if it is an unsupported extension. Ifpath
has no extension, implementations may append their preferred extension.- Parameters:
- Returns:
path – If data is successfully written, return the
path
that was written. Otherwise, if nothing was done, returnNone
.- Return type:
str or None
Analysis hooks#
- napari.plugins.hook_specifications.napari_experimental_provide_function() function | List[function] [source]#
Provide function(s) that can be passed to magicgui.
This hook specification is marked as experimental as the API or how the returned value is handled may change here more frequently then the rest of the codebase.
- Returns:
function(s) – Implementations should provide either a single function, or a list of functions. Note that this does not preclude specifying multiple separate implementations in the same module or class. The functions should have Python type annotations so that magicgui can generate a widget from them.
- Return type:
FunctionType or list of FunctionType
Examples
>>> from napari.types import ImageData, LayerDataTuple >>> >>> def my_function(image : ImageData) -> LayerDataTuple: >>> # process the image >>> result = -image >>> # return it + some layer properties >>> return result, {'colormap':'turbo'} >>> >>> @napari_hook_implementation >>> def napari_experimental_provide_function(): >>> return my_function
GUI hooks#
- napari.plugins.hook_specifications.napari_experimental_provide_theme() Dict[str, Dict[str, str | Tuple | List]] [source]#
Provide GUI with a set of colors used through napari. This hook allows you to provide additional color schemes so you can accomplish your desired styling.
Themes are provided as dict with several required fields and correctly formatted color values. Colors can be specified using color names (e.g.
white
), hex color (e.g.#ff5733
), rgb color in 0-255 range (e.g.rgb(255, 0, 127)
or as 3- or 4-element tuples or lists (e.g.(255, 0, 127)
. The Theme model will automatically handle the conversion.See
Theme
for more detail of what are the required keys.- Returns:
themes – Sequence of dictionaries containing new color schemes to be used by napari. You can replace existing themes by using the same names.
- Return type:
Examples
>>> def get_new_theme() -> Dict[str, Dict[str, Union[str, Tuple, List]]: ... # specify theme(s) that should be added to napari ... themes = { ... "super_dark": { ... "name": "super_dark", ... "background": "rgb(12, 12, 12)", ... "foreground": "rgb(65, 72, 81)", ... "primary": "rgb(90, 98, 108)", ... "secondary": "rgb(134, 142, 147)", ... "highlight": "rgb(106, 115, 128)", ... "text": "rgb(240, 241, 242)", ... "icon": "rgb(209, 210, 212)", ... "warning": "rgb(153, 18, 31)", ... "current": "rgb(0, 122, 204)", ... "syntax_style": "native", ... "console": "rgb(0, 0, 0)", ... "canvas": "black", ... } ... } ... return themes >>> >>> @napari_hook_implementation >>> def napari_experimental_provide_theme(): ... return get_new_theme()
- napari.plugins.hook_specifications.napari_experimental_provide_dock_widget() AugmentedWidget | List[AugmentedWidget] [source]#
Provide functions that return widgets to be docked in the viewer.
This hook specification is marked as experimental as the API or how the returned value is handled may change here more frequently then the rest of the codebase.
- Returns:
result – A “callable” in this context is a class or function that, when called, returns an instance of either a
QWidget
or aFunctionGui
.Implementations of this hook specification must return a callable, or a tuple of
(callable, dict)
, where the dict contains keyword arguments fornapari.qt.Window.add_dock_widget()
. (note, however, thatshortcut=
keyword is not yet supported).Implementations may also return a list, in which each item must be a callable or
(callable, dict)
tuple. Note that this does not preclude specifying multiple separate implementations in the same module or class.- Return type:
Examples
An example with a QtWidget:
>>> from qtpy.QtWidgets import QWidget >>> from napari_plugin_engine import napari_hook_implementation >>> >>> class MyWidget(QWidget): ... def __init__(self, napari_viewer): ... self.viewer = napari_viewer ... super().__init__() ... ... # initialize layout ... layout = QGridLayout() ... ... # add a button ... btn = QPushButton('Click me!', self) ... def trigger(): ... print("napari has", len(napari_viewer.layers), "layers") ... btn.clicked.connect(trigger) ... layout.addWidget(btn) ... ... # activate layout ... self.setLayout(layout) >>> >>> @napari_hook_implementation >>> def napari_experimental_provide_dock_widget(): ... return MyWidget
An example using magicgui:
>>> from magicgui import magic_factory >>> from napari_plugin_engine import napari_hook_implementation >>> >>> @magic_factory(auto_call=True, threshold={'max': 2 ** 16}) >>> def threshold( ... data: 'napari.types.ImageData', threshold: int ... ) -> 'napari.types.LabelsData': ... return (data > threshold).astype(int) >>> >>> @napari_hook_implementation >>> def napari_experimental_provide_dock_widget(): ... return threshold
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.