diff --git a/npe2/_plugin_manager.py b/npe2/_plugin_manager.py index 946a5dd0..baa9603f 100644 --- a/npe2/_plugin_manager.py +++ b/npe2/_plugin_manager.py @@ -531,6 +531,40 @@ def get_writer( # Nothing got found return None, path + # Accessing Contributions + + def properties(self, config_key: str) -> Set[Any]: + """ + Get plugin properties specifications for the provided key + + Properties are NOT merged until this method is called, and the state is + not saved. This lazy properties discovery is preferrable as it ensures + that the returned list is consistent with the list of registered plugins. + + Parameters + ---------- + config_key : str + Key specified by plugin(s) + + Returns + ------- + Set[Any] + All values specified by all plugins for key config_key + """ + + properties_values: Set[Any] = set() + # Search all manifests for the presence of config key + for manifest in self._manifests.values(): + if config_key not in manifest.properties: + continue + value = manifest.properties[config_key] + if isinstance(value, Iterable) and not isinstance(value, str): + properties_values.update(value) + else: + properties_values.add(value) + + return properties_values + class PluginContext: """An object that can contain information for a plugin over its lifetime.""" diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index 663d29f8..597f18a4 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -6,7 +6,7 @@ from logging import getLogger from pathlib import Path from textwrap import dedent -from typing import Iterator, NamedTuple, Optional, Sequence, Union +from typing import Any, Dict, Iterator, NamedTuple, Optional, Sequence, Union from pydantic import Extra, Field, ValidationError, root_validator, validator from pydantic.error_wrappers import ErrorWrapper @@ -126,6 +126,13 @@ class Config: exclude=True, ) + properties: Dict[str, Any] = Field( + default={}, + description="Properties for global values." + "May be appended to by other plugins" + "who wish to have a say in shared state", + ) + def __init__(self, **data): super().__init__(**data) if self.package_metadata is None and self.name: diff --git a/tests/sample/my_plugin/napari.yaml b/tests/sample/my_plugin/napari.yaml index 9a6bc15f..7b764c6f 100644 --- a/tests/sample/my_plugin/napari.yaml +++ b/tests/sample/my_plugin/napari.yaml @@ -91,3 +91,15 @@ contributions: - display_name: Random internet image key: internet_image uri: https://picsum.photos/1024 +properties: + check_num: 10 + check_num_list: + - 1 + - 2 + - 3 + check_str: 'foo' + check_str_list: + - 'a' + - 'b' + - 'c' + check_bool: true diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 00000000..4aac6220 --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,64 @@ +from npe2 import PluginManager +from npe2.manifest.schema import PluginManifest + +SAMPLE_PLUGIN_NAME = "my-plugin" + + +def test_properties_empty(): + """Ensures unpopulated properties before plugin discovery.""" + pm = PluginManager() + assert pm.properties("check_str") == set() + + +def test_properties_single_plugin(sample_manifest): + """Ensures populated properties after plugin registry.""" + pm = PluginManager() + pm.register(sample_manifest) + assert pm.properties("check_str") == {"foo"} + assert pm.properties("check_str_list") == {"a", "b", "c"} + assert pm.properties("check_num") == {10} + assert pm.properties("check_num_list") == {1, 2, 3} + assert pm.properties("check_bool") == {True} + + +def test_properties_single_plugin_unregister(sample_manifest): + """Ensures unpopulated properties after plugin unregistry.""" + pm = PluginManager() + pm.register(sample_manifest) + pm.unregister("my-plugin") + assert pm.properties("check_str") == set() + + +def test_properties_multiple_plugins(sample_manifest): + """Ensures population for multiple plugins""" + pm = PluginManager() + + # Add a plugin + pm.register(sample_manifest) + assert pm.properties("check_str") == {"foo"} + + # Add a second plugin + manifest2 = PluginManifest(name="my-second-plugin", properties={"check_str": "bar"}) + pm.register(manifest2) + # Ensure its properties is appended + assert pm.properties("check_str") == {"foo", "bar"} + + # Add a third plugin + manifest3 = PluginManifest(name="my-third-plugin", properties={"check_str": "foo"}) + pm.register(manifest3) + # Ensure its properties is merged + assert pm.properties("check_str") == {"foo", "bar"} + + +def test_properties_not_added(sample_manifest): + """Ensures unpopulation for unused property""" + pm = PluginManager() + + # Add a plugin + pm.register(sample_manifest) + assert pm.properties("not_added") == set() + + # Add a third plugin + manifest2 = PluginManifest(name="my-third-plugin") + pm.register(manifest2) + assert pm.properties("not_added") == set()