npe2 0.7.8rc0__tar.gz → 0.7.9rc0__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 (113) hide show
  1. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/ci.yml +13 -1
  2. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.pre-commit-config.yaml +2 -2
  3. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/PKG-INFO +2 -2
  4. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/example_manifest.yaml +19 -0
  5. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/render.py +5 -3
  6. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_contributions.md.jinja +19 -1
  7. npe2-0.7.9rc0/_docs/templates/_npe2_menus_guide.md.jinja +96 -0
  8. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_sample_data_guide.md.jinja +4 -0
  9. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/codecov.yml +1 -1
  10. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/docs/_config.yml +2 -1
  11. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/docs/index.md +0 -2
  12. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/pyproject.toml +1 -1
  13. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_fetch.py +40 -2
  14. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/cli.py +11 -1
  15. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/io_utils.py +1 -1
  16. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_npe1_adapter.py +1 -1
  17. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_contributions.py +9 -2
  18. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_sample_data.py +3 -1
  19. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_submenu.py +7 -0
  20. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_cli.py +9 -0
  21. npe2-0.7.9rc0/tests/test_fetch.py +307 -0
  22. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_manifest.py +1 -0
  23. npe2-0.7.8rc0/docs/_toc.yml +0 -14
  24. npe2-0.7.8rc0/tests/test_fetch.py +0 -99
  25. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/ISSUE_TEMPLATE.md +0 -0
  26. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/dependabot.yml +0 -0
  27. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/test_all_plugins.yml +0 -0
  28. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/test_conversion.yml +0 -0
  29. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/update_changelog.yml +0 -0
  30. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github_changelog_generator +0 -0
  31. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.gitignore +0 -0
  32. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/CHANGELOG.md +0 -0
  33. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/LICENSE +0 -0
  34. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/Makefile +0 -0
  35. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/README.md +0 -0
  36. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/example_plugin/__init__.py +0 -0
  37. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/example_plugin/some_module.py +0 -0
  38. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_manifest.md.jinja +0 -0
  39. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_readers_guide.md.jinja +0 -0
  40. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_widgets_guide.md.jinja +0 -0
  41. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_writers_guide.md.jinja +0 -0
  42. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/docs/requirements.txt +0 -0
  43. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/__init__.py +0 -0
  44. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/__main__.py +0 -0
  45. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_command_registry.py +0 -0
  46. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_dynamic_plugin.py +0 -0
  47. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/__init__.py +0 -0
  48. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_compile.py +0 -0
  49. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_from_npe1.py +0 -0
  50. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_setuputils.py +0 -0
  51. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_visitors.py +0 -0
  52. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_plugin_manager.py +0 -0
  53. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_pydantic_compat.py +0 -0
  54. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_pytest_plugin.py +0 -0
  55. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_setuptools_plugin.py +0 -0
  56. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/implements.py +0 -0
  57. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/implements.pyi +0 -0
  58. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/__init__.py +0 -0
  59. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_bases.py +0 -0
  60. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_package_metadata.py +0 -0
  61. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_validators.py +0 -0
  62. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/__init__.py +0 -0
  63. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_commands.py +0 -0
  64. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_configuration.py +0 -0
  65. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_icon.py +0 -0
  66. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_json_schema.py +0 -0
  67. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_keybindings.py +0 -0
  68. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_menus.py +0 -0
  69. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_readers.py +0 -0
  70. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_themes.py +0 -0
  71. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_widgets.py +0 -0
  72. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_writers.py +0 -0
  73. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/menus.py +0 -0
  74. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/package_metadata.py +0 -0
  75. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/schema.py +0 -0
  76. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/utils.py +0 -0
  77. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/plugin_manager.py +0 -0
  78. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/py.typed +0 -0
  79. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/types.py +0 -0
  80. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/conftest.py +0 -0
  81. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/my_module/__init__.py +0 -0
  82. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/my_module/_a.py +0 -0
  83. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/my_module/_b.py +0 -0
  84. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/setup.cfg +0 -0
  85. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/METADATA +0 -0
  86. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/RECORD +0 -0
  87. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/entry_points.txt +0 -0
  88. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/top_level.txt +0 -0
  89. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1_module/__init__.py +0 -0
  90. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/setup.cfg +0 -0
  91. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/_with_decorators.py +0 -0
  92. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin/__init__.py +0 -0
  93. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin/napari.yaml +0 -0
  94. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin-1.2.3.dist-info/METADATA +0 -0
  95. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin-1.2.3.dist-info/entry_points.txt +0 -0
  96. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin-1.2.3.dist-info/top_level.txt +0 -0
  97. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test__io_utils.py +0 -0
  98. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_all_plugins.py +0 -0
  99. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_compile.py +0 -0
  100. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_config_contribution.py +0 -0
  101. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_contributions.py +0 -0
  102. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_conversion.py +0 -0
  103. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_docs.py +0 -0
  104. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_implements.py +0 -0
  105. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_npe1_adapter.py +0 -0
  106. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_package_meta.py +0 -0
  107. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_plugin_manager.py +0 -0
  108. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_pm_module.py +0 -0
  109. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_pytest_plugin.py +0 -0
  110. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_setuptools_plugin.py +0 -0
  111. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_tmp_plugin.py +0 -0
  112. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_utils.py +0 -0
  113. {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_validations.py +0 -0
@@ -83,7 +83,7 @@ jobs:
83
83
  python -m pip install pytest pytest-pretty scikit-image[data] zarr xarray hypothesis matplotlib
84
84
 
85
85
  - name: Run napari plugin headless tests
86
- run: pytest -W 'ignore::DeprecationWarning' napari/plugins napari/settings napari/layers napari/components
86
+ run: pytest -W 'ignore::DeprecationWarning' src/napari/plugins src/napari/settings src/napari/layers src/napari/components
87
87
  working-directory: napari-from-github
88
88
 
89
89
  test_docs_render:
@@ -104,6 +104,18 @@ jobs:
104
104
  run: python _docs/render.py
105
105
  env:
106
106
  NPE2_SCHEMA: "_schema.json"
107
+ - name: Build jupyter book
108
+ # install dependencies, generate toc, then build
109
+ run: |
110
+ pip install jupyter-book sphinx-tabs furo
111
+ cd docs
112
+ jupyter-book toc from-project . -f jb-book > _toc.yml
113
+ jupyter-book build .
114
+ # Upload the book's HTML as an artifact
115
+ - name: Upload artifact
116
+ uses: actions/upload-pages-artifact@v3
117
+ with:
118
+ path: "docs/_build/html"
107
119
 
108
120
  upload_coverage:
109
121
  needs: test
@@ -19,12 +19,12 @@ repos:
19
19
  - id: black
20
20
 
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
22
- rev: v0.9.4
22
+ rev: v0.12.2
23
23
  hooks:
24
24
  - id: ruff
25
25
 
26
26
  - repo: https://github.com/pre-commit/mirrors-mypy
27
- rev: v1.14.1
27
+ rev: v1.16.1
28
28
  hooks:
29
29
  - id: mypy
30
30
  additional_dependencies:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npe2
3
- Version: 0.7.8rc0
3
+ Version: 0.7.9rc0
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
@@ -19,8 +19,8 @@ Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.8
22
- Requires-Dist: appdirs
23
22
  Requires-Dist: build>=1
23
+ Requires-Dist: platformdirs
24
24
  Requires-Dist: psygnal>=0.3.0
25
25
  Requires-Dist: pydantic
26
26
  Requires-Dist: pyyaml
@@ -16,6 +16,15 @@ contributions:
16
16
  - id: example-plugin.do_threshold
17
17
  title: Perform threshold on image, return new image
18
18
  python_name: example_plugin.some_module:threshold
19
+ - id: example-plugin.threshold_otsu
20
+ title: Threshold using Otsu's method
21
+ python_name: example_plugin.some_module:threshold_otsu
22
+ - id: example-plugin.threshold_li
23
+ title: Threshold using Li's method
24
+ python_name: example_plugin.some_module:threshold_li
25
+ - id: example-plugin.all_thresholds
26
+ title: All thresholds
27
+ python_name: example_plugin.some_module:all_thresholds
19
28
  - id: example-plugin.threshold_widget
20
29
  title: Make threshold widget with magic_factory
21
30
  python_name: example_plugin.some_module:widget_factory
@@ -38,6 +47,16 @@ contributions:
38
47
  - command: example-plugin.do_threshold
39
48
  display_name: Threshold
40
49
  autogenerate: true
50
+ menus:
51
+ napari/layers/segment:
52
+ - submenu: threshold
53
+ - command: example-plugin.all_thresholds
54
+ threshold:
55
+ - command: example-plugin.threshold_otsu
56
+ - command: example-plugin.threshold_li
57
+ submenus:
58
+ - id: threshold
59
+ label: Thresholding
41
60
  themes:
42
61
  - label: "Monokai"
43
62
  id: "monokai"
@@ -182,13 +182,15 @@ def main(dest: Path = _BUILD):
182
182
  env.filters["has_guide"] = has_guide
183
183
 
184
184
  dest.mkdir(exist_ok=True, parents=True)
185
- schema = PluginManifest.schema()
186
185
  if local_schema := os.getenv("NPE2_SCHEMA"):
187
186
  with open(local_schema) as f:
188
187
  schema = json.load(f)
189
188
  else:
190
- with urlopen(SCHEMA_URL) as response:
191
- schema = json.load(response)
189
+ try:
190
+ schema = PluginManifest.schema()
191
+ except Exception:
192
+ with urlopen(SCHEMA_URL) as response:
193
+ schema = json.load(response)
192
194
 
193
195
  contributions = schema["definitions"]["ContributionPoints"]["properties"]
194
196
  context = {
@@ -21,10 +21,21 @@ is being discussed.
21
21
  (contributions-{{ contrib_name|replace('_', '-') }})=
22
22
  ## `contributions.{{contrib_name}}`
23
23
  {# check if this is a single type, or a union #}
24
- {%- if contrib['items']['anyOf'] is defined %}
24
+ {%- if contrib.type == 'object' and contrib.additionalProperties is defined %}
25
+ {# Handle object types like menus #}
26
+ {%- if contrib['additionalProperties']['items']['anyOf'] is defined %}
27
+ {%- set type_names = contrib['additionalProperties']['items']['anyOf']|map(attribute='$ref')|map("replace", "#/definitions/", "")|list %}
28
+ {%- set union = True %}
29
+ {%- else %}
30
+ {%- set type_names = [contrib['additionalProperties']['items']['$ref']|replace("#/definitions/", "")] %}
31
+ {%- set union = False %}
32
+ {%- endif -%}
33
+ {%- elif contrib['items']['anyOf'] is defined %}
34
+ {# Handle array types with union #}
25
35
  {%- set type_names = contrib['items']['anyOf']|map(attribute='$ref')|map("replace", "#/definitions/", "")|list %}
26
36
  {%- set union = True %}
27
37
  {%- else %}
38
+ {# Handle array types with single type #}
28
39
  {%- set type_names = [contrib['items']['$ref']|replace("#/definitions/", "")] %}
29
40
  {%- set union = False %}
30
41
  {%- endif -%}
@@ -34,10 +45,17 @@ This contribution accepts {{ type_names|length }} schema types
34
45
  ```
35
46
  {%- endif -%}
36
47
 
48
+ {%- if contrib.type == 'object' and contrib.additionalProperties is defined %}
49
+ {# For object types like menus, use the contribution description directly #}
50
+ {{ contrib.description }}
51
+ {%- endif %}
52
+
37
53
  {%- for tname in type_names -%}
38
54
  {% set type = schema['definitions'][tname] %}
39
55
  {% if union %}##### {{loop.index}}. {{type.title}}{% endif %}
56
+ {%- if contrib.type != 'object' or contrib.additionalProperties is not defined %}
40
57
  {{ type.description }}
58
+ {%- endif %}
41
59
 
42
60
  {% if contrib_name|has_guide %}
43
61
  See the [{{ contrib_name.title()|replace('_', ' ') }} Guide]({{ contrib_name|replace('_', '-') }}-contribution-guide)
@@ -0,0 +1,96 @@
1
+ (menus-contribution-guide)=
2
+ ## Menus
3
+
4
+ Menu contributions enable plugin developers to add new items or submenus to *existing* napari menus, allowing commands to be executed directly through the menu interface. This is a great mechanism for exposing simple commands or widgets that are generally applicable to a wide range of data.
5
+
6
+ ### `MenuItem` and `Submenu` Contributions
7
+
8
+ Menu entries are defined using two primary mechanisms in the plugin manifest:
9
+
10
+ - **MenuItem**: Adds a command to an existing menu location.
11
+ - **Submenu**: Creates a new submenu (referenced by ID) that can itself contain menu items or other submenus.
12
+
13
+ The structure of menu contributions requires first defining a command, then mapping it to a menu location.
14
+
15
+ ### Example: Thresholding submenu
16
+
17
+ ::::{tab-set}
18
+ :::{tab-item} manifest
19
+
20
+ ```yaml
21
+ name: napari-demo
22
+ display_name: Demo plugin
23
+
24
+ contributions:
25
+ commands:
26
+ - id: napari-demo.all_thresholds
27
+ title: Try All Thresholds
28
+ python_name: napari_demo:all_thresholds
29
+ - id: napari-demo.threshold_otsu
30
+ title: Otsu Threshold
31
+ python_name: napari_demo:threshold_otsu
32
+ - id: napari-demo.threshold_li
33
+ title: Li Threshold
34
+ python_name: napari_demo:threshold_li
35
+
36
+ menus:
37
+ napari/layers/segment:
38
+ - submenu: threshold
39
+ - command: napari-demo.all_thresholds
40
+ threshold:
41
+ - command: napari-demo.threshold_otsu
42
+ - command: napari-demo.threshold_li
43
+
44
+ submenus:
45
+ - id: threshold
46
+ label: Thresholding
47
+ ```
48
+ :::
49
+
50
+ :::{tab-item} python implementation
51
+
52
+ ```python
53
+ # napari_demo module
54
+ def all_thresholds(viewer: 'napari.viewer.Viewer'):
55
+ ...
56
+
57
+ def threshold_otsu(image: 'napari.types.ImageData') -> 'napari.types.LabelsData':
58
+ ...
59
+
60
+ def threshold_li(image: 'napari.types.ImageData') -> 'napari.types.LabelsData':
61
+ ...
62
+ ```
63
+ :::
64
+ ::::
65
+
66
+ ### Guidelines
67
+
68
+ - **Use menus for discoverability**: Menu contributions surface useful plugin functionality in an intuitive way.
69
+ - **Separate UI concerns**: Commands exposed via menus should avoid opening dialogs unnecessarily unless the user has selected them.
70
+
71
+
72
+ ### Menu ID Reference
73
+
74
+ Here's the full list of contributable menus. [source](https://github.com/napari/napari/blob/main/src/napari/_app_model/constants/_menus.py).
75
+
76
+ ```
77
+ napari/file/new_layer
78
+ napari/file/io_utilities
79
+ napari/file/acquire
80
+
81
+ napari/layers/visualize
82
+ napari/layers/annotate
83
+
84
+ napari/layers/data
85
+ napari/layers/layer_type
86
+
87
+ napari/layers/transform
88
+ napari/layers/measure
89
+
90
+ napari/layers/filter
91
+ napari/layers/register
92
+ napari/layers/project
93
+ napari/layers/segment
94
+ napari/layers/track
95
+ napari/layers/classify
96
+ ```
@@ -10,6 +10,10 @@ It can take the form of a **Sample Data URI** that points to a static resource
10
10
  (such as a file included in the plugin distribution, or a remote resource),
11
11
  or **Sample Data Function** that generates layer data on demand.
12
12
 
13
+ Note that unlike reader contributions, sample data contributions are
14
+ **always** expected to return data, so returning `[(None,)]`
15
+ will cause an error.
16
+
13
17
  ### Sample Data example
14
18
 
15
19
  ::::{tab-set}
@@ -2,4 +2,4 @@ coverage:
2
2
  status:
3
3
  project:
4
4
  default:
5
- target: 100%
5
+ target: 98%
@@ -45,7 +45,8 @@ sphinx:
45
45
  html_theme_options: {}
46
46
  pygments_style: solarized-dark
47
47
  suppress_warnings: ["myst.header"]
48
- exclude_patterns: ['**/_*.md'] # includes
48
+ # build the generated files in this repo to preview them
49
+ # exclude_patterns: ['**/_*.md'] # includes
49
50
  templates_path:
50
51
  - '_templates'
51
52
  intersphinx_mapping:
@@ -2,5 +2,3 @@
2
2
 
3
3
  This is a stub file for building the plugin documentation.
4
4
  It is not used and should not be edited
5
-
6
- [go to plugins section](./plugins/index)
@@ -32,7 +32,7 @@ classifiers = [
32
32
  ]
33
33
  dependencies = [
34
34
  "PyYAML",
35
- "appdirs",
35
+ "platformdirs",
36
36
  "build>=1",
37
37
  "psygnal>=0.3.0",
38
38
  "pydantic",
@@ -160,9 +160,47 @@ def _get_manifest_from_zip_url(url: str) -> PluginManifest:
160
160
  --------
161
161
  $ npe2 fetch https://github.com/org/project/archive/refs/heads/master.zip
162
162
  """
163
+ from npe2.manifest import PluginManifest
164
+
165
+ def find_manifest_file(root: Path) -> Optional[Path]:
166
+ """Recursively find a napari manifest file."""
167
+ # Check current directory for manifest files
168
+ for filename in ["napari.yaml", "napari.yml"]:
169
+ manifest_path = root / filename
170
+ if manifest_path.exists():
171
+ return manifest_path
172
+
173
+ # Check for pyproject.toml with napari config
174
+ pyproject_path = root / "pyproject.toml"
175
+ if pyproject_path.exists():
176
+ try:
177
+ import tomllib
178
+ except ImportError:
179
+ import tomli as tomllib # type: ignore
180
+
181
+ with open(pyproject_path, "rb") as f:
182
+ data = tomllib.load(f)
183
+ if "tool" in data and "napari" in data["tool"]:
184
+ return pyproject_path
185
+
186
+ # Recursively search subdirectories for the manifest file
187
+ for item in root.iterdir():
188
+ if item.is_dir() and not item.name.startswith("."):
189
+ result = find_manifest_file(item)
190
+ if result:
191
+ return result
192
+ return None
193
+
163
194
  with _tmp_zip_download(url) as zip_path:
164
- src_dir = next(Path(zip_path).iterdir()) # find first directory
165
- return _build_src_and_extract_manifest(src_dir)
195
+ # In a zip file, we do not need to build a wheel. We can extract
196
+ # the manifest directly from the extracted files.
197
+ manifest_file = find_manifest_file(Path(zip_path))
198
+ if manifest_file:
199
+ return PluginManifest.from_file(manifest_file)
200
+ else:
201
+ # Keep original behavior to try to build a wheel as a fallback
202
+ src_dir = next(Path(zip_path).iterdir())
203
+ return _build_src_and_extract_manifest(src_dir)
166
204
 
167
205
 
168
206
  def _get_manifest_from_wheel_url(url: str) -> PluginManifest:
@@ -11,7 +11,11 @@ from npe2 import PluginManager, PluginManifest, __version__
11
11
  if TYPE_CHECKING:
12
12
  from rich.console import RenderableType
13
13
 
14
- app = typer.Typer(no_args_is_help=True)
14
+ app = typer.Typer(
15
+ no_args_is_help=True,
16
+ context_settings={"help_option_names": ["-h", "--help"]},
17
+ rich_markup_mode="rich",
18
+ )
15
19
 
16
20
 
17
21
  def _show_version_and_exit(value: bool) -> None:
@@ -498,4 +502,10 @@ def compile(
498
502
 
499
503
 
500
504
  def main():
505
+ import sys
506
+
507
+ # If no arguments provided, show help without error box
508
+ if len(sys.argv) == 1:
509
+ sys.argv.append("--help")
510
+
501
511
  app()
@@ -319,5 +319,5 @@ def _write(
319
319
 
320
320
  # napari_get_writer-style writers don't always return a list
321
321
  # though strictly speaking they should?
322
- result = [res] if isinstance(res, str) else res or [] # type: ignore
322
+ result = [res] if isinstance(res, str) else res or []
323
323
  return (result, writer) if return_writer else result
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  from shutil import rmtree
9
9
  from typing import List, Sequence
10
10
 
11
- from appdirs import user_cache_dir
11
+ from platformdirs import user_cache_dir
12
12
 
13
13
  from npe2._inspection._from_npe1 import manifest_from_npe1
14
14
  from npe2.manifest import PackageMetadata
@@ -36,8 +36,15 @@ class ContributionPoints(BaseModel):
36
36
  widgets: Optional[List[WidgetContribution]]
37
37
  sample_data: Optional[List[SampleDataContribution]]
38
38
  themes: Optional[List[ThemeContribution]]
39
- menus: Dict[str, List[MenuItem]] = Field(default_factory=dict, hide_docs=True)
40
- submenus: Optional[List[SubmenuContribution]] = Field(None, hide_docs=True)
39
+ menus: Dict[str, List[MenuItem]] = Field(
40
+ default_factory=dict,
41
+ description="Add menu items to existing napari menus."
42
+ "A menu item can be a command, such as open a widget, or a submenu."
43
+ "Using menu items, nested hierarchies can be created within napari menus."
44
+ "This allows you to organize your plugin's contributions within"
45
+ "napari's menu structure.",
46
+ )
47
+ submenus: Optional[List[SubmenuContribution]]
41
48
  keybindings: Optional[List[KeyBindingContribution]] = Field(None, hide_docs=True)
42
49
 
43
50
  configuration: List[ConfigurationContribution] = Field(
@@ -32,7 +32,9 @@ class SampleDataGenerator(_SampleDataContribution, Executable[List[LayerData]]):
32
32
  """Contribute a callable command that creates data on demand."""
33
33
 
34
34
  command: str = Field(
35
- ..., description="Identifier of a command that returns layer data tuple."
35
+ ...,
36
+ description="Identifier of a command that returns layer data tuple. "
37
+ "Note that this command cannot return `[(None,)]`.",
36
38
  )
37
39
 
38
40
  def open(
@@ -6,6 +6,13 @@ from ._icon import Icon
6
6
 
7
7
 
8
8
  class SubmenuContribution(BaseModel):
9
+ """Contributes a submenu that can contain menu items or other submenus.
10
+
11
+ Submenus allow you to organize menu items into hierarchical structures.
12
+ Each submenu defines an id, label, and optional icon that can be
13
+ referenced by menu items to create nested menu structures.
14
+ """
15
+
9
16
  id: str = Field(description="Identifier of the menu to display as a submenu.")
10
17
  label: str = Field(
11
18
  description="The label of the menu item which leads to this submenu."
@@ -212,3 +212,12 @@ def test_cli_version():
212
212
  def test_compile(compiled_plugin_dir):
213
213
  result = runner.invoke(app, ["compile", str(compiled_plugin_dir)])
214
214
  assert "id: my_compiled_plugin.my-plugin.generate_random_data" in result.output
215
+
216
+
217
+ def test_main_no_args_shows_help(monkeypatch, capsys):
218
+ monkeypatch.setattr(sys, "argv", ["npe2"])
219
+ with pytest.raises(SystemExit) as e:
220
+ main()
221
+ assert e.value.code == 0
222
+ captured = capsys.readouterr()
223
+ assert "Usage:" in captured.out or "Usage:" in captured.err
@@ -0,0 +1,307 @@
1
+ import os
2
+ import urllib.request
3
+ from importlib.metadata import PackageNotFoundError
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ from npe2 import PluginManifest, fetch_manifest
9
+ from npe2._inspection._fetch import (
10
+ _get_manifest_from_zip_url,
11
+ _manifest_from_pypi_sdist,
12
+ get_hub_plugin,
13
+ get_manifest_from_wheel,
14
+ get_pypi_url,
15
+ )
16
+
17
+
18
+ def test_fetch_npe2_manifest():
19
+ mf = fetch_manifest("napari-omero")
20
+ assert mf.name == "napari-omero"
21
+ assert any(mf.contributions.dict().values())
22
+ assert mf.npe1_shim is False
23
+
24
+
25
+ @pytest.mark.skip("package looks deleted from pypi")
26
+ def test_fetch_npe1_manifest_with_writer():
27
+ mf = fetch_manifest("dummy-test-plugin", version="0.1.3")
28
+ assert mf.name == "example-plugin"
29
+ assert mf.contributions.writers
30
+ # Test will eventually fail when example-plugin is updated to npe2
31
+ # This is here as a sentinel
32
+ assert mf.npe1_shim is True
33
+
34
+
35
+ def test_fetch_npe1_manifest_with_sample_data():
36
+ mf = fetch_manifest("napari-kics")
37
+ assert mf.name == "napari-kics"
38
+ assert mf.contributions.sample_data
39
+ # Test will eventually fail when napari-kics is updated to npe2
40
+ # This is here as a sentinel
41
+ assert mf.npe1_shim is True
42
+
43
+
44
+ def test_fetch_npe1_manifest_dock_widget_as_attribute():
45
+ # This tests is just to add coverage of a specific branch of code in the
46
+ # napari_experimental_provide_dock_widget parser, (where the return value
47
+ # is a dotted attribute, rather than a direct name). I only saw it in
48
+ # brainreg-segment.
49
+ mf = fetch_manifest("brainreg-segment", version="0.2.18")
50
+ assert mf.name == "brainreg-segment"
51
+ assert mf.contributions.widgets
52
+ # This is here as a sentinel
53
+ assert mf.npe1_shim is True
54
+
55
+
56
+ @pytest.mark.parametrize("version", [None, "0.1.0"])
57
+ @pytest.mark.parametrize("packagetype", ["sdist", "bdist_wheel", None])
58
+ def test_get_pypi_url(version, packagetype):
59
+ assert "npe2" in get_pypi_url("npe2", version=version, packagetype=packagetype)
60
+
61
+
62
+ def test_from_pypi_wheel_bdist_missing():
63
+ error = PackageNotFoundError("No bdist_wheel releases found")
64
+ with patch("npe2._inspection._fetch.get_pypi_url", side_effect=error):
65
+ with pytest.raises(PackageNotFoundError):
66
+ fetch_manifest("my-package")
67
+
68
+
69
+ @pytest.mark.skipif(not os.getenv("CI"), reason="slow, only run on CI")
70
+ def test_manifest_from_sdist():
71
+ mf = _manifest_from_pypi_sdist("zarpaint")
72
+ assert mf.name == "zarpaint"
73
+
74
+
75
+ def test_get_manifest_from_wheel(tmp_path):
76
+ url = "https://files.pythonhosted.org/packages/f0/cc/7f6fbce81be3eb73266f398e49df92859ba247134eb086704dd70b43819a/affinder-0.2.3-py3-none-any.whl"
77
+ dest = tmp_path / "affinder-0.2.3-py3-none-any.whl"
78
+ urllib.request.urlretrieve(url, dest)
79
+ mf = get_manifest_from_wheel(dest)
80
+ assert mf.name == "affinder"
81
+
82
+
83
+ def test_get_hub_plugin():
84
+ info = get_hub_plugin("napari-svg")
85
+ assert info["name"] == "napari-svg"
86
+
87
+
88
+ @pytest.mark.skipif(not os.getenv("CI"), reason="slow, only run on CI")
89
+ @pytest.mark.parametrize(
90
+ "url",
91
+ [
92
+ "https://files.pythonhosted.org/packages/fb/01/e59bc1d6ac96f84ce9d7a46cc5422250e047958ead6c5693ed386cf94003/napari_dv-0.3.0.tar.gz",
93
+ "https://files.pythonhosted.org/packages/5d/ae/17779e12ce60d8329306963e1a8dec608465caee582440011ff0c1310715/example_plugin-0.0.7-py3-none-any.whl",
94
+ "git+https://github.com/napari/dummy-test-plugin.git@npe1",
95
+ # this one doesn't use setuptools_scm, can check direct zip without clone
96
+ "https://github.com/jo-mueller/napari-stl-exporter/archive/refs/heads/main.zip",
97
+ ],
98
+ )
99
+ def test_fetch_urls(url):
100
+ assert isinstance(fetch_manifest(url), PluginManifest)
101
+
102
+
103
+ def test_get_manifest_from_zip_url(tmp_path, monkeypatch):
104
+ # Mock the _tmp_zip_download context manager
105
+ mock_zip_path = tmp_path / "extracted"
106
+ mock_zip_path.mkdir()
107
+
108
+ # Create a mock source directory inside the extracted zip
109
+ src_dir = mock_zip_path / "plugin-source-1.0"
110
+ src_dir.mkdir()
111
+
112
+ # Mock the _build_src_and_extract_manifest to return a test manifest
113
+ test_manifest = PluginManifest(name="test-plugin")
114
+
115
+ def mock_build_src_and_extract_manifest(src_dir):
116
+ return test_manifest
117
+
118
+ from contextlib import contextmanager
119
+
120
+ @contextmanager
121
+ def mock_tmp_zip_download(url):
122
+ yield mock_zip_path
123
+
124
+ monkeypatch.setattr(
125
+ "npe2._inspection._fetch._tmp_zip_download", mock_tmp_zip_download
126
+ )
127
+ monkeypatch.setattr(
128
+ "npe2._inspection._fetch._build_src_and_extract_manifest",
129
+ mock_build_src_and_extract_manifest,
130
+ )
131
+
132
+ # Test the function
133
+ result = _get_manifest_from_zip_url("https://example.com/plugin.zip")
134
+
135
+ # Verify the result
136
+ assert result == test_manifest
137
+ assert result.name == "test-plugin"
138
+
139
+
140
+ def test_get_manifest_from_zip_url_with_pyproject_toml_tomllib(tmp_path, monkeypatch):
141
+ # Test pyproject.toml with napari config using tomllib
142
+ mock_zip_path = tmp_path / "extracted"
143
+ mock_zip_path.mkdir()
144
+
145
+ # Create pyproject.toml with napari config
146
+ pyproject_content = b"""
147
+ [tool.napari]
148
+ name = "test-plugin"
149
+ """
150
+ pyproject_path = mock_zip_path / "pyproject.toml"
151
+ pyproject_path.write_bytes(pyproject_content)
152
+
153
+ from contextlib import contextmanager
154
+
155
+ @contextmanager
156
+ def mock_tmp_zip_download(url):
157
+ yield mock_zip_path
158
+
159
+ monkeypatch.setattr(
160
+ "npe2._inspection._fetch._tmp_zip_download", mock_tmp_zip_download
161
+ )
162
+
163
+ # Mock PluginManifest.from_file to return a test manifest
164
+ test_manifest = PluginManifest(name="test-plugin")
165
+ monkeypatch.setattr(
166
+ "npe2.manifest.PluginManifest.from_file", lambda path: test_manifest
167
+ )
168
+
169
+ # Test the function
170
+ result = _get_manifest_from_zip_url("https://example.com/plugin.zip")
171
+
172
+ # Verify the result
173
+ assert result == test_manifest
174
+ assert result.name == "test-plugin"
175
+
176
+
177
+ def test_get_manifest_from_zip_url_with_pyproject_toml_tomli(tmp_path, monkeypatch):
178
+ # Test pyproject.toml with napari config using tomli fallback
179
+ mock_zip_path = tmp_path / "extracted"
180
+ mock_zip_path.mkdir()
181
+
182
+ # Create pyproject.toml with napari config
183
+ pyproject_content = b"""
184
+ [tool.napari]
185
+ name = "test-plugin"
186
+ """
187
+ pyproject_path = mock_zip_path / "pyproject.toml"
188
+ pyproject_path.write_bytes(pyproject_content)
189
+
190
+ from contextlib import contextmanager
191
+
192
+ @contextmanager
193
+ def mock_tmp_zip_download(url):
194
+ yield mock_zip_path
195
+
196
+ monkeypatch.setattr(
197
+ "npe2._inspection._fetch._tmp_zip_download", mock_tmp_zip_download
198
+ )
199
+
200
+ # Mock PluginManifest.from_file to return a test manifest
201
+ test_manifest = PluginManifest(name="test-plugin")
202
+ monkeypatch.setattr(
203
+ "npe2.manifest.PluginManifest.from_file", lambda path: test_manifest
204
+ )
205
+
206
+ # Mock tomllib to not exist, forcing tomli fallback
207
+ import sys
208
+
209
+ original_modules = sys.modules.copy()
210
+ if "tomllib" in sys.modules:
211
+ del sys.modules["tomllib"]
212
+
213
+ try:
214
+ # Test the function
215
+ result = _get_manifest_from_zip_url("https://example.com/plugin.zip")
216
+
217
+ # Verify the result
218
+ assert result == test_manifest
219
+ assert result.name == "test-plugin"
220
+ finally:
221
+ # Restore modules
222
+ sys.modules.update(original_modules)
223
+
224
+
225
+ def test_get_manifest_from_zip_url_with_pyproject_toml_no_napari(tmp_path, monkeypatch):
226
+ # Test pyproject.toml without napari config
227
+ mock_zip_path = tmp_path / "extracted"
228
+ mock_zip_path.mkdir()
229
+
230
+ # Create pyproject.toml without napari config
231
+ pyproject_content = b"""
232
+ [tool.other]
233
+ name = "other-tool"
234
+ """
235
+ pyproject_path = mock_zip_path / "pyproject.toml"
236
+ pyproject_path.write_bytes(pyproject_content)
237
+
238
+ # Create a mock source directory for fallback
239
+ src_dir = mock_zip_path / "plugin-source"
240
+ src_dir.mkdir()
241
+
242
+ from contextlib import contextmanager
243
+
244
+ @contextmanager
245
+ def mock_tmp_zip_download(url):
246
+ yield mock_zip_path
247
+
248
+ monkeypatch.setattr(
249
+ "npe2._inspection._fetch._tmp_zip_download", mock_tmp_zip_download
250
+ )
251
+
252
+ # Mock the fallback to build from source
253
+ test_manifest = PluginManifest(name="test-plugin")
254
+ monkeypatch.setattr(
255
+ "npe2._inspection._fetch._build_src_and_extract_manifest",
256
+ lambda src_dir: test_manifest,
257
+ )
258
+
259
+ # Test the function
260
+ result = _get_manifest_from_zip_url("https://example.com/plugin.zip")
261
+
262
+ # Verify the result (should use fallback since no napari config)
263
+ assert result == test_manifest
264
+ assert result.name == "test-plugin"
265
+
266
+
267
+ def test_pyproject_toml_napari_section_coverage(tmp_path, monkeypatch):
268
+ # Direct test of the find_manifest_file function to ensure coverage
269
+ from npe2._inspection._fetch import _get_manifest_from_zip_url
270
+
271
+ mock_zip_path = tmp_path / "extracted"
272
+ mock_zip_path.mkdir()
273
+
274
+ # Create nested directory structure
275
+ nested_dir = mock_zip_path / "plugin" / "src"
276
+ nested_dir.mkdir(parents=True)
277
+
278
+ # Create pyproject.toml with napari config in nested dir
279
+ pyproject_content = b"""
280
+ [tool.napari]
281
+ name = "test-plugin"
282
+ display_name = "Test Plugin"
283
+ """
284
+ pyproject_path = nested_dir / "pyproject.toml"
285
+ pyproject_path.write_bytes(pyproject_content)
286
+
287
+ # Create a napari.yaml manifest
288
+ manifest_content = """
289
+ name: test-plugin
290
+ display_name: Test Plugin
291
+ """
292
+ manifest_path = nested_dir / "napari.yaml"
293
+ manifest_path.write_text(manifest_content)
294
+
295
+ from contextlib import contextmanager
296
+
297
+ @contextmanager
298
+ def mock_tmp_zip_download(url):
299
+ yield mock_zip_path
300
+
301
+ monkeypatch.setattr(
302
+ "npe2._inspection._fetch._tmp_zip_download", mock_tmp_zip_download
303
+ )
304
+
305
+ # Test the function - it should find the napari.yaml first
306
+ result = _get_manifest_from_zip_url("https://example.com/plugin.zip")
307
+ assert result.name == "test-plugin"
@@ -44,6 +44,7 @@ def test_discover(uses_sample_plugin):
44
44
  assert error is None
45
45
 
46
46
 
47
+ @pytest.mark.filterwarnings("ignore:Implicit None on return values is deprecated")
47
48
  def test_discover_errors(tmp_path: Path):
48
49
  """testing various discovery errors"""
49
50
  # package with proper `napari.manifest` entry_point, but invalid pointer to
@@ -1,14 +0,0 @@
1
- format: jb-article
2
- root: index
3
- sections:
4
- # this is meant to match the plugins toc at napari
5
- - file: plugins/index
6
- sections:
7
- - file: plugins/first_plugin
8
- - file: plugins/manifest
9
- - file: plugins/contributions
10
- - file: plugins/guides
11
- - file: plugins/test_deploy
12
- - file: plugins/best_practices
13
- - file: plugins/npe2_migration_guide
14
- - file: plugins/npe1
@@ -1,99 +0,0 @@
1
- import os
2
- import urllib.request
3
- from importlib.metadata import PackageNotFoundError
4
- from unittest.mock import patch
5
-
6
- import pytest
7
-
8
- from npe2 import PluginManifest, fetch_manifest
9
- from npe2._inspection._fetch import (
10
- _manifest_from_pypi_sdist,
11
- get_hub_plugin,
12
- get_manifest_from_wheel,
13
- get_pypi_url,
14
- )
15
-
16
-
17
- def test_fetch_npe2_manifest():
18
- mf = fetch_manifest("napari-omero")
19
- assert mf.name == "napari-omero"
20
- assert any(mf.contributions.dict().values())
21
- assert mf.npe1_shim is False
22
-
23
-
24
- @pytest.mark.skip("package looks deleted from pypi")
25
- def test_fetch_npe1_manifest_with_writer():
26
- mf = fetch_manifest("dummy-test-plugin", version="0.1.3")
27
- assert mf.name == "example-plugin"
28
- assert mf.contributions.writers
29
- # Test will eventually fail when example-plugin is updated to npe2
30
- # This is here as a sentinel
31
- assert mf.npe1_shim is True
32
-
33
-
34
- def test_fetch_npe1_manifest_with_sample_data():
35
- mf = fetch_manifest("napari-kics")
36
- assert mf.name == "napari-kics"
37
- assert mf.contributions.sample_data
38
- # Test will eventually fail when napari-kics is updated to npe2
39
- # This is here as a sentinel
40
- assert mf.npe1_shim is True
41
-
42
-
43
- def test_fetch_npe1_manifest_dock_widget_as_attribute():
44
- # This tests is just to add coverage of a specific branch of code in the
45
- # napari_experimental_provide_dock_widget parser, (where the return value
46
- # is a dotted attribute, rather than a direct name). I only saw it in
47
- # brainreg-segment.
48
- mf = fetch_manifest("brainreg-segment", version="0.2.18")
49
- assert mf.name == "brainreg-segment"
50
- assert mf.contributions.widgets
51
- # This is here as a sentinel
52
- assert mf.npe1_shim is True
53
-
54
-
55
- @pytest.mark.parametrize("version", [None, "0.1.0"])
56
- @pytest.mark.parametrize("packagetype", ["sdist", "bdist_wheel", None])
57
- def test_get_pypi_url(version, packagetype):
58
- assert "npe2" in get_pypi_url("npe2", version=version, packagetype=packagetype)
59
-
60
-
61
- def test_from_pypi_wheel_bdist_missing():
62
- error = PackageNotFoundError("No bdist_wheel releases found")
63
- with patch("npe2._inspection._fetch.get_pypi_url", side_effect=error):
64
- with pytest.raises(PackageNotFoundError):
65
- fetch_manifest("my-package")
66
-
67
-
68
- @pytest.mark.skipif(not os.getenv("CI"), reason="slow, only run on CI")
69
- def test_manifest_from_sdist():
70
- mf = _manifest_from_pypi_sdist("zarpaint")
71
- assert mf.name == "zarpaint"
72
-
73
-
74
- def test_get_manifest_from_wheel(tmp_path):
75
- url = "https://files.pythonhosted.org/packages/f0/cc/7f6fbce81be3eb73266f398e49df92859ba247134eb086704dd70b43819a/affinder-0.2.3-py3-none-any.whl"
76
- dest = tmp_path / "affinder-0.2.3-py3-none-any.whl"
77
- urllib.request.urlretrieve(url, dest)
78
- mf = get_manifest_from_wheel(dest)
79
- assert mf.name == "affinder"
80
-
81
-
82
- def test_get_hub_plugin():
83
- info = get_hub_plugin("napari-svg")
84
- assert info["name"] == "napari-svg"
85
-
86
-
87
- @pytest.mark.skipif(not os.getenv("CI"), reason="slow, only run on CI")
88
- @pytest.mark.parametrize(
89
- "url",
90
- [
91
- "https://files.pythonhosted.org/packages/fb/01/e59bc1d6ac96f84ce9d7a46cc5422250e047958ead6c5693ed386cf94003/napari_dv-0.3.0.tar.gz",
92
- "https://files.pythonhosted.org/packages/5d/ae/17779e12ce60d8329306963e1a8dec608465caee582440011ff0c1310715/example_plugin-0.0.7-py3-none-any.whl",
93
- "git+https://github.com/napari/dummy-test-plugin.git@npe1",
94
- # this one doesn't use setuptools_scm, can check direct zip without clone
95
- "https://github.com/jo-mueller/napari-stl-exporter/archive/refs/heads/main.zip",
96
- ],
97
- )
98
- def test_fetch_urls(url):
99
- assert isinstance(fetch_manifest(url), PluginManifest)
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