diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d732f26..fcbc5929 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,7 +180,7 @@ jobs: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }} - - uses: softprops/action-gh-release@v1 + - uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: generate_release_notes: true diff --git a/src/npe2/_plugin_manager.py b/src/npe2/_plugin_manager.py index d364cdeb..01322d6f 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, None) 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