Repository: dask/dask

Issue:
`da.Array.__setitem__` blindly assumes that the chunks are writeable
If one calls `da.Array.__getitem__(idx).__setitem__(())` where idx selects a scalar, all seems fine but crashes on `compute()`.
The object returned by `__getitem__` is another da.Array, which is writeable, but internally the chunk contains a `np.generic`, which is not.
Moving on to `__setitem__`, dask blindly assumes that its chunks are always `np.ndarray` objects:

```python
>>> import dask.array as da
>>> x = da.zeros(())
>>> x[()] = 1
>>> x.compute()
array(1.)

>>> y = da.zeros(1)
>>> z = y[0]  # a writeable da.Array, but the chunk is a read-only np.generic
>>> z[()] = 1  # No failure here
>>> z.compute()  # TypeError: 'numpy.float64' object does not support item assignment
```

In the above snippet, x and z are outwardly identical, but z's graph is corrupted.
The same code works fine when the meta is a cupy array, because `__getitem__` results in chunks which are 0-dimensional, writeable `cp.Array` objects.

Discussion hints:
github-actions[bot]: ## Unit Test Results
_See [test report](https://dask.github.io/dask/test_report.html) for an extended history of previous test failures. This is useful for diagnosing flaky tests._

     15 files  ± 0       15 suites  ±0   4h 29m 31s ⏱️ + 6m 14s
 17 188 tests + 1   15 995 ✅ +1   1 193 💤 ±0  0 ❌ ±0 
211 787 runs  +13  194 529 ✅ +9  17 258 💤 +4  0 ❌ ±0 

Results for commit 969513a0. ± Comparison against base commit c5524337.

[test-results]:data:application/gzip;base64,H4sIAAkMpWcC/03MSQ6DMAyF4augrLvAmd3LVGkGKSpDFZIV6t0bEASW/2f5rSTEwS/k2YF4dGQpMV/lSjI5ztPWEhRUqte83xVoffZrKdbuT4jiwk/8bgjImgUTh2p9A5/SnA5JZdqmKYDS6ug2jVxQbHhMKyr0abfpve/Ldh7HmGsQlCiAmZ4zSYFrZEq+UXqKgTtDneeGogu2J78/817OARoBAAA=


Solution patch:
diff --git a/dask/array/slicing.py b/dask/array/slicing.py
index 473c42646..aebbfbf0f 100644
--- a/dask/array/slicing.py
+++ b/dask/array/slicing.py
@@ -2020,9 +2020,9 @@ def setitem(x, v, indices):
 
     Parameters
     ----------
-    x : numpy array
+    x : numpy/cupy/etc. array
         The array to be assigned to.
-    v : numpy array
+    v : numpy/cupy/etc. array
         The values which will be assigned.
     indices : list of `slice`, `int`, or numpy array
         The indices describing the elements of x to be assigned from
@@ -2038,7 +2038,7 @@ def setitem(x, v, indices):
 
     Returns
     -------
-    numpy array
+    numpy/cupy/etc. array
         A new independent array with assigned elements, unless v is
         empty (i.e. has zero size) in which case then the input array
         is returned and the indices are ignored.
@@ -2086,8 +2086,11 @@ def setitem(x, v, indices):
     if not np.ma.isMA(x) and np.ma.isMA(v):
         x = x.view(np.ma.MaskedArray)
 
-    # Copy the array to guarantee no other objects are corrupted
-    x = x.copy()
+    # Copy the array to guarantee no other objects are corrupted.
+    # When x is the output of a scalar __getitem__ call, it is a
+    # np.generic, which is read-only. Convert it to a (writeable)
+    # 0-d array. x could also be a cupy array etc.
+    x = np.asarray(x) if isinstance(x, np.generic) else x.copy()
 
     # Do the assignment
     try:


Test patch:
diff --git a/dask/array/tests/test_array_core.py b/dask/array/tests/test_array_core.py
index f6873bf41..b136b5302 100644
--- a/dask/array/tests/test_array_core.py
+++ b/dask/array/tests/test_array_core.py
@@ -5771,3 +5771,17 @@ def test_load_store_chunk():
     )
     expected = np.array([])
     assert all(actual == expected)
+
+
+def test_scalar_setitem():
+    """After a da.Array.__getitem__ call that returns a scalar, the chunk contains a
+    read-only np.generic instead of a writeable np.ndarray. This is a specific quirk of
+    numpy; cupy and other backends always return a 0-dimensional array.
+    Make sure that __setitem__ still works.
+    """
+    x = da.zeros(1)
+    y = x[0]
+    assert isinstance(y.compute(), np.generic)
+    y[()] = 2
+    assert_eq(y, 2.0)
+    assert isinstance(y.compute(), np.ndarray)