Skip to content

himena.plugins

register_hidden_function = partial(register_function, menus=[], palette=False) module-attribute

A shorthand for register_function(menus=[], palette=False).

AppActionRegistry

Source code in src\himena\plugins\actions.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
class AppActionRegistry:
    _global_instance: AppActionRegistry | None = None

    def __init__(self):
        self._actions: dict[str, Action] = {}
        self._actions_dynamic: set[str] = set()
        self._submenu_titles: dict[str, str] = {
            MenuId.FILE_NEW: "New ...",
            MenuId.TOOLS_DOCK: "Dock widgets",
        }
        self._submenu_groups: dict[str, str] = {
            MenuId.FILE_NEW: "00_new",
            MenuId.TOOLS_DOCK: "00_dock",
        }
        self._installed_plugins: list[str] = []
        self._plugin_default_configs: dict[str, PluginConfigTuple] = {}
        self._modification_trackers: dict[str, Callable[[_T, _T], ReproduceArgs]] = {}
        self._app_tips: list[AppTip] = []
        self._try_load_app_tips()

    @classmethod
    def instance(cls) -> AppActionRegistry:
        """Get the global instance of the registry."""
        if cls._global_instance is None:
            cls._global_instance = cls()
        return cls._global_instance

    def _try_load_app_tips(self) -> None:
        tip_path = Path(__file__).parent.parent / "resources" / "tips.json"
        try:
            with tip_path.open("r") as f:
                out = json.load(f)
            for each in out:
                self._app_tips.append(
                    AppTip(
                        short=each.get("short", ""),
                        long=each.get("long", ""),
                    )
                )
        except Exception as e:
            _LOGGER.error("Failed to load app tips: %s", e)

    def pick_a_tip(self) -> AppTip | None:
        """Pick a random tip."""
        if len(self._app_tips) == 0:
            return None
        tip = random.choice(self._app_tips)
        return AppTip(short=tip.short, long=tip.long)

    def add_action(self, action: Action, is_dynamic: bool = False) -> None:
        """Add an action to the registry."""
        id_ = action.id
        if id_ in self._actions:
            raise ValueError(f"Action ID {id_} already exists.")
        self._actions[id_] = action
        if is_dynamic:
            self._actions_dynamic.add(id_)

    @property
    def installed_plugins(self) -> list[str]:
        """List of modules or python paths that are installed as plugins."""
        return self._installed_plugins

    def iter_actions(self, app: HimenaApplication) -> Iterator[Action]:
        for id_, action in self._actions.items():
            if id_ not in app.commands:
                yield action

    def submenu_title(self, id: str) -> str:
        """Get the title of a submenu."""
        if title := self._submenu_titles.get(id):
            return title
        return id.split("/")[-1].title()

    def submenu_group(self, id: str) -> str | None:
        """Get the group of a submenu."""
        return self._submenu_groups.get(id, None)

    @property
    def submenu_titles(self) -> dict[str, str]:
        return self._submenu_titles

    def install_to(
        self,
        app: HimenaApplication,
        actions: list[Action] | None = None,
    ) -> list[str]:
        """Install actions to the application.

        This method automatically adds submenus if they are not already exists, and
        returns the list of added root menu IDs. Note that this does NOT updates the
        GUI menubar and toolbar.
        """
        # look for existing menu items
        if actions is None:
            actions = list(self.iter_actions(app))
        existing_menu_ids = {_id.value for _id in MenuId if "/" not in _id.value}
        for menu_id, menu in app.menus:
            existing_menu_ids.add(menu_id)
            for each in menu:
                if isinstance(each, SubmenuItem):
                    existing_menu_ids.add(each.submenu)

        added_menu_ids = OrderedSet[str]()
        for action in actions:
            if action.menus is not None:
                ids = [a.id for a in action.menus]
                added_menu_ids.update(ids)

        # add submenus if not exists
        to_add: list[tuple[str, SubmenuItem]] = []
        new_menu_ids: list[str] = []

        for place in added_menu_ids - existing_menu_ids:
            place_components = place.split("/")
            if len(place_components) == 1:
                new_menu_ids.append(place)
            for i in range(1, len(place_components)):
                menu_id = "/".join(place_components[:i])
                submenu = "/".join(place_components[: i + 1])
                if submenu in existing_menu_ids:
                    continue
                title = self.submenu_title(submenu)
                group = self.submenu_group(submenu)
                item = SubmenuItem(title=title, submenu=submenu, group=group)
                to_add.append((menu_id, item))

        app.register_actions(actions)
        app.menus.append_menu_items(to_add)
        app._dynamic_command_ids.update(self._actions_dynamic)
        return new_menu_ids
installed_plugins property

List of modules or python paths that are installed as plugins.

add_action(action, is_dynamic=False)

Add an action to the registry.

Source code in src\himena\plugins\actions.py
144
145
146
147
148
149
150
151
def add_action(self, action: Action, is_dynamic: bool = False) -> None:
    """Add an action to the registry."""
    id_ = action.id
    if id_ in self._actions:
        raise ValueError(f"Action ID {id_} already exists.")
    self._actions[id_] = action
    if is_dynamic:
        self._actions_dynamic.add(id_)
install_to(app, actions=None)

Install actions to the application.

This method automatically adds submenus if they are not already exists, and returns the list of added root menu IDs. Note that this does NOT updates the GUI menubar and toolbar.

Source code in src\himena\plugins\actions.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def install_to(
    self,
    app: HimenaApplication,
    actions: list[Action] | None = None,
) -> list[str]:
    """Install actions to the application.

    This method automatically adds submenus if they are not already exists, and
    returns the list of added root menu IDs. Note that this does NOT updates the
    GUI menubar and toolbar.
    """
    # look for existing menu items
    if actions is None:
        actions = list(self.iter_actions(app))
    existing_menu_ids = {_id.value for _id in MenuId if "/" not in _id.value}
    for menu_id, menu in app.menus:
        existing_menu_ids.add(menu_id)
        for each in menu:
            if isinstance(each, SubmenuItem):
                existing_menu_ids.add(each.submenu)

    added_menu_ids = OrderedSet[str]()
    for action in actions:
        if action.menus is not None:
            ids = [a.id for a in action.menus]
            added_menu_ids.update(ids)

    # add submenus if not exists
    to_add: list[tuple[str, SubmenuItem]] = []
    new_menu_ids: list[str] = []

    for place in added_menu_ids - existing_menu_ids:
        place_components = place.split("/")
        if len(place_components) == 1:
            new_menu_ids.append(place)
        for i in range(1, len(place_components)):
            menu_id = "/".join(place_components[:i])
            submenu = "/".join(place_components[: i + 1])
            if submenu in existing_menu_ids:
                continue
            title = self.submenu_title(submenu)
            group = self.submenu_group(submenu)
            item = SubmenuItem(title=title, submenu=submenu, group=group)
            to_add.append((menu_id, item))

    app.register_actions(actions)
    app.menus.append_menu_items(to_add)
    app._dynamic_command_ids.update(self._actions_dynamic)
    return new_menu_ids
instance() classmethod

Get the global instance of the registry.

Source code in src\himena\plugins\actions.py
115
116
117
118
119
120
@classmethod
def instance(cls) -> AppActionRegistry:
    """Get the global instance of the registry."""
    if cls._global_instance is None:
        cls._global_instance = cls()
    return cls._global_instance
pick_a_tip()

Pick a random tip.

Source code in src\himena\plugins\actions.py
137
138
139
140
141
142
def pick_a_tip(self) -> AppTip | None:
    """Pick a random tip."""
    if len(self._app_tips) == 0:
        return None
    tip = random.choice(self._app_tips)
    return AppTip(short=tip.short, long=tip.long)
submenu_group(id)

Get the group of a submenu.

Source code in src\himena\plugins\actions.py
169
170
171
def submenu_group(self, id: str) -> str | None:
    """Get the group of a submenu."""
    return self._submenu_groups.get(id, None)
submenu_title(id)

Get the title of a submenu.

Source code in src\himena\plugins\actions.py
163
164
165
166
167
def submenu_title(self, id: str) -> str:
    """Get the title of a submenu."""
    if title := self._submenu_titles.get(id):
        return title
    return id.split("/")[-1].title()

ReaderPlugin

Source code in src\himena\plugins\io.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class ReaderPlugin(_IOPluginBase):
    def __init__(
        self,
        reader: Callable[[Path | list[Path]], WidgetDataModel],
        matcher: Callable[[Path | list[Path]], bool] | None = None,
        *,
        priority: int = 100,
    ):
        super().__init__(reader, matcher, priority=priority)
        self._skip_if_list = False
        if hasattr(reader, "__annotations__"):
            annot_types = list(reader.__annotations__.values())
            if len(annot_types) == 1 and annot_types[0] in (
                Path,
                "Path",
                ForwardRef("Path"),
            ):
                self._skip_if_list = True

    def read(self, path: Path | list[Path]) -> WidgetDataModel:
        """Read file(s) and return a data model."""
        if isinstance(path, list):
            paths: list[Path] = []
            for p in path:
                if not p.exists():
                    raise FileNotFoundError(f"File {p!r} does not exist.")
                paths.append(p)
            out = self._func(paths)
        else:
            path = Path(path)
            if not path.exists():
                raise FileNotFoundError(f"File {path!r} does not exist.")
            out = self._func(path)
        if not isinstance(out, WidgetDataModel):
            raise TypeError(f"Reader plugin {self!r} did not return a WidgetDataModel.")
        return out

    __call__ = read

    def match_model_type(self, path: Path | list[Path]) -> str | None:
        """True if the reader can read the file."""
        if self._skip_if_list and isinstance(path, list):
            return None
        if self._matcher is None:
            return None
        out = self._matcher(path)
        if out is None or isinstance(out, str):
            return out
        raise TypeError(f"Matcher {self._matcher!r} did not return a string.")

    def define_matcher(self, matcher: Callable[[Path | list[Path]], str | None]):
        """Mark a function as a matcher.

        The matcher function should return a type string if the reader can read the
        file, or None otherwise. If the reader function is annotated with `Path`, only
        single Path input is forwarded to the matcher function, otherwise both `Path`
        and `list[Path]` will be considered.

        Examples
        --------
        A reader plugin that reads only text files:

        ```python
        @my_reader.define_matcher
        def _(path: Path):
            if path.suffix == ".txt":
                return "text"
            return None
        ```
        """
        # NOTE: matcher don't have to return the priority. If users want to define
        # a plugin that has different priority for different file type, they can just
        # split the plugin function into two.
        if self._matcher is self._undefined_matcher:
            raise ValueError(f"Matcher for {self!r} is already defined.")
        self._matcher = matcher
        return matcher

    def read_and_update_source(self, source: Path | list[Path]) -> WidgetDataModel:
        """Update workflow to a local-reader method if it is not set."""
        model = self.read(source)
        if len(model.workflow) == 0:
            model = model._with_source(source=source, plugin=self.plugin)
        return model
define_matcher(matcher)

Mark a function as a matcher.

The matcher function should return a type string if the reader can read the file, or None otherwise. If the reader function is annotated with Path, only single Path input is forwarded to the matcher function, otherwise both Path and list[Path] will be considered.

Examples:

A reader plugin that reads only text files:

@my_reader.define_matcher
def _(path: Path):
    if path.suffix == ".txt":
        return "text"
    return None
Source code in src\himena\plugins\io.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def define_matcher(self, matcher: Callable[[Path | list[Path]], str | None]):
    """Mark a function as a matcher.

    The matcher function should return a type string if the reader can read the
    file, or None otherwise. If the reader function is annotated with `Path`, only
    single Path input is forwarded to the matcher function, otherwise both `Path`
    and `list[Path]` will be considered.

    Examples
    --------
    A reader plugin that reads only text files:

    ```python
    @my_reader.define_matcher
    def _(path: Path):
        if path.suffix == ".txt":
            return "text"
        return None
    ```
    """
    # NOTE: matcher don't have to return the priority. If users want to define
    # a plugin that has different priority for different file type, they can just
    # split the plugin function into two.
    if self._matcher is self._undefined_matcher:
        raise ValueError(f"Matcher for {self!r} is already defined.")
    self._matcher = matcher
    return matcher
match_model_type(path)

True if the reader can read the file.

Source code in src\himena\plugins\io.py
107
108
109
110
111
112
113
114
115
116
def match_model_type(self, path: Path | list[Path]) -> str | None:
    """True if the reader can read the file."""
    if self._skip_if_list and isinstance(path, list):
        return None
    if self._matcher is None:
        return None
    out = self._matcher(path)
    if out is None or isinstance(out, str):
        return out
    raise TypeError(f"Matcher {self._matcher!r} did not return a string.")
read(path)

Read file(s) and return a data model.

Source code in src\himena\plugins\io.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def read(self, path: Path | list[Path]) -> WidgetDataModel:
    """Read file(s) and return a data model."""
    if isinstance(path, list):
        paths: list[Path] = []
        for p in path:
            if not p.exists():
                raise FileNotFoundError(f"File {p!r} does not exist.")
            paths.append(p)
        out = self._func(paths)
    else:
        path = Path(path)
        if not path.exists():
            raise FileNotFoundError(f"File {path!r} does not exist.")
        out = self._func(path)
    if not isinstance(out, WidgetDataModel):
        raise TypeError(f"Reader plugin {self!r} did not return a WidgetDataModel.")
    return out
read_and_update_source(source)

Update workflow to a local-reader method if it is not set.

Source code in src\himena\plugins\io.py
146
147
148
149
150
151
def read_and_update_source(self, source: Path | list[Path]) -> WidgetDataModel:
    """Update workflow to a local-reader method if it is not set."""
    model = self.read(source)
    if len(model.workflow) == 0:
        model = model._with_source(source=source, plugin=self.plugin)
    return model

ReproduceArgs dataclass

Command and its arguments to reproduce a user modification.

Parameters:

Name Type Description Default
command_id str
required
Source code in src\himena\plugins\actions.py
79
80
81
82
83
84
85
86
@dataclass
class ReproduceArgs:
    """Command and its arguments to reproduce a user modification."""

    command_id: str
    with_params: dict[str, Any] = field(
        default_factory=dict
    )  # values must be serializable

WriterPlugin

Source code in src\himena\plugins\io.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
class WriterPlugin(_IOPluginBase):
    def __init__(
        self,
        writer: Callable[[WidgetDataModel, Path], Any],
        matcher: Callable[[Path | list[Path]], bool] | None = None,
        *,
        priority: int = 100,
    ):
        super().__init__(writer, matcher, priority=priority)
        if arg := get_widget_data_model_type_arg(writer):
            self._value_type_filter = arg
        else:
            self._value_type_filter = None

    def write(self, model: WidgetDataModel, path: Path) -> None:
        return self._func(model, path)

    __call__ = write

    def match_input(self, model: WidgetDataModel, path: Path) -> bool:
        if self._value_type_filter is not None and not isinstance(
            model.value, self._value_type_filter
        ):
            return False
        return self._matcher(model, path)

    def define_matcher(
        self, matcher: Callable[[WidgetDataModel, Path], bool]
    ) -> WriterPlugin:
        """Define how to match the input data model and the save path to this writer.

        Examples
        --------
        ```python
        @my_writer.define_matcher
        def _(model: WidgetDataModel, path: Path) -> bool:
            return path.suffix == ".txt" and model.type == "text"
        """
        self._matcher = matcher
        return self
define_matcher(matcher)

Define how to match the input data model and the save path to this writer.

Examples:

```python @my_writer.define_matcher def _(model: WidgetDataModel, path: Path) -> bool: return path.suffix == ".txt" and model.type == "text"

Source code in src\himena\plugins\io.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def define_matcher(
    self, matcher: Callable[[WidgetDataModel, Path], bool]
) -> WriterPlugin:
    """Define how to match the input data model and the save path to this writer.

    Examples
    --------
    ```python
    @my_writer.define_matcher
    def _(model: WidgetDataModel, path: Path) -> bool:
        return path.suffix == ".txt" and model.type == "text"
    """
    self._matcher = matcher
    return self

add_default_status_tip(short, long)

Add a status tip that will be randomly shown in the status bar.

Source code in src\himena\plugins\actions.py
610
611
612
613
614
615
616
def add_default_status_tip(
    short: str,
    long: str,
) -> None:
    """Add a status tip that will be randomly shown in the status bar."""
    reg = AppActionRegistry.instance()
    reg._app_tips.append(AppTip(short=str(short), long=str(long)))

config_field(default=MISSING, *, default_factory=MISSING, tooltip=None, label=None, choices=None, widget_type=None, enabled=None, visible=None, **kwargs)

Field used for plugin config dataclass.

@dataclass
class MyPluginConfig:
    my_field: str = config_field("abc", tooltip="how to use this")
Source code in src\himena\plugins\config.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def config_field(
    default=MISSING,
    *,
    default_factory=MISSING,
    tooltip: str | None = None,
    label: str | None = None,
    choices: list[str] | list[tuple[str, Any]] | None = None,
    widget_type: Any | None = None,
    enabled: bool | None = None,
    visible: bool | None = None,
    **kwargs,
):
    """Field used for plugin config dataclass.

    ```python
    @dataclass
    class MyPluginConfig:
        my_field: str = config_field("abc", tooltip="how to use this")
    ```
    """
    metadata = kwargs.copy()
    if tooltip is not None:
        metadata["tooltip"] = tooltip
    if label is not None:
        metadata["label"] = label
    if choices is not None:
        metadata["choices"] = choices
    if enabled is not None:
        metadata["enabled"] = enabled
    if visible is not None:
        metadata["visible"] = visible
    if widget_type is not None:
        metadata["widget_type"] = widget_type

    return field(
        default=default,
        default_factory=default_factory,
        compare=False,
        metadata=metadata,
    )

configure_gui(f=None, *, title=None, preview=False, auto_close=True, show_parameter_labels=True, gui_options=None, result_as='window', **kwargs)

configure_gui(f: _F, *, title: str | None = None, preview: bool = False, auto_close: bool = True, show_parameter_labels: bool = True, gui_options: dict[str, Any] | None = None, result_as: Literal['window', 'below', 'right'] = 'window', **kwargs) -> _F
configure_gui(*, title: str | None = None, preview: bool = False, auto_close: bool = True, show_parameter_labels: bool = True, gui_options: dict[str, Any] | None = None, result_as: Literal['window', 'below', 'right'] = 'window', **kwargs) -> Callable[[_F], _F]

Configure the parametric GUI.

This decorator sets the configuration options for the parametric GUI window.

@configure_gui(a={"label": "A", "widget_type": "FloatSlider"})
def my_func(a: float):
    pass

Parameters:

Name Type Description Default
title str

The title of the parametric GUI window. If not provided, this title will be determined by the action title where this function is returned.

None
preview bool

If true, a preview toggle switch will be added to the GUI window. When the switch is on, the function will be called and the result will be displayed. Note that configure_gui does not consider whether the preview is a heavy operation.

False
auto_close bool

If true, the parametric GUI window will be closed automatically after the function is executed.

True
show_parameter_labels bool

If true, the parameter names will be shown in the GUI window.

True
gui_options dict

Additional GUI options to be passed to the magicgui decorator. Keys can also be passed as variable keyword arguments **kwargs.

None
Source code in src\himena\plugins\_signature.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def configure_gui(
    f=None,
    *,
    title: str | None = None,
    preview: bool = False,
    auto_close: bool = True,
    show_parameter_labels: bool = True,
    gui_options: dict[str, Any] | None = None,
    result_as: Literal["window", "below", "right"] = "window",
    **kwargs,
):
    """Configure the parametric GUI.

    This decorator sets the configuration options for the parametric GUI window.

    ``` python
    @configure_gui(a={"label": "A", "widget_type": "FloatSlider"})
    def my_func(a: float):
        pass
    ```

    Parameters
    ----------
    title : str, optional
        The title of the parametric GUI window. If not provided, this title will be
        determined by the action title where this function is returned.
    preview : bool, default False
        If true, a preview toggle switch will be added to the GUI window. When the
        switch is on, the function will be called and the result will be displayed. Note
        that `configure_gui` does not consider whether the preview is a heavy operation.
    auto_close : bool, default True
        If true, the parametric GUI window will be closed automatically after the
        function is executed.
    show_parameter_labels : bool, default True
        If true, the parameter names will be shown in the GUI window.
    gui_options : dict, optional
        Additional GUI options to be passed to the `magicgui` decorator. Keys can also
        be passed as variable keyword arguments **kwargs.
    """
    kwargs = dict(**kwargs, **(gui_options or {}))

    def _inner(f):
        sig = inspect.signature(f)
        new_params = sig.parameters.copy()
        if var_kwargs_name := _get_var_kwargs_name(sig):
            new_params.pop(var_kwargs_name)

        for k, v in kwargs.items():
            if k not in new_params:
                if var_kwargs_name is None:
                    raise TypeError(f"{k!r} is not a valid parameter for {f!r}.")
                # This allows using **kwargs in the target function so that magicgui
                # widget can be created for a variable number of parameters.
                param = inspect.Parameter(name=k, kind=inspect.Parameter.KEYWORD_ONLY)
            else:
                param = sig.parameters[k]
            if isinstance(v, dict) and "annotation" in v:
                param_annotation = v.pop("annotation")
            else:
                param_annotation = param.annotation
            # unwrap Annotated types
            if not _is_annotated(param_annotation):
                annot = _prioritize_choices(param_annotation, v)
                param = param.replace(annotation=Annotated[annot, v])
            else:
                typ, meta = _split_annotated_type(param_annotation)
                meta.update(v)
                typ = _prioritize_choices(typ, meta)
                param = param.replace(annotation=Annotated[typ, meta])
            new_params[k] = param
        # update the signature with the normalize one
        sig = sig.replace(parameters=list(new_params.values()))
        f.__signature__ = sig
        f.__annotations__ = {k: v.annotation for k, v in sig.parameters.items()}
        if sig.return_annotation is not inspect.Parameter.empty:
            f.__annotations__["return"] = sig.return_annotation

        GuiConfiguration(
            title=title,
            preview=preview,
            auto_close=auto_close,
            show_parameter_labels=show_parameter_labels,
            result_as=result_as,
        ).set(f)
        return f

    return _inner if f is None else _inner(f)

configure_submenu(submenu_id, title=None, *, group=None)

Register a configuration for submenu(s).

Parameters:

Name Type Description Default
submenu_id str or iterable of str

Submenu ID(s) to configure.

required
title str

Specify the title of the submenu.

None
group str

Specify the group ID of the submenu.

None
Source code in src\himena\plugins\actions.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def configure_submenu(
    submenu_id: str | Iterable[str],
    title: str | None = None,
    *,
    group: str | None = None,
) -> None:
    """Register a configuration for submenu(s).

    Parameters
    ----------
    submenu_id : str or iterable of str
        Submenu ID(s) to configure.
    title : str, optional
        Specify the title of the submenu.
    group : str, optional
        Specify the group ID of the submenu.
    """
    if isinstance(submenu_id, str):
        submenu_id = [submenu_id]
    for sid in submenu_id:
        if title is not None:
            AppActionRegistry.instance()._submenu_titles[sid] = title
        if group is not None:
            AppActionRegistry.instance()._submenu_groups[sid] = group

install_plugins(app, plugins)

Install plugins to the application.

Source code in src\himena\plugins\install.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def install_plugins(
    app: HimenaApplication, plugins: list[str]
) -> list[PluginInstallResult]:
    """Install plugins to the application."""
    from himena.plugins import AppActionRegistry
    from himena.profile import load_app_profile

    reg = AppActionRegistry.instance()
    results = []
    show_import_time = app.attributes.get("print_import_time", False)
    if show_import_time:
        print("==================")
        print("Plugin import time")
        print("==================")
    for name in plugins:
        if name in reg._installed_plugins:
            continue
        _time_0 = timer()
        _exc = None
        if isinstance(name, str):
            if name.endswith(".py"):
                if not Path(name).exists():
                    _LOGGER.error(
                        f"Plugin file {name} does not exists but is listed in the "
                        "application profile."
                    )
                    continue
                import runpy

                runpy.run_path(name)
            else:
                try:
                    import_module(name)
                except ModuleNotFoundError:
                    _LOGGER.error(
                        f"Plugin module {name} is not installed but is listed in the "
                        "application profile."
                    )
                    continue
                except Exception as e:
                    msg = "".join(
                        traceback.format_exception(type(e), e, e.__traceback__)
                    )
                    _LOGGER.error(
                        f"Error installing plugin {name}, traceback follows:\n{msg}"
                    )
                    _exc = e
        else:
            raise TypeError(f"Invalid plugin type: {type(name)}")
        _msec = (timer() - _time_0) * 1000
        if show_import_time and _exc is None:
            color = _color_for_time(_msec)
            print(f"{color}{name}\t{_msec:.3f} msec\033[0m")
        results.append(PluginInstallResult(name, _msec, _exc))
    reg.install_to(app)
    reg._installed_plugins.extend(plugins)
    prof = load_app_profile(app.name)

    for k, cfg in reg._plugin_default_configs.items():
        prof.plugin_configs.setdefault(k, cfg.as_dict())

    prof.save()
    return results

override_keybindings(app, prof)

Override keybindings in the application.

Source code in src\himena\plugins\install.py
84
85
86
87
88
89
90
91
92
def override_keybindings(app: HimenaApplication, prof: AppProfile) -> None:
    """Override keybindings in the application."""
    for ko in prof.keybinding_overrides:
        if kb := app.keybindings.get_keybinding(ko.command_id):
            app.keybindings._keybindings.remove(kb)
        app.keybindings.register_keybinding_rule(
            ko.command_id,
            KeyBindingRule(primary=ko.key),
        )

register_conversion_rule(*args, **kwargs)

register_conversion_rule(func: _F, type_from: str, type_to: str, *, keybindings: KeyBindingsType | None = None, command_id: str | None = None) -> _F
register_conversion_rule(type_from: str, type_to: str, *, keybindings: KeyBindingsType | None = None, command_id: str | None = None) -> Callable[[_F], _F]

Register a function as a conversion rule.

A conversion rule will be added to the model menu under "Convert > ..." submenu. Essentially, this method does nothing more than register_function, but using this registration method is recommended for the sake of clarity and following the conventions.

from himena.plugins import register_conversion_rule

@register_conversion_rule("text", "table")
def convert_text_to_table(model: WidgetDataModel) -> WidgetDataModel:
    ...  # convert the data type
Source code in src\himena\plugins\actions.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def register_conversion_rule(*args, **kwargs):
    """Register a function as a conversion rule.

    A conversion rule will be added to the model menu under "Convert > ..." submenu.
    Essentially, this method does nothing more than `register_function`, but using this
    registration method is recommended for the sake of clarity and following the
    conventions.

    ```python
    from himena.plugins import register_conversion_rule

    @register_conversion_rule("text", "table")
    def convert_text_to_table(model: WidgetDataModel) -> WidgetDataModel:
        ...  # convert the data type
    ```
    """
    if len(args) == 0:
        no_func = True
    else:
        if isinstance(args[0], str):
            return register_conversion_rule(None, *args, **kwargs)
        no_func = args[0] is None

    def inner(func):
        annot = getattr(func, "__annotations__", {})
        annot.setdefault("return", WidgetDataModel)
        func.__annotations__ = annot
        action = _make_conversion_rule(func, *args, **kwargs)
        AppActionRegistry.instance().add_action(action)
        return func

    return inner if no_func else inner(args[0])

register_dock_widget_action(widget_factory=None, *, menus=None, title=None, area=DockArea.RIGHT, allowed_areas=None, keybindings=None, singleton=False, plugin_configs=None, command_id=None, icon=None)

register_dock_widget_action(widget_factory: _F, *, menus: str | Sequence[str] | None = None, title: str | None = None, area: DockArea | DockAreaString = DockArea.RIGHT, allowed_areas: Sequence[DockArea | DockAreaString] | None = None, keybindings: KeyBindingsType | None = None, singleton: bool = False, plugin_configs: PluginConfigType | None = None, command_id: str | None = None, icon: str | None = None) -> _F
register_dock_widget_action(widget_factory: None = None, *, menus: str | Sequence[str] | None = None, title: str | None = None, area: DockArea | DockAreaString = DockArea.RIGHT, allowed_areas: Sequence[DockArea | DockAreaString] | None = None, keybindings: KeyBindingsType | None = None, singleton: bool = False, plugin_configs: PluginConfigType | None = None, command_id: str | None = None, icon: str | None = None) -> Callable[[_F], _F]

Register a widget factory as a dock widget function.

Parameters:

Name Type Description Default
widget_factory callable

Class of dock widget, or a factory function for the dock widget.

None
menus str or sequence of str

Menu ID or list of menu IDs where the action will be added.

None
title str

Title of the dock widget.

None
area DockArea or DockAreaString

Initial area of the dock widget.

RIGHT
allowed_areas sequence of DockArea or DockAreaString

List of areas that is allowed for the dock widget.

None
keybindings sequence of keybinding rule

Keybindings to trigger the dock widget.

None
singleton bool

If true, the registered dock widget will constructed only once.

False
plugin_configs (dict, dataclass or BaseModel)

Default configuration for the plugin. This config will be saved in the application profile and will be used to update the dock widget via the method update_configs(self, cfg) -> None. This argument must be a dict, dataclass or pydantic.BaseModel. If a dict, the format must be like:

plugin_configs = {
   "config_0": {"value": 0, "tooltip": ...},
   "config_1": {"value": "xyz", "tooltip": ...},
}

where only "value" is required. If a dataclass or pydantic.BaseModel, field objects will be used instead of the dict.

@dataclass
class MyPluginConfig:
    config_0: int = Field(default=0, metadata={"tooltip": ...})
    config_1: str = Field(default="xyz", metadata={"tooltip": ...})
plugin_configs = MyPluginConfig()
None
command_id str

Command ID. If not given, the function name will be used.

None
icon str

Iconify icon key.

None
Source code in src\himena\plugins\widget_plugins.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def register_dock_widget_action(
    widget_factory=None,
    *,
    menus: str | Sequence[str] | None = None,
    title: str | None = None,
    area: DockArea | DockAreaString = DockArea.RIGHT,
    allowed_areas: Sequence[DockArea | DockAreaString] | None = None,
    keybindings=None,
    singleton: bool = False,
    plugin_configs: PluginConfigType | None = None,
    command_id: str | None = None,
    icon: str | None = None,
):
    """Register a widget factory as a dock widget function.

    Parameters
    ----------
    widget_factory : callable, optional
        Class of dock widget, or a factory function for the dock widget.
    menus : str or sequence of str, optional
        Menu ID or list of menu IDs where the action will be added.
    title : str, optional
        Title of the dock widget.
    area : DockArea or DockAreaString, optional
        Initial area of the dock widget.
    allowed_areas : sequence of DockArea or DockAreaString, optional
        List of areas that is allowed for the dock widget.
    keybindings : sequence of keybinding rule, optional
        Keybindings to trigger the dock widget.
    singleton : bool, default False
        If true, the registered dock widget will constructed only once.
    plugin_configs : dict, dataclass or pydantic.BaseModel, optional
        Default configuration for the plugin. This config will be saved in the
        application profile and will be used to update the dock widget via the method
        `update_configs(self, cfg) -> None`. This argument must be a dict, dataclass
        or pydantic.BaseModel. If a dict, the format must be like:

        ``` python
        plugin_configs = {
           "config_0": {"value": 0, "tooltip": ...},
           "config_1": {"value": "xyz", "tooltip": ...},
        }
        ```

        where only "value" is required. If a dataclass or pydantic.BaseModel, field
        objects will be used instead of the dict.

        ``` python
        @dataclass
        class MyPluginConfig:
            config_0: int = Field(default=0, metadata={"tooltip": ...})
            config_1: str = Field(default="xyz", metadata={"tooltip": ...})
        plugin_configs = MyPluginConfig()
        ```
    command_id : str, optional
        Command ID. If not given, the function name will be used.
    icon : str, optional
        Iconify icon key.
    """
    kbs = normalize_keybindings(keybindings)
    if menus is None:
        menus = [MenuId.TOOLS_DOCK]

    def _inner(wf: Callable):
        _command_id = command_id_from_func(wf, command_id)
        _callback = DockWidgetCallback(
            wf,
            title=title,
            area=area,
            allowed_areas=allowed_areas,
            singleton=singleton,
            uuid=uuid.uuid4(),
            command_id=_command_id,
        )
        if singleton:
            toggle_rule = ToggleRule(get_current=_callback.widget_visible)
        else:
            toggle_rule = None
        action = Action(
            id=_command_id,
            title=_callback._title,
            tooltip=tooltip_from_func(wf),
            callback=_callback,
            menus=norm_menus(menus),
            keybindings=kbs,
            toggled=toggle_rule,
            icon=icon,
            icon_visible_in_menu=False,
        )
        reg = AppActionRegistry.instance()
        reg.add_action(action)
        if plugin_configs:
            cfg_type = type(plugin_configs)
            reg._plugin_default_configs[_command_id] = PluginConfigTuple(
                _callback._title,
                plugin_configs,
                cfg_type,
            )
        return wf

    return _inner if widget_factory is None else _inner(widget_factory)

register_function(func=None, *, menus='plugins', title=None, types=None, enablement=None, keybindings=None, run_async=False, command_id=None, icon=None, palette=True)

register_function(*, menus: str | Sequence[str] = 'plugins', title: str | None = None, types: str | Sequence[str] | None = None, enablement: BoolOp | None = None, keybindings: Sequence[KeyBindingRule] | None = None, run_async: bool = False, command_id: str | None = None, icon: str | None = None, palette: bool = True) -> None
register_function(func: _F, *, menus: str | Sequence[str] = 'plugins', title: str | None = None, types: str | Sequence[str] | None = None, enablement: BoolOp | None = None, keybindings: Sequence[KeyBindingRule] | None = None, run_async: bool = False, command_id: str | None = None, icon: str | None = None, palette: bool = True) -> _F

Register a function as a callback of a plugin action.

This function can be used either as a decorator or a simple function.

Parameters:

Name Type Description Default
func callable

Function to register as an action.

None
menus str or sequence of str

Menu(s) to add the action. Submenus are separated by /.

"plugins"
title str

Title of the action. Name of the function will be used if not given.

None
types

The type parameter(s) allowed as the WidgetDataModel. If this parameter is given, action will be grayed out if the active window does not satisfy the listed types.

None
enablement

Expression that describes when the action will be enabled. As this argument is a generalized version of types argument, you cannot use both of them.

None
run_async bool

If true, the function will be executed asynchronously. Note that if the function updates the GUI, running it asynchronously may cause issues.

False
command_id str

Command ID. If not given, the function qualname will be used.

None
icon str

Iconify icon key to use for the action.

None
palette bool

If true, the action will be added to the command palette.

True
Source code in src\himena\plugins\actions.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def register_function(
    func=None,
    *,
    menus="plugins",
    title=None,
    types=None,
    enablement=None,
    keybindings=None,
    run_async=False,
    command_id=None,
    icon=None,
    palette=True,
):
    """Register a function as a callback of a plugin action.

    This function can be used either as a decorator or a simple function.

    Parameters
    ----------
    func : callable, optional
        Function to register as an action.
    menus : str or sequence of str, default "plugins"
        Menu(s) to add the action. Submenus are separated by `/`.
    title : str, optional
        Title of the action. Name of the function will be used if not given.
    types: str or sequence of str, optional
        The `type` parameter(s) allowed as the WidgetDataModel. If this parameter
        is given, action will be grayed out if the active window does not satisfy
        the listed types.
    enablement: Expr, optional
        Expression that describes when the action will be enabled. As this argument
        is a generalized version of `types` argument, you cannot use both of them.
    run_async : bool, default False
        If true, the function will be executed asynchronously. Note that if the function
        updates the GUI, running it asynchronously may cause issues.
    command_id : str, optional
        Command ID. If not given, the function qualname will be used.
    icon : str, optional
        Iconify icon key to use for the action.
    palette : bool, default True
        If true, the action will be added to the command palette.
    """

    def _inner(f: _F) -> _F:
        action = make_action_for_function(
            f,
            menus=menus,
            title=title,
            types=types,
            enablement=enablement,
            keybindings=keybindings,
            run_async=run_async,
            command_id=command_id,
            icon=icon,
            palette=palette,
        )
        AppActionRegistry.instance().add_action(action)
        return f

    return _inner if func is None else _inner(func)

register_modification_tracker(type)

Register a modification tracker.

Source code in src\himena\plugins\actions.py
595
596
597
598
599
600
601
602
603
604
605
606
607
def register_modification_tracker(
    type: str,
) -> Callable[[Callable[[_T, _T], ReproduceArgs]], Callable[[_T, _T], ReproduceArgs]]:
    """Register a modification tracker."""
    reg = AppActionRegistry.instance()
    if type in reg._modification_trackers:
        raise ValueError(f"Modification tracker for {type} already exists.")

    def inner(fn):
        reg._modification_trackers[type] = fn
        return fn

    return inner

register_previewer_class(type_, widget_class)

Register a widget class for previewing the given model type.

Source code in src\himena\plugins\widget_class.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def register_previewer_class(type_: str, widget_class: type):
    """Register a widget class for previewing the given model type."""

    def inner(wcls):
        import himena.qt

        widget_id = get_widget_class_id(wcls)
        if existing_class := _WIDGET_ID_TO_WIDGET_CLASS.get(widget_id):
            raise ValueError(
                f"Widget class with ID {widget_id!r} already exists ({existing_class})."
            )
        _WIDGET_ID_TO_WIDGET_CLASS[widget_id] = wcls
        himena.qt.register_widget_class(type_, wcls, priority=-10)
        fn = OpenDataInFunction(type_, wcls)
        AppActionRegistry.instance().add_action(fn.to_action(), is_dynamic=True)
        fn = PreviewDataInFunction(type_, wcls)
        AppActionRegistry.instance().add_action(fn.to_action(), is_dynamic=True)
        return type_

    return inner if widget_class is None else inner(widget_class)

register_reader_plugin(reader=None, *, priority=100)

register_reader_plugin(reader: Callable[[Path | list[Path]], WidgetDataModel], *, priority: int = 100) -> ReaderPlugin
register_reader_plugin(*, priority: int = 100) -> Callable[[Callable[[Path | list[Path]], WidgetDataModel]], ReaderPlugin]

Register a reader plugin function.

Decorate a function to register it as a reader plugin. The function should take a Path or a list of Paths as input and return a WidgetDataModel.

from himena.plugins import register_reader_plugin

@register_reader_plugin
def my_reader(path) -> WidgetDataModel:
    ...  # read file and return a WidgetDataModel

You will need to define a matcher function to tell whether this function can read a path using define_matcher method.

from himena import StandardType

@my_reader.define_matcher
def _(path: Path):
    if path.suffix == ".txt":
        return StandardType.TEXT  # StandardType.TEXT == "text"
    return None

Parameters:

Name Type Description Default
priority int

Priority of choosing this reader when multiple readers are available. The default value 100 is higher than the himena builtin readers, so that your reader will prioritized over the default ones. If priority is less than 0, it will not be used unless users intentionally choose this plugin.

100
Source code in src\himena\plugins\io.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
def register_reader_plugin(reader=None, *, priority=100):
    """Register a reader plugin function.

    Decorate a function to register it as a reader plugin. The function should take a
    `Path` or a list of `Path`s as input and return a WidgetDataModel.

    ``` python
    from himena.plugins import register_reader_plugin

    @register_reader_plugin
    def my_reader(path) -> WidgetDataModel:
        ...  # read file and return a WidgetDataModel
    ```

    You will need to define a matcher function to tell whether this function can read
    a path using `define_matcher` method.

    ```python
    from himena import StandardType

    @my_reader.define_matcher
    def _(path: Path):
        if path.suffix == ".txt":
            return StandardType.TEXT  # StandardType.TEXT == "text"
        return None
    ```

    Parameters
    ----------
    priority : int, default 100
        Priority of choosing this reader when multiple readers are available. The
        default value 100 is higher than the himena builtin readers, so that your reader
        will prioritized over the default ones. If priority is less than 0, it will not
        be used unless users intentionally choose this plugin.
    """

    def _inner(func):
        if not callable(func):
            raise ValueError("Reader plugin must be callable.")
        ins = ReaderStore().instance()

        reader_plugin = ReaderPlugin(func, priority=priority)
        ins.add_reader(reader_plugin)
        return reader_plugin

    return _inner if reader is None else _inner(reader)

register_widget_class(type_, widget_class=None, priority=100, plugin_configs=None)

register_widget_class(type_: str, widget_class: _T, priority: int = 100, plugin_configs: PluginConfigType | None = None) -> _T
register_widget_class(type_: str, widget_class: None, priority: int = 100, plugin_configs: PluginConfigType | None = None) -> Callable[[_T], _T]

Register a frontend widget class for the given model type.

The __init__ method of the registered class must not take any argument. The class must implement update_model method to update the widget state from a WidgetDataModel.

@register_widget("text")
class MyTextEdit(QtW.QPlainTextEdit):
    def update_model(self, model: WidgetDataModel):
        self.setPlainText(model.value)

There are other method names that can be implemented to make the widget more functional.

  • to_model(self) -> WidgetDataModel:
  • model_type(self) -> str:
  • control_widget(self) -> <widget>:
  • is_modified(self) -> bool:
  • set_modified(self, modified: bool):
  • size_hint(self) -> tuple[int, int]:
  • is_editable(self) -> bool:
  • set_editable(self, editable: bool):
  • dropped_callback(self, other: WidgetDataModel):
  • allowed_drop_types(self) -> list[str]:
  • display_name(cls) -> str:
  • theme_changed_callback(self, theme: Theme):
  • widget_activated_callback(self):
  • widget_closed_callback(self):
  • widget_resized_callback(self, size_old, size_new):
Source code in src\himena\plugins\widget_class.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def register_widget_class(type_, widget_class=None, priority=100, plugin_configs=None):
    """Register a frontend widget class for the given model type.

    The `__init__` method of the registered class must not take any argument. The class
    must implement `update_model` method to update the widget state from a
    WidgetDataModel.

    ``` python
    @register_widget("text")
    class MyTextEdit(QtW.QPlainTextEdit):
        def update_model(self, model: WidgetDataModel):
            self.setPlainText(model.value)
    ```

    There are other method names that can be implemented to make the widget more
    functional.

    - `to_model(self) -> WidgetDataModel`:
    - `model_type(self) -> str`:
    - `control_widget(self) -> <widget>`:
    - `is_modified(self) -> bool`:
    - `set_modified(self, modified: bool)`:
    - `size_hint(self) -> tuple[int, int]`:
    - `is_editable(self) -> bool`:
    - `set_editable(self, editable: bool)`:
    - `dropped_callback(self, other: WidgetDataModel)`:
    - `allowed_drop_types(self) -> list[str]`:
    - `display_name(cls) -> str`:
    - `theme_changed_callback(self, theme: Theme)`:
    - `widget_activated_callback(self)`:
    - `widget_closed_callback(self)`:
    - `widget_resized_callback(self, size_old, size_new)`:
    """

    def inner(wcls):
        import himena.qt

        widget_id = get_widget_class_id(wcls)
        is_multi_registration = False
        if existing_class := _WIDGET_ID_TO_WIDGET_CLASS.get(widget_id):
            if existing_class is wcls:
                is_multi_registration = True
            else:
                raise ValueError(
                    f"Widget class with ID {widget_id!r} already assigned for "
                    f"{existing_class}; you must assign a unique ID for each class."
                )
        _WIDGET_ID_TO_WIDGET_CLASS[widget_id] = wcls
        himena.qt.register_widget_class(type_, wcls, priority=priority)
        fn = OpenDataInFunction(type_, wcls)
        reg = AppActionRegistry.instance()
        reg.add_action(fn.to_action(), is_dynamic=True)
        if not is_multi_registration:
            wcls.__himena_model_type__ = type_

        if plugin_configs:
            cfg_type = type(plugin_configs)
            if widget_id in reg._plugin_default_configs:
                warnings.warn(
                    f"Plugin config for {widget_id!r} already registered; "
                    f"overwriting with new config {plugin_configs}.",
                    UserWarning,
                    stacklevel=2,
                )
            reg._plugin_default_configs[widget_id] = PluginConfigTuple(
                get_display_name(wcls, sep=" ", class_id=False),
                plugin_configs,
                cfg_type,
            )
        return wcls

    return inner if widget_class is None else inner(widget_class)

register_writer_plugin(writer=None, *, priority=100)

register_writer_plugin(writer: Callable[[WidgetDataModel, Path], Any], *, priority: int = 100) -> WriterPlugin
register_writer_plugin(*, priority: int = 100) -> Callable[[Callable[[WidgetDataModel, Path], Any]], WriterPlugin]

Register a writer plugin function.

Decorate a function to register it as a writer plugin. The function should take a Path as a save path and a WidgetDataModel.

from himena.plugins import register_writer_plugin

@register_writer_plugin
def my_writer(path, model) -> None:
    ...  # read file and return a WidgetDataModel

You will need to define a matcher function to tell whether this function can write a data model to the specified path using define_matcher method. Unlike reader plugins, matchers should return bool.

from himena import StandardType, WidgetDataModel

@my_writer.define_matcher
def _(path: Path, model: WidgetDataModel):
    if path.suffix == ".txt":
        return True
    return False

Parameters:

Name Type Description Default
priority int

Priority of choosing this writer when multiple writers are available. The default value 100 is higher than the himena builtin writers, so that your writer will prioritized over the default ones. If priority is less than 0, it will not be used unless users intentionally choose this plugin.

100
Source code in src\himena\plugins\io.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def register_writer_plugin(writer=None, *, priority=100):
    """Register a writer plugin function.

    Decorate a function to register it as a writer plugin. The function should take a
    `Path` as a save path and a `WidgetDataModel`.

    ``` python
    from himena.plugins import register_writer_plugin

    @register_writer_plugin
    def my_writer(path, model) -> None:
        ...  # read file and return a WidgetDataModel
    ```

    You will need to define a matcher function to tell whether this function can write
    a data model to the specified path using `define_matcher` method. Unlike reader
    plugins, matchers should return bool.

    ```python
    from himena import StandardType, WidgetDataModel

    @my_writer.define_matcher
    def _(path: Path, model: WidgetDataModel):
        if path.suffix == ".txt":
            return True
        return False
    ```

    Parameters
    ----------
    priority : int, default 100
        Priority of choosing this writer when multiple writers are available. The
        default value 100 is higher than the himena builtin writers, so that your writer
        will prioritized over the default ones. If priority is less than 0, it will not
        be used unless users intentionally choose this plugin.
    """

    def _inner(func):
        if not callable(func):
            raise ValueError("Writer plugin must be callable.")
        ins = WriterStore().instance()

        writer_plugin = WriterPlugin(func, priority=priority)
        ins.add_writer(writer_plugin)
        return writer_plugin

    return _inner if writer is None else _inner(writer)

update_config_context(config_class, plugin_id=None, *, update_widget=False)

update_config_context(plugin_id: str, *, update_widget: bool = False) -> Iterator[PluginConfigType]
update_config_context(config_class: type[_C], plugin_id: str | None = None, *, update_widget: bool = False) -> Iterator[_C]

Context manager for updating plugin config.

Source code in src\himena\plugins\widget_plugins.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
@contextmanager
def update_config_context(
    config_class: type | str,
    plugin_id: str | None = None,
    *,
    update_widget: bool = False,
):
    """Context manager for updating plugin config."""
    from himena.widgets import current_instance

    ui = current_instance()
    if isinstance(config_class, str):
        if plugin_id is not None:
            raise TypeError("No overload matches the input.")
        _config_class, _plugin_id = None, config_class
    else:
        _config_class = config_class
        _plugin_id = plugin_id

    reg = AppActionRegistry.instance()
    if _plugin_id is None:
        for _id, _config in reg._plugin_default_configs.items():
            if isinstance(_config.config, _config_class):
                _plugin_id = _id
                break
        else:
            raise ValueError(
                f"Cannot find plugin ID for the config class: {config_class}."
            )
    plugin_config = reg._plugin_default_configs[_plugin_id]
    if not isinstance(plugin_config.config, _config_class):
        raise TypeError(
            f"Plugin ID {_plugin_id} does not match the config class {config_class}."
        )
    cur_config = plugin_config.config
    yield cur_config
    prof = ui.app_profile
    all_configs = prof.plugin_configs.copy()

    cfg_dict = all_configs[_plugin_id] = plugin_config.as_dict()
    prof.with_plugin_configs(all_configs).save()

    # update existing dock widgets with the new config
    if update_widget and (cb := WidgetCallbackBase.instance_for_command_id(_plugin_id)):
        params = {}
        for key, opt in cfg_dict.items():
            params[key] = opt["value"]
        for widget in cb._all_widgets:
            # the internal widget should always has the method "update_configs"
            widget.update_configs(params)

validate_protocol(f)

Check if the method is allowed as a himena protocol.

Source code in src\himena\plugins\_checker.py
36
37
38
39
40
def validate_protocol(f: _T) -> _T:
    """Check if the method is allowed as a himena protocol."""
    if f.__name__ not in _ALLOWED_METHODS:
        raise ValueError(f"Method {f} is not an allowed protocol name.")
    return f

widget_classes()

Get the mapping of widget ID to widget class.

Source code in src\himena\plugins\widget_class.py
119
120
121
122
123
124
125
126
127
def widget_classes() -> MappingProxyType[str, type]:
    """Get the mapping of widget ID to widget class."""
    from himena.qt.registry._api import _APP_TYPE_TO_QWIDGET

    out = {}
    for widget_list in _APP_TYPE_TO_QWIDGET.values():
        for item in widget_list:
            out[item.type] = item.widget_class
    return MappingProxyType(out)