Repository: napari/napari

Issue:
Error when interacting with shapes: `TypeError: argument of type 'numpy.int32' is not iterable`
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->

## To Reproduce

Steps to reproduce the behavior:

1. Create a shape layer
2. Create a shape
3. Use either the `Selecte vertices` or `Select shapes` modes

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->
- Traceback:

<details>

```python-traceback
TypeError                                 Traceback (most recent call last)
~\anaconda3\envs\napari-dev\lib\site-packages\vispy\app\backends\_qt.py in paintGL(self=<vispy.app.backends._qt.CanvasBackendDesktop object>)
    901         # (0, 0, self.width(), self.height()))
    902         self._vispy_canvas.set_current()
--> 903         self._vispy_canvas.events.draw(region=None)
        self._vispy_canvas.events.draw = <vispy.util.event.EventEmitter object at 0x000002482CD5CDC0>
        global region = undefined
    904
    905         # Clear the alpha channel with QOpenGLWidget (Qt >= 5.4), otherwise the

~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py in __call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'region': None})
    451                     raise RuntimeError('EventEmitter loop detected!')
    452
--> 453                 self._invoke_callback(cb, event)
        self._invoke_callback = <bound method EventEmitter._invoke_callback of <vispy.util.event.EventEmitter object at 0x000002482CD5CDC0>>
        cb = <bound method VispyCanvas.on_draw of <napari._vispy.canvas.VispyCanvas object at 0x000002482CD4A410>>
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
    454                 if event.blocked:
    455                     break

~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py in _invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCanvas.on_draw of <napari._vispy.canvas.VispyCanvas object>>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    469             cb(event)
    470         except Exception:
--> 471             _handle_exception(self.ignore_callback_errors,
        global _handle_exception = <function _handle_exception at 0x00000248224A3E20>
        self.ignore_callback_errors = False
        self.print_callback_errors = 'reminders'
        self = <vispy.util.event.EventEmitter object at 0x000002482CD5CDC0>
        global cb_event = undefined
        cb = <bound method VispyCanvas.on_draw of <napari._vispy.canvas.VispyCanvas object at 0x000002482CD4A410>>
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
    472                               self.print_callback_errors,
    473                               self, cb_event=(cb, event))

~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py in _invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCanvas.on_draw of <napari._vispy.canvas.VispyCanvas object>>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    467     def _invoke_callback(self, cb, event):
    468         try:
--> 469             cb(event)
        cb = <bound method VispyCanvas.on_draw of <napari._vispy.canvas.VispyCanvas object at 0x000002482CD4A410>>
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
    470         except Exception:
    471             _handle_exception(self.ignore_callback_errors,

E:\Acer\Documentos\Quansight\Napari\napari\napari\_vispy\canvas.py in on_draw(self=<napari._vispy.canvas.VispyCanvas object>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    527             else:
    528                 displayed_axes = list(self.viewer.dims.displayed[-nd:])
--> 529             layer._update_draw(
        layer._update_draw = <bound method Shapes._update_draw of <Shapes layer 'Shapes' at 0x2483a032f80>>
        global scale_factor = undefined
        self.viewer.camera.zoom = 0.6784840624999999
        global corner_pixels_displayed = undefined
        canvas_corners_world = <class 'numpy.ndarray'> (2, 2) float64
        displayed_axes = [0, 1]
        global shape_threshold = undefined
        self._scene_canvas.size = (499, 528)
    530                 scale_factor=1 / self.viewer.camera.zoom,
    531                 corner_pixels_displayed=canvas_corners_world[

E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py in _update_draw(self=<Shapes layer 'Shapes'>, scale_factor=1.4738739718001852, corner_pixels_displayed=<class 'numpy.ndarray'> (2, 2) float64, shape_threshold=(499, 528))
   2731             scale_factor, corner_pixels_displayed, shape_threshold
   2732         )
-> 2733         self._set_highlight(force=True)
        self._set_highlight = <bound method Shapes._set_highlight of <Shapes layer 'Shapes' at 0x2483a032f80>>
        global force = undefined
   2734
   2735     def _get_value(self, position):

E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py in _set_highlight(self=<Shapes layer 'Shapes'>, force=True)
   2539         self._value_stored = copy(self._value)
   2540         self._drag_box_stored = copy(self._drag_box)
-> 2541         self.events.highlight()
        self.events.highlight = <napari.utils.events.event.EventEmitter object at 0x000002483A05D420>
   2542
   2543     def _finish_drawing(self, event=None):

E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\events\event.py in __call__(self=<napari.utils.events.event.EventEmitter object>, *args=(), **kwargs={})
    761                     continue
    762
--> 763                 self._invoke_callback(cb, event if pass_event else None)
        self._invoke_callback = <bound method EventEmitter._invoke_callback of <napari.utils.events.event.EventEmitter object at 0x000002483A05D420>>
        cb = <bound method VispyShapesLayer._on_highlight_change of <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000002483A05E290>>
        event = <Event blocked=False handled=False native=None source=None sources=[] type='highlight'>
        pass_event = False
    764                 if event.blocked:
    765                     break

E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\events\event.py in _invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method VispyShapesLayer._on_highlight_cha...ri._vispy.layers.shapes.VispyShapesLayer object>>, event=None)
    799                 self.disconnect(cb)
    800                 return
--> 801             _handle_exception(
        global _handle_exception = <function _handle_exception at 0x00000248224A3E20>
        self.ignore_callback_errors = False
        self.print_callback_errors = 'reminders'
        self = <napari.utils.events.event.EventEmitter object at 0x000002483A05D420>
        global cb_event = undefined
        cb = <bound method VispyShapesLayer._on_highlight_change of <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000002483A05E290>>
        event = None
    802                 self.ignore_callback_errors,
    803                 self.print_callback_errors,

E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\events\event.py in _invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method VispyShapesLayer._on_highlight_cha...ri._vispy.layers.shapes.VispyShapesLayer object>>, event=None)
    788                 cb(event)
    789             else:
--> 790                 cb()
        cb = <bound method VispyShapesLayer._on_highlight_change of <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000002483A05E290>>
    791         except Exception as e:  # noqa: BLE001
    792             # dead Qt object with living python pointer. not importing Qt

E:\Acer\Documentos\Quansight\Napari\napari\napari\_vispy\layers\shapes.py in _on_highlight_change(self=<napari._vispy.layers.shapes.VispyShapesLayer object>)
     63
     64         # Compute the vertices and faces of any shape outlines
---> 65         vertices, faces = self.layer._outline_shapes()
        vertices = undefined
        faces = undefined
        self.layer._outline_shapes = <bound method Shapes._outline_shapes of <Shapes layer 'Shapes' at 0x2483a032f80>>
     66
     67         if vertices is None or len(vertices) == 0 or len(faces) == 0:

E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py in _outline_shapes(self=<Shapes layer 'Shapes'>)
   2418                 index = self._value[0]
   2419
-> 2420             centers, offsets, triangles = self._data_view.outline(index)
        centers = undefined
        offsets = undefined
        triangles = undefined
        self._data_view.outline = <bound method ShapeList.outline of <napari.layers.shapes._shape_list.ShapeList object at 0x000002483A05D870>>
        index = 0
   2421             vertices = centers + (
   2422                 self._normalized_scale_factor * self._highlight_width * offsets

E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shape_list.py in outline(self=<napari.layers.shapes._shape_list.ShapeList object>, indices=0)
    963         else:
    964             meshes = self._mesh.triangles_index
--> 965             triangle_indices = [
        triangle_indices = undefined
        i = undefined
        global x = undefined
        global enumerate = undefined
        meshes = <class 'numpy.ndarray'> (10, 2) int32
        indices = 0
    966                 i
    967                 for i, x in enumerate(meshes)

E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shape_list.py in <listcomp>(.0=<enumerate object>)
    966                 i
    967                 for i, x in enumerate(meshes)
--> 968                 if x[0] in indices and x[1] == 1
        x = <class 'numpy.ndarray'> (2,) int32
        global indices = undefined
    969             ]
    970             meshes = self._mesh.vertices_index

TypeError: argument of type 'numpy.int32' is not iterable

```

</details>

- Preview:

![shapes](https://github.com/napari/napari/assets/16781833/9b26f5e5-869a-4658-8883-6c7475844067)


## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->
No error should happen

## Environment

 - Please copy and paste the information at napari info option in help menubar here:

<details>

```
napari: 0.5.0a2.dev318+ge7ef9393
Platform: Windows-10-10.0.19045-SP0
Python: 3.10.9 | packaged by conda-forge | (main, Jan 11 2023, 15:15:40) [MSC v.1916 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.9
NumPy: 1.24.3
SciPy: 1.9.3
Dask: 2022.10.2
VisPy: 0.13.0
magicgui: 0.6.0
superqt: 0.5.3
in-n-out: 0.1.5
app-model: 0.1.4
npe2: 0.7.2

OpenGL:
- GL version: 4.5.0 - Build 25.20.100.6444
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1366x768, scale 1.0
- screen 2: resolution 1600x900, scale 1.0

Settings path:
- C:\Users\dalth\AppData\Local\napari\napari-dev_4454fdbf170edefe23827a4bf512389a6806af69\settings.yaml

```

</details>

Happens to me running from main at e7ef9393b but seems like removing that commit (I think is a pre-commit one) prevents the error to appear 🤔 



Discussion:
I can reproduce on my arm64 macOS.
The issue is from this:
https://github.com/napari/napari/pull/6128/commits/a71879f224d5cefc738a5654dd6b71aab50f59cf#diff-80d007989a4c8641d9ab98d8719805b67c5e55f9913ad0292a58738ec7be7579
So here (and 981):
https://github.com/napari/napari/blob/a71879f224d5cefc738a5654dd6b71aab50f59cf/napari/layers/shapes/_shape_list.py#L954
np.int32/np.int64 is not `int`, so the wrong branch is taken.
Could fix with: 
`if isinstance(indices, (int, np.integer)):`
Does the signature need fixing then too?

It's that or use the old logic to check if a list?
> np.int32/np.int64 is not `int`, so the wrong branch is taken.
> Could fix with:
> `if isinstance(indices, (int, np.integer)):`
> Does the signature need fixing then too?

I prefer to revert if to `if not isintance(indices, Sequence)` as we could get also dask, zarr dtypes etc. 

Resolving patch:
diff --git a/napari/layers/shapes/_shape_list.py b/napari/layers/shapes/_shape_list.py
--- a/napari/layers/shapes/_shape_list.py
+++ b/napari/layers/shapes/_shape_list.py
@@ -951,43 +951,52 @@ def outline(self, indices: Union[int, Sequence[int]]):
         triangles : np.ndarray
             Mx3 array of any indices of vertices for triangles of outline
         """
-        if isinstance(indices, int):
-            triangle_indices = np.all(
-                self._mesh.triangles_index == [indices, 1], axis=1
-            )
-            triangle_indices = np.where(triangle_indices)[0]
-            vertices_indices = np.all(
-                self._mesh.vertices_index == [indices, 1], axis=1
-            )
-            vertices_indices = np.where(vertices_indices)[0]
-        else:
-            meshes = self._mesh.triangles_index
-            triangle_indices = [
-                i
-                for i, x in enumerate(meshes)
-                if x[0] in indices and x[1] == 1
-            ]
-            meshes = self._mesh.vertices_index
-            vertices_indices = [
-                i
-                for i, x in enumerate(meshes)
-                if x[0] in indices and x[1] == 1
-            ]
+        if not isinstance(indices, Sequence):
+            indices = [indices]
+        return self.outlines(indices)
+
+    def outlines(self, indices: Sequence[int]):
+        """Finds outlines of shapes listed in indices
+
+        Parameters
+        ----------
+        indices : Sequence[int]
+            Location in list of the shapes to be outline.
+
+        Returns
+        -------
+        centers : np.ndarray
+            Nx2 array of centers of outline
+        offsets : np.ndarray
+            Nx2 array of offsets of outline
+        triangles : np.ndarray
+            Mx3 array of any indices of vertices for triangles of outline
+        """
+        indices_set = set(indices)
+        meshes = self._mesh.triangles_index
+        triangle_indices = [
+            i
+            for i, x in enumerate(meshes)
+            if x[0] in indices_set and x[1] == 1
+        ]
+        meshes = self._mesh.vertices_index
+        vertices_indices = [
+            i
+            for i, x in enumerate(meshes)
+            if x[0] in indices_set and x[1] == 1
+        ]
 
         offsets = self._mesh.vertices_offsets[vertices_indices]
         centers = self._mesh.vertices_centers[vertices_indices]
         triangles = self._mesh.triangles[triangle_indices]
 
-        if not isinstance(indices, int):
-            t_ind = self._mesh.triangles_index[triangle_indices][:, 0]
-            inds = self._mesh.vertices_index[vertices_indices][:, 0]
-            starts = np.unique(inds, return_index=True)[1]
-            for i, ind in enumerate(indices):
-                inds = t_ind == ind
-                adjust_index = starts[i] - vertices_indices[starts[i]]
-                triangles[inds] = triangles[inds] + adjust_index
-        else:
-            triangles = triangles - vertices_indices[0]
+        t_ind = self._mesh.triangles_index[triangle_indices][:, 0]
+        inds = self._mesh.vertices_index[vertices_indices][:, 0]
+        starts = np.unique(inds, return_index=True)[1]
+        for i, ind in enumerate(indices):
+            inds = t_ind == ind
+            adjust_index = starts[i] - vertices_indices[starts[i]]
+            triangles[inds] = triangles[inds] + adjust_index
 
         return centers, offsets, triangles
 


Test patch:
diff --git a/napari/layers/shapes/_tests/test_shape_list.py b/napari/layers/shapes/_tests/test_shape_list.py
--- a/napari/layers/shapes/_tests/test_shape_list.py
+++ b/napari/layers/shapes/_tests/test_shape_list.py
@@ -23,6 +23,40 @@ def test_adding_to_shape_list():
     assert shape_list.shapes[0] == shape
 
 
+def test_shape_list_outline():
+    """Test ShapeList outline method."""
+    np.random.seed(0)
+    data = 20 * np.random.random((4, 2))
+    shape = Rectangle(data)
+    shape_list = ShapeList()
+
+    shape_list.add(shape)
+
+    # Check passing an int
+    outline_by_index = shape_list.outline(0)
+    assert isinstance(outline_by_index, tuple)
+
+    # Check passing a list
+    outline_by_index_list = shape_list.outline([0])
+    assert isinstance(outline_by_index_list, tuple)
+
+    # Check return value for `int` and `list` are the same
+    for value_by_idx, value_by_idx_list in zip(
+        outline_by_index, outline_by_index_list
+    ):
+        assert np.array_equal(value_by_idx, value_by_idx_list)
+
+    # Check passing a `numpy.int_` (`numpy.int32/64` depending on platform)
+    outline_by_index_np = shape_list.outline(np.int_(0))
+    assert isinstance(outline_by_index_np, tuple)
+
+    # Check return value for `int` and `numpy.int_` are the same
+    for value_by_idx, value_by_idx_np in zip(
+        outline_by_index, outline_by_index_np
+    ):
+        assert np.array_equal(value_by_idx, value_by_idx_np)
+
+
 def test_nD_shapes():
     """Test adding shapes to ShapeList."""
     np.random.seed(0)