Repository: pydata/xarray (Python)

Issue:
Decode_cf fails when scale_factor is a length-1 list
Some datasets I work with have `scale_factor` and `add_offset` encoded as length-1 lists. The following code worked as of Xarray 0.16.1

```python
import xarray as xr
ds = xr.DataArray([0, 1, 2], name='foo',
                  attrs={'scale_factor': [0.01],
                         'add_offset': [1.0]}).to_dataset()
xr.decode_cf(ds)
```

In 0.16.2 (just released) and current master, it fails with this error

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-2-a0b01d6a314b> in <module>
      2                   attrs={'scale_factor': [0.01],
      3                          'add_offset': [1.0]}).to_dataset()
----> 4 xr.decode_cf(ds)

~/Code/xarray/xarray/conventions.py in decode_cf(obj, concat_characters, mask_and_scale, decode_times, decode_coords, drop_variables, use_cftime, decode_timedelta)
    587         raise TypeError("can only decode Dataset or DataStore objects")
    588 
--> 589     vars, attrs, coord_names = decode_cf_variables(
    590         vars,
    591         attrs,

~/Code/xarray/xarray/conventions.py in decode_cf_variables(variables, attributes, concat_characters, mask_and_scale, decode_times, decode_coords, drop_variables, use_cftime, decode_timedelta)
    490             and stackable(v.dims[-1])
    491         )
--> 492         new_vars[k] = decode_cf_variable(
    493             k,
    494             v,

~/Code/xarray/xarray/conventions.py in decode_cf_variable(name, var, concat_characters, mask_and_scale, decode_times, decode_endianness, stack_char_dim, use_cftime, decode_timedelta)
    333             variables.CFScaleOffsetCoder(),
    334         ]:
--> 335             var = coder.decode(var, name=name)
    336 
    337     if decode_timedelta:

~/Code/xarray/xarray/coding/variables.py in decode(self, variable, name)
    271             dtype = _choose_float_dtype(data.dtype, "add_offset" in attrs)
    272             if np.ndim(scale_factor) > 0:
--> 273                 scale_factor = scale_factor.item()
    274             if np.ndim(add_offset) > 0:
    275                 add_offset = add_offset.item()

AttributeError: 'list' object has no attribute 'item'
```

I'm very confused, because this feels quite similar to #4471, and I thought it was resolved #4485.
However, the behavior is different with `'scale_factor': np.array([0.01])`. That works fine--no error.

How might I end up with a dataset with `scale_factor` as a python list? It happens when I open a netcdf file using the `h5netcdf` engine (documented by @gerritholl in https://github.com/pydata/xarray/issues/4471#issuecomment-702018925) and then write it to zarr. The numpy array gets encoded as a list in the zarr json metadata. 🙃 

This problem would go away if we could resolve the discrepancies between the two engines' treatment of scalar attributes.




Discussion:
I guess we need `np.asarray(scale_factor).item()`
But what did we do before?
I think it just did `np.array([1, 2, 3.]) * [5]` numpy coverts the `[5]` to an array with one element, which is why it worked:

```python
xr.coding.variables._scale_offset_decoding(np.array([1, 2, 3.]), [5], None, np.float)

```

https://github.com/pydata/xarray/blob/49d03d246ce657e0cd3be0582334a1a023c4b374/xarray/coding/variables.py#L217
Ok then I am 👍  on @dcherian's solution.



Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 51b16d65a62..88994a5bfc0 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -75,6 +75,8 @@ Bug fixes
 - Add ``missing_dims`` parameter to transpose (:issue:`4647`, :pull:`4767`). By `Daniel Mesejo <https://github.com/mesejo>`_.
 - Resolve intervals before appending other metadata to labels when plotting (:issue:`4322`, :pull:`4794`).
   By `Justus Magin <https://github.com/keewis>`_.
+- Fix regression when decoding a variable with a ``scale_factor`` and ``add_offset`` given
+  as a list of length one (:issue:`4631`) by `Mathias Hauser <https://github.com/mathause>`_.
 - Expand user directory paths (e.g. ``~/``) in :py:func:`open_mfdataset` and
   :py:meth:`Dataset.to_zarr` (:issue:`4783`, :pull:`4795`).
   By `Julien Seguinot <https://github.com/juseg>`_.
diff --git a/xarray/coding/variables.py b/xarray/coding/variables.py
index 80277e9cd4e..b035ff82086 100644
--- a/xarray/coding/variables.py
+++ b/xarray/coding/variables.py
@@ -270,9 +270,9 @@ def decode(self, variable, name=None):
             add_offset = pop_to(attrs, encoding, "add_offset", name=name)
             dtype = _choose_float_dtype(data.dtype, "add_offset" in attrs)
             if np.ndim(scale_factor) > 0:
-                scale_factor = scale_factor.item()
+                scale_factor = np.asarray(scale_factor).item()
             if np.ndim(add_offset) > 0:
-                add_offset = add_offset.item()
+                add_offset = np.asarray(add_offset).item()
             transform = partial(
                 _scale_offset_decoding,
                 scale_factor=scale_factor,


Test patch:
diff --git a/xarray/tests/test_coding.py b/xarray/tests/test_coding.py
index 0f191049284..e0df7782aa7 100644
--- a/xarray/tests/test_coding.py
+++ b/xarray/tests/test_coding.py
@@ -8,7 +8,7 @@
 from xarray.coding import variables
 from xarray.conventions import decode_cf_variable, encode_cf_variable
 
-from . import assert_equal, assert_identical, requires_dask
+from . import assert_allclose, assert_equal, assert_identical, requires_dask
 
 with suppress(ImportError):
     import dask.array as da
@@ -105,3 +105,15 @@ def test_scaling_converts_to_float32(dtype):
     roundtripped = coder.decode(encoded)
     assert_identical(original, roundtripped)
     assert roundtripped.dtype == np.float32
+
+
+@pytest.mark.parametrize("scale_factor", (10, [10]))
+@pytest.mark.parametrize("add_offset", (0.1, [0.1]))
+def test_scaling_offset_as_list(scale_factor, add_offset):
+    # test for #4631
+    encoding = dict(scale_factor=scale_factor, add_offset=add_offset)
+    original = xr.Variable(("x",), np.arange(10.0), encoding=encoding)
+    coder = variables.CFScaleOffsetCoder()
+    encoded = coder.encode(original)
+    roundtripped = coder.decode(encoded)
+    assert_allclose(original, roundtripped)