Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PluginManifest.properties for cross-plugin configuration #149

Closed
wants to merge 11 commits into from
34 changes: 34 additions & 0 deletions npe2/_plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
9 changes: 8 additions & 1 deletion npe2/manifest/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions tests/sample/my_plugin/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
64 changes: 64 additions & 0 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
@@ -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()