Repository: dask/dask

Issue:
reshape incorrectly handles floats
Related to #758,

The following code behaves as expected:

```
import dask.array as da
import numpy as np

data = np.arange(int(1e6))
dask_array = da.from_array(data, chunks=(100, ))

window_size = float(100.1)
number_of_windows = 10

sliced = dask_array[:window_size*number_of_windows].reshape((window_size, number_of_windows)).compute()

#####

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-29-ae89621fb406> in <module>()
      8 number_of_windows = 10
      9 
---> 10 sliced = dask_array[:window_size*number_of_windows].reshape((window_size, number_of_windows)).compute()

/Users/jeremy/anaconda/lib/python3.5/site-packages/dask/array/core.py in reshape(self, *shape)
    888         if len(shape) == 1 and not isinstance(shape[0], Number):
    889             shape = shape[0]
--> 890         return reshape(self, shape)
    891 
    892     @wraps(topk)

/Users/jeremy/anaconda/lib/python3.5/site-packages/dask/array/core.py in reshape(array, shape)
   2222     if np.prod(shape) != array.size:
   2223         raise ValueError('total size of new array must be unchanged')
-> 2224     return unravel(ravel(array), shape)
   2225 
   2226 

/Users/jeremy/anaconda/lib/python3.5/site-packages/dask/array/core.py in unravel(array, shape)
   2207             return stack([unravel(array[n * trailing_size
   2208                                         : (n + 1) * trailing_size], shape[1:])
-> 2209                           for n in range(shape[0])])
   2210 
   2211 

TypeError: 'float' object cannot be interpreted as an integer
```

But using -1 to infer the float dimension gives a less helpful error:

```

import dask.array as da
import numpy as np

data = np.arange(int(1e6))
dask_array = da.from_array(data, chunks=(100, ))

window_size = float(100.1)
number_of_windows = 10

sliced = dask_array[:window_size*number_of_windows].reshape((-1, number_of_windows)).compute()

######

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-28-44273dd5d7c1> in <module>()
      8 number_of_windows = 10
      9 
---> 10 sliced = dask_array[:window_size*number_of_windows].reshape((-1, number_of_windows)).compute()

/Users/jeremy/anaconda/lib/python3.5/site-packages/dask/array/core.py in reshape(self, *shape)
    888         if len(shape) == 1 and not isinstance(shape[0], Number):
    889             shape = shape[0]
--> 890         return reshape(self, shape)
    891 
    892     @wraps(topk)

/Users/jeremy/anaconda/lib/python3.5/site-packages/dask/array/core.py in reshape(array, shape)
   2221 
   2222     if np.prod(shape) != array.size:
-> 2223         raise ValueError('total size of new array must be unchanged')
   2224     return unravel(ravel(array), shape)
   2225 

ValueError: total size of new array must be unchanged
```



Discussion:

Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -2466,12 +2466,13 @@ def unravel(array, shape):
 
 @wraps(np.reshape)
 def reshape(array, shape):
-    shape = tuple(shape)
+    from .slicing import sanitize_index
+    shape = tuple(map(sanitize_index, shape))
     known_sizes = [s for s in shape if s != -1]
     if len(known_sizes) < len(shape):
         if len(known_sizes) - len(shape) > 1:
             raise ValueError('can only specify one unknown dimension')
-        missing_size = int(array.size / np.prod(known_sizes))
+        missing_size = sanitize_index(array.size / np.prod(known_sizes))
         shape = tuple(missing_size if s == -1 else s for s in shape)
 
     if np.prod(shape) != array.size:
diff --git a/dask/array/slicing.py b/dask/array/slicing.py
--- a/dask/array/slicing.py
+++ b/dask/array/slicing.py
@@ -2,6 +2,7 @@
 
 from itertools import product
 from math import ceil
+from numbers import Number
 from operator import getitem, add
 
 import numpy as np
@@ -11,27 +12,47 @@
 from ..compatibility import long
 
 
-def sanitize_index_elements(ind):
+def sanitize_index(ind):
     """ Sanitize the elements for indexing along one axis
 
-    >>> sanitize_index_elements([2, 3, 5])
+    >>> sanitize_index([2, 3, 5])
     [2, 3, 5]
-    >>> sanitize_index_elements([True, False, True, False])
+    >>> sanitize_index([True, False, True, False])
     [0, 2]
-    >>> sanitize_index_elements(np.array([1, 2, 3]))
+    >>> sanitize_index(np.array([1, 2, 3]))
     [1, 2, 3]
-    >>> sanitize_index_elements(np.array([False, True, True]))
+    >>> sanitize_index(np.array([False, True, True]))
     [1, 2]
-    >>> type(sanitize_index_elements(np.int32(0)))
+    >>> type(sanitize_index(np.int32(0)))
     <type 'int'>
+    >>> sanitize_index(1.0)
+    1
+    >>> sanitize_index(0.5)
+    Traceback (most recent call last):
+    ...
+    IndexError: Bad index.  Must be integer-like: 0.5
     """
-    if isinstance(ind, np.integer):
-        ind = int(ind)
+    if isinstance(ind, Number):
+        ind2 = int(ind)
+        if ind2 != ind:
+            raise IndexError("Bad index.  Must be integer-like: %s" % ind)
+        else:
+            return ind2
     if isinstance(ind, np.ndarray):
         ind = ind.tolist()
     if isinstance(ind, list) and ind and isinstance(ind[0], bool):
         ind = [a for a, b in enumerate(ind) if b]
-    return ind
+        return ind
+    if isinstance(ind, list):
+        return [sanitize_index(i) for i in ind]
+    if isinstance(ind, slice):
+        return slice(sanitize_index(ind.start),
+                     sanitize_index(ind.stop),
+                     sanitize_index(ind.step))
+    if ind is None:
+        return ind
+
+    raise IndexError("Invalid index", i)
 
 
 def slice_array(out_name, in_name, blockdims, index):
@@ -95,7 +116,7 @@ def slice_array(out_name, in_name, blockdims, index):
     slice_slices_and_integers - handle everything else
     """
     index = replace_ellipsis(len(blockdims), index)
-    index = tuple(map(sanitize_index_elements, index))
+    index = tuple(map(sanitize_index, index))
     blockdims = tuple(map(tuple, blockdims))
 
     # x[:, :, :] - Punt and return old value
@@ -116,15 +137,13 @@ def slice_array(out_name, in_name, blockdims, index):
     return dsk_out, bd_out
 
 
+
 def slice_with_newaxes(out_name, in_name, blockdims, index):
     """
     Handle indexing with Nones
 
     Strips out Nones then hands off to slice_wrap_lists
     """
-    assert all(isinstance(ind, (slice, int, long, list, type(None)))
-               for ind in index)
-
     # Strip Nones from index
     index2 = tuple([ind for ind in index if ind is not None])
     where_none = [i for i, ind in enumerate(index) if ind is None]


Test patch:
diff --git a/dask/array/tests/test_array_core.py b/dask/array/tests/test_array_core.py
--- a/dask/array/tests/test_array_core.py
+++ b/dask/array/tests/test_array_core.py
@@ -1559,6 +1559,18 @@ def test_point_slicing_with_full_slice():
         assert result.shape[0] == k
 
 
+def test_slice_with_floats():
+    d = da.ones((5,), chunks=(3,))
+    with pytest.raises(IndexError):
+        d[1.5]
+    with pytest.raises(IndexError):
+        d[0:1.5]
+    with pytest.raises(IndexError):
+        d[[1, 1.5]]
+
+
+
+
 def test_vindex_errors():
     d = da.ones((5, 5, 5), chunks=(3, 3, 3))
     assert raises(IndexError, lambda: d.vindex[0])