Repository: dask/dask
Pull request #11947 (2025-05-14 15:08:38)

Fix xarray slicing regression
Turns out removing this operation is not entirely safe. I wasn't truly able to understand why it is unsafe. The dask case is generating a corrupt graph while the xarray case raises a ` ValueError: dimensions` exception with this reproducer

closes https://github.com/pydata/xarray/issues/10321

This reverts a performance optimization introduced to address https://github.com/dask/dask/issues/11735

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._

      9 files  ± 0        9 suites  ±0   3h 22m 57s ⏱️ + 5m 35s
 18 034 tests + 2   16 817 ✅ + 1   1 216 💤 ±0  1 ❌ +1 
161 380 runs  +18  149 238 ✅ +12  12 133 💤  - 3  9 ❌ +9 

For more details on these failures, see [this check](https://github.com/dask/dask/runs/42224531294).

Results for commit f666a567. ± Comparison against base commit abdb435d.

[test-results]:data:application/gzip;base64,H4sIAEW8JGgC/03MTQ7CIBCG4as0rF1ApwzgZQwtkBD7YyisGu/uVAVdvs9kvoOFOPudXTtz6dheYm7hSrI5biul6IVSRHTM51loDkPt216m6UTUQv3wHh+fT2wWbJxPa+BT2hIJJ0llfU+jAF27TQ+mB92wTQNU+06b2v/L07YsMVOwgIhWopLgFY49SA7W+hGcdwbdAMrQCYxkzxdqJ89zGAEAAA==


Patch:
diff --git a/dask/_task_spec.py b/dask/_task_spec.py
index 9fd4f75a7..f751439d3 100644
--- a/dask/_task_spec.py
+++ b/dask/_task_spec.py
@@ -143,17 +143,13 @@ def parse_input(obj: Any) -> object:
         if any(isinstance(v, GraphNode) for v in parsed_dict.values()):
             return Dict(parsed_dict)
 
-    if isinstance(obj, (list, set, tuple)):
+    if isinstance(obj, (list, set)):
         parsed_collection = tuple(parse_input(o) for o in obj)
         if any(isinstance(o, GraphNode) for o in parsed_collection):
             if isinstance(obj, list):
                 return List(*parsed_collection)
             if isinstance(obj, set):
                 return Set(*parsed_collection)
-            if isinstance(obj, tuple):
-                if is_namedtuple_instance(obj):
-                    return _wrap_namedtuple_task(None, obj, parse_input)
-                return Tuple(*parsed_collection)
 
     return obj
 
diff --git a/dask/array/slicing.py b/dask/array/slicing.py
index 76419dc17..c08d9f359 100644
--- a/dask/array/slicing.py
+++ b/dask/array/slicing.py
@@ -590,7 +590,14 @@ def take(outname, inname, chunks, index, axis=0):
 
         arange = arange_safe(np.sum(chunks[axis]), like=index)
         if len(index) == len(arange) and np.abs(index - arange).sum() == 0:
-            raise SlicingNoop()
+            # TODO: This should be a real no-op, but the call stack is
+            # too deep to do this efficiently for now
+            chunk_tuples = product(*(range(len(c)) for i, c in enumerate(chunks)))
+            graph = {
+                (outname,) + c: Alias((outname,) + c, (inname,) + c)
+                for c in chunk_tuples
+            }
+            return tuple(chunks), graph
 
         average_chunk_size = int(sum(chunks[axis]) / len(chunks[axis]))
 


Test patch:
diff --git a/dask/array/tests/test_slicing.py b/dask/array/tests/test_slicing.py
index daec4c1fd..b325830d7 100644
--- a/dask/array/tests/test_slicing.py
+++ b/dask/array/tests/test_slicing.py
@@ -259,6 +259,13 @@ def test_slice_array_2d():
     assert expected == result
 
 
+def test_mixed_index():
+    da_array = da.ones((1, 1, 31, 40))
+    new = da_array[(np.array([0]), 0, slice(None), slice(None))]
+    assert isinstance(new, da.Array)
+    assert_eq(new, da_array[0])
+
+
 def test_slice_optimizations():
     # bar[:]
     with pytest.raises(SlicingNoop):
@@ -327,8 +334,9 @@ def test_take_sorted():
     chunks, dsk = take("y-y", "x", [(20, 20, 20, 20)], [1, 3, 5, 47], axis=0)
     assert len(dsk) == 6
     assert chunks == ((4,),)
-    with pytest.raises(SlicingNoop):
-        take("y", "x", [(20, 20, 20, 20)], np.arange(0, 80), axis=0)
+    chunks, dsk = take("y", "x", [(20, 20, 20, 20)], np.arange(0, 80), axis=0)
+    assert len(dsk) == 4
+    assert chunks == ((20, 20, 20, 20),)
 
 
 def test_slicing_chunks():
diff --git a/dask/array/tests/test_xarray.py b/dask/array/tests/test_xarray.py
index f35e1bd42..e3c0d6f36 100644
--- a/dask/array/tests/test_xarray.py
+++ b/dask/array/tests/test_xarray.py
@@ -154,3 +154,13 @@ def test_shared_tasks(wrap_xarray):
     else:
         assert isinstance(comp_res[0], np.ndarray)
     assert total_calls == in1.blocks.size
+
+
+def test_slicing():
+    ds = xr.Dataset(
+        {"z": (("t", "p", "y", "x"), np.ones((1, 1, 31, 40)))},
+    )
+    ds = ds.chunk()
+    subset = ds.isel(t=[0], p=0).z[:, ::10, ::10][:, ::-1, :]
+    subset2 = ds.isel(t=[0]).isel(p=0).z[:, ::10, ::10][:, ::-1, :]
+    assert_eq(subset.data, subset2.data)
diff --git a/dask/tests/test_task_spec.py b/dask/tests/test_task_spec.py
index 4986c3efb..36074e98e 100644
--- a/dask/tests/test_task_spec.py
+++ b/dask/tests/test_task_spec.py
@@ -499,32 +499,6 @@ def test_parse_graph_namedtuple_legacy(typ, args, kwargs):
     assert new_dsk["foo"]() == typ(*args, **kwargs)
 
 
-@pytest.mark.parametrize(
-    "typ, args, kwargs",
-    [
-        (PlainNamedTuple, ["some-data"], {}),
-        (NewArgsNamedTuple, ["some", "data", "more"], {}),
-        (NewArgsExNamedTuple, ["some", "data", "more"], {"another": "data"}),
-    ],
-)
-def test_parse_namedtuple(typ, args, kwargs):
-    def func(x):
-        return x
-
-    obj = typ(*args, **kwargs)
-    t = Task("foo", func, parse_input(obj))
-
-    assert t() == obj
-
-    # The other test tuple do weird things to their input
-    if typ is PlainNamedTuple:
-        args = tuple([TaskRef("b")] + list(args)[1:])
-        obj = typ(*args, **kwargs)
-        t = Task("foo", func, parse_input(obj))
-        assert t.dependencies == {"b"}
-        assert t({"b": "foo"}) == typ(*tuple(["foo"] + list(args)[1:]), **kwargs)
-
-
 def test_pickle_literals():
     np = pytest.importorskip("numpy")
     obj = DataNode("foo", np.transpose)