Repository: dask/dask

Issue:
`DataFrame.set_index` with empty `divisions` does not raise informative error
This may seem like a rather silly thing to validate, but it's easy to accidentally pass in empty divisions if you're using a divisions list generated elsewhere, or mess up typing `range(1000, 2)` instead of `range(0, 1000, 2)` like I did.

Would be nice to have a short, informative traceback in this case instead of this inscrutable error.

```python
result.set_index("id", divisions=[])
```

```python-traceback
File ~/Library/Caches/pypoetry/virtualenvs/workshop-one-3dbJnD_1-py3.9/lib/python3.9/site-packages/dask/dataframe/core.py:4609, in DataFrame.set_index(***failed resolving arguments***)
   4606 else:
   4607     from .shuffle import set_index
-> 4609     return set_index(
   4610         self,
   4611         other,
   4612         drop=drop,
   4613         npartitions=npartitions,
   4614         divisions=divisions,
   4615         **kwargs,
   4616     )

File ~/Library/Caches/pypoetry/virtualenvs/workshop-one-3dbJnD_1-py3.9/lib/python3.9/site-packages/dask/dataframe/shuffle.py:229, in set_index(df, index, npartitions, shuffle, compute, drop, upsample, divisions, partition_size, **kwargs)
    226         result = set_sorted_index(df, index, drop=drop, divisions=divisions)
    227         return result.map_partitions(M.sort_index)
--> 229 return set_partition(
    230     df, index, divisions, shuffle=shuffle, drop=drop, compute=compute, **kwargs
    231 )

File ~/Library/Caches/pypoetry/virtualenvs/workshop-one-3dbJnD_1-py3.9/lib/python3.9/site-packages/dask/dataframe/shuffle.py:324, in set_partition(df, index, divisions, max_branch, drop, shuffle, compute)
    319     partitions = index.map_partitions(
    320         set_partitions_pre, divisions=divisions, meta=meta
    321     )
    322     df2 = df.assign(_partitions=partitions, _index=index)
--> 324 df3 = rearrange_by_column(
    325     df2,
    326     "_partitions",
    327     max_branch=max_branch,
    328     npartitions=len(divisions) - 1,
    329     shuffle=shuffle,
    330     compute=compute,
    331     ignore_index=True,
    332 )
    334 if np.isscalar(index):
    335     df4 = df3.map_partitions(
    336         set_index_post_scalar,
    337         index_name=index,
    338         drop=drop,
    339         column_dtype=df.columns.dtype,
    340     )

File ~/Library/Caches/pypoetry/virtualenvs/workshop-one-3dbJnD_1-py3.9/lib/python3.9/site-packages/dask/dataframe/shuffle.py:486, in rearrange_by_column(df, col, npartitions, max_branch, shuffle, compute, ignore_index)
    481 # if the requested output partitions < input partitions
    482 # we repartition first as shuffling overhead is
    483 # proportionate to the number of input partitions
    485 if npartitions is not None and npartitions < df.npartitions:
--> 486     df = df.repartition(npartitions=npartitions)
    488 if shuffle == "disk":
    489     return rearrange_by_column_disk(df, col, npartitions, compute=compute)

File ~/Library/Caches/pypoetry/virtualenvs/workshop-one-3dbJnD_1-py3.9/lib/python3.9/site-packages/dask/dataframe/core.py:1319, in _Frame.repartition(self, divisions, npartitions, partition_size, freq, force)
   1317     return repartition_size(self, partition_size)
   1318 elif npartitions is not None:
-> 1319     return repartition_npartitions(self, npartitions)
   1320 elif divisions is not None:
   1321     return repartition(self, divisions, force=force)

File ~/Library/Caches/pypoetry/virtualenvs/workshop-one-3dbJnD_1-py3.9/lib/python3.9/site-packages/dask/dataframe/core.py:6975, in repartition_npartitions(df, npartitions)
   6970     npartitions_ratio = df.npartitions / npartitions
   6971     new_partitions_boundaries = [
   6972         int(new_partition_index * npartitions_ratio)
   6973         for new_partition_index in range(npartitions + 1)
   6974     ]
-> 6975     return _repartition_from_boundaries(df, new_partitions_boundaries, new_name)
   6976 else:
   6977     original_divisions = divisions = pd.Series(df.divisions)

File ~/Library/Caches/pypoetry/virtualenvs/workshop-one-3dbJnD_1-py3.9/lib/python3.9/site-packages/dask/dataframe/core.py:7019, in _repartition_from_boundaries(df, new_partitions_boundaries, new_name)
   7017 if not isinstance(new_partitions_boundaries, list):
   7018     new_partitions_boundaries = list(new_partitions_boundaries)
-> 7019 if new_partitions_boundaries[0] > 0:
   7020     new_partitions_boundaries.insert(0, 0)
   7021 if new_partitions_boundaries[-1] < df.npartitions:

IndexError: list index out of range
```


Discussion:

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -6715,6 +6715,8 @@ def check_divisions(divisions):
     if not isinstance(divisions, (list, tuple)):
         raise ValueError("New division must be list or tuple")
     divisions = list(divisions)
+    if len(divisions) == 0:
+        raise ValueError("New division must not be empty")
     if divisions != sorted(divisions):
         raise ValueError("New division must be sorted")
     if len(divisions[:-1]) != len(list(unique(divisions[:-1]))):


Test patch:
diff --git a/dask/dataframe/tests/test_shuffle.py b/dask/dataframe/tests/test_shuffle.py
--- a/dask/dataframe/tests/test_shuffle.py
+++ b/dask/dataframe/tests/test_shuffle.py
@@ -388,6 +388,16 @@ def throw(*args, **kwargs):
         ddf.set_index("x", divisions=[3, 1, 5])
 
 
+def test_set_index_with_empty_divisions():
+    df = pd.DataFrame({"x": [1, 2, 3, 4]})
+
+    ddf = dd.from_pandas(df, npartitions=2)
+
+    # Divisions must not be empty
+    with pytest.raises(ValueError):
+        ddf.set_index("x", divisions=[])
+
+
 def test_set_index_divisions_2():
     df = pd.DataFrame({"x": [1, 2, 3, 4, 5, 6], "y": list("abdabd")})
     ddf = dd.from_pandas(df, 2)