Skip to content

himena.profile

AppProfile

Model of a profile.

Parameters:

Name Type Description Default
name str

Name of the profile.

'default'
version str

Version of this profile created.

'0.2.6'
plugins list[str]

List of plugins to load.

['himena_builtins.qt.console', 'himena_builtins.qt.explorer', 'himena_builtins.qt.favorites', 'himena_builtins.qt.history', 'himena_builtins.qt.output', 'himena_builtins.qt.full_workflow', 'himena_builtins.qt.plot', 'himena_builtins.qt.array', 'himena_builtins.qt.basic', 'himena_builtins.qt.dataframe', 'himena_builtins.qt.image', 'himena_builtins.qt.ipynb', 'himena_builtins.qt.rois', 'himena_builtins.qt.stack', 'himena_builtins.qt.table', 'himena_builtins.qt.text', 'himena_builtins.tools.array', 'himena_builtins.tools.conversions', 'himena_builtins.tools.dataframe', 'himena_builtins.tools.dict', 'himena_builtins.tools.image', 'himena_builtins.tools.others', 'himena_builtins.tools.plot', 'himena_builtins.tools.table', 'himena_builtins.tools.text', 'himena_builtins.io', 'himena_builtins.new', 'himena_builtins.user_modifications']
theme str

Theme to use.

'light-green'
startup_commands list[tuple[str, dict[str, Any] | None]]

Startup commands that will be executed when the app starts.

<dynamic>
keybinding_overrides list[KeyBindingOverride]

Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

<dynamic>
plugin_configs dict[str, dict[str, Any]]

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

<class 'dict'>
warning_filters list[WarningFilter]

Filters that determine which warnings are shown in the GUI.

<dynamic>
Source code in src\himena\profile.py
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
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
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
class AppProfile(BaseModel):
    """Model of a profile."""

    name: str = Field(
        default="default",
        description="Name of the profile.",
        frozen=True,
    )
    version: str = Field(
        default_factory=_current_version,
        description="Version of this profile created.",
        frozen=True,
    )
    plugins: list[str] = Field(
        default_factory=_default_plugins, description="List of plugins to load."
    )
    theme: str = Field(default="light-green", description="Theme to use.")
    startup_commands: list[tuple[str, dict[str, Any] | None]] = Field(
        default_factory=list,
        description="Startup commands that will be executed when the app starts.",
    )
    keybinding_overrides: list[KeyBindingOverride] = Field(default_factory=list)
    plugin_configs: dict[str, dict[str, Any]] = Field(default_factory=dict)
    warning_filters: list[WarningFilter] = Field(
        default_factory=list,
        description="Filters that determine which warnings are shown in the GUI.",
    )

    @classmethod
    def from_json(cls, path) -> "AppProfile":
        """Construct an AppProfile from a json file."""
        with open(path) as f:
            data = json.load(f)
        version_saved = Version(data.get("version", "0.0.1"))
        self = cls(**data)
        if version_saved < Version(_current_version()):
            for place, version_added in DEFAULT_PLUGINS:
                if version_added > version_saved and place not in self.plugins:
                    # Add the default plugin that is implemented after the profile was
                    # saved.
                    self.plugins.append(place)
        return self

    @classmethod
    def default(cls, save: bool = False) -> "AppProfile":
        """Return the default profile."""
        prof = AppProfile()
        if save and not (profile_dir() / f"{prof.name}.json").exists():
            prof.save()
        return prof

    def save(self, path: str | Path | None = None) -> None:
        """Save profile as a json file."""
        if path is None:
            path = self.profile_path()
        json_string = json.dumps(self.model_dump(), indent=4)
        with open(path, "w") as f:
            f.write(json_string)
        return None

    def profile_path(self) -> Path:
        """Path to this profile."""
        return profile_dir() / f"{self.name}.json"

    def with_name(self, name: str) -> "AppProfile":
        """Return a new profile with a new name."""
        return self.model_copy(update={"name": name})

    def with_plugins(self, plugins: list[str]) -> "AppProfile":
        """Return a new profile with new plugins."""
        return self.model_copy(update={"plugins": plugins})

    def with_plugin_configs(self, configs: dict[str, dict[str, Any]]) -> "AppProfile":
        """Return a new profile with new plugin configs."""
        return self.model_copy(update={"plugin_configs": configs})

    def with_warning_filters(self, filters: list[WarningFilter]) -> "AppProfile":
        """Return a new profile with new warning filters."""
        return self.model_copy(update={"warning_filters": list(filters)})

    def is_warning_filtered(self, warning: warnings.WarningMessage) -> bool:
        """True if the warning should not be shown in the GUI.

        Filters are checked in order and the first matched one determines the result,
        just like the filters of the built-in `warnings` module.
        """
        for filt in self.warning_filters:
            if filt.matches(warning):
                return True
        return False

    def with_keybinding_override(self, key: str, command_id: str) -> "AppProfile":
        """Return a new profile with new keybind overrides."""
        _overrides = self.keybinding_overrides.copy()
        for entry in _overrides:
            if entry.command_id == command_id:
                if key:
                    entry.key = key
                else:
                    _overrides.remove(entry)
                break
        else:
            if key:
                _overrides.append(KeyBindingOverride(key=key, command_id=command_id))
        return self.model_copy(update={"keybinding_overrides": _overrides})

    def update_plugin_config(self, plugin_id: str, **kwargs) -> None:
        """Update the config of the plugin specified by `plugin_id`"""
        from himena.plugins import AppActionRegistry
        from himena.plugins.widget_plugins import WidgetCallbackBase

        reg = AppActionRegistry.instance()
        configs = self.plugin_configs.copy()
        # NOTE: during development, keys of cur_config and configs[plugin_id] may
        # differ. `cur_config` has all the keys that should exist in the current
        # implementation.
        cur_config = reg._plugin_default_configs[plugin_id].as_dict()
        if plugin_id in configs:
            # Profile already has the plugin config
            for ckey, cval in configs[plugin_id].items():
                if ckey in cur_config:
                    cur_config[ckey] = cval
        for k, v in kwargs.items():
            if k in cur_config:
                cur_config[k]["value"] = v
        configs[plugin_id] = cur_config
        self.with_plugin_configs(configs).save()

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

    @field_validator("name")
    def _validate_name(cls, value):
        # check if value is a valid file name
        if not all(c in ALLOWED_LETTERS for c in value):
            raise ValueError(f"Invalid profile name: {value}")
        return value
default(save=False) classmethod

Return the default profile.

Source code in src\himena\profile.py
202
203
204
205
206
207
208
@classmethod
def default(cls, save: bool = False) -> "AppProfile":
    """Return the default profile."""
    prof = AppProfile()
    if save and not (profile_dir() / f"{prof.name}.json").exists():
        prof.save()
    return prof
from_json(path) classmethod

Construct an AppProfile from a json file.

Source code in src\himena\profile.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
@classmethod
def from_json(cls, path) -> "AppProfile":
    """Construct an AppProfile from a json file."""
    with open(path) as f:
        data = json.load(f)
    version_saved = Version(data.get("version", "0.0.1"))
    self = cls(**data)
    if version_saved < Version(_current_version()):
        for place, version_added in DEFAULT_PLUGINS:
            if version_added > version_saved and place not in self.plugins:
                # Add the default plugin that is implemented after the profile was
                # saved.
                self.plugins.append(place)
    return self
is_warning_filtered(warning)

True if the warning should not be shown in the GUI.

Filters are checked in order and the first matched one determines the result, just like the filters of the built-in warnings module.

Source code in src\himena\profile.py
239
240
241
242
243
244
245
246
247
248
def is_warning_filtered(self, warning: warnings.WarningMessage) -> bool:
    """True if the warning should not be shown in the GUI.

    Filters are checked in order and the first matched one determines the result,
    just like the filters of the built-in `warnings` module.
    """
    for filt in self.warning_filters:
        if filt.matches(warning):
            return True
    return False
profile_path()

Path to this profile.

Source code in src\himena\profile.py
219
220
221
def profile_path(self) -> Path:
    """Path to this profile."""
    return profile_dir() / f"{self.name}.json"
save(path=None)

Save profile as a json file.

Source code in src\himena\profile.py
210
211
212
213
214
215
216
217
def save(self, path: str | Path | None = None) -> None:
    """Save profile as a json file."""
    if path is None:
        path = self.profile_path()
    json_string = json.dumps(self.model_dump(), indent=4)
    with open(path, "w") as f:
        f.write(json_string)
    return None
update_plugin_config(plugin_id, **kwargs)

Update the config of the plugin specified by plugin_id

Source code in src\himena\profile.py
265
266
267
268
269
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
def update_plugin_config(self, plugin_id: str, **kwargs) -> None:
    """Update the config of the plugin specified by `plugin_id`"""
    from himena.plugins import AppActionRegistry
    from himena.plugins.widget_plugins import WidgetCallbackBase

    reg = AppActionRegistry.instance()
    configs = self.plugin_configs.copy()
    # NOTE: during development, keys of cur_config and configs[plugin_id] may
    # differ. `cur_config` has all the keys that should exist in the current
    # implementation.
    cur_config = reg._plugin_default_configs[plugin_id].as_dict()
    if plugin_id in configs:
        # Profile already has the plugin config
        for ckey, cval in configs[plugin_id].items():
            if ckey in cur_config:
                cur_config[ckey] = cval
    for k, v in kwargs.items():
        if k in cur_config:
            cur_config[k]["value"] = v
    configs[plugin_id] = cur_config
    self.with_plugin_configs(configs).save()

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

Return a new profile with new keybind overrides.

Source code in src\himena\profile.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def with_keybinding_override(self, key: str, command_id: str) -> "AppProfile":
    """Return a new profile with new keybind overrides."""
    _overrides = self.keybinding_overrides.copy()
    for entry in _overrides:
        if entry.command_id == command_id:
            if key:
                entry.key = key
            else:
                _overrides.remove(entry)
            break
    else:
        if key:
            _overrides.append(KeyBindingOverride(key=key, command_id=command_id))
    return self.model_copy(update={"keybinding_overrides": _overrides})
with_name(name)

Return a new profile with a new name.

Source code in src\himena\profile.py
223
224
225
def with_name(self, name: str) -> "AppProfile":
    """Return a new profile with a new name."""
    return self.model_copy(update={"name": name})
with_plugin_configs(configs)

Return a new profile with new plugin configs.

Source code in src\himena\profile.py
231
232
233
def with_plugin_configs(self, configs: dict[str, dict[str, Any]]) -> "AppProfile":
    """Return a new profile with new plugin configs."""
    return self.model_copy(update={"plugin_configs": configs})
with_plugins(plugins)

Return a new profile with new plugins.

Source code in src\himena\profile.py
227
228
229
def with_plugins(self, plugins: list[str]) -> "AppProfile":
    """Return a new profile with new plugins."""
    return self.model_copy(update={"plugins": plugins})
with_warning_filters(filters)

Return a new profile with new warning filters.

Source code in src\himena\profile.py
235
236
237
def with_warning_filters(self, filters: list[WarningFilter]) -> "AppProfile":
    """Return a new profile with new warning filters."""
    return self.model_copy(update={"warning_filters": list(filters)})

KeyBindingOverride

Parameters:

Name Type Description Default
key str
required
command_id str
required
Source code in src\himena\profile.py
 95
 96
 97
 98
 99
100
class KeyBindingOverride(BaseModel):
    key: str
    command_id: str

    def to_normed_str(self) -> str:
        return self.key.replace(", ", " ")

WarningFilter

Filter that determines whether a warning is shown in the GUI.

Parameters:

Name Type Description Default
category str

Name of the warning category, such as DeprecationWarning or the fully qualified name numpy.exceptions.VisibleDeprecationWarning. Subclasses of the category also match. An empty string matches any category.

''
message str

Regular expression searched in the warning message. An empty string matches any message.

''
module str

Regular expression searched in the path of the file that raised the warning. An empty string matches any file.

''
Source code in src\himena\profile.py
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
class WarningFilter(BaseModel):
    """Filter that determines whether a warning is shown in the GUI."""

    category: str = Field(
        default="",
        description="Name of the warning category, such as `DeprecationWarning` or "
        "the fully qualified name `numpy.exceptions.VisibleDeprecationWarning`. "
        "Subclasses of the category also match. An empty string matches any category.",
    )
    message: str = Field(
        default="",
        description="Regular expression searched in the warning message. An empty "
        "string matches any message.",
    )
    module: str = Field(
        default="",
        description="Regular expression searched in the path of the file that raised "
        "the warning. An empty string matches any file.",
    )

    def matches(self, warning: warnings.WarningMessage) -> bool:
        """True if the given warning matches this filter."""
        return (
            self._matches_category(warning.category)
            and _regex_search(self.message, str(warning.message))
            and _regex_search(self.module, _norm_path(warning.filename))
        )

    def _matches_category(self, category: Any) -> bool:
        if not self.category:
            return True
        if not (isinstance(category, type) and issubclass(category, Warning)):
            return False
        for cls in category.__mro__:
            if not issubclass(cls, Warning):
                continue  # BaseException, Exception and object
            if self.category in (cls.__name__, f"{cls.__module__}.{cls.__qualname__}"):
                return True
        return False
matches(warning)

True if the given warning matches this filter.

Source code in src\himena\profile.py
123
124
125
126
127
128
129
def matches(self, warning: warnings.WarningMessage) -> bool:
    """True if the given warning matches this filter."""
    return (
        self._matches_category(warning.category)
        and _regex_search(self.message, str(warning.message))
        and _regex_search(self.module, _norm_path(warning.filename))
    )

data_dir()

Get the user data directory.

Source code in src\himena\profile.py
29
30
31
32
33
def data_dir() -> Path:
    """Get the user data directory."""
    if not USER_DATA_DIR.exists():
        USER_DATA_DIR.mkdir(parents=True)
    return USER_DATA_DIR

new_app_profile(name)

Create a new profile.

Source code in src\himena\profile.py
333
334
335
336
337
338
339
340
def new_app_profile(name: str) -> AppProfile:
    """Create a new profile."""
    path = profile_dir() / f"{name}.json"
    if path.exists():
        raise ValueError(f"Profile {name!r} already exists.")
    profile = AppProfile.default().with_name(name)
    profile.save(path)
    return profile

patch_user_data_dir(path)

Change the user data directory to avoid pytest updates the local state.

Source code in src\himena\profile.py
17
18
19
20
21
22
23
24
25
26
@contextmanager
def patch_user_data_dir(path: str | Path):
    """Change the user data directory to avoid pytest updates the local state."""
    global USER_DATA_DIR
    old = USER_DATA_DIR
    USER_DATA_DIR = Path(path)
    try:
        yield
    finally:
        USER_DATA_DIR = old

remove_app_profile(name)

Remove an existing profile.

Source code in src\himena\profile.py
343
344
345
346
def remove_app_profile(name: str) -> None:
    """Remove an existing profile."""
    path = profile_dir() / f"{name}.json"
    return path.unlink()