Plugins#

DataLab supports a robust plugin architecture, allowing users to extend the application’s features without modifying its core. Plugins can introduce new processing tools, data import/export formats, or custom GUI elements — all seamlessly integrated into the platform.

What is a plugin?#

A plugin is a Python module that is automatically loaded by DataLab at startup. It can define new features or modify existing ones.

To be recognized as a plugin, the file must:

  • Be a Python module whose name starts with datalab_ (e.g. datalab_myplugin.py),

  • Contain a class that inherits from datalab.plugins.PluginBase,

  • Include a class attribute named PLUGIN_INFO, which must be an instance of datalab.plugins.PluginInfo,

  • Implement the create_actions method.

This PLUGIN_INFO object is used by DataLab to retrieve metadata such as the plugin name, type, and menu integration.

Note

Only Python files whose names start with datalab_ will be scanned for plugins.

DataLab supports three categories of plugins, each with its own purpose and registration mechanism:

  • Processing and visualization plugins Add custom actions for signal or image processing. These may include new computation functions, data visualization tools, or interactive dialogs. Integrated into a dedicated submenu of the “Plugins” menu.

  • Input/Output plugins Define new file formats (read and/or write) handled transparently by DataLab’s I/O framework. These plugins extend compatibility with custom or third-party data formats.

  • HDF5 plugins Special plugins that support HDF5 files with domain-specific tree structures. These allow DataLab to interpret signals or images organized in non-standard ways.

Where to put a plugin?#

Plugins are automatically discovered at startup from multiple locations:

  • The user plugin directory: Typically ~/.DataLab/plugins on Linux/macOS or C:/Users/YourName/.DataLab/plugins on Windows.

  • A custom plugin directory: Configurable in DataLab’s preferences.

  • The standalone distribution directory: If using a frozen (standalone) build, the plugins folder located next to the executable is scanned.

  • The internal datalab/plugins folder (not recommended for user plugins): This location is reserved for built-in or bundled plugins and should not be modified manually.

Managing plugins in DataLab#

The Plugins menu provides two dedicated actions:

  • Configure plugins… Opens the plugin configuration dialog where you can enable or disable plugins individually. After saving changes, DataLab can reload plugins immediately without restarting the application.

  • Reload plugins Reloads plugin modules from disk without restarting DataLab.

When reloading plugins, DataLab performs the following steps:

  1. Unregister currently active plugins,

  2. Clear plugin actions from signal and image panels,

  3. Re-discover and reload plugin modules,

  4. Re-register enabled plugins,

  5. Recreate plugin actions and refresh menus.

This workflow allows iterative plugin development while DataLab is running.

Note

Plugin enable/disable state is persisted in DataLab settings. Disabled plugins remain listed in the configuration dialog and can be re-enabled later. The global third-party plugins setting in Preferences is also applied immediately: disabling it removes plugin actions and greys out the Plugins menu and status indicator, while enabling it reloads plugins automatically.

Hot-reload workflow for plugin development#

The hot-reload feature is designed to accelerate the plugin development cycle. Here is the recommended workflow:

  1. Start DataLab normally.

  2. Create or edit your plugin file (e.g. datalab_myplugin.py) in one of the plugin directories (e.g. ~/.DataLab/plugins).

  3. In DataLab, use Plugins > Reload plugins to pick up your changes instantly.

  4. Test your plugin actions directly in the running application.

  5. Iterate: edit the file, reload, test — without restarting DataLab.

To selectively enable or disable specific plugins during development, use Plugins > Configure plugins…. The dialog lists all discovered plugins with their name, version, description, and file path. Toggling a plugin takes effect immediately after closing the dialog.

Plugin API helpers#

Plugins inheriting from datalab.plugins.PluginBase have direct access to useful helpers:

  • self.signalpanel and self.imagepanel: access to panel APIs and action handlers,

  • self.proxy: a datalab.control.proxy.LocalProxy instance for object creation and processing,

  • show_warning, show_error, show_info, ask_yesno: convenience dialog methods,

  • edit_new_signal_parameters and edit_new_image_parameters: helpers for object parameter dialogs.

These helpers simplify plugin code and keep it consistent with DataLab behavior.

How to develop a plugin?#

The recommended approach to developing a plugin is to derive from an existing example and adapt it to your needs. You can explore the source code in the datalab/plugins folder or refer to community-contributed examples.

Note

Most of DataLab’s signal and image processing functionalities have been externalized into a dedicated library called Sigima (https://sigima.readthedocs.io/en/latest/). When developing DataLab plugins, you will typically import and use many Sigima functions and features to perform data processing, analysis, and visualization tasks. Sigima provides a comprehensive set of tools for scientific data manipulation that can be leveraged directly in your plugins.

To develop in your usual Python environment (e.g., with an IDE like Spyder), you can:

  1. Install DataLab in your Python environment, using one of the following methods:

  2. Or add the `datalab` package manually to your Python path:

    • Download the source from the PyPI page,

    • Unzip the archive,

    • Add the datalab directory to your PYTHONPATH (e.g., using the PYTHONPATH Manager in Spyder).

Note

Even if you’ve installed datalab in your environment, you cannot run the full DataLab application directly from an IDE. You must launch DataLab via the command line or using the installer-created shortcut to properly test your plugin.

Example: processing plugin#

Here is a minimal example of a plugin that prints a message when activated:

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Test Data Plugin for DataLab
----------------------------

This plugin is an example of DataLab plugin. It provides test data samples
and some actions to test DataLab functionalities.
"""

from __future__ import annotations

import sigima.tests.data as test_data
from sigima.io.image import ImageIORegistry
from sigima.io.signal import SignalIORegistry
from sigima.tests import helpers

from datalab.config import _
from datalab.plugins import PluginBase, PluginInfo
from datalab.utils.qthelpers import create_progress_bar

# ------------------------------------------------------------------------------
# All computation functions must be defined as global functions, otherwise
# they cannot be pickled and sent to the worker process
# ------------------------------------------------------------------------------


class PluginTestData(PluginBase):
    """DataLab Test Data Plugin"""

    PLUGIN_INFO = PluginInfo(
        name=_("Test data"),
        version="1.0.0",
        description=_("Testing DataLab functionalities"),
    )

    def load_test_objs(
        self, registry_class: type[SignalIORegistry | ImageIORegistry], title: str
    ) -> None:
        """Load all test objects from a given registry class

        Args:
            registry_class: Registry class (SignalIORegistry or ImageIORegistry)
            title: Progress bar title

        Returns:
            List of (filename, object) tuples
        """
        test_objs = list(helpers.read_test_objects(registry_class))
        with create_progress_bar(self.signalpanel, title, max_=len(test_objs)) as prog:
            for i_obj, (_fname, obj) in enumerate(test_objs):
                prog.setValue(i_obj + 1)
                if prog.wasCanceled():
                    break
                if obj is not None:
                    self.proxy.add_object(obj)

    # Signal processing features ------------------------------------------------
    def create_paracetamol_signal(self) -> None:
        """Create paracetamol signal"""
        obj = test_data.create_paracetamol_signal()
        self.proxy.add_object(obj)

    # Image processing features ------------------------------------------------
    def create_peak_image(self) -> None:
        """Create 2D peak image"""
        obj = self.imagepanel.new_object(add_to_panel=False)
        if obj is not None:
            param = test_data.PeakDataParam.create(size=max(obj.data.shape))
            self.imagepanel.processor.update_param_defaults(param)
            if param.edit(self.main):
                obj.data, _coords = test_data.get_peak2d_data(param)
                self.proxy.add_object(obj)

    def create_sincos_image(self) -> None:
        """Create 2D sin cos image"""
        newparam = self.edit_new_image_parameters(hide_type=True)
        if newparam is not None:
            obj = test_data.create_sincos_image(newparam)
            self.proxy.add_object(obj)

    def create_noisy_gaussian_image(self) -> None:
        """Create 2D noisy gauss image"""
        newparam = self.edit_new_image_parameters(hide_height=True, hide_type=True)
        if newparam is not None:
            obj = test_data.create_noisy_gaussian_image(newparam, add_annotations=False)
            self.proxy.add_object(obj)

    def create_multigaussian_image(self) -> None:
        """Create 2D multi gauss image"""
        newparam = self.edit_new_image_parameters(hide_height=True, hide_type=True)
        if newparam is not None:
            obj = test_data.create_multigaussian_image(newparam)
            self.proxy.add_object(obj)

    def create_2dstep_image(self) -> None:
        """Create 2D step image"""
        newparam = self.edit_new_image_parameters(hide_type=True)
        if newparam is not None:
            obj = test_data.create_2dstep_image(newparam)
            self.proxy.add_object(obj)

    def create_ring_image(self) -> None:
        """Create 2D ring image"""
        param = test_data.RingParam(_("Ring"))
        if param.edit(self.main):
            obj = test_data.create_ring_image(param)
            self.proxy.add_object(obj)

    def create_annotated_image(self) -> None:
        """Create annotated image"""
        obj = test_data.create_annotated_image()
        self.proxy.add_object(obj)

    def create_grid_gaussian_image(self) -> None:
        """Create image with a grid of gaussian spots"""
        param = test_data.GridOfGaussianImages(_("Grid of Gaussian Images"))
        if param.edit(self.main):
            obj = test_data.create_grid_of_gaussian_images(param)
            self.proxy.add_object(obj)

    def _load_all_test_signals(self) -> None:
        """Load all test signals."""
        self.load_test_objs(SignalIORegistry, _("Load all test signals"))

    def _load_all_test_images(self) -> None:
        """Load all test images."""
        self.load_test_objs(ImageIORegistry, _("Load all test images"))

    # Plugin menu entries ------------------------------------------------------
    def create_actions(self) -> None:
        """Create actions"""
        # Signal Panel ----------------------------------------------------------
        sah = self.signalpanel.acthandler
        with sah.new_menu(_("Test data")):
            sah.new_action(
                _("Load spectrum of paracetamol"),
                triggered=self.create_paracetamol_signal,
                select_condition="always",
            )
            _title = _("Load all test signals")
            sah.new_action(
                _("Load all test signals"),
                triggered=self._load_all_test_signals,
                select_condition="always",
                separator=True,
            )
        # Image Panel -----------------------------------------------------------
        iah = self.imagepanel.acthandler
        with iah.new_menu(_("Test data")):
            iah.new_action(
                _("Create image with peaks"),
                triggered=self.create_peak_image,
                select_condition="always",
                separator=True,
            )
            iah.new_action(
                _("Create 2D sin cos image"),
                triggered=self.create_sincos_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create 2D noisy gaussian image"),
                triggered=self.create_noisy_gaussian_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create 2D multi gaussian image"),
                triggered=self.create_multigaussian_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create annotated image"),
                triggered=self.create_annotated_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create 2D step image"),
                triggered=self.create_2dstep_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create ring image"),
                triggered=self.create_ring_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create image with a grid of gaussian spots"),
                triggered=self.create_grid_gaussian_image,
                select_condition="always",
            )
            _title = _("Load all test images")
            iah.new_action(
                _("Load all test images"),
                triggered=self._load_all_test_images,
                select_condition="always",
                separator=True,
            )

Example: input/output plugin#

Here is a simple example of a plugin that adds new file formats to DataLab.

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Image file formats Plugin for DataLab
-------------------------------------

This plugin is an example of DataLab plugin.
It provides image file formats from cameras, scanners, and other acquisition devices.
"""

import struct

import numpy as np
from sigima.io.base import FormatInfo
from sigima.io.image.base import SingleImageFormatBase

# ==============================================================================
# Thales Pixium FXD file format
# ==============================================================================


class FXDFile:
    """Class implementing Thales Pixium FXD Image file reading feature

    Args:
        fname (str): path to FXD file
        debug (bool): debug mode
    """

    HEADER = "<llllllffl"

    def __init__(self, fname: str = None, debug: bool = False) -> None:
        self.__debug = debug
        self.file_format = None  # long
        self.nbcols = None  # long
        self.nbrows = None  # long
        self.nbframes = None  # long
        self.pixeltype = None  # long
        self.quantlevels = None  # long
        self.maxlevel = None  # float
        self.minlevel = None  # float
        self.comment_length = None  # long
        self.fname = None
        self.data = None
        if fname is not None:
            self.load(fname)

    def __repr__(self) -> str:
        """Return a string representation of the object"""
        info = (
            ("Image width", f"{self.nbcols:d}"),
            ("Image Height", f"{self.nbrows:d}"),
            ("Frame number", f"{self.nbframes:d}"),
            ("File format", f"{self.file_format:d}"),
            ("Pixel type", f"{self.pixeltype:d}"),
            ("Quantlevels", f"{self.quantlevels:d}"),
            ("Min. level", f"{self.minlevel:f}"),
            ("Max. level", f"{self.maxlevel:f}"),
            ("Comment length", f"{self.comment_length:d}"),
        )
        desc_len = max(len(d) for d in list(zip(*info))[0]) + 3
        res = ""
        for description, value in info:
            res += ("{:" + str(desc_len) + "}{}\n").format(description + ": ", value)

        res = object.__repr__(self) + "\n" + res
        return res

    def load(self, fname: str) -> None:
        """Load header and image pixel data

        Args:
            fname (str): path to FXD file
        """
        with open(fname, "rb") as data_file:
            header_s = struct.Struct(self.HEADER)
            record = data_file.read(9 * 4)
            unpacked_rec = header_s.unpack(record)
            (
                self.file_format,
                self.nbcols,
                self.nbrows,
                self.nbframes,
                self.pixeltype,
                self.quantlevels,
                self.maxlevel,
                self.minlevel,
                self.comment_length,
            ) = unpacked_rec
            if self.__debug:
                print(unpacked_rec)
                print(self)
            data_file.seek(128 + self.comment_length)
            if self.pixeltype == 0:
                size, dtype = 4, np.float32
            elif self.pixeltype == 1:
                size, dtype = 2, np.uint16
            elif self.pixeltype == 2:
                size, dtype = 1, np.uint8
            else:
                raise NotImplementedError(f"Unsupported pixel type: {self.pixeltype}")
            block = data_file.read(self.nbrows * self.nbcols * size)
        data = np.frombuffer(block, dtype=dtype)
        self.data = data.reshape(self.nbrows, self.nbcols)


class FXDImageFormat(SingleImageFormatBase):
    """Object representing Thales Pixium (FXD) image file type"""

    FORMAT_INFO = FormatInfo(
        name="Thales Pixium",
        extensions="*.fxd",
        readable=True,
        writeable=False,
    )

    @staticmethod
    def read_data(filename: str) -> np.ndarray:
        """Read data and return it

        Args:
            filename (str): path to FXD file

        Returns:
            np.ndarray: image data
        """
        fxd_file = FXDFile(filename)
        return fxd_file.data


# ==============================================================================
# Dürr NDT XYZ file format
# ==============================================================================


class XYZImageFormat(SingleImageFormatBase):
    """Object representing Dürr NDT XYZ image file type"""

    FORMAT_INFO = FormatInfo(
        name="Dürr NDT",
        extensions="*.xyz",
        readable=True,
        writeable=True,
    )

    @staticmethod
    def read_data(filename: str) -> np.ndarray:
        """Read data and return it

        Args:
            filename (str): path to XYZ file

        Returns:
            np.ndarray: image data
        """
        with open(filename, "rb") as fdesc:
            cols = int(np.fromfile(fdesc, dtype=np.uint16, count=1)[0])
            rows = int(np.fromfile(fdesc, dtype=np.uint16, count=1)[0])
            arr = np.fromfile(fdesc, dtype=np.uint16, count=cols * rows)
            arr = arr.reshape((rows, cols))
        return np.fliplr(arr)

    @staticmethod
    def write_data(filename: str, data: np.ndarray) -> None:
        """Write data to file

        Args:
            filename: File name
            data: Image array data
        """
        data = np.fliplr(data)
        with open(filename, "wb") as fdesc:
            fdesc.write(np.array(data.shape[1], dtype=np.uint16).tobytes())
            fdesc.write(np.array(data.shape[0], dtype=np.uint16).tobytes())
            fdesc.write(data.tobytes())

Example templates used by the test suite#

DataLab also provides plugin templates used by integration tests in datalab/tests/features/plugins/templates. They are useful as development references for:

  • basic valid plugin structure,

  • nested plugin menus,

  • plugins with dialog actions,

  • plugins with many actions,

  • plugins with long descriptions.

The corresponding feature tests are located in datalab/tests/features/plugins/test_plugins.py and cover plugin lifecycle, hot-reload behavior, error handling, duplicate names, and configuration filtering.

Other examples#

Other examples of plugins can be found in the plugins/examples directory of the DataLab source code (explore here on GitHub).

Migrating from v0.20 to v1.0#

If you have existing plugins written for DataLab v0.20, please refer to the migration guide for detailed instructions on updating your plugins to work with DataLab v1.0.

Public API#

DataLab plugin system#

DataLab plugin system provides a way to extend the application with new functionalities.

Plugins are Python modules that relies on two classes:

  • PluginInfo, which stores information about the plugin

  • PluginBase, which is the base class for all plugins

Plugins may also extends DataLab I/O features by providing new image or signal formats. To do so, they must provide a subclass of ImageFormatBase or SignalFormatBase, in which format information is defined using the FormatInfo class.

class datalab.plugins.PluginRegistry(name, bases, attrs)[source]#

Metaclass for registering plugins

classmethod get_plugin_classes() list[type[PluginBase]][source]#

Return plugin classes

classmethod get_plugins() list[PluginBase][source]#

Return plugin instances

classmethod get_plugin(name_or_class: str | type[PluginBase]) PluginBase | None[source]#

Return plugin instance

classmethod register_plugin(plugin: PluginBase)[source]#

Register plugin

classmethod unregister_plugin(plugin: PluginBase)[source]#

Unregister plugin

classmethod unregister_all_plugins()[source]#

Unregister all plugins

classmethod clear_plugin_classes() None[source]#

Clear registered plugin classes.

This is mainly useful when reloading plugin modules at runtime.

classmethod add_discovery_error(tb_text: str) None[source]#

Record an error traceback that occurred during plugin discovery.

Parameters:

tb_text – Formatted traceback string

classmethod get_discovery_errors() list[str][source]#

Return error tracebacks collected during plugin discovery.

Returns:

List of formatted traceback strings (may be empty)

classmethod clear_discovery_errors() None[source]#

Clear recorded discovery errors.

classmethod add_failed_plugin(name: str, filepath: str, tb_text: str) None[source]#

Record a plugin that failed to load or instantiate.

Parameters:
  • name – Module or plugin class name

  • filepath – File path of the plugin module

  • tb_text – Formatted traceback string

classmethod get_failed_plugins() list[FailedPluginInfo][source]#

Return structured info about plugins that failed to load.

Returns:

List of FailedPluginInfo objects (may be empty)

classmethod clear_failed_plugins() None[source]#

Clear recorded failed plugin info.

classmethod get_plugin_info(html: bool = True) str[source]#

Return plugin information (names, versions, descriptions) in html format

Parameters:

html – return html formatted text (default: True)

class datalab.plugins.FailedPluginInfo(name: str, filepath: str, traceback: str)[source]#

Information about a plugin that failed to load or instantiate.

class datalab.plugins.PluginInfo(name: str = None, version: str = '0.0.0', description: str = '', icon: str = None)[source]#

Plugin info

class datalab.plugins.PluginBaseMeta(name, bases, namespace, /, **kwargs)[source]#

Mixed metaclass to avoid conflicts

class datalab.plugins.PluginBase[source]#

Plugin base class

property signalpanel: SignalPanel#

Return signal panel

property imagepanel: ImagePanel#

Return image panel

show_warning(message: str)[source]#

Show warning message

show_error(message: str)[source]#

Show error message

show_info(message: str)[source]#

Show info message

ask_yesno(message: str, title: str | None = None, cancelable: bool = False) bool[source]#

Ask yes/no question

edit_new_signal_parameters(title: str | None = None, size: int | None = None) NewSignalParam[source]#

Create and edit new signal parameter dataset

Parameters:
  • title – title of the new signal

  • size – size of the new signal (default: None, get from current signal)

Returns:

New signal parameter dataset (or None if canceled)

edit_new_image_parameters(title: str | None = None, shape: tuple[int, int] | None = None, hide_height: bool = False, hide_width: bool = False, hide_type: bool = True, hide_dtype: bool = False) NewImageParam | None[source]#

Create and edit new image parameter dataset

Parameters:
  • title – title of the new image

  • shape – shape of the new image (default: None, get from current image)

  • hide_height – hide image heigth parameter (default: False)

  • hide_width – hide image width parameter (default: False)

  • hide_type – hide image type parameter (default: True)

  • hide_dtype – hide image data type parameter (default: False)

Returns:

New image parameter dataset (or None if canceled)

is_registered()[source]#

Return True if plugin is registered

register(main: main.DLMainWindow) None[source]#

Register plugin

unregister()[source]#

Unregister plugin

register_hooks()[source]#

Register plugin hooks

unregister_hooks()[source]#

Unregister plugin hooks

abstractmethod create_actions()[source]#

Create actions

datalab.plugins.discover_plugins() list[type[PluginBase]][source]#

Discover plugins using naming convention

This function reloads or imports all modules matching the DataLab plugin naming scheme ("{MOD_NAME}_*"). Plugin classes are then registered automatically via the PluginRegistry metaclass.

Import errors for individual plugins are captured and logged so that one broken plugin does not prevent the others from loading. Error tracebacks are accumulated in PluginRegistry class attributes so that callers (e.g. the main window) can replay them into the internal console once it is ready.

Returns:

List of imported/reloaded plugin modules

datalab.plugins.reload_plugin_modules() None[source]#

Reload plugin modules and reset plugin classes.

This helper is intended for hot-reloading plugins at runtime. It:

  • Updates the plugin search path

  • Clears the plugin class registry

  • Reloads or imports all modules matching the plugin naming convention

datalab.plugins.discover_v020_plugins() list[tuple[str, str]][source]#

Discover v0.20 plugins (with cdl_ prefix) without importing them

Returns:

List of tuples (plugin_name, directory_path) for discovered v0.20 plugins

datalab.plugins.get_available_plugins() list[PluginBase][source]#

Instantiate and get available plugins

Returns:

List of available plugins (as instances)