From 490131a4de9bb9e41400f17f998d76a112f3822e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:33:31 +1000 Subject: [PATCH 1/3] ci(dependabot): bump softprops/action-gh-release from 1 to 2 (#353) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Juan Nunez-Iglesias --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 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 2/3] 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 From c564891dcfcd05064a52dcfd53ac611d4e880efc Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Mon, 29 Jul 2024 07:45:04 -0400 Subject: [PATCH 3/3] On disable, dont try to pop a key that doesn't exist (#356) --- src/npe2/_plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/npe2/_plugin_manager.py b/src/npe2/_plugin_manager.py index 5ecddb65..01322d6f 100644 --- a/src/npe2/_plugin_manager.py +++ b/src/npe2/_plugin_manager.py @@ -486,7 +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._command_menu_map.pop(plugin_name, None) self.events.enablement_changed({}, {plugin_name}) def is_disabled(self, plugin_name: str) -> bool: