Repository: pydata/xarray

Issue:
apply_ufunc silently neglects arguments if `len(input_core_dims) < args`
From [SO](https://stackoverflow.com/questions/51680659/disparity-between-result-of-numpy-gradient-applied-directly-and-applied-using-xa/51690873#51690873)

In the following script, the second argument is silently neglected,
```python
da = xr.DataArray(np.random.randn(4, 3), coords={'x': [5, 7, 9, 11]}, dims=('x', 'y'))
xr.apply_ufunc(np.gradient, da, da.coords['x'].values, kwargs={'axis': -1},
               input_core_dims=[['x']], output_core_dims=[['x']],
               output_dtypes=[da.dtype])
```
This is because we need to the same number of `input_core_dims` to the number of arguments,

https://github.com/pydata/xarray/blob/56381ef444c5e699443e8b4e08611060ad5c9507/xarray/core/computation.py#L535-L538

The correct scipt might be
`input_core_dims=[['x']]` -> `input_core_dims=[['x'], []]`.

I think we can raise a more friendly error if the size of `input_core_dims` are wrong.

EDIT:
Or we can automatically insert an empty tuple or `None` for non-xarray object?
`input_core_dims` for non-xarray object sounds a little strange.


Discussion:

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index e48c59d0..fd359ee1 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -56,11 +56,17 @@ Enhancements
 Bug fixes
 ~~~~~~~~~
 
-- Fixed ``DataArray.to_iris()`` failure while creating ``DimCoord`` by 
+- Fixed ``DataArray.to_iris()`` failure while creating ``DimCoord`` by
   falling back to creating ``AuxCoord``. Fixed dependency on ``var_name``
   attribute being set.
   (:issue:`2201`)
   By `Thomas Voigt <https://github.com/tv3141>`_.
+- Tests can be run in parallel with pytest-xdist
+
+- Now :py:func:`xr.apply_ufunc` raises a ValueError when the size of
+``input_core_dims`` is inconsistent with the number of arguments.
+  (:issue:`2341`)
+  By `Keisuke Fujii <https://github.com/fujiisoup>`_.
 
 .. _whats-new.0.10.8:
 
diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py
index c5043ce8..a64ca858 100644
--- a/xarray/backends/zarr.py
+++ b/xarray/backends/zarr.py
@@ -1,6 +1,5 @@
 from __future__ import absolute_import, division, print_function
 
-from itertools import product
 from distutils.version import LooseVersion
 
 import numpy as np
diff --git a/xarray/convert.py b/xarray/convert.py
index bc639c68..6cff7210 100644
--- a/xarray/convert.py
+++ b/xarray/convert.py
@@ -2,7 +2,7 @@
 """
 from __future__ import absolute_import, division, print_function
 
-from collections import Counter, OrderedDict
+from collections import Counter
 
 import numpy as np
 import pandas as pd
diff --git a/xarray/core/computation.py b/xarray/core/computation.py
index 1f771a41..bdba72cb 100644
--- a/xarray/core/computation.py
+++ b/xarray/core/computation.py
@@ -920,6 +920,11 @@ def apply_ufunc(func, *args, **kwargs):
 
     if input_core_dims is None:
         input_core_dims = ((),) * (len(args))
+    elif len(input_core_dims) != len(args):
+        raise ValueError(
+            'input_core_dims must be None or a tuple with the length same to '
+            'the number of arguments. Given input_core_dims: {}, '
+            'number of args: {}.'.format(input_core_dims, len(args)))
 
     signature = _UFuncSignature(input_core_dims, output_core_dims)
 
diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py
index 4b9645e0..1ddb0235 100644
--- a/xarray/plot/utils.py
+++ b/xarray/plot/utils.py
@@ -3,8 +3,6 @@ from __future__ import absolute_import, division, print_function
 import warnings
 
 import numpy as np
-import pandas as pd
-import pkg_resources
 import textwrap
 
 from ..core.pycompat import basestring


Test patch:
diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py
index 4d6ca731..e763af49 100644
--- a/xarray/tests/test_coding_times.py
+++ b/xarray/tests/test_coding_times.py
@@ -16,10 +16,12 @@ from . import (assert_array_equal, has_cftime_or_netCDF4,
                requires_cftime_or_netCDF4, has_cftime, has_dask)
 
 
-_NON_STANDARD_CALENDARS = {'noleap', '365_day', '360_day',
-                           'julian', 'all_leap', '366_day'}
-_ALL_CALENDARS = _NON_STANDARD_CALENDARS.union(
-    coding.times._STANDARD_CALENDARS)
+_NON_STANDARD_CALENDARS_SET = {'noleap', '365_day', '360_day',
+                               'julian', 'all_leap', '366_day'}
+_ALL_CALENDARS = sorted(_NON_STANDARD_CALENDARS_SET.union(
+    coding.times._STANDARD_CALENDARS))
+_NON_STANDARD_CALENDARS = sorted(_NON_STANDARD_CALENDARS_SET)
+_STANDARD_CALENDARS = sorted(coding.times._STANDARD_CALENDARS)
 _CF_DATETIME_NUM_DATES_UNITS = [
     (np.arange(10), 'days since 2000-01-01'),
     (np.arange(10).astype('float64'), 'days since 2000-01-01'),
@@ -49,7 +51,7 @@ _CF_DATETIME_NUM_DATES_UNITS = [
 ]
 _CF_DATETIME_TESTS = [num_dates_units + (calendar,) for num_dates_units,
                       calendar in product(_CF_DATETIME_NUM_DATES_UNITS,
-                                          coding.times._STANDARD_CALENDARS)]
+                                          _STANDARD_CALENDARS)]
 
 
 @np.vectorize
@@ -161,7 +163,7 @@ def test_decode_cf_datetime_non_iso_strings():
 @pytest.mark.skipif(not has_cftime_or_netCDF4, reason='cftime not installed')
 @pytest.mark.parametrize(
     ['calendar', 'enable_cftimeindex'],
-    product(coding.times._STANDARD_CALENDARS, [False, True]))
+    product(_STANDARD_CALENDARS, [False, True]))
 def test_decode_standard_calendar_inside_timestamp_range(
         calendar, enable_cftimeindex):
     if enable_cftimeindex:
@@ -263,7 +265,7 @@ def test_decode_dates_outside_timestamp_range(
 @pytest.mark.skipif(not has_cftime_or_netCDF4, reason='cftime not installed')
 @pytest.mark.parametrize(
     ['calendar', 'enable_cftimeindex'],
-    product(coding.times._STANDARD_CALENDARS, [False, True]))
+    product(_STANDARD_CALENDARS, [False, True]))
 def test_decode_standard_calendar_single_element_inside_timestamp_range(
         calendar, enable_cftimeindex):
     if enable_cftimeindex:
@@ -329,7 +331,7 @@ def test_decode_single_element_outside_timestamp_range(
 @pytest.mark.skipif(not has_cftime_or_netCDF4, reason='cftime not installed')
 @pytest.mark.parametrize(
     ['calendar', 'enable_cftimeindex'],
-    product(coding.times._STANDARD_CALENDARS, [False, True]))
+    product(_STANDARD_CALENDARS, [False, True]))
 def test_decode_standard_calendar_multidim_time_inside_timestamp_range(
         calendar, enable_cftimeindex):
     if enable_cftimeindex:
@@ -681,7 +683,7 @@ def test_decode_cf_enable_cftimeindex(calendar, enable_cftimeindex):
         ds[v].attrs['calendar'] = calendar
 
     if (not has_cftime and enable_cftimeindex and
-       calendar not in coding.times._STANDARD_CALENDARS):
+       calendar not in _STANDARD_CALENDARS):
         with pytest.raises(ValueError):
             with set_options(enable_cftimeindex=enable_cftimeindex):
                 ds = decode_cf(ds)
@@ -690,7 +692,7 @@ def test_decode_cf_enable_cftimeindex(calendar, enable_cftimeindex):
             ds = decode_cf(ds)
 
         if (enable_cftimeindex and
-           calendar not in coding.times._STANDARD_CALENDARS):
+           calendar not in _STANDARD_CALENDARS):
             assert ds.test.dtype == np.dtype('O')
         else:
             assert ds.test.dtype == np.dtype('M8[ns]')
diff --git a/xarray/tests/test_computation.py b/xarray/tests/test_computation.py
index e30e7e31..ca8e4e59 100644
--- a/xarray/tests/test_computation.py
+++ b/xarray/tests/test_computation.py
@@ -274,6 +274,22 @@ def test_apply_input_core_dimension():
     assert_identical(expected_dataset_x,
                      first_element(dataset.groupby('y'), 'x'))
 
+    def multiply(*args):
+        val = args[0]
+        for arg in args[1:]:
+            val = val * arg
+        return val
+
+    # regression test for GH:2341
+    with pytest.raises(ValueError):
+        apply_ufunc(multiply, data_array, data_array['y'].values,
+                    input_core_dims=[['y']], output_core_dims=[['y']])
+    expected = xr.DataArray(multiply(data_array, data_array['y']),
+                            dims=['x', 'y'], coords=data_array.coords)
+    actual = apply_ufunc(multiply, data_array, data_array['y'].values,
+                         input_core_dims=[['y'], []], output_core_dims=[['y']])
+    assert_identical(expected, actual)
+
 
 def test_apply_output_core_dimension():