Repository: napari/napari

Issue:
Fill does not work for xarray data for labels layer
## 🐛 Bug
Using `fill` is broken for the labels layer when backed by an xarray.

It doesn't fill and throws this error:
```
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/vispy/app/backends/_qt.py", line 443, in mousePressEvent
    self._vispy_mouse_press(
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/vispy/app/base.py", line 181, in _vispy_mouse_press
    ev = self._vispy_canvas.events.mouse_press(**kwargs)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/vispy/util/event.py", line 455, in __call__
    self._invoke_callback(cb, event)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/vispy/util/event.py", line 473, in _invoke_callback
    _handle_exception(self.ignore_callback_errors,
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/vispy/util/event.py", line 471, in _invoke_callback
    cb(event)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/napari/_qt/qt_viewer.py", line 682, in on_mouse_press
    mouse_press_callbacks(layer, event)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/napari/utils/interactions.py", line 101, in mouse_press_callbacks
    next(gen)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/napari/layers/labels/_labels_mouse_bindings.py", line 35, in draw
    layer.fill(layer.coordinates, new_label)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/napari/layers/labels/labels.py", line 776, in fill
    matches = labels == old_label
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/xarray/core/dataarray.py", line 2991, in func
    f(self.variable, other_variable)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/xarray/core/nputils.py", line 73, in array_eq
    return _ensure_bool_is_ndarray(self == other, self, other)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/xarray/core/variable.py", line 2305, in func
    result = Variable(dims, new_data, attrs=attrs)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/xarray/core/variable.py", line 342, in __init__
    self._dims = self._parse_dimensions(dims)
  File "/home/ian/mambaforge/envs/micro/lib/python3.8/site-packages/xarray/core/variable.py", line 603, in _parse_dimensions
    raise ValueError(
ValueError: dimensions () must have the same length as the number of data dimensions, ndim=2
```

## To Reproduce
```python
from skimage import data
from scipy import ndimage as ndi
import napari
import xarray as xr

with napari.gui_qt():
    blobs = xr.DataArray(data.binary_blobs(length=128, volume_fraction=0.1, n_dim=3), dims=('T','Y','X')).astype(float)
    viewer = napari.view_image(blobs.astype(float), name='blobs')
    labels = xr.DataArray(ndi.label(blobs)[0])
    label_layer = viewer.add_labels(labels)
    label_layer.mode = 'fill'
```
try to fill one of the blobs.

## Expected behavior
Blob is paintbucket filled.

## Environment
```
napari: 0.4.7.dev14+gc473058
Platform: Linux-5.8.0-7630-generic-x86_64-with-glibc2.31
System: Pop!_OS 20.04 LTS
Python: 3.9.1 | packaged by conda-forge | (default, Jan 26 2021, 01:34:10) [GCC 9.3.0]
Qt: 5.15.2
PyQt5: 5.15.2
NumPy: 1.19.5
SciPy: 1.6.0
Dask: 2021.01.1
VisPy: 0.6.6

OpenGL:
- GL version: 4.6 (Compatibility Profile) Mesa 20.2.6
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1080, scale 1.0

Plugins:
- console: 0.0.3
- svg: 0.1.4
```


## Additional context
Partial xarray support for the labels layer added in https://github.com/napari/napari/pull/2191 which fixed the behavior when in `paint` mode.


Discussion:
Maybe a better fix for https://github.com/napari/napari/issues/2190 would have been to check if `data` is an xarray in `_ImageBase` and if so then to do `self._data = data.values` 
Haha I was working on #2339 and thinking exactly this, and boom here comes your issue 😂 
As commented in #2376, I have concerns about that approach - I think at that point I'd just say we don't support xarray and please pass us the numpy array rather than silently convert for people. Maybe we can improve the fill more generally and add an xarray test?
As an update this no longer break explicitly but does try to allocate a stupendously large array:
Use this script:
```python
import xarray as xr
import napari

import numpy as np
arr = xr.DataArray(np.zeros((50, 512, 512), dtype=np.uint16))
viewer = napari.Viewer()
viewer.add_labels(arr)
napari.run()

```

and then try to fill

```
---------------------------------------------------------------------------
MemoryError                               Traceback (most recent call last)
~/mambaforge/envs/micro/lib/python3.9/site-packages/vispy/app/backends/_qt.py in mousePressEvent(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, ev=<PyQt5.QtGui.QMouseEvent object>)
    500         if self._vispy_canvas is None:
    501             return
--> 502         self._vispy_mouse_press(
        self._vispy_mouse_press = <bound method BaseCanvasBackend._vispy_mouse_press of <vispy.app.backends._qt.CanvasBackendDesktop object at 0x7fd4257a3280>>
        global native = undefined
        ev = <PyQt5.QtGui.QMouseEvent object at 0x7fd4257fd040>
        global pos = undefined
        global _get_event_xy = <function _get_event_xy at 0x7fd470812a60>
        global button = undefined
        global BUTTONMAP.get = <built-in method get of dict object at 0x7fd4708246c0>
        ev.button = <built-in method button of QMouseEvent object at 0x7fd4257fd040>
        global modifiers = undefined
        self._modifiers = <bound method QtBaseCanvasBackend._modifiers of <vispy.app.backends._qt.CanvasBackendDesktop object at 0x7fd4257a3280>>
    503             native=ev,
    504             pos=_get_event_xy(ev),

~/mambaforge/envs/micro/lib/python3.9/site-packages/vispy/app/base.py in _vispy_mouse_press(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, **kwargs={'button': 1, 'buttons': [], 'last_event': <MouseEvent blocked=False button=None buttons=[]...urces=[] time=1638561113.3018866 type=mouse_move>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (499, 263), 'press_event': None})
    182         # default method for delivering mouse press events to the canvas
    183         kwargs.update(self._vispy_mouse_data)
--> 184         ev = self._vispy_canvas.events.mouse_press(**kwargs)
        ev = undefined
        self._vispy_canvas.events.mouse_press = <vispy.util.event.EventEmitter object at 0x7fd4257aa1c0>
        kwargs = {'native': <PyQt5.QtGui.QMouseEvent object at 0x7fd4257fd040>, 'pos': (499, 263), 'button': 1, 'modifiers': (), 'buttons': [], 'press_event': None, 'last_event': <MouseEvent blocked=False button=None buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x7fd4257fd040> pos=[499 263] press_event=None source=None sources=[] time=1638561113.3018866 type=mouse_move>, 'last_mouse_press': None}
    185         if self._vispy_mouse_data['press_event'] is None:
    186             self._vispy_mouse_data['press_event'] = ev

~/mambaforge/envs/micro/lib/python3.9/site-packages/vispy/util/event.py in __call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'button': 1, 'buttons': [], 'last_event': <MouseEvent blocked=False button=None buttons=[]...urces=[] time=1638561113.3018866 type=mouse_move>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (499, 263), 'press_event': 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 0x7fd4257aa1c0>>
        cb = <bound method QtViewer.on_mouse_press of <napari._qt.qt_viewer.QtViewer object at 0x7fd470ad8790>>
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x7fd4257fd040> pos=[499 263] press_event=None source=None sources=[] time=1638561113.539664 type=mouse_press>
    454                 if event.blocked:
    455                     break

~/mambaforge/envs/micro/lib/python3.9/site-packages/vispy/util/event.py in _invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method QtViewer.on_mouse_press of <napari._qt.qt_viewer.QtViewer object>>, event=<MouseEvent blocked=False button=1 buttons=[] de...urces=[] time=1638561113.539664 type=mouse_press>)
    469             cb(event)
    470         except Exception:
--> 471             _handle_exception(self.ignore_callback_errors,
        global _handle_exception = <function _handle_exception at 0x7fd4261b5310>
        self.ignore_callback_errors = False
        self.print_callback_errors = 'reminders'
        self = <vispy.util.event.EventEmitter object at 0x7fd4257aa1c0>
        global cb_event = undefined
        cb = <bound method QtViewer.on_mouse_press of <napari._qt.qt_viewer.QtViewer object at 0x7fd470ad8790>>
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x7fd4257fd040> pos=[499 263] press_event=None source=None sources=[] time=1638561113.539664 type=mouse_press>
    472                               self.print_callback_errors,
    473                               self, cb_event=(cb, event))

~/mambaforge/envs/micro/lib/python3.9/site-packages/vispy/util/event.py in _invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method QtViewer.on_mouse_press of <napari._qt.qt_viewer.QtViewer object>>, event=<MouseEvent blocked=False button=1 buttons=[] de...urces=[] time=1638561113.539664 type=mouse_press>)
    467     def _invoke_callback(self, cb, event):
    468         try:
--> 469             cb(event)
        cb = <bound method QtViewer.on_mouse_press of <napari._qt.qt_viewer.QtViewer object at 0x7fd470ad8790>>
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x7fd4257fd040> pos=[499 263] press_event=None source=None sources=[] time=1638561113.539664 type=mouse_press>
    470         except Exception:
    471             _handle_exception(self.ignore_callback_errors,

~/mambaforge/envs/micro/lib/python3.9/site-packages/napari/_qt/qt_viewer.py in on_mouse_press(self=<napari._qt.qt_viewer.QtViewer object>, event=<MouseEvent blocked=False button=1 buttons=[] de...urces=[] time=1638561113.539664 type=mouse_press>)
    894             The vispy event that triggered this method.
    895         """
--> 896         self._process_mouse_event(mouse_press_callbacks, event)
        self._process_mouse_event = <bound method QtViewer._process_mouse_event of <napari._qt.qt_viewer.QtViewer object at 0x7fd470ad8790>>
        global mouse_press_callbacks = <function mouse_press_callbacks at 0x7fd471583d30>
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x7fd4257fd040> pos=[499 263] press_event=None source=None sources=[] time=1638561113.539664 type=mouse_press>
    897 
    898     def on_mouse_move(self, event):

~/mambaforge/envs/micro/lib/python3.9/site-packages/napari/_qt/qt_viewer.py in _process_mouse_event(self=<napari._qt.qt_viewer.QtViewer object>, mouse_callbacks=<function mouse_press_callbacks>, event=<ReadOnlyWrapper at 0x7fd4249c5bc0 for MouseEvent>)
    853         layer = self.viewer.layers.selection.active
    854         if layer is not None:
--> 855             mouse_callbacks(layer, event)
        mouse_callbacks = <function mouse_press_callbacks at 0x7fd471583d30>
        layer = <Labels layer 'arr' at 0x7fd4249aa1f0>
        event = <ReadOnlyWrapper at 0x7fd4249c5bc0 for MouseEvent at 0x7fd4249b7850>
    856 
    857     def on_mouse_wheel(self, event):

~/mambaforge/envs/micro/lib/python3.9/site-packages/napari/utils/interactions.py in mouse_press_callbacks(obj=<Labels layer 'arr'>, event=<ReadOnlyWrapper at 0x7fd4249c5bc0 for MouseEvent>)
    146         if inspect.isgenerator(gen):
    147             try:
--> 148                 next(gen)
        global next = undefined
        gen = <generator object draw at 0x7fd424a0d200>
    149                 # now store iterated genenerator
    150                 obj._mouse_drag_gen[mouse_drag_func] = gen

~/mambaforge/envs/micro/lib/python3.9/site-packages/napari/layers/labels/_labels_mouse_bindings.py in draw(layer=<Labels layer 'arr'>, event=<ReadOnlyWrapper at 0x7fd4249c5bc0 for MouseEvent>)
     40             layer.paint(coordinates, new_label)
     41         elif layer._mode == Mode.FILL:
---> 42             layer.fill(coordinates, new_label)
        layer.fill = <bound method Labels.fill of <Labels layer 'arr' at 0x7fd4249aa1f0>>
        coordinates = (0.0, 179.95039820946852, 71.67612463520015)
        new_label = 1
     43     else:  # still add an item to undo queue
     44         # when dragging, if we start a drag outside the layer, we will

~/mambaforge/envs/micro/lib/python3.9/site-packages/napari/layers/labels/labels.py in fill(self=<Labels layer 'arr'>, coord=(0.0, 179.95039820946852, 71.67612463520015), new_label=1, refresh=True)
   1116             (
   1117                 match_indices,
-> 1118                 np.array(self.data[match_indices], copy=True),
        global np.array = <built-in function array>
        self.data = <xarray.DataArray (dim_0: 50, dim_1: 512, dim_2: 512)>
<class 'numpy.ndarray'> (50, 512, 512) uint16
Dimensions without coordinates: dim_0, dim_1, dim_2
        match_indices = (<class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64)
        global copy = undefined
   1119                 new_label,
   1120             )

~/mambaforge/envs/micro/lib/python3.9/site-packages/xarray/core/dataarray.py in __getitem__(self=<xarray.DataArray (dim_0: 50, dim_1: 512, dim_2:...mensions without coordinates: dim_0, dim_1, dim_2, key=(<class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64))
    743         else:
    744             # xarray-style array indexing
--> 745             return self.isel(indexers=self._item_key_to_dict(key))
        self.isel = <bound method DataArray.isel of <xarray.DataArray (dim_0: 50, dim_1: 512, dim_2: 512)>
<class 'numpy.ndarray'> (50, 512, 512) uint16
Dimensions without coordinates: dim_0, dim_1, dim_2>
        global indexers = undefined
        self._item_key_to_dict = <bound method DataArray._item_key_to_dict of <xarray.DataArray (dim_0: 50, dim_1: 512, dim_2: 512)>
<class 'numpy.ndarray'> (50, 512, 512) uint16
Dimensions without coordinates: dim_0, dim_1, dim_2>
        key = (<class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64)
    746 
    747     def __setitem__(self, key: Any, value: Any) -> None:

~/mambaforge/envs/micro/lib/python3.9/site-packages/xarray/core/dataarray.py in isel(self=<xarray.DataArray (dim_0: 50, dim_1: 512, dim_2:...mensions without coordinates: dim_0, dim_1, dim_2, indexers={'dim_0': <class 'numpy.ndarray'> (262144,) int64, 'dim_1': <class 'numpy.ndarray'> (262144,) int64, 'dim_2': <class 'numpy.ndarray'> (262144,) int64}, drop=False, missing_dims='raise', **indexers_kwargs={})
   1206         # lists, or zero or one-dimensional np.ndarray's
   1207 
-> 1208         variable = self._variable.isel(indexers, missing_dims=missing_dims)
        variable = undefined
        self._variable.isel = <bound method Variable.isel of <xarray.Variable (dim_0: 50, dim_1: 512, dim_2: 512)>
<class 'numpy.ndarray'> (50, 512, 512) uint16>
        indexers = {'dim_0': <class 'numpy.ndarray'> (262144,) int64, 'dim_1': <class 'numpy.ndarray'> (262144,) int64, 'dim_2': <class 'numpy.ndarray'> (262144,) int64}
        missing_dims = 'raise'
   1209 
   1210         coords = {}

~/mambaforge/envs/micro/lib/python3.9/site-packages/xarray/core/variable.py in isel(self=<xarray.Variable (dim_0: 50, dim_1: 512, dim_2: 512)>
<class 'numpy.ndarray'> (50, 512, 512) uint16, indexers={'dim_0': <class 'numpy.ndarray'> (262144,) int64, 'dim_1': <class 'numpy.ndarray'> (262144,) int64, 'dim_2': <class 'numpy.ndarray'> (262144,) int64}, missing_dims='raise', **indexers_kwargs={})
   1191 
   1192         key = tuple(indexers.get(dim, slice(None)) for dim in self.dims)
-> 1193         return self[key]
        self = <xarray.Variable (dim_0: 50, dim_1: 512, dim_2: 512)>
<class 'numpy.ndarray'> (50, 512, 512) uint16
        key = (<class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64)
   1194 
   1195     def squeeze(self, dim=None):

~/mambaforge/envs/micro/lib/python3.9/site-packages/xarray/core/variable.py in __getitem__(self=<xarray.Variable (dim_0: 50, dim_1: 512, dim_2: 512)>
<class 'numpy.ndarray'> (50, 512, 512) uint16, key=(<class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64))
    784         """
    785         dims, indexer, new_order = self._broadcast_indexes(key)
--> 786         data = as_indexable(self._data)[indexer]
        data = undefined
        global as_indexable = <function as_indexable at 0x7fd431b00550>
        self._data = <class 'numpy.ndarray'> (50, 512, 512) uint16
        indexer = OuterIndexer((<class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64, <class 'numpy.ndarray'> (262144,) int64))
    787         if new_order:
    788             data = np.moveaxis(data, range(len(new_order)), new_order)

~/mambaforge/envs/micro/lib/python3.9/site-packages/xarray/core/indexing.py in __getitem__(self=NumpyIndexingAdapter(array=<class 'numpy.ndarray'> (50, 512, 512) uint16), key=(<class 'numpy.ndarray'> (262144, 1, 1) int64, <class 'numpy.ndarray'> (1, 262144, 1) int64, <class 'numpy.ndarray'> (1, 1, 262144) int64))
   1158     def __getitem__(self, key):
   1159         array, key = self._indexing_array_and_key(key)
-> 1160         return array[key]
        array = <class 'numpy.ndarray'> (50, 512, 512) uint16
        key = (<class 'numpy.ndarray'> (262144, 1, 1) int64, <class 'numpy.ndarray'> (1, 262144, 1) int64, <class 'numpy.ndarray'> (1, 1, 262144) int64)
   1161 
   1162     def __setitem__(self, key, value):

MemoryError: Unable to allocate 32.0 PiB for an array with shape (262144, 262144, 262144) and data type uint16
```
The cause of the last reported error is due to how xarray handles indices of type `tuple[ndarray, ndarray, ndarray]` differently to numpy (orthogonal vs. vectorized). See the similar napari bug #2190 or the [xarray docs for more details](https://numpy.org/doc/stable/reference/generated/numpy.ix_.html).

The obvious/simple fix here is to use the same approach as in #2191 - special case the vectorized indices when data is an `xarray.DataArray`. Using it for `paint` but not for `fill` seems inconsistent, so my suggestion is to do that for now, unless there's a good reason not to. As reviewers of that PR, @sofroniewn, @jni : any thoughts on this?

I guess the dream was to support a more general fix like in #2376, but that got closed, so we may also want to open an issue to track that kind of approach.


Resolving patch:
diff --git a/napari/layers/labels/labels.py b/napari/layers/labels/labels.py
--- a/napari/layers/labels/labels.py
+++ b/napari/layers/labels/labels.py
@@ -6,6 +6,8 @@
 import pandas as pd
 from scipy import ndimage as ndi
 
+from napari.utils.misc import _is_array_type
+
 from ...utils import config
 from ...utils._dtype import normalize_dtype
 from ...utils.colormaps import (
@@ -1196,10 +1198,13 @@ def fill(self, coord, new_label, refresh=True):
                     j += 1
                 else:
                     match_indices.append(np.full(n_idx, d, dtype=np.intp))
-            match_indices = tuple(match_indices)
         else:
             match_indices = match_indices_local
 
+        match_indices = _coerce_indices_for_vectorization(
+            self.data, match_indices
+        )
+
         self._save_history(
             (
                 match_indices,
@@ -1268,18 +1273,7 @@ def paint(self, coord, new_label, refresh=True):
         else:
             slice_coord = slice_coord_temp
 
-        slice_coord = tuple(slice_coord)
-
-        # Fix indexing for xarray if necessary
-        # See http://xarray.pydata.org/en/stable/indexing.html#vectorized-indexing
-        # for difference from indexing numpy
-        try:
-            import xarray as xr
-
-            if isinstance(self.data, xr.DataArray):
-                slice_coord = tuple(xr.DataArray(i) for i in slice_coord)
-        except ImportError:
-            pass
+        slice_coord = _coerce_indices_for_vectorization(self.data, slice_coord)
 
         # slice coord is a tuple of coordinate arrays per dimension
         # subset it if we want to only paint into background/only erase
@@ -1435,3 +1429,18 @@ def _get_properties(
 
     class Labels(Labels, _OctreeImageBase):
         pass
+
+
+def _coerce_indices_for_vectorization(array, indices: list) -> tuple:
+    """Coerces indices so that they can be used for vectorized indexing in the given data array."""
+    if _is_array_type(array, 'xarray.DataArray'):
+        # Fix indexing for xarray if necessary
+        # See http://xarray.pydata.org/en/stable/indexing.html#vectorized-indexing
+        # for difference from indexing numpy
+        try:
+            import xarray as xr
+
+            return tuple(xr.DataArray(i) for i in indices)
+        except ImportError:
+            pass
+    return tuple(indices)
diff --git a/napari/utils/misc.py b/napari/utils/misc.py
--- a/napari/utils/misc.py
+++ b/napari/utils/misc.py
@@ -18,6 +18,7 @@
     Any,
     Callable,
     Iterable,
+    Iterator,
     Optional,
     Type,
     TypeVar,
@@ -465,6 +466,13 @@ def _quiet_array_equal(*a, **k):
         return np.array_equal(*a, **k)
 
 
+def _arraylike_short_names(obj) -> Iterator[str]:
+    """Yield all the short names of an array-like or its class."""
+    type_ = type(obj) if not inspect.isclass(obj) else obj
+    for base in type_.mro():
+        yield f'{base.__module__.split(".", maxsplit=1)[0]}.{base.__name__}'
+
+
 def pick_equality_operator(obj) -> Callable[[Any, Any], bool]:
     """Return a function that can check equality between ``obj`` and another.
 
@@ -488,8 +496,6 @@ def pick_equality_operator(obj) -> Callable[[Any, Any], bool]:
     """
     import operator
 
-    type_ = type(obj) if not inspect.isclass(obj) else obj
-
     # yes, it's a little riskier, but we are checking namespaces instead of
     # actual `issubclass` here to avoid slow import times
     _known_arrays = {
@@ -499,15 +505,35 @@ def pick_equality_operator(obj) -> Callable[[Any, Any], bool]:
         'zarr.Array': operator.is_,  # zarr.core.Array
         'xarray.DataArray': _quiet_array_equal,  # xarray.core.dataarray.DataArray
     }
-    for base in type_.mro():
-        key = f'{base.__module__.split(".", maxsplit=1)[0]}.{base.__name__}'
-        func = _known_arrays.get(key)
+
+    for name in _arraylike_short_names(obj):
+        func = _known_arrays.get(name)
         if func:
             return func
 
     return operator.eq
 
 
+def _is_array_type(array, type_name: str) -> bool:
+    """Checks if an array-like instance or class is of the type described by a short name.
+
+    This is useful when you want to check the type of array-like quickly without
+    importing its package, which might take a long time.
+
+    Parameters
+    ----------
+    array
+        The array-like object.
+    type_name : str
+        The short name of the type to test against (e.g. 'numpy.ndarray', 'xarray.DataArray').
+
+    Returns
+    -------
+    True if the array is associated with the type name.
+    """
+    return type_name in _arraylike_short_names(array)
+
+
 def dir_hash(
     path: Union[str, Path], include_paths=True, ignore_hidden=True
 ) -> str:


Test patch:
diff --git a/napari/layers/labels/_tests/test_labels.py b/napari/layers/labels/_tests/test_labels.py
--- a/napari/layers/labels/_tests/test_labels.py
+++ b/napari/layers/labels/_tests/test_labels.py
@@ -986,6 +986,22 @@ def test_fill_tensorstore():
         np.testing.assert_array_equal(modified_labels, np.asarray(data))
 
 
+def test_fill_with_xarray():
+    """See https://github.com/napari/napari/issues/2374"""
+    data = xr.DataArray(np.zeros((5, 4, 4), dtype=int))
+    layer = Labels(data)
+
+    layer.fill((0, 2, 2), 1)
+
+    np.testing.assert_array_equal(layer.data[0, :, :], np.ones((4, 4)))
+    np.testing.assert_array_equal(layer.data[1:, :, :], np.zeros((4, 4, 4)))
+    # In the associated issue, using xarray.DataArray caused memory allocation
+    # problems due to different read indexing rules, so check that the data
+    # saved for undo has the expected vectorized shape and values.
+    undo_data = layer._undo_history[0][0][1]
+    np.testing.assert_array_equal(undo_data, np.zeros((16,)))
+
+
 @pytest.mark.parametrize(
     'scale', list(itertools.product([-2, 2], [-0.5, 0.5], [-0.5, 0.5]))
 )
diff --git a/napari/utils/_tests/test_misc.py b/napari/utils/_tests/test_misc.py
--- a/napari/utils/_tests/test_misc.py
+++ b/napari/utils/_tests/test_misc.py
@@ -6,6 +6,7 @@
 
 from napari.utils.misc import (
     StringEnum,
+    _is_array_type,
     _quiet_array_equal,
     abspath_or_url,
     ensure_iterable,
@@ -197,3 +198,16 @@ class MyNPArray(np.ndarray):
     eq = pick_equality_operator(np.asarray([]))
     # make sure this doesn't warn
     assert not eq(np.asarray([]), np.asarray([], '<U32'))
+
+
+def test_is_array_type_with_xarray():
+    import numpy as np
+    import xarray as xr
+
+    assert _is_array_type(xr.DataArray(), 'xarray.DataArray')
+    assert not _is_array_type(xr.DataArray(), 'xr.DataArray')
+    assert not _is_array_type(
+        xr.DataArray(), 'xarray.core.dataarray.DataArray'
+    )
+    assert not _is_array_type([], 'xarray.DataArray')
+    assert not _is_array_type(np.array([]), 'xarray.DataArray')