Code source de datalab.plugins.tools

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

"""Declarations of plugin-owned tools listed in the Applications catalog.

DataLab Desktop and DataLab-Web share this module.
"""

from __future__ import annotations

import dataclasses
import enum
from collections.abc import Sequence

from sigima.objects import ImageObj, SignalObj

from datalab.plugins.recipes import RecipeObjectType
from datalab.plugins.resources import LOCAL_ID_PATTERN, split_package_resource

__all__ = ["PluginTool", "ToolSelection", "tool_accepts_selection"]


[docs] class ToolSelection(str, enum.Enum): """Selection a tool needs to be opened (names follow DataLab's conditions).""" NONE = "none" EXACTLY_ONE = "exactly_one" AT_LEAST_ONE = "at_least_one" AT_LEAST_TWO = "at_least_two"
[docs] @dataclasses.dataclass(frozen=True) class PluginTool: """Tool with its own user interface, exposed by an application plugin Recipes cover headless analyses run by DataLab; a tool covers any other interaction owned by the plugin (wizard, interactive editor, data preparation, instrument...). DataLab lists tools in the Applications catalog and in the plugin's submenu of the *Plugins* menu. A tool is opened either by a ``launcher`` method, called without arguments, or by an ``instrument`` method returning a :class:`datalab.plugins.instruments.PluginInstrument`, which DataLab shows in a window with a live view and the instrument settings. Args: id: plugin-local tool ID (lowercase letters, digits, ``.``, ``_``, ``-``) title: tool title launcher: name of the plugin method called, without arguments, to open the tool description: short description of what the tool does icon: ``package:path`` resource (SVG or bitmap) or DataLab icon file name; ``None`` falls back to the plugin icon instrument: name of the plugin method returning the instrument shown by DataLab (exclusive with ``launcher``) object_type: type of the objects the tool works on or creates; the tool is listed in the menu of that panel (``None``: both panels) selection: selection needed to open the tool, counting objects of ``object_type`` only """ id: str title: str launcher: str | None = None description: str = "" icon: str | None = None instrument: str | None = None object_type: RecipeObjectType | None = None selection: ToolSelection = ToolSelection.NONE def __post_init__(self) -> None: """Validate identity, opening method, selection, and icon resource.""" if not isinstance(self.id, str) or not LOCAL_ID_PATTERN.fullmatch(self.id): raise ValueError( "Plugin tool ID must contain lowercase letters, digits, '.', '_' or '-'" ) if not isinstance(self.title, str) or not self.title.strip(): raise ValueError("Plugin tool title must be a non-empty string") if (self.launcher is None) == (self.instrument is None): raise ValueError("Plugin tool needs either a launcher or an instrument") for method_name in (self.launcher, self.instrument): if method_name is not None and ( not isinstance(method_name, str) or not method_name.isidentifier() ): raise ValueError("Plugin tool launcher must be a method name") if not isinstance(self.description, str): raise TypeError("Plugin tool description must be a string") if self.icon is not None: if not isinstance(self.icon, str) or not self.icon.strip(): raise ValueError("Plugin tool icon must be a non-empty string") if ":" in self.icon: split_package_resource(self.icon, "Plugin tool icon") if self.object_type is not None: object.__setattr__(self, "object_type", RecipeObjectType(self.object_type)) object.__setattr__(self, "selection", ToolSelection(self.selection))
[docs] def tool_accepts_selection( tool: PluginTool, objects: Sequence[SignalObj | ImageObj] ) -> bool: """Return True if the selected objects allow opening the tool. Args: tool: tool to open objects: selected objects Returns: True if the number of selected objects of the tool's object type matches the tool's selection """ object_class = { RecipeObjectType.SIGNAL: SignalObj, RecipeObjectType.IMAGE: ImageObj, None: (SignalObj, ImageObj), }[tool.object_type] count = sum(isinstance(obj, object_class) for obj in objects) return { ToolSelection.NONE: True, ToolSelection.EXACTLY_ONE: count == 1, ToolSelection.AT_LEAST_ONE: count >= 1, ToolSelection.AT_LEAST_TWO: count >= 2, }[tool.selection]