Repository: napari/napari

Issue:
__setitem__ of EventedList does not work for lists with array-like items
trying to use `EventedList.__setitem__()` (`napari.utils.events.EventedList`) with array-like items causes a `ValueError` because the check on line 110 [here](https://github.com/napari/napari/blob/ee525896141dd9213cbc9e7ab2af2156bc9c6803/napari/utils/events/containers/_evented_list.py#L110) errors when checking equality of such objects.

```python
  File "/opt/miniconda3/envs/napari-animation/lib/python3.8/site-packages/napari/utils/events/containers/_evented_list.py", line 110, in __setitem__
    if value == old:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
```



Discussion:
yeah, good point.  I guess we haven't needed it for holding array-like objects yet!  We can fix this, but I'd really like to avoid having to try to do semi-complicated type checking every time we set an item in that basic list.  In the evented_dataclass we let subclasses define their own equality checking functions... it'd be nice to be able to avoid that in this case of a simple mutable sequence.  Is this for napari-animation?  Can you give a little more detail?
Yeah that definitely wouldn't be a nice situation to be in, I guess the
check is there to avoid doing useless work?

It was indeed for napari-animation - I was trying to replace specific items
in a list of dictionaries with a different dictionary, the dictionaries
contained arrays (thumbnails for the key frames of the animation).

Here's a simple example which breaks it in the same way, although with an
array directly rather than a dict

```python
import numpy as np
from napari.utils.events import EventedList

data = np.random.random((32, 32))

my_list = [data, data, data]
my_list[0] = data # expected behaviour, works for normal list

my_evented_list = EventedList([data, data, data])
my_evented_list[0] = data
```

```
ValueError                                Traceback (most recent call last)
<ipython-input-13-efa50eac8140> in <module>
      8
      9 my_evented_list = EventedList([data, data, data])
---> 10 my_evented_list[0] = data

/opt/miniconda3/envs/py38/lib/python3.8/site-packages/napari/utils/events/containers/_evented_list.py
in __setitem__(self, key, value)
    108     def __setitem__(self, key, value):
    109         old = self._list[key]
--> 110         if value == old:
    111             return
    112         if isinstance(key, slice):

ValueError: The truth value of an array with more than one element is
ambiguous. Use a.any() or a.all()
```

I have a workaround for my current use case so no worries on that front.

In terms of 'fixing' this, is there any other case where a `__setitem__`
method on a list would raise a `ValueError`? If not, a simple, ugly
solution could be a try, except block which just proceeds with the
modification in the case of that specific error. Interested to hear how
else this could be approached though, especially without a bunch of
checking!

On Sat, 16 Jan 2021 at 20:16, Talley Lambert <notifications@github.com>
wrote:

> yeah, good point. I guess we haven't needed it for holding array-like
> objects yet! We can fix this, but I'd really like to avoid having to try to
> do semi-complicated type checking every time we set an item in that basic
> list. In the evented_dataclass we let subclasses define their own equality
> checking functions... it'd be nice to be able to avoid that in this case of
> a simple mutable sequence. Is this for napari-animation? Can you give a
> little more detail?
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/napari/napari/issues/2117#issuecomment-761625911>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABXYBYA4DDF2CZ6I57YYDBTS2HXZBANCNFSM4WFPFU5A>
> .
>

and of course markdown doesn't render when sent by email :laughing: 
> In terms of 'fixing' this, is there any other case where a `__setitem__` method on a list would raise a `ValueError`?

no not really really. and in this case, it would set just fine, we're just trying to avoid emitting an event unnecessarily (if the object really hasn't changed).

> If not, a simple, ugly solution could be a try, except block which just proceeds with the modification in the case of that specific error. Interested to hear how else this could be approached though, especially without a bunch of checking!

@jni has suggested that try/except blocks are actually somewhat "heavy" ... I just did some quick tests between three options `try/except`, `np.array_equal()` (which also works for non arrays), and just using `if value is old` instead of `if value == old` ... the "`is`" check was _by far_ the fastest (not surprisingly, it's also the most strict).  with try catch coming in somewhere around np.array_equal.  I think in this case, using `is` is probably a fine solution.  me mostly use this for the layerlist, where really, we care about object identity more than anything else.

Does that make sense?  would you be able to make a PR for that?


Resolving patch:
diff --git a/napari/utils/events/containers/_evented_list.py b/napari/utils/events/containers/_evented_list.py
--- a/napari/utils/events/containers/_evented_list.py
+++ b/napari/utils/events/containers/_evented_list.py
@@ -107,7 +107,7 @@ def __init__(
 
     def __setitem__(self, key, value):
         old = self._list[key]
-        if value == old:
+        if value is old:  # https://github.com/napari/napari/pull/2120
             return
         if isinstance(key, slice):
             if not isinstance(value, Iterable):


Test patch:
diff --git a/napari/utils/events/_tests/test_evented_list.py b/napari/utils/events/_tests/test_evented_list.py
--- a/napari/utils/events/_tests/test_evented_list.py
+++ b/napari/utils/events/_tests/test_evented_list.py
@@ -1,6 +1,7 @@
 from collections.abc import MutableSequence
 from unittest.mock import Mock
 
+import numpy as np
 import pytest
 
 from napari.utils.events import EmitterGroup, EventedList, NestableEventedList
@@ -402,3 +403,10 @@ def test_event_group_depr():
         assert events["b"] == events["a"]
     with pytest.raises(KeyError):
         events["c"].connect()
+
+
+def test_array_like_setitem():
+    """Test that EventedList.__setitem__ works for array-like items"""
+    array = np.array((10, 10))
+    evented_list = EventedList([array])
+    evented_list[0] = array