Plugin contributions#
This page provides guides on many of the plugin contribution patterns. Each provides a general overview of the purpose of the contribution and an example implementation. For details on the type and meaning of each field in a specific contribution, See the contributions reference
Readers#
Reader plugins may add support for new filetypes to napari.
They are invoked whenever viewer.open('some/path')
is used on the
command line, or when a user opens a file in the graphical user interface by
dropping a file into the canvas, or using File -> Open...
The command
provided by a reader contribution is expected to be a function
that accepts a path (str
) or a list of paths and:
returns
None
(if it does not want to accept the given path)returns a new function (a
ReaderFunction
) that is capable of doing the reading.
The ReaderFunction
will be passed the same path (or list of paths) and
is expected to return a list of LayerData tuples.
In the rare case that a reader plugin would like to “claim” a file, but not
actually add any data to the viewer, the ReaderFunction
may return
the special value [(None,)]
.
Accepting directories
A reader may indicate that it accepts directories by
setting contributions.readers.<reader>.accepts_directories
to True
;
otherwise, they will not be invoked when a directory is passed to viewer.open
.
Reader example#
python implementation
# example_plugin.some_module
PathLike = str
PathOrPaths = Union[PathLike, Sequence[PathLike]]
ReaderFunction = Callable[[PathOrPaths], List[LayerData]]
def get_reader(path: "PathOrPaths") -> Optional["ReaderFunction"]:
# If we recognize the format, we return the actual reader function
if isinstance(path, str) and path.endswith(".xyz"):
return xyz_file_reader
# otherwise we return None.
return None
def xyz_file_reader(path: "PathOrPaths") -> List["LayerData"]:
data = ... # somehow read data from path
layer_attributes = {"name": "etc..."}
return [(data, layer_attributes)]
manifest
See Readers contribution reference for field details.
contributions:
commands:
- id: example-plugin.read_xyz
title: Read ".xyz" files
python_name: example_plugin.some_module:get_reader
readers:
- command: example-plugin.read_xyz
filename_patterns:
- '*.xyz'
accepts_directories: false
Deprecated!
This demonstrates the now-deprecated napari-plugin-engine
pattern.
python implementation
from napari_plugin_engine import napari_hook_implementation
@napari_hook_implementation
def napari_get_reader(path: PathOrPaths) -> Optional[ReaderFunction]:
# If we recognize the format, we return the actual reader function
if isinstance(path, str) and path.endswith(".xyz"):
return xyz_file_reader
# otherwise we return None.
return None
def xyz_file_reader(path: PathOrPaths) -> List[LayerData]:
data = ... # somehow read data from path
layer_properties = {"name": "etc..."}
return [(data, layer_properties)]
Writers#
Writer plugins add support for exporting data from napari.
They are invoked whenever viewer.layers.save('some/path.ext')
is used on the command line, or when a user requests to save one
or more layers in the graphical user interface with
File -> Save Selected Layer(s)...
or Save All Layers...
Important
This guide describes the second generation (npe2
) plugin specification.
New plugins should no longer use the old napari_get_writer
hook
specification from the first generation napari_plugin_engine
.
Writer plugin function signatures#
Writer plugins are functions that:
Accept a destination path and data from one or more layers in the viewer
Write layer data and associated attributes to disk
Return a list of strings containing the path(s) that were successfully written.
They must follow one of two calling conventions (where the convention used
is determined by the layer_type
constraints provided
by the corresponding writer contribution in the manifest).
1. single-layer writer#
Single-layer writers will receive a path, layer data, and a dict
of layer
attributes, (e.g. {'name': 'My Layer', 'opacity': 0.6}
)
def single_layer_writer(path: str, data: Any, attributes: dict) -> List[str]:
...
The formal type is as follows:
DataType = Any # usually something like a numpy array, but varies by layer
LayerAttributes = dict
SingleWriterFunction = Callable[[str, DataType, LayerAttributes], List[str]]
2. multi-layer writer#
Multi-layer writers will receive a path, and a list of full layer data tuples.
def multi_layer_writer(path: str, layer_data: List[FullLayerData]) -> List[str]:
...
The formal type is as follows:
DataType = Any # usually something like a numpy array, but varies by layer
LayerAttributes = dict
LayerName = Literal["graph", "image", "labels", "points", "shapes", "surface", "tracks", "vectors"]
FullLayerData = Tuple[DataType, LayerAttributes, LayerName]
MultiWriterFunction = Callable[[str, List[FullLayerData]], List[str]]
Layer type constraints#
Individual writer contributions are determined to be single-layer writers or
multi-layer writers based on their writer.layer_types
constraints
provided in the contribution metadata.
A writer plugin declares that it can accept between m and n layers of a
specific type (where 0 ≤ m ≤ n), using regex-like syntax with the special
characters ?
, +
and *
:
image
: Writes exactly 1 image layer.image?
: Writes 0 or 1 image layers.image+
: Writes 1 or more image layers.image*
: Writes 0 or more image layers.image{k}
: Writes exactly k image layers.image{m,n}
: Writes between m and n layers (inclusive range). Must have m <= n.
A writer plugin will only be invoked when its layer_types
constraint is
compatible with the layer type(s) that the user is saving. When a type is not
present in the list of constraints, it is assumed the writer is not
compatible with that type.
Consider this example contributions section in a manifest:
contributions:
writers:
- command: example-plugin.some_writer
layer_types: ["image+", "points?"]
filename_extensions: [".ext"]
This writer would be considered when 1 or more Image
layers and 0 or 1
Points
layers are selected (i.e. the Points
layer is optional). This
writer would not be selected when the user tries to save an image
and a vectors
layer, because vectors
is not listed in the layer_types
.
Writer example#
python implementation
# example_plugin.some_module
def write_points(path: str, layer_data: Any, attributes: Dict[str, Any]) -> List[str]:
with open(path, "w"):
... # save layer_data and attributes to file
# return path to any file(s) that were successfully written
return [path]
manifest
See Writers contribution reference for field details.
contributions:
commands:
- id: example-plugin.write_points
title: Save points layer to csv
python_name: example_plugin.some_module:write_points
writers:
- command: example-plugin.write_points
layer_types:
- points
filename_extensions:
- .csv
Deprecated!
This demonstrates the now-deprecated napari-plugin-engine
pattern.
python implementation
from napari_plugin_engine import napari_hook_implementation
@napari_hook_implementation
def napari_write_points(path: str, data: Any, meta: dict) -> Optional[str]:
"""Write points data and metadata into a path.
Parameters
----------
path : str
Path to file, directory, or resource (like a URL).
data : array (N, D)
Points layer data
meta : dict
Points metadata.
Returns
-------
path : str or None
If data is successfully written, return the ``path`` that was written.
Otherwise, if nothing was done, return ``None``.
"""
Widgets#
Widget plugin contributions allow developers to add novel graphical
elements (aka “widgets”) to the user interface. For a full introduction to
creating napari
widgets see Creating widgets.
Widgets can request
access to the viewer instance in which they are docked, enabling a broad
range of functionality: essentially, anything that can be done with the
napari Viewer
and Layer
APIs can be accomplished with widgets.
Important
Because this is a powerful and open-ended plugin specification, we
ask that plugin developers take additional care when providing widget plugins.
Make sure to only use public methods on the viewer
and layer
instances.
Also, be mindful of the fact that the user may be using your plugin along with
other plugins or workflows: try to only modify layers added by your plugin, or
specifically requested by the user.
The widget specification requires that the plugin provide napari with a
callable object that, when called, returns an instance of a widget.
Here “widget” means a subclass of QtWidgets.QWidget
or magicgui.widgets.Widget
,
or a FunctionGui
. Additionally, the plugin can provide an arbitrary function if using
‘autogenerate’, which requests that napari autogenerate a widget using
magicgui.magicgui
(see item 3 below).
There are a few commonly used patterns that fulfill this Callable[..., Widget]
specification:
Provide a
class
object that is a subclass ofQtWidgets.QWidget
ormagicgui.widgets.Widget
:from qtpy.QtWidgets import QWidget class MyPluginWidget(QWidget): def __init__(self, viewer: 'napari.viewer.Viewer', parent=None): super().__init__(parent) self._viewer = viewer
Provide a
magicgui.magic_factory
object:from magicgui import magic_factory @magic_factory def create_widget(image: 'napari.types.ImageData') -> 'napari.types.ImageData': ...
(reminder, in the example above, each time the
magic_factory
-decoratedcreate_widget()
function is called, it returns a new widget instance –– just as we need for the widget specification.)Lastly, you can provide an arbitrary function and request that napari autogenerate a widget using
magicgui.magicgui
. In the first generationnapari_plugin_engine
, this was thenapari_experimental_provide_function
hook specification. In the newnpe2
pattern, one uses theautogenerate
field in the WidgetContribution.
For more examples see Creating widgets and GUI gallery examples (only a subset involve widgets). Additionally, cookiecutter-napari-plugin has more robust widget examples that you can adapt to your needs.
Note
Notice that napari
type annotations are strings and not imported. This is to
avoid including napari
as a plugin dependency when not strictly required.
Widget example#
python implementation
# example_plugin.some_module
Widget = Union["magicgui.widgets.Widget", "qtpy.QtWidgets.QWidget"]
class MyWidget(QWidget):
"""Any QtWidgets.QWidget or magicgui.widgets.Widget subclass can be used."""
def __init__(self, viewer: "napari.viewer.Viewer", parent=None):
super().__init__(parent)
...
@magic_factory
def widget_factory(
image: "napari.types.ImageData", threshold: int
) -> "napari.types.LabelsData":
"""Generate thresholded image.
This pattern uses magicgui.magic_factory directly to turn a function
into a callable that returns a widget.
"""
return (image > threshold).astype(int)
def threshold(
image: "napari.types.ImageData", threshold: int
) -> "napari.types.LabelsData":
"""Generate thresholded image.
This function will be turned into a widget using `autogenerate: true`.
"""
return (image > threshold).astype(int)
manifest
See Widgets contribution reference for field details.
contributions:
commands:
- id: example-plugin.my_widget
title: Open my widget
python_name: example_plugin.some_module:MyWidget
- id: example-plugin.threshold_widget
title: Make threshold widget with magic_factory
python_name: example_plugin.some_module:widget_factory
- id: example-plugin.do_threshold
title: Perform threshold on image, return new image
python_name: example_plugin.some_module:threshold
widgets:
- command: example-plugin.my_widget
display_name: Wizard
- command: example-plugin.threshold_widget
display_name: Threshold
- command: example-plugin.do_threshold
display_name: Threshold
autogenerate: true
Deprecated!
This demonstrates the now-deprecated napari-plugin-engine
pattern.
python implementation
from qtpy.QtWidgets import QWidget
from napari_plugin_engine import napari_hook_implementation
class AnimationWizard(QWidget):
def __init__(self, viewer: "napari.viewer.Viewer", parent=None):
super().__init__(parent)
...
@magic_factory
def widget_factory(
image: "napari.types.ImageData", threshold: int
) -> "napari.types.LabelsData":
"""Generate thresholded image.
This pattern uses magicgui.magic_factory directly to turn a function
into a callable that returns a widget.
"""
return (image > threshold).astype(int)
def threshold(
image: "napari.types.ImageData", threshold: int
) -> "napari.types.LabelsData":
"""Generate thresholded image.
This function will be turned into a widget using `autogenerate: true`.
"""
return (image > threshold).astype(int)
# in the first generation plugin engine, these widgets were declared
# using special `napari_hook_implementation`-decorated functions.
@napari_hook_implementation
def napari_experimental_provide_dock_widget():
return [AnimationWizard, widget_factory]
@napari_hook_implementation
def napari_experimental_provide_function():
return [threshold]
Sample Data#
This contribution point allows plugin developers to contribute sample data
that will be accessible in the napari interface via the File > Open Sample
menu, or via the command line with viewer.open_sample
.
Sample data can be useful for demonstrating the functionality of a given plugin. It can take the form of a Sample Data URI that points to a static resource (such as a file included in the plugin distribution, or a remote resource), or Sample Data Function that generates layer data on demand.
Sample Data example#
python implementation
# example_plugin.some_module
def create_fractal() -> List["LayerData"]:
"""An example of a Sample Data Function.
Note: Sample Data with URIs don't need python code.
"""
data = ... # do something cool to create a fractal
return [(data, {"name": "My cool fractal"})]
manifest
See Sample Data contribution reference for field details.
contributions:
commands:
- id: example-plugin.data.fractal
title: Create fractal image
python_name: example_plugin.some_module:create_fractal
sample_data:
- command: example-plugin.data.fractal
key: fractal
display_name: Fractal
- key: napari
display_name: Tabueran Kiribati
uri: https://en.wikipedia.org/wiki/Napari#/media/File:Tabuaeran_Kiribati.jpg
Deprecated!
This demonstrates the now-deprecated napari-plugin-engine
pattern.
python implementation
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,
}
}
The LayerData tuple#
When transfering data to and from plugins, napari does not pass Layer
objects
directly. Instead, it passes (mostly) pure-python and array-like types,
deconstructed into a tuple
that we refer to as a LayerData
tuple. This type shows
up often in plugins and is explained here.
Note that when writing your own plugin, type annotations are optional,
except in the case of magicgui
function widgets.
For several types related to LayerData
tuples, napari defines a type alias
which better indicates a value’s functional role in a plugin.
We describe these below.
Informal description#
(data, [attributes, [layer_type]])
A LayerData
tuple is a tuple of length 1, 2, or 3 whose items, in order, are:
The
data
object that would be used forlayer.data
(such as a numpy array for theImage
layer)(Optional). A
dict
of layer attributes, suitable for passing as keyword arguments to the corresponding layer constructor (e.g.{'opacity': 0.7}
)(Optional). A lower case
str
indicating the layer type (e.g.'image'
,'labels'
, etc…). If not provided (i.e. if the tuple is only of length 2), the layer type is assumed to be'image
’.
Formal type definition#
Formally, the typing for LayerData
looks like this:
LayerData = Union[Tuple[DataType], Tuple[DataType, LayerProps], FullLayerData]
where …
from typing import Literal, Protocol, Sequence
LayerTypeName = Literal[
"image", "labels", "points", "shapes", "surface", "tracks", "vectors"
]
LayerProps = Dict
DataType = Union[ArrayLike, Sequence[ArrayLike]]
FullLayerData = Tuple[DataType, LayerProps, LayerTypeName]
LayerData = Union[Tuple[DataType], Tuple[DataType, LayerProps], FullLayerData]
# where "ArrayLike" is very roughly ...
class ArrayLike(Protocol):
shape: Tuple[int, ...]
ndim: int
dtype: np.dtype
def __array__(self) -> np.ndarray: ...
def __getitem__(self, key) -> ArrayLike: ...
# the main point is that we're more concerned with structural
# typing than literal array types (e.g. numpy, dask, xarray, etc...)
Examples#
Assume that data
is a numpy array:
import numpy as np
data = np.random.rand(64, 64)
All of the following are valid LayerData
tuples:
# the first three are equivalent, just an image array with default settings
(data,)
(data, {})
(data, {}, 'image')
# provide kwargs for image contructor
(data, {'name': 'My Image', 'colormap': 'red'})
# labels layer instead of image:
(data.astype(int), {'name': 'My Labels', 'blending': 'additive'}, 'labels')
Creation from a Layer
instance.#
Note, the as_layer_data_tuple()
method will create a layer data
tuple from a given layer
>>> img = Image(np.random.rand(2, 2), colormap='green', scale=(4, 4))
>>> img.as_layer_data_tuple()
Out[7]:
(
array([[0.94414642, 0.89192899],
[0.21258344, 0.85242735]]),
{
'name': 'Image',
'metadata': {},
'scale': [4.0, 4.0],
'translate': [0.0, 0.0],
'rotate': [[1.0, 0.0], [0.0, 1.0]],
'shear': [0.0],
'opacity': 1,
'blending': 'translucent',
'visible': True,
'experimental_clipping_planes': [],
'rgb': False,
'multiscale': False,
'colormap': 'green',
'contrast_limits': [0.2125834437981784, 0.9441464162780605],
'interpolation': 'nearest',
'rendering': 'mip',
'experimental_slicing_plane': {'normal': (1.0, 0.0, 0.0), 'position': (0.0, 0.0, 0.0), 'enabled': False, 'thickness': 1.0},
'iso_threshold': 0.5,
'attenuation': 0.05,
'gamma': 1
},
'image'
)
Adding to the viewer#
To add a LayerData
tuple to the napari viewer, use :meth:Layer.create
:
>>> image_layer_data = (data, {'name': 'My Image', 'colormap': 'red'}, 'image')
>>> viewer = napari.current_viewer()
>>> viewer.add_layer(napari.layers.Layer.create(*image_layer_data))
The only attribute that can’t be passed to napari.layers.Layer.create
that is otherwise valid for a LayerData
tuple is ‘channel_axis’.