Repository: dask/dask

Issue:
Default chunks=None creates unusable array
When using `dask[array]`, most commands to create array objects claim to _require_ a `chunks=` kwarg in the docs. However, a dask array object is successfully created with the default `chunks=None`. Then it fails when `.compute()` is called, with a less than informative traceback.

Minimal example:

``` python
import dask.array as da

has_chunks = da.random.random(size=(100, 100), chunks=(100, 100))
no_chunks = da.random.random(size=(100, 100))  # chunks left at default (None), succeeds

has_chunks.mean(axis=0).compute()  # succeeds
no_chunks.mean(axis=0).compute()  # fails - traceback not particularly informative (see below)
```

Unless some future functionality is intended to automatically set `chunks` if `chunks=None`, I'd propose this workflow should fail earlier at the point `chunks` is not provided to be consistent with the docs. Alternatively, the error when `.compute()` is called on an array with `chunks=None` could be more informative.

Traceback:

``` python
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-22-f80a4f9b0fcd> in <module>()
      1 nparr = np.random.random(size=(100, 100))
      2 darr = da.ones((100, 100))
----> 3 darr.mean(axis=0).compute()

/src/anaconda/lib/python2.7/site-packages/dask/array/core.pyc in compute(self, **kwargs)
    721     @wraps(compute)
    722     def compute(self, **kwargs):
--> 723         result, = compute(self, **kwargs)
    724         return result
    725 

/src/anaconda/lib/python2.7/site-packages/dask/array/core.pyc in compute(*args, **kwargs)
    514 
    515     results2 = tuple(concatenate3(x) if arg.shape else unpack_singleton(x)
--> 516                      for x, arg in zip(results, args))
    517     return results2
    518 

/src/anaconda/lib/python2.7/site-packages/dask/array/core.pyc in <genexpr>((x, arg))
    514 
    515     results2 = tuple(concatenate3(x) if arg.shape else unpack_singleton(x)
--> 516                      for x, arg in zip(results, args))
    517     return results2
    518 

/src/anaconda/lib/python2.7/site-packages/dask/array/core.pyc in concatenate3(arrays)
   2035     """
   2036     arrays = concrete(arrays)
-> 2037     ndim = ndimlist(arrays)
   2038     if not ndim:
   2039         return arrays

/src/anaconda/lib/python2.7/site-packages/dask/array/core.pyc in ndimlist(seq)
   2014         return 0
   2015     else:
-> 2016         return 1 + ndimlist(seq[0])
   2017 
   2018 

IndexError: list index out of range
```



Discussion:
Oh yes, indeed.  I can see how that would trip up almost any incoming user.

This _definitely_ didn't just happen to me...

On it.


Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -442,7 +442,7 @@ def map_blocks(func, *arrs, **kwargs):
         chunks = tuple([nb * (bs,)
                         for nb, bs in zip(result.numblocks, chunks)])
     if chunks is not None:
-        result.chunks = chunks
+        result._chunks = chunks
 
     return result
 
@@ -605,12 +605,14 @@ class Array(object):
         block sizes along each dimension
     """
 
-    __slots__ = 'dask', 'name', 'chunks', '_dtype'
+    __slots__ = 'dask', 'name', '_chunks', '_dtype'
 
     def __init__(self, dask, name, chunks, dtype=None, shape=None):
         self.dask = dask
         self.name = name
-        self.chunks = normalize_chunks(chunks, shape)
+        self._chunks = normalize_chunks(chunks, shape)
+        if self._chunks is None:
+            raise ValueError(chunks_none_error_message)
         if dtype is not None:
             dtype = np.dtype(dtype)
         self._dtype = dtype
@@ -627,6 +629,16 @@ def numblocks(self):
     def shape(self):
         return tuple(map(sum, self.chunks))
 
+    def _get_chunks(self):
+        return self._chunks
+
+    def _set_chunks(self, chunks):
+        raise TypeError("Can not set chunks directly\n\n"
+            "Please use the rechunk method instead:\n"
+            "  x.rechunk(%s)" % str(chunks))
+
+    chunks = property(_get_chunks, _set_chunks, "chunks property")
+
     def __len__(self):
         return sum(self.chunks[0])
 
@@ -1076,15 +1088,14 @@ def normalize_chunks(chunks, shape=None):
     >>> normalize_chunks((), shape=(0, 0))  #  respects null dimensions
     ((), ())
     """
+    if chunks is None:
+        raise ValueError(chunks_none_error_message)
     if isinstance(chunks, list):
         chunks = tuple(chunks)
     if isinstance(chunks, Number):
         chunks = (chunks,) * len(shape)
-    if not chunks:
-        if shape is None:
-            chunks = ()
-        else:
-            chunks = ((),) * len(shape)
+    if not chunks and shape and all(s == 0 for s in shape):
+        chunks = ((),) * len(shape)
 
     if shape is not None:
         chunks = tuple(c if c is not None else s for c, s in zip(chunks, shape))
@@ -1752,6 +1763,13 @@ def choose(a, choices):
 """.strip()
 
 
+chunks_none_error_message = """
+You must specify a chunks= keyword argument.
+This specifies the chunksize of your array blocks.
+
+See the following documentation page for details:
+  http://dask.pydata.org/en/latest/array-creation.html#chunks
+""".strip()
 
 @wraps(np.where)
 def where(condition, x=None, y=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
@@ -1082,3 +1082,23 @@ def test_take_dask_from_numpy():
 
 def test_normalize_chunks():
     assert normalize_chunks(3, (4, 6)) == ((3, 1), (3, 3))
+
+
+def test_raise_on_no_chunks():
+    x = da.ones(6, chunks=3)
+    try:
+        Array(x.dask, x.name, chunks=None, dtype=x.dtype, shape=None)
+        assert False
+    except ValueError as e:
+        assert "dask.pydata.org" in str(e)
+
+    assert raises(ValueError, lambda: da.ones(6))
+
+
+def test_chunks_is_immutable():
+    x = da.ones(6, chunks=3)
+    try:
+        x.chunks = 2
+        assert False
+    except TypeError as e:
+        assert 'rechunk(2)' in str(e)