Repository: napari/napari

Issue:
Proposal: Make `layer.metadata` a read-only attribute (whose value is still mutable)
## 🚀 Feature
So far, we've treated `layer.metadata` as "whatever the user wants it to be"... (which really implies no type whatsoever)  which is fine for solo-usage... but tricky when multiple plugins fight for it.  Here's an interesting case where `napari-pyclesperanto` makes the assumption that `layer.metadata` is a dict, but napari-aicsimagio (I'm assuming) makes it a callable that lazily gets some metadata. 

https://forum.image.sc/t/napari-pyclesperanto-assistant-issue-in-apple-silicon-m1-arm64-native-env/55055/2

I feel certain that was probably my own damn fault, since ome-types will "detect" if `layer.metadata` returns an instance of `ome_types.OME`, and will display a nice metadata tree if you use it's dock widget.

we're obviously a ways from providing a common metadata structure that plugins can "share", but we should probably make `layer.metata` a `property` without a setter for now to make sure it remains a mutable mapping.
(very sorry @JacksonMaxfield ... we should figure out a namespace deeper in `metadata` where we can play together) 

cc @haesleinhuepf @jni @sofroniewn 


Discussion:
This issue has been mentioned on **Image.sc Forum**. There might be relevant details there:

https://forum.image.sc/t/napari-pyclesperanto-assistant-issue-in-apple-silicon-m1-arm64-native-env/55055/9

> we should probably make layer.metata a property without a setter for now to make sure it remains a mutable mapping.

This seems wise to me.

> we're obviously a ways from providing a common metadata structure that plugins can "share"

yeah, not quite sure what we should do here! Maybe something "opt-in" where we could define metadata schemas that plugins could depend on and share, but they are defined in their own 3rd party repos rather than being baked into napari. Maybe something do explore with `ome_types.OME`.

We could add a hookspec for `add metadata schema` and then have plugins like `ome_types` that define these. A plugin like `napari-pyclesperanto` or `napari-aicsimagio` could then choose to use that schema.

But we could also have a plugin that provides some DICOM metadata schema or a neuroimaging metadata schema and a family of plugins that use though ....

Not quite sure, but i do like the idea of metadata schemas being provided by other plugins and not something the core of napari attempts to define.

cc @nclack and @andy-sweet who might have ideas about this too
> Maybe something do explore with ome_types.OME.

for now, `ome_types` will look for `layer.metadata['ome_types']`
> for now, ome_types will look for layer.metadata['ome_types']

yeah, maybe good to put them inside like that too, could let you use multiple metadata schemas together if you wanted

`layer.metadata['ome_types']` and `layer.metadata['nifty']` say



Resolving patch:
diff --git a/napari/layers/base/base.py b/napari/layers/base/base.py
--- a/napari/layers/base/base.py
+++ b/napari/layers/base/base.py
@@ -181,7 +181,7 @@ def __init__(
 
         self._source = current_source()
         self.dask_optimized_slicing = configure_dask(data)
-        self.metadata = metadata or {}
+        self._metadata = dict(metadata or {})
         self._opacity = opacity
         self._blending = Blending(blending)
         self._visible = visible
@@ -300,6 +300,25 @@ def name(self):
         """str: Unique name of the layer."""
         return self._name
 
+    @name.setter
+    def name(self, name):
+        if name == self.name:
+            return
+        if not name:
+            name = self._basename()
+        self._name = name
+        self.events.name()
+
+    @property
+    def metadata(self) -> dict:
+        """Key/value map for user-stored data."""
+        return self._metadata
+
+    @metadata.setter
+    def metadata(self, value: dict) -> None:
+        self._metadata.clear()
+        self._metadata.update(value)
+
     @property
     def source(self):
         return self._source
@@ -313,15 +332,6 @@ def loaded(self) -> bool:
         """
         return True
 
-    @name.setter
-    def name(self, name):
-        if name == self.name:
-            return
-        if not name:
-            name = self._basename()
-        self._name = name
-        self.events.name()
-
     @property
     def opacity(self):
         """float: Opacity value between 0.0 and 1.0."""


Test patch: