Repository: napari/napari

Issue:
Regression: Points.events.data is no longer fired from the data setter
## 🐛 Bug

While investigating affinder CI failures, I found that #5967 broke affinder's tests. (btw if you've never heard of `git bisect run` you should try it! 😍) The reason is [this line](https://github.com/napari/napari/pull/5967/files#diff-0eb960578d63238e8d320dcc3e9fa3b225b34f9b08827835c3ed0bff447f977cL599) which removes the data event emitter from the setter but doesn't replace it with the new style.

It seems to have been simply missed. @melonora was this intentional? I can't imagine the reason for it other than an oversight! 😅 

Fix incoming.

## To Reproduce

```python
import napari
import numpy as np


layer = napari.layers.Points(np.random.random((5, 3)))

layer.events.data.connect(lambda ev: print(ev))

print('adding a single point')
layer.add(np.random.random((1, 3)))  # prints ok

print('setting data')
layer.data = np.random.random((8, 3))  # nothing happens

print('done')
```

Result:

```
adding a single point
Event
setting data
done
```

Expected result (and result before 96516dad057aa8d31328f889ce73d40a800eb67d):

```
adding a single point
Event
setting data
Event
done
```

## Environment

napari main / napari 0.4.18.


Discussion:
oops :astonished:, this I believe is indeed an oversight. For sure it was not intentional.

Resolving patch:
diff --git a/napari/layers/points/points.py b/napari/layers/points/points.py
--- a/napari/layers/points/points.py
+++ b/napari/layers/points/points.py
@@ -525,6 +525,17 @@ def data(self) -> np.ndarray:
 
     @data.setter
     def data(self, data: Optional[np.ndarray]):
+        """Set the data array and emit a corresponding event."""
+        self._set_data(data)
+        self.events.data(
+            value=self.data,
+            action=ActionType.CHANGE.value,
+            data_indices=slice(None),
+            vertex_indices=((),),
+        )
+
+    def _set_data(self, data: Optional[np.ndarray]):
+        """Set the .data array attribute, without emitting an event."""
         data, _ = fix_data_points(data, self.ndim)
         cur_npoints = len(self._data)
         self._data = data
@@ -1914,7 +1925,7 @@ def add(self, coords):
             Point or points to add to the layer data.
         """
         cur_points = len(self.data)
-        self.data = np.append(self.data, np.atleast_2d(coords), axis=0)
+        self._set_data(np.append(self.data, np.atleast_2d(coords), axis=0))
         self.events.data(
             value=self.data,
             action=ActionType.ADD.value,
@@ -1949,7 +1960,7 @@ def remove_selected(self):
                     self._value -= offset
                     self._value_stored -= offset
 
-            self.data = np.delete(self.data, index, axis=0)
+            self._set_data(np.delete(self.data, index, axis=0))
             self.events.data(
                 value=self.data,
                 action=ActionType.REMOVE.value,


Test patch:
diff --git a/napari/layers/points/_tests/test_points.py b/napari/layers/points/_tests/test_points.py
--- a/napari/layers/points/_tests/test_points.py
+++ b/napari/layers/points/_tests/test_points.py
@@ -2486,3 +2486,24 @@ def test_point_selection_remains_evented_after_update():
     assert isinstance(layer.selected_data, Selection)
     layer.selected_data = {0, 1}
     assert isinstance(layer.selected_data, Selection)
+
+
+def test_points_data_setter_emits_event():
+    data = np.random.random((5, 2))
+    emitted_events = Mock()
+    layer = Points(data)
+    layer.events.data.connect(emitted_events)
+    layer.data = np.random.random((5, 2))
+    emitted_events.assert_called_once()
+
+
+def test_points_add_delete_only_emit_one_event():
+    data = np.random.random((5, 2))
+    emitted_events = Mock()
+    layer = Points(data)
+    layer.events.data.connect(emitted_events)
+    layer.add(np.random.random(2))
+    assert emitted_events.call_count == 1
+    layer.selected_data = {3}
+    layer.remove_selected()
+    assert emitted_events.call_count == 2