From eeeeaccfc4a2a56c906d40badd62a1f963197622 Mon Sep 17 00:00:00 2001 From: Draga Doncila Pop <17995243+DragaDoncila@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:48:52 +0700 Subject: [PATCH] Add dictionary mapping `command_id` to its associated `MenuCommand`s for each plugin (#348) Adding this dictionary to support quick mapping of widgets/commands to their menus for implementing contributable menus . Prior to this PR, you would have to search all menu contributions for the command of the contribution you were currently processing. This map allows you direct access to the menus this command needs to live in. This is a precursor to potentially rearchitecting the manifest schema entirely to be "command-first". --------- Co-authored-by: Juan Nunez-Iglesias Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/npe2/_plugin_manager.py | 22 +++++++++++++++++++++- tests/test_plugin_manager.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/npe2/_plugin_manager.py b/src/npe2/_plugin_manager.py index d364cdeb..5ecddb65 100644 --- a/src/npe2/_plugin_manager.py +++ b/src/npe2/_plugin_manager.py @@ -4,7 +4,7 @@ import os import urllib import warnings -from collections import Counter +from collections import Counter, defaultdict from fnmatch import fnmatch from importlib import metadata from logging import getLogger @@ -38,6 +38,7 @@ if TYPE_CHECKING: from .manifest.contributions import ( CommandContribution, + MenuCommand, MenuItem, ReaderContribution, SampleDataContribution, @@ -235,6 +236,9 @@ def __init__( self._manifests: Dict[PluginName, PluginManifest] = {} self.events = PluginManagerEvents(self) self._npe1_adapters: List[NPE1Adapter] = [] + self._command_menu_map: Dict[ + str, Dict[str, Dict[str, List[MenuCommand]]] + ] = defaultdict(dict) # up to napari 0.4.15, discovery happened in the init here # so if we're running on an older version of napari, we need to discover @@ -358,14 +362,28 @@ def register( self._npe1_adapters.append(manifest) else: self._contrib.index_contributions(manifest) + self._populate_command_menu_map(manifest) self.events.plugins_registered.emit({manifest}) + def _populate_command_menu_map(self, manifest: PluginManifest): + # map of manifest -> command -> menu_id -> list[items] + self._command_menu_map[manifest.name] = defaultdict(lambda: defaultdict(list)) + menu_map = self._command_menu_map[manifest.name] # just for conciseness below + for menu_id, menu_items in manifest.contributions.menus.items() or (): + # command IDs are keys in map + # each value is a dict menu_id: list of MenuCommands + # for the command and menu + for item in menu_items: + if (command_id := getattr(item, "command", None)) is not None: + menu_map[command_id][menu_id].append(item) + def unregister(self, key: PluginName): """Unregister plugin named `key`.""" if key not in self._manifests: raise ValueError(f"No registered plugin named {key!r}") # pragma: no cover self.deactivate(key) self._contrib.remove_contributions(key) + self._command_menu_map.pop(key) self._manifests.pop(key) def activate(self, key: PluginName) -> PluginContext: @@ -448,6 +466,7 @@ def enable(self, plugin_name: PluginName) -> None: mf = self._manifests.get(plugin_name) if mf is not None: self._contrib.index_contributions(mf) + self._populate_command_menu_map(mf) self.events.enablement_changed({plugin_name}, {}) def disable(self, plugin_name: PluginName) -> None: @@ -467,6 +486,7 @@ def disable(self, plugin_name: PluginName) -> None: self._disabled_plugins.add(plugin_name) self._contrib.remove_contributions(plugin_name) + self._command_menu_map.pop(plugin_name) self.events.enablement_changed({}, {plugin_name}) def is_disabled(self, plugin_name: str) -> bool: diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py index 8d1af5b9..6c9fae58 100644 --- a/tests/test_plugin_manager.py +++ b/tests/test_plugin_manager.py @@ -236,3 +236,33 @@ def dummy_error(): pm.get_context("test").register_disposable(dummy_error) pm.deactivate("test") assert caplog.records[0].msg == "Error while disposing test; This is an error" + + +def test_command_menu_map(uses_sample_plugin, plugin_manager: PluginManager): + """Test that the command menu map is correctly populated.""" + pm = PluginManager.instance() + assert SAMPLE_PLUGIN_NAME in pm._manifests + assert SAMPLE_PLUGIN_NAME in pm._command_menu_map + + # contains correct commands + command_menu_map = pm._command_menu_map[SAMPLE_PLUGIN_NAME] + assert "my-plugin.hello_world" in command_menu_map + assert "my-plugin.another_command" in command_menu_map + + # commands point to correct menus + assert len(cmd_menu := command_menu_map["my-plugin.hello_world"]) == 1 + assert "/napari/layer_context" in cmd_menu + assert len(cmd_menu := command_menu_map["my-plugin.another_command"]) == 1 + assert "mysubmenu" in cmd_menu + + # enable/disable + pm.disable(SAMPLE_PLUGIN_NAME) + assert SAMPLE_PLUGIN_NAME not in pm._command_menu_map + pm.enable(SAMPLE_PLUGIN_NAME) + assert SAMPLE_PLUGIN_NAME in pm._command_menu_map + + # register/unregister + pm.unregister(SAMPLE_PLUGIN_NAME) + assert SAMPLE_PLUGIN_NAME not in pm._command_menu_map + pm.register(SAMPLE_PLUGIN_NAME) + assert SAMPLE_PLUGIN_NAME in pm._command_menu_map