Repository: xgcm/xhistogram

Issue:
numpy / dask verison compatibility bug
There is a bug in xhistogram with numpy version >= 1.17 and dask version < 2.3

```python
import xarray as xr
import numpy as np
from xhistogram.xarray import histogram

nt, nx = 100, 30
da = xr.DataArray(np.random.randn(nt, nx), dims=['time', 'x'],
                  name='foo').chunk({'time': 1})
bins = np.linspace(-4, 4, 20)
h = histogram(da, bins=[bins], dim=['x'])
```

This should be lazy. However, as reported by @stb2145 in https://github.com/pangeo-data/pangeo/issues/690, certain numpy / dask combination produce the warning
```
/srv/conda/envs/notebook/lib/python3.7/site-packages/dask/array/core.py:1263: FutureWarning: The `numpy.moveaxis` function is not implemented by Dask array. You may want to use the da.map_blocks function or something similar to silence this warning. Your code may stop working in a future release.
  FutureWarning,
```
and evaluates eagerly.

In https://github.com/pangeo-data/pangeo/issues/690 we found a workaround involving setting the environment variable `NUMPY_EXPERIMENTAL_ARRAY_FUNCTION=0`. However, in the meantime, the root issues was fixed in dask (https://github.com/dask/dask/issues/2559).

We can avoid this bug by requiring dask >= 2.3.0 as a dependency for xhistogram. I guess that's the easiest way to go. 

Comments:

Patch:
diff --git a/doc/contributing.rst b/doc/contributing.rst
index 9ccbb9e..fd4ba77 100644
--- a/doc/contributing.rst
+++ b/doc/contributing.rst
@@ -141,17 +141,17 @@ Release History
 ---------------
 
 v0.3.2 (not released)
-~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~
 
-- Fix bug producing TypeError when weights is provided with
+- Fix bug producing TypeError when `weights` is provided with
   `keep_coords=True` :issue:`78`. By
   `Dougie Squire <https://github.com/dougiesquire>`_.
-- Raise TypeError when weights is a dask array and bin edges are
+- Raise TypeError when `weights` is a dask array and bin edges are
   not explicitly provided :issue:`12`. By
   `Dougie Squire <https://github.com/dougiesquire>`_.
 
 v0.3.1
-~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~
 
 - Add DOI badge and CITATION.cff. By
   `Julius Busecke <https://github.com/jbusecke>`_.
diff --git a/setup.py b/setup.py
index 4481b45..532192a 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ CLASSIFIERS = [
     "Topic :: Scientific/Engineering",
 ]
 
-INSTALL_REQUIRES = ["xarray>=0.12.0", "dask[array]", "numpy>=1.17"]
+INSTALL_REQUIRES = ["xarray>=0.12.0", "dask[array]>=2.3.0", "numpy>=1.17"]
 PYTHON_REQUIRES = ">=3.7"
 
 DESCRIPTION = "Fast, flexible, label-aware histograms for numpy and xarray"
diff --git a/xhistogram/core.py b/xhistogram/core.py
index 04cef56..181325f 100644
--- a/xhistogram/core.py
+++ b/xhistogram/core.py
@@ -278,9 +278,9 @@ def histogram(
         When bin edges are specified, all but the last (righthand-most) bin include
         the left edge and exclude the right edge. The last bin includes both edges.
 
-        A TypeError will be raised if args contains dask arrays and bins are not
-        specified explicitly as an array or list of arrays. This is because other
-        bin specifications trigger computation.
+        A TypeError will be raised if args or weights contains dask arrays and bins
+        are not specified explicitly as an array or list of arrays. This is because
+        other bin specifications trigger computation.
     range : (float, float) or a list of (float, float), optional
         If a list, there should be one entry for each item in ``args``.
         The range specifications are as follows:
@@ -336,7 +336,7 @@ def histogram(
     ndim = a0.ndim
     n_inputs = len(args)
 
-    is_dask_array = any([dask.is_dask_collection(a) for a in args])
+    is_dask_array = any([dask.is_dask_collection(a) for a in list(args) + [weights]])
 
     if axis is not None:
         axis = np.atleast_1d(axis)
diff --git a/xhistogram/xarray.py b/xhistogram/xarray.py
index f1b7976..975aa9b 100644
--- a/xhistogram/xarray.py
+++ b/xhistogram/xarray.py
@@ -43,9 +43,9 @@ def histogram(
         When bin edges are specified, all but the last (righthand-most) bin include
         the left edge and exclude the right edge. The last bin includes both edges.
 
-        A TypeError will be raised if args contains dask arrays and bins are not
-        specified explicitly as an array or list of arrays. This is because other
-        bin specifications trigger computation.
+        A TypeError will be raised if args or weights contains dask arrays and bins
+        are not specified explicitly as an array or list of arrays. This is because
+        other bin specifications trigger computation.
     range : (float, float) or a list of (float, float), optional
         If a list, there should be one entry for each item in ``args``.
         The range specifications are as follows:


Test patch:
diff --git a/xhistogram/test/test_core.py b/xhistogram/test/test_core.py
index db0bf99..0868dff 100644
--- a/xhistogram/test/test_core.py
+++ b/xhistogram/test/test_core.py
@@ -9,10 +9,12 @@ from ..core import (
     _ensure_correctly_formatted_bins,
     _ensure_correctly_formatted_range,
 )
-from .fixtures import empty_dask_array
+from .fixtures import empty_dask_array, example_dataarray
 
 import pytest
 
+import contextlib
+
 
 bins_int = 10
 bins_str = "auto"
@@ -271,15 +273,44 @@ def test_histogram_shape(use_dask, block_size):
             assert isinstance(c, dsa.Array)
 
 
-def test_histogram_dask():
-    """Test that fails with dask arrays and inappropriate bins"""
+@pytest.mark.parametrize("arg_type", ["dask", "numpy"])
+@pytest.mark.parametrize("weights_type", ["dask", "numpy", None])
+@pytest.mark.parametrize("bins_type", ["int", "str", "numpy"])
+def test_histogram_dask(arg_type, weights_type, bins_type):
+    """Test that a TypeError is raised with dask arrays and inappropriate bins"""
     shape = 10, 15, 12, 20
-    b = empty_dask_array(shape, chunks=(1,) + shape[1:])
-    histogram(b, bins=bins_arr)  # Should work when bins is all numpy arrays
-    with pytest.raises(TypeError):  # Should fail otherwise
-        histogram(b, bins=bins_int)
-        histogram(b, bins=bins_str)
-        histogram(b, b, bins=[bins_arr, bins_int])
+
+    if arg_type == "dask":
+        arg = empty_dask_array(shape)
+    else:
+        arg = example_dataarray(shape)
+
+    if weights_type == "dask":
+        weights = empty_dask_array(shape)
+    elif weights_type == "numpy":
+        weights = example_dataarray(shape)
+    else:
+        weights = None
+
+    if bins_type == "int":
+        bins = bins_int
+    elif bins_type == "str":
+        bins = bins_str
+    else:
+        bins = bins_arr
+
+    # TypeError should be returned when
+    # 1. args or weights is a dask array and bins is not a numpy array, or
+    # 2. bins is a string and weights is a numpy array
+    cond_1 = ((arg_type == "dask") | (weights_type == "dask")) & (bins_type != "numpy")
+    cond_2 = (weights_type == "numpy") & (bins_type == "str")
+    should_TypeError = cond_1 | cond_2
+
+    with contextlib.ExitStack() as stack:
+        if should_TypeError:
+            stack.enter_context(pytest.raises(TypeError))
+        histogram(arg, bins=bins, weights=weights)
+        histogram(arg, arg, bins=[bins, bins], weights=weights)
 
 
 @pytest.mark.parametrize(