Contribution Guides#

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

hook specification

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:

  1. Accept a destination path and data from one or more layers in the viewer

  2. Write layer data and associated attributes to disk

  3. 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

hook specification

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 plugins allow developers to contribute novel graphical elements (aka “widgets”) to the user interface. These 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:

  1. Provide a class object that is a subclass of QtWidgets.QWidget or magicgui.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
    
  2. 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-decorated create_widget() function is called, it returns a new widget instance –– just as we need for the widget specification.)

  3. Lastly, you can provide an arbitrary function and request that napari autogenerate a widget using magicgui.magicgui. In the first generation napari_plugin_engine, this was the napari_experimental_provide_function hook specification. In the new npe2 pattern, one uses the autogenerate field in the WidgetContribution.

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

hook_specification

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

hook specification

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.

Informal description#

(data, [attributes, [layer_type]])

A LayerData tuple is a tuple of length 1, 2, or 3 whose items, in order, are:

  1. The data object that would be used for layer.data (such as a numpy array for the Image layer)

  2. (Optional). A dict of layer attributes, suitable for passing as keyword arguments to the corresponding layer constructor (e.g. {'opacity': 0.7})

  3. (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’.