Repository: napari/napari

Issue:
Selecting points fails if a property value has dtype `object`
## 🐛 Bug

Selecting points will raise an error if the points layer has a property aarray with dtype `object`. This leaves in an inconsistent state where `Points.selected_data` is updated, but `Points.current_property` is broken.



## To Reproduce
```python
#!/usr/bin/env python3

import napari
import numpy as np

pt = np.random.rand(100, 2)
props = {'prop': np.zeros(100, dtype=object)}

v = napari.Viewer()
pl = v.add_points(pt, properties=props)

sele = np.arange(50)

pl.selected_data = sele
```
this raises this error:
```python
~/.local/lib/python3.9/site-packages/napari/layers/points/points.py in selected_data(self, selected_data)
   1032                 self.current_size = size
   1033 
-> 1034         properties = {
   1035             k: np.unique(v[index], axis=0) for k, v in self.properties.items()
   1036         }

~/.local/lib/python3.9/site-packages/napari/layers/points/points.py in <dictcomp>(.0)
   1033 
   1034         properties = {
-> 1035             k: np.unique(v[index], axis=0) for k, v in self.properties.items()
   1036         }
   1037         n_unique_properties = np.array([len(v) for v in properties.values()])

<__array_function__ internals> in unique(*args, **kwargs)

/usr/lib/python3.9/site-packages/numpy/lib/arraysetops.py in unique(ar, return_index, return_inverse, return_counts, axis)
    293         # There's no good way to do this for object arrays, etc...
    294         msg = 'The axis argument to unique is not supported for dtype {dt}'
--> 295         raise TypeError(msg.format(dt=ar.dtype)) from e
    296 
    297     def reshape_uniq(uniq):

TypeError: The axis argument to unique is not supported for dtype object
```

At this point, `pl.selected_data` is right, but `pl.current_properties` is not.

## Expected behavior

I would expect either to be disallowed to set properties with dtype object (in case this can't be solved), or that this should work.

## Environment

```yaml
napari: 0.4.8
Platform: Linux-5.12.5-arch1-1-x86_64-with-glibc2.33
System: Arch Linux
Python: 3.9.5 (default, May 12 2021, 17:14:51) [GCC 10.2.0]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.20.2
SciPy: 1.6.3
Dask: 2020.12.0
VisPy: 0.6.6

OpenGL:
- GL version: 4.6.0 NVIDIA 465.31
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1080, scale 1.0
- screen 2: resolution 1920x1200, scale 1.0

Plugins:
- animation: 0.0.1
- console: 0.0.3
- mrcfile-reader: 0.1.2
- napari-em-reader: 0.1.0
- napari-properties-plotter: 0.1.dev2+g2734460.d20210520
- ndtiffs: 0.1.1
- scikit-image: 0.4.8
- svg: 0.1.4
```


Discussion:
Unexpectedly, this problem is absent with the labels layer. Something worked out better there :P

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
@@ -1009,9 +1009,13 @@ def selected_data(self, selected_data):
             with self.block_update_properties():
                 self.current_size = size
 
-        properties = {
-            k: np.unique(v[index], axis=0) for k, v in self.properties.items()
-        }
+        properties = {}
+        for k, v in self.properties.items():
+            # pandas uses `object` as dtype for strings by default, which
+            # combined with the axis argument breaks np.unique
+            axis = 0 if v.ndim > 1 else None
+            properties[k] = np.unique(v[index], axis=axis)
+
         n_unique_properties = np.array([len(v) for v in properties.values()])
         if np.all(n_unique_properties == 1):
             with self.block_update_properties():
diff --git a/napari/layers/shapes/shapes.py b/napari/layers/shapes/shapes.py
--- a/napari/layers/shapes/shapes.py
+++ b/napari/layers/shapes/shapes.py
@@ -1071,10 +1071,14 @@ def selected_data(self, selected_data):
                 edge_width = edge_width[0]
                 with self.block_update_properties():
                     self.current_edge_width = edge_width
-            properties = {
-                k: np.unique(v[selected_data_indices], axis=0)
-                for k, v in self.properties.items()
-            }
+
+            properties = {}
+            for k, v in self.properties.items():
+                # pandas uses `object` as dtype for strings by default, which
+                # combined with the axis argument breaks np.unique
+                axis = 0 if v.ndim > 1 else None
+                properties[k] = np.unique(v[selected_data_indices], axis=axis)
+
             n_unique_properties = np.array(
                 [len(v) for v in properties.values()]
             )


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
@@ -674,6 +674,16 @@ def test_text_error(properties):
         Points(data, properties=copy(properties), text=123)
 
 
+def test_select_properties_object_dtype():
+    """selecting points when they have a property of object dtype should not fail"""
+    # pandas uses object as dtype for strings by default
+    properties = pd.DataFrame({'color': ['red', 'green']})
+    pl = Points(np.ones((2, 2)), properties=properties)
+    selection = {0, 1}
+    pl.selected_data = selection
+    assert pl.selected_data == selection
+
+
 def test_refresh_text():
     """Test refreshing the text after setting new properties"""
     shape = (10, 2)
diff --git a/napari/layers/shapes/_tests/test_shapes.py b/napari/layers/shapes/_tests/test_shapes.py
--- a/napari/layers/shapes/_tests/test_shapes.py
+++ b/napari/layers/shapes/_tests/test_shapes.py
@@ -274,6 +274,16 @@ def test_text_error(properties):
         Shapes(data, properties=copy(properties), text=123)
 
 
+def test_select_properties_object_dtype():
+    """selecting points when they have a property of object dtype should not fail"""
+    # pandas uses object as dtype for strings by default
+    properties = pd.DataFrame({'color': ['red', 'green']})
+    pl = Shapes(np.ones((2, 2, 2)), properties=properties)
+    selection = {0, 1}
+    pl.selected_data = selection
+    assert pl.selected_data == selection
+
+
 def test_refresh_text():
     """Test refreshing the text after setting new properties"""
     shape = (10, 4, 2)