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.
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/ci.yml +13 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.pre-commit-config.yaml +2 -2
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/PKG-INFO +2 -2
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/example_manifest.yaml +19 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/render.py +5 -3
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_contributions.md.jinja +19 -1
- npe2-0.7.9rc0/_docs/templates/_npe2_menus_guide.md.jinja +96 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_sample_data_guide.md.jinja +4 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/codecov.yml +1 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/docs/_config.yml +2 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/docs/index.md +0 -2
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/pyproject.toml +1 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_fetch.py +40 -2
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/cli.py +11 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/io_utils.py +1 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_npe1_adapter.py +1 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_contributions.py +9 -2
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_sample_data.py +3 -1
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_submenu.py +7 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_cli.py +9 -0
- npe2-0.7.9rc0/tests/test_fetch.py +307 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_manifest.py +1 -0
- npe2-0.7.8rc0/docs/_toc.yml +0 -14
- npe2-0.7.8rc0/tests/test_fetch.py +0 -99
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/ISSUE_TEMPLATE.md +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/dependabot.yml +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/test_all_plugins.yml +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/test_conversion.yml +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github/workflows/update_changelog.yml +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.github_changelog_generator +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/.gitignore +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/CHANGELOG.md +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/LICENSE +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/Makefile +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/README.md +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/example_plugin/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/example_plugin/some_module.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_manifest.md.jinja +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_readers_guide.md.jinja +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_widgets_guide.md.jinja +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/_docs/templates/_npe2_writers_guide.md.jinja +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/docs/requirements.txt +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/__main__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_command_registry.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_dynamic_plugin.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_compile.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_from_npe1.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_setuputils.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_inspection/_visitors.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_plugin_manager.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_pydantic_compat.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_pytest_plugin.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/_setuptools_plugin.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/implements.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/implements.pyi +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_bases.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_package_metadata.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/_validators.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_commands.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_configuration.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_icon.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_json_schema.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_keybindings.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_menus.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_readers.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_themes.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_widgets.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/contributions/_writers.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/menus.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/package_metadata.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/schema.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/manifest/utils.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/plugin_manager.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/py.typed +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/src/npe2/types.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/conftest.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/my_module/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/my_module/_a.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/my_module/_b.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/fixtures/my-compiled-plugin/setup.cfg +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/METADATA +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/RECORD +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/entry_points.txt +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/top_level.txt +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1_module/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/setup.cfg +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/_with_decorators.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin/__init__.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin/napari.yaml +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin-1.2.3.dist-info/METADATA +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin-1.2.3.dist-info/entry_points.txt +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/sample/my_plugin-1.2.3.dist-info/top_level.txt +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test__io_utils.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_all_plugins.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_compile.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_config_contribution.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_contributions.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_conversion.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_docs.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_implements.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_npe1_adapter.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_package_meta.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_plugin_manager.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_pm_module.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_pytest_plugin.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_setuptools_plugin.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_tmp_plugin.py +0 -0
- {npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/test_utils.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
191
|
-
schema =
|
|
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
|
|
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}
|
|
@@ -45,7 +45,8 @@ sphinx:
|
|
|
45
45
|
html_theme_options: {}
|
|
46
46
|
pygments_style: solarized-dark
|
|
47
47
|
suppress_warnings: ["myst.header"]
|
|
48
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
165
|
-
|
|
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(
|
|
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 []
|
|
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
|
|
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(
|
|
40
|
-
|
|
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
|
-
...,
|
|
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
|
npe2-0.7.8rc0/docs/_toc.yml
DELETED
|
@@ -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
|
|
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
|
{npe2-0.7.8rc0 → npe2-0.7.9rc0}/tests/npe1-plugin/npe1-plugin-0.0.1.dist-info/entry_points.txt
RENAMED
|
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
|