diff --git a/npe2/manifest/_validators.py b/npe2/manifest/_validators.py index 7dfaaf44..4504e9a3 100644 --- a/npe2/manifest/_validators.py +++ b/npe2/manifest/_validators.py @@ -66,3 +66,16 @@ def display_name(v: str) -> str: "non-word character." ) return v + + +def icon_path(v: str) -> str: + if not v: + return "" + if v.startswith("http"): + if not v.startswith("https://"): + raise ValueError( + f"{v} is not a valid icon URL. It must start with 'https://'" + ) + return v + assert isinstance(v, str), f"{v} must be a string" + return v diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index 1e29fa5a..7a9b5749 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -74,6 +74,18 @@ class Config: "results, change this to `'hidden'`.", ) + icon: str = Field( + "", + description="The path to a square PNG icon of at least 128x128 pixels (256x256 " + "for Retina screens). May be one of:\n" + " - a secure (https) URL\n" + " - a path relative to the manifest file, (must be shipped in the sdist)\n" + " - a string in the format `{package}:{resource}`, where `package` and " + "`resource` are arguments to `importlib.resources.path(package, resource)`, " + "(e.g. `top_module.some_folder:my_logo.png`).", + ) + _validate_icon_path = validator("icon", allow_reuse=True)(_validators.icon_path) + # Plugins rely on certain guarantees to interoperate propertly with the # plugin engine. These include the manifest specification, conventions # around python packaging, command api's, etc. Together these form a diff --git a/tests/sample/my_plugin/napari.yaml b/tests/sample/my_plugin/napari.yaml index 59109b5f..856214a5 100644 --- a/tests/sample/my_plugin/napari.yaml +++ b/tests/sample/my_plugin/napari.yaml @@ -2,6 +2,7 @@ name: my-plugin display_name: My Plugin on_activate: my_plugin:activate on_deactivate: my_plugin:deactivate +icon: https://picsum.photos/256 contributions: commands: - id: my-plugin.hello_world diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 46142918..073d897b 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -178,3 +178,7 @@ def test_visibility(): with pytest.raises(ValidationError): mf = PluginManifest(name="myplugin", visibility="other") + + +def test_icon(): + PluginManifest(name="myplugin", icon="my_plugin:myicon.png") diff --git a/tests/test_validations.py b/tests/test_validations.py index c2413f1f..807b1712 100644 --- a/tests/test_validations.py +++ b/tests/test_validations.py @@ -80,6 +80,11 @@ def _mutator_schema_version_too_high(data): data["schema_version"] = "999.999.999" +def _mutator_invalid_icon(data): + """is not a valid icon URL. It must start with 'https://'""" + data["icon"] = "http://example.com/icon.png" + + @pytest.mark.parametrize( "mutator", [ @@ -95,6 +100,7 @@ def _mutator_schema_version_too_high(data): _mutator_writer_invalid_file_extension_1, _mutator_writer_invalid_file_extension_2, _mutator_schema_version_too_high, + _mutator_invalid_icon, ], ) def test_invalid(mutator, uses_sample_plugin):