Skip to content

Commit

Permalink
Add dictionary mapping command_id to its associated MenuCommands …
Browse files Browse the repository at this point in the history
…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 <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 21, 2024
1 parent 490131a commit eeeeacc
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 1 deletion.
22 changes: 21 additions & 1 deletion src/npe2/_plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,6 +38,7 @@
if TYPE_CHECKING:
from .manifest.contributions import (
CommandContribution,
MenuCommand,
MenuItem,
ReaderContribution,
SampleDataContribution,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit eeeeacc

Please sign in to comment.