npe2 0.7.5__tar.gz → 0.7.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {npe2-0.7.5 → npe2-0.7.6}/.github/dependabot.yml +5 -0
  2. {npe2-0.7.5 → npe2-0.7.6}/.github/workflows/ci.yml +8 -8
  3. {npe2-0.7.5 → npe2-0.7.6}/.github/workflows/test_conversion.yml +4 -4
  4. {npe2-0.7.5 → npe2-0.7.6}/.github/workflows/update_changelog.yml +1 -1
  5. {npe2-0.7.5 → npe2-0.7.6}/PKG-INFO +1 -1
  6. {npe2-0.7.5 → npe2-0.7.6}/_docs/example_plugin/some_module.py +4 -5
  7. {npe2-0.7.5 → npe2-0.7.6}/_docs/templates/_npe2_widgets_guide.md.jinja +15 -2
  8. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_plugin_manager.py +21 -1
  9. {npe2-0.7.5 → npe2-0.7.6}/tests/test_plugin_manager.py +30 -0
  10. {npe2-0.7.5 → npe2-0.7.6}/.github/ISSUE_TEMPLATE.md +0 -0
  11. {npe2-0.7.5 → npe2-0.7.6}/.github/workflows/test_all_plugins.yml +0 -0
  12. {npe2-0.7.5 → npe2-0.7.6}/.github_changelog_generator +0 -0
  13. {npe2-0.7.5 → npe2-0.7.6}/.gitignore +0 -0
  14. {npe2-0.7.5 → npe2-0.7.6}/.pre-commit-config.yaml +0 -0
  15. {npe2-0.7.5 → npe2-0.7.6}/CHANGELOG.md +0 -0
  16. {npe2-0.7.5 → npe2-0.7.6}/LICENSE +0 -0
  17. {npe2-0.7.5 → npe2-0.7.6}/Makefile +0 -0
  18. {npe2-0.7.5 → npe2-0.7.6}/README.md +0 -0
  19. {npe2-0.7.5 → npe2-0.7.6}/_docs/example_manifest.yaml +0 -0
  20. {npe2-0.7.5 → npe2-0.7.6}/_docs/example_plugin/__init__.py +0 -0
  21. {npe2-0.7.5 → npe2-0.7.6}/_docs/render.py +0 -0
  22. {npe2-0.7.5 → npe2-0.7.6}/_docs/templates/_npe2_contributions.md.jinja +0 -0
  23. {npe2-0.7.5 → npe2-0.7.6}/_docs/templates/_npe2_manifest.md.jinja +0 -0
  24. {npe2-0.7.5 → npe2-0.7.6}/_docs/templates/_npe2_readers_guide.md.jinja +0 -0
  25. {npe2-0.7.5 → npe2-0.7.6}/_docs/templates/_npe2_sample_data_guide.md.jinja +0 -0
  26. {npe2-0.7.5 → npe2-0.7.6}/_docs/templates/_npe2_writers_guide.md.jinja +0 -0
  27. {npe2-0.7.5 → npe2-0.7.6}/codecov.yml +0 -0
  28. {npe2-0.7.5 → npe2-0.7.6}/docs/_config.yml +0 -0
  29. {npe2-0.7.5 → npe2-0.7.6}/docs/_toc.yml +0 -0
  30. {npe2-0.7.5 → npe2-0.7.6}/docs/index.md +0 -0
  31. {npe2-0.7.5 → npe2-0.7.6}/docs/requirements.txt +0 -0
  32. {npe2-0.7.5 → npe2-0.7.6}/pyproject.toml +0 -0
  33. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/__init__.py +0 -0
  34. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/__main__.py +0 -0
  35. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_command_registry.py +0 -0
  36. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_dynamic_plugin.py +0 -0
  37. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_inspection/__init__.py +0 -0
  38. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_inspection/_compile.py +0 -0
  39. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_inspection/_fetch.py +0 -0
  40. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_inspection/_from_npe1.py +0 -0
  41. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_inspection/_setuputils.py +0 -0
  42. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_inspection/_visitors.py +0 -0
  43. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_pydantic_compat.py +0 -0
  44. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_pytest_plugin.py +0 -0
  45. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/_setuptools_plugin.py +0 -0
  46. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/cli.py +0 -0
  47. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/implements.py +0 -0
  48. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/implements.pyi +0 -0
  49. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/io_utils.py +0 -0
  50. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/__init__.py +0 -0
  51. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/_bases.py +0 -0
  52. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/_npe1_adapter.py +0 -0
  53. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/_package_metadata.py +0 -0
  54. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/_validators.py +0 -0
  55. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/__init__.py +0 -0
  56. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_commands.py +0 -0
  57. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_configuration.py +0 -0
  58. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_contributions.py +0 -0
  59. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_icon.py +0 -0
  60. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_json_schema.py +0 -0
  61. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_keybindings.py +0 -0
  62. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_menus.py +0 -0
  63. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_readers.py +0 -0
  64. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_sample_data.py +0 -0
  65. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_submenu.py +0 -0
  66. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_themes.py +0 -0
  67. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_widgets.py +0 -0
  68. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/contributions/_writers.py +0 -0
  69. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/menus.py +0 -0
  70. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/package_metadata.py +0 -0
  71. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/schema.py +0 -0
  72. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/manifest/utils.py +0 -0
  73. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/plugin_manager.py +0 -0
  74. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/py.typed +0 -0
  75. {npe2-0.7.5 → npe2-0.7.6}/src/npe2/types.py +0 -0
  76. {npe2-0.7.5 → npe2-0.7.6}/tests/conftest.py +0 -0
  77. {npe2-0.7.5 → npe2-0.7.6}/tests/fixtures/my-compiled-plugin/my_module/__init__.py +0 -0
  78. {npe2-0.7.5 → npe2-0.7.6}/tests/fixtures/my-compiled-plugin/my_module/_a.py +0 -0
  79. {npe2-0.7.5 → npe2-0.7.6}/tests/fixtures/my-compiled-plugin/my_module/_b.py +0 -0
  80. {npe2-0.7.5 → npe2-0.7.6}/tests/fixtures/my-compiled-plugin/setup.cfg +0 -0
  81. {npe2-0.7.5 → npe2-0.7.6}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/METADATA +0 -0
  82. {npe2-0.7.5 → npe2-0.7.6}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/RECORD +0 -0
  83. {npe2-0.7.5 → npe2-0.7.6}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/entry_points.txt +0 -0
  84. {npe2-0.7.5 → npe2-0.7.6}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/top_level.txt +0 -0
  85. {npe2-0.7.5 → npe2-0.7.6}/tests/npe1-plugin/npe1_module/__init__.py +0 -0
  86. {npe2-0.7.5 → npe2-0.7.6}/tests/npe1-plugin/setup.cfg +0 -0
  87. {npe2-0.7.5 → npe2-0.7.6}/tests/sample/_with_decorators.py +0 -0
  88. {npe2-0.7.5 → npe2-0.7.6}/tests/sample/my_plugin/__init__.py +0 -0
  89. {npe2-0.7.5 → npe2-0.7.6}/tests/sample/my_plugin/napari.yaml +0 -0
  90. {npe2-0.7.5 → npe2-0.7.6}/tests/sample/my_plugin-1.2.3.dist-info/METADATA +0 -0
  91. {npe2-0.7.5 → npe2-0.7.6}/tests/sample/my_plugin-1.2.3.dist-info/entry_points.txt +0 -0
  92. {npe2-0.7.5 → npe2-0.7.6}/tests/sample/my_plugin-1.2.3.dist-info/top_level.txt +0 -0
  93. {npe2-0.7.5 → npe2-0.7.6}/tests/test__io_utils.py +0 -0
  94. {npe2-0.7.5 → npe2-0.7.6}/tests/test_all_plugins.py +0 -0
  95. {npe2-0.7.5 → npe2-0.7.6}/tests/test_cli.py +0 -0
  96. {npe2-0.7.5 → npe2-0.7.6}/tests/test_compile.py +0 -0
  97. {npe2-0.7.5 → npe2-0.7.6}/tests/test_config_contribution.py +0 -0
  98. {npe2-0.7.5 → npe2-0.7.6}/tests/test_contributions.py +0 -0
  99. {npe2-0.7.5 → npe2-0.7.6}/tests/test_conversion.py +0 -0
  100. {npe2-0.7.5 → npe2-0.7.6}/tests/test_docs.py +0 -0
  101. {npe2-0.7.5 → npe2-0.7.6}/tests/test_fetch.py +0 -0
  102. {npe2-0.7.5 → npe2-0.7.6}/tests/test_implements.py +0 -0
  103. {npe2-0.7.5 → npe2-0.7.6}/tests/test_manifest.py +0 -0
  104. {npe2-0.7.5 → npe2-0.7.6}/tests/test_npe1_adapter.py +0 -0
  105. {npe2-0.7.5 → npe2-0.7.6}/tests/test_package_meta.py +0 -0
  106. {npe2-0.7.5 → npe2-0.7.6}/tests/test_pm_module.py +0 -0
  107. {npe2-0.7.5 → npe2-0.7.6}/tests/test_pytest_plugin.py +0 -0
  108. {npe2-0.7.5 → npe2-0.7.6}/tests/test_setuptools_plugin.py +0 -0
  109. {npe2-0.7.5 → npe2-0.7.6}/tests/test_tmp_plugin.py +0 -0
  110. {npe2-0.7.5 → npe2-0.7.6}/tests/test_utils.py +0 -0
  111. {npe2-0.7.5 → npe2-0.7.6}/tests/test_validations.py +0 -0
@@ -8,3 +8,8 @@ updates:
8
8
  interval: "weekly"
9
9
  commit-message:
10
10
  prefix: "ci(dependabot):"
11
+
12
+ groups:
13
+ github-actions:
14
+ patterns:
15
+ - "actions/*"
@@ -36,7 +36,7 @@ jobs:
36
36
  - uses: actions/checkout@v4
37
37
 
38
38
  - name: Set up Python ${{ matrix.python-version }}
39
- uses: actions/setup-python@v4
39
+ uses: actions/setup-python@v5
40
40
  with:
41
41
  python-version: ${{ matrix.python-version }}
42
42
 
@@ -70,7 +70,7 @@ jobs:
70
70
  path: napari-from-github
71
71
  fetch-depth: 0
72
72
 
73
- - uses: actions/setup-python@v4
73
+ - uses: actions/setup-python@v5
74
74
  with:
75
75
  python-version: "3.10"
76
76
  - name: Install
@@ -90,7 +90,7 @@ jobs:
90
90
  runs-on: ubuntu-latest
91
91
  steps:
92
92
  - uses: actions/checkout@v4
93
- - uses: actions/setup-python@v4
93
+ - uses: actions/setup-python@v5
94
94
  with:
95
95
  python-version: "3.x"
96
96
  - name: Build schema
@@ -110,9 +110,9 @@ jobs:
110
110
  if: always()
111
111
  runs-on: ubuntu-latest
112
112
  steps:
113
- - uses: actions/checkout@v3
113
+ - uses: actions/checkout@v4
114
114
 
115
- - uses: actions/setup-python@v4
115
+ - uses: actions/setup-python@v5
116
116
  with:
117
117
  python-version: "3.x"
118
118
  cache-dependency-path: setup.cfg
@@ -138,7 +138,7 @@ jobs:
138
138
  python -Im coverage report --format=markdown --skip-empty --skip-covered >> $GITHUB_STEP_SUMMARY
139
139
 
140
140
  - name: Upload coverage data
141
- uses: codecov/codecov-action@v3
141
+ uses: codecov/codecov-action@v4
142
142
  with:
143
143
  fail_ci_if_error: true
144
144
  token: ${{ secrets.CODECOV_TOKEN }}
@@ -155,7 +155,7 @@ jobs:
155
155
  - uses: actions/checkout@v4
156
156
 
157
157
  - name: Set up Python
158
- uses: actions/setup-python@v4
158
+ uses: actions/setup-python@v5
159
159
  with:
160
160
  python-version: "3.x"
161
161
 
@@ -180,7 +180,7 @@ jobs:
180
180
  TWINE_USERNAME: __token__
181
181
  TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
182
182
 
183
- - uses: softprops/action-gh-release@v1
183
+ - uses: softprops/action-gh-release@v2
184
184
  if: startsWith(github.ref, 'refs/tags/')
185
185
  with:
186
186
  generate_release_notes: true
@@ -30,7 +30,7 @@ jobs:
30
30
 
31
31
  steps:
32
32
  - uses: tlambert03/setup-qt-libs@v1
33
- - uses: actions/setup-python@v4
33
+ - uses: actions/setup-python@v5
34
34
  with:
35
35
  python-version: 3.9
36
36
 
@@ -61,7 +61,7 @@ jobs:
61
61
 
62
62
  - name: Test Conversion
63
63
  id: test-without-napari
64
- uses: aganders3/headless-gui@v1
64
+ uses: aganders3/headless-gui@v2
65
65
  continue-on-error: true
66
66
  with:
67
67
  run: npe2 convert ./plugin_repo
@@ -73,13 +73,13 @@ jobs:
73
73
  - name: Test Conversion again with napari
74
74
  id: test-with-napari
75
75
  if: ${{ steps.test-without-napari.outcome == 'failure' }}
76
- uses: aganders3/headless-gui@v1
76
+ uses: aganders3/headless-gui@v2
77
77
  with:
78
78
  run: npe2 convert ./plugin_repo
79
79
 
80
80
  - name: Test Conversion again with napari
81
81
  if: ${{ steps.test-without-napari.outcome == 'failure' && steps.test-with-napari.outcome == 'failure' }}
82
- uses: aganders3/headless-gui@v1
82
+ uses: aganders3/headless-gui@v2
83
83
  with:
84
84
  # try without modifying directory
85
85
  run: npe2 convert -n ${{ matrix.plugin }}
@@ -19,7 +19,7 @@ jobs:
19
19
  token: ${{ secrets.GITHUB_TOKEN }}
20
20
  repo: napari/npe2
21
21
  - name: Create Pull Request
22
- uses: peter-evans/create-pull-request@v5
22
+ uses: peter-evans/create-pull-request@v6
23
23
  with:
24
24
  token: ${{ secrets.GITHUB_TOKEN }}
25
25
  commit-message: Automatic changelog update
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: npe2
3
- Version: 0.7.5
3
+ Version: 0.7.6
4
4
  Summary: napari plugin engine v2
5
5
  Project-URL: homepage, https://github.com/napari/npe2
6
6
  Project-URL: repository, https://github.com/napari/npe2
@@ -5,11 +5,10 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
5
5
  from magicgui import magic_factory
6
6
  from qtpy.QtWidgets import QWidget
7
7
 
8
- from npe2.types import LayerData, PathOrPaths, ReaderFunction
9
-
10
8
  if TYPE_CHECKING:
11
9
  import napari.types
12
10
  import napari.viewer
11
+ from npe2.types import LayerData, PathOrPaths, ReaderFunction
13
12
 
14
13
 
15
14
  def write_points(path: str, layer_data: Any, attributes: Dict[str, Any]) -> List[str]:
@@ -20,7 +19,7 @@ def write_points(path: str, layer_data: Any, attributes: Dict[str, Any]) -> List
20
19
  return [path]
21
20
 
22
21
 
23
- def get_reader(path: PathOrPaths) -> Optional[ReaderFunction]:
22
+ def get_reader(path: "PathOrPaths") -> Optional["ReaderFunction"]:
24
23
  # If we recognize the format, we return the actual reader function
25
24
  if isinstance(path, str) and path.endswith(".xyz"):
26
25
  return xyz_file_reader
@@ -28,7 +27,7 @@ def get_reader(path: PathOrPaths) -> Optional[ReaderFunction]:
28
27
  return None
29
28
 
30
29
 
31
- def xyz_file_reader(path: PathOrPaths) -> List[LayerData]:
30
+ def xyz_file_reader(path: "PathOrPaths") -> List["LayerData"]:
32
31
  data = ... # somehow read data from path
33
32
  layer_attributes = {"name": "etc..."}
34
33
  return [(data, layer_attributes)]
@@ -64,7 +63,7 @@ def threshold(
64
63
  return (image > threshold).astype(int)
65
64
 
66
65
 
67
- def create_fractal() -> List[LayerData]:
66
+ def create_fractal() -> List["LayerData"]:
68
67
  """An example of a Sample Data Function.
69
68
 
70
69
  Note: Sample Data with URIs don't need python code.
@@ -1,8 +1,11 @@
1
1
  (widgets-contribution-guide)=
2
2
  ## Widgets
3
3
 
4
- Widget plugins allow developers to contribute novel graphical
5
- elements (aka "widgets") to the user interface. These widgets can request
4
+ Widget plugin contributions allow developers to add novel graphical
5
+ elements (aka "widgets") to the user interface. For a full introduction to
6
+ creating `napari` widgets see [](creating-widgets).
7
+
8
+ Widgets can request
6
9
  access to the viewer instance in which they are docked, enabling a broad
7
10
  range of functionality: essentially, anything that can be done with the
8
11
  napari `Viewer` and `Layer` APIs can be accomplished with widgets.
@@ -58,6 +61,16 @@ specification:
58
61
  hook specification. In the new `npe2` pattern, one uses the `autogenerate`
59
62
  field in the [WidgetContribution](contributions-widgets).
60
63
 
64
+ For more examples see [](creating-widgets) and
65
+ [GUI gallery examples](https://napari.org/stable/_tags/gui.html) (only a subset
66
+ involve widgets). Additionally,
67
+ [cookiecutter-napari-plugin](https://github.com/napari/cookiecutter-napari-plugin)
68
+ has more robust widget examples that you can adapt to your needs.
69
+
70
+ ```{note}
71
+ Notice that `napari` type annotations are strings and not imported. This is to
72
+ avoid including `napari` as a plugin dependency when not strictly required.
73
+ ```
61
74
 
62
75
  ### Widget example
63
76
 
@@ -4,7 +4,7 @@ import contextlib
4
4
  import os
5
5
  import urllib
6
6
  import warnings
7
- from collections import Counter
7
+ from collections import Counter, defaultdict
8
8
  from fnmatch import fnmatch
9
9
  from importlib import metadata
10
10
  from logging import getLogger
@@ -38,6 +38,7 @@ from .types import PathLike, PythonName
38
38
  if TYPE_CHECKING:
39
39
  from .manifest.contributions import (
40
40
  CommandContribution,
41
+ MenuCommand,
41
42
  MenuItem,
42
43
  ReaderContribution,
43
44
  SampleDataContribution,
@@ -235,6 +236,9 @@ class PluginManager:
235
236
  self._manifests: Dict[PluginName, PluginManifest] = {}
236
237
  self.events = PluginManagerEvents(self)
237
238
  self._npe1_adapters: List[NPE1Adapter] = []
239
+ self._command_menu_map: Dict[
240
+ str, Dict[str, Dict[str, List[MenuCommand]]]
241
+ ] = defaultdict(dict)
238
242
 
239
243
  # up to napari 0.4.15, discovery happened in the init here
240
244
  # so if we're running on an older version of napari, we need to discover
@@ -358,14 +362,28 @@ class PluginManager:
358
362
  self._npe1_adapters.append(manifest)
359
363
  else:
360
364
  self._contrib.index_contributions(manifest)
365
+ self._populate_command_menu_map(manifest)
361
366
  self.events.plugins_registered.emit({manifest})
362
367
 
368
+ def _populate_command_menu_map(self, manifest: PluginManifest):
369
+ # map of manifest -> command -> menu_id -> list[items]
370
+ self._command_menu_map[manifest.name] = defaultdict(lambda: defaultdict(list))
371
+ menu_map = self._command_menu_map[manifest.name] # just for conciseness below
372
+ for menu_id, menu_items in manifest.contributions.menus.items() or ():
373
+ # command IDs are keys in map
374
+ # each value is a dict menu_id: list of MenuCommands
375
+ # for the command and menu
376
+ for item in menu_items:
377
+ if (command_id := getattr(item, "command", None)) is not None:
378
+ menu_map[command_id][menu_id].append(item)
379
+
363
380
  def unregister(self, key: PluginName):
364
381
  """Unregister plugin named `key`."""
365
382
  if key not in self._manifests:
366
383
  raise ValueError(f"No registered plugin named {key!r}") # pragma: no cover
367
384
  self.deactivate(key)
368
385
  self._contrib.remove_contributions(key)
386
+ self._command_menu_map.pop(key)
369
387
  self._manifests.pop(key)
370
388
 
371
389
  def activate(self, key: PluginName) -> PluginContext:
@@ -448,6 +466,7 @@ class PluginManager:
448
466
  mf = self._manifests.get(plugin_name)
449
467
  if mf is not None:
450
468
  self._contrib.index_contributions(mf)
469
+ self._populate_command_menu_map(mf)
451
470
  self.events.enablement_changed({plugin_name}, {})
452
471
 
453
472
  def disable(self, plugin_name: PluginName) -> None:
@@ -467,6 +486,7 @@ class PluginManager:
467
486
 
468
487
  self._disabled_plugins.add(plugin_name)
469
488
  self._contrib.remove_contributions(plugin_name)
489
+ self._command_menu_map.pop(plugin_name)
470
490
  self.events.enablement_changed({}, {plugin_name})
471
491
 
472
492
  def is_disabled(self, plugin_name: str) -> bool:
@@ -236,3 +236,33 @@ def test_plugin_context_dispose_error(caplog):
236
236
  pm.get_context("test").register_disposable(dummy_error)
237
237
  pm.deactivate("test")
238
238
  assert caplog.records[0].msg == "Error while disposing test; This is an error"
239
+
240
+
241
+ def test_command_menu_map(uses_sample_plugin, plugin_manager: PluginManager):
242
+ """Test that the command menu map is correctly populated."""
243
+ pm = PluginManager.instance()
244
+ assert SAMPLE_PLUGIN_NAME in pm._manifests
245
+ assert SAMPLE_PLUGIN_NAME in pm._command_menu_map
246
+
247
+ # contains correct commands
248
+ command_menu_map = pm._command_menu_map[SAMPLE_PLUGIN_NAME]
249
+ assert "my-plugin.hello_world" in command_menu_map
250
+ assert "my-plugin.another_command" in command_menu_map
251
+
252
+ # commands point to correct menus
253
+ assert len(cmd_menu := command_menu_map["my-plugin.hello_world"]) == 1
254
+ assert "/napari/layer_context" in cmd_menu
255
+ assert len(cmd_menu := command_menu_map["my-plugin.another_command"]) == 1
256
+ assert "mysubmenu" in cmd_menu
257
+
258
+ # enable/disable
259
+ pm.disable(SAMPLE_PLUGIN_NAME)
260
+ assert SAMPLE_PLUGIN_NAME not in pm._command_menu_map
261
+ pm.enable(SAMPLE_PLUGIN_NAME)
262
+ assert SAMPLE_PLUGIN_NAME in pm._command_menu_map
263
+
264
+ # register/unregister
265
+ pm.unregister(SAMPLE_PLUGIN_NAME)
266
+ assert SAMPLE_PLUGIN_NAME not in pm._command_menu_map
267
+ pm.register(SAMPLE_PLUGIN_NAME)
268
+ assert SAMPLE_PLUGIN_NAME in pm._command_menu_map
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes