Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions datalab/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ class MainSection(conf.Section, metaclass=conf.SectionMeta):
current_tab = conf.Option()
plugins_enabled = conf.Option()
plugins_enabled_list = conf.Option() # List of enabled plugin names
plugins_path = conf.Option()
plugins_path = conf.Option() # Deprecated: single-directory string, kept for
# backward compatibility. Use plugins_path_list instead.
plugins_path_list = conf.Option() # List of extra plugin directories
tour_enabled = conf.Option()
v020_plugins_warning_ignore = conf.Option() # True: do not warn, False: warn

Expand Down Expand Up @@ -525,6 +527,59 @@ class Conf(conf.Configuration, metaclass=conf.ConfMeta):
ai = AISection()


def normalize_plugin_paths(paths: list[str] | tuple[str, ...] | None) -> list[str]:
"""Normalize a list of plugin directories and drop duplicates/empties."""
normalized: list[str] = []
seen: set[str] = set()
for raw_path in paths or []:
if not raw_path:
continue
path = osp.normpath(osp.abspath(osp.expanduser(raw_path)))
if path in seen:
continue
seen.add(path)
normalized.append(path)
return normalized


def get_user_plugin_paths() -> list[str]:
"""Return user-configured extra plugin directories.

Reads from ``plugins_path_list`` (list of directories). For backward
compatibility, the deprecated ``plugins_path`` single-directory string is
also merged into ``plugins_path_list`` if this is empty.
"""
fixed_default = osp.normpath(Conf.get_path("plugins"))

# New list-based option (primary)
path_list = Conf.main.plugins_path_list.get([])
if path_list is None:
path_list = []
candidates = list(path_list)

# Migrate deprecated single-directory option into the list
legacy_path = Conf.main.plugins_path.get("")
if legacy_path and isinstance(legacy_path, str):
norm_legacy = osp.normpath(osp.abspath(osp.expanduser(legacy_path)))
if not candidates and norm_legacy != fixed_default:
candidates.append(legacy_path)
Conf.main.plugins_path_list.set(candidates)

normalized = normalize_plugin_paths(candidates)
return [path for path in normalized if path != fixed_default]


def set_user_plugin_paths(paths: list[str] | tuple[str, ...]) -> None:
"""Persist user-configured extra plugin directories.

Writes to ``plugins_path_list``. The deprecated ``plugins_path`` is left
untouched so that older DataLab versions can still find at least one
user-configured directory.
"""
normalized = normalize_plugin_paths(list(paths))
Conf.main.plugins_path_list.set(normalized)


def get_old_log_fname(fname):
"""Return old log fname from current log fname"""
return osp.splitext(fname)[0] + ".1.log"
Expand Down Expand Up @@ -555,7 +610,8 @@ def initialize():
Conf.main.plugins_enabled_list.get(
None
) # None = all enabled, [] = none, list = specific
Conf.main.plugins_path.get(Conf.get_path("plugins"))
Conf.main.plugins_path.get("") # Deprecated: kept for backward compat
Conf.main.plugins_path_list.get([])
Conf.main.tour_enabled.get(True)
Conf.main.v020_plugins_warning_ignore.get(False)
# Console section
Expand Down
11 changes: 11 additions & 0 deletions datalab/data/icons/edit/open_file_source.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions datalab/data/icons/io/show_in_folder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 27 additions & 22 deletions datalab/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import time
import traceback
import webbrowser
from datetime import datetime
from typing import TYPE_CHECKING

import guidata.dataset as gds
Expand Down Expand Up @@ -158,6 +159,8 @@ def __init__(self, console=None, hide_on_close=False): # pylint: disable=too-ma
execenv.log(self, "Starting initialization")

self.ready_flag = True
self.started_at = datetime.now().astimezone()
self.plugins_last_load_at = self.started_at

self.hide_on_close = hide_on_close
self.__old_size: tuple[int, int] | None = None
Expand Down Expand Up @@ -1051,6 +1054,10 @@ def __register_plugins(self) -> None:
# None = all plugins enabled (default), [] = no plugins, list = specific plugins
enabled_list = Conf.main.plugins_enabled_list.get(None)

if not Conf.main.plugins_enabled.get():
self.plugins_last_load_at = datetime.now().astimezone()
return

for plugin_class in PluginRegistry.get_plugin_classes():
try:
# Check if plugin is enabled before instantiation
Expand Down Expand Up @@ -1092,6 +1099,8 @@ def __register_plugins(self) -> None:
plugin_class.__name__, filepath or "", tb_text
)

self.plugins_last_load_at = datetime.now().astimezone()

def __flush_startup_errors(self) -> None:
"""Write any buffered startup errors to the internal console.

Expand Down Expand Up @@ -1129,6 +1138,11 @@ def __configure_plugins(self) -> None:
dialog = PluginConfigDialog(self)
dialog.exec()

def set_plugins_enabled(self, enabled: bool) -> None:
"""Apply the global third-party plugin enabled state."""
Conf.main.plugins_enabled.set(enabled)
self.__apply_plugins_enabled_setting()

def reload_plugins(self) -> None:
"""Reload third-party plugins at runtime.

Expand All @@ -1143,8 +1157,9 @@ def reload_plugins(self) -> None:
self,
_("Plugins"),
_(
"Third-party plugins are disabled. Enable them in the "
"Settings dialog to use this feature."
"Third-party plugins are disabled. Enable them again "
"from the plugin configuration dialog to use this "
"feature."
),
)
return
Expand Down Expand Up @@ -1220,6 +1235,7 @@ def reload_plugins(self) -> None:
# Update plugin status in the status bar
self.pluginstatus.update_status()
self.__update_plugins_availability()
self.plugins_last_load_at = datetime.now().astimezone()

def __configure_statusbar(self, console: bool) -> None:
"""Configure status bar
Expand Down Expand Up @@ -1255,12 +1271,11 @@ def __update_plugins_availability(self) -> None:
"""Update plugin-related UI according to third-party plugin setting."""
plugins_enabled = Conf.main.plugins_enabled.get()

if self.plugins_menu is not None:
self.plugins_menu.setEnabled(plugins_enabled)
if self.reload_plugins_action is not None:
self.reload_plugins_action.setEnabled(plugins_enabled)

for action in (self.reload_plugins_action, self.configure_plugins_action):
if action is not None:
action.setEnabled(plugins_enabled)
if self.configure_plugins_action is not None:
self.configure_plugins_action.setEnabled(True)

if hasattr(self, "pluginstatus") and self.pluginstatus is not None:
self.pluginstatus.update_status()
Expand All @@ -1277,11 +1292,6 @@ def __apply_plugins_enabled_setting(self) -> None:
for panel in (self.signalpanel, self.imagepanel):
panel.acthandler.clear_plugin_actions()

PluginRegistry.clear_plugin_classes()
PluginRegistry.clear_failed_plugins()
PluginRegistry.clear_discovery_errors()
self._startup_errors.clear()

self.__update_actions(update_other_data_panel=True)
self.__update_plugins_availability()

Expand Down Expand Up @@ -1867,8 +1877,6 @@ def __update_actions(self, update_other_data_panel: bool = False) -> None:
panel.selection_changed()
self.signalpanel_toolbar.setVisible(is_signal)
self.imagepanel_toolbar.setVisible(not is_signal)
if self.plugins_menu is not None:
self.plugins_menu.setEnabled(Conf.main.plugins_enabled.get())

def __tab_index_changed(self, index: int) -> None:
"""Switch from signal to image mode, or vice-versa"""
Expand Down Expand Up @@ -1898,12 +1906,11 @@ def __update_generic_menu(self, menu: QW.QMenu | None = None) -> None:
# no plugin has registered actions yet (so that new plugins can be
# discovered after they are added on disk).
if menu is self.plugins_menu:
if Conf.main.plugins_enabled.get():
actions = list(actions) + [
None,
self.configure_plugins_action,
self.reload_plugins_action,
]
actions = list(actions) + [
None,
self.configure_plugins_action,
self.reload_plugins_action,
]
add_actions(menu, actions)

def __update_file_menu(self) -> None:
Expand Down Expand Up @@ -2513,8 +2520,6 @@ def __edit_settings(self) -> None: # pylint: disable=too-many-branches,too-many
self.__update_color_mode()
if option == "show_console_on_error":
self.__update_console_show_mode()
if option == "plugins_enabled":
self.__apply_plugins_enabled_setting()
if option == "plot_toolbar_position":
for dock in self.docks.values():
widget = dock.widget()
Expand Down
Loading
Loading