Repository: pydata/xarray

Issue:
Multiple methods fail on a DataArray with scalar dimension
#### Code Sample, a copy-pastable example if possible

```python
import numpy as np
import xarray as xr
a = np.arange(20).reshape(1,2,2,5)
coords = dict(t=0, z=np.arange(2), y=np.arange(2), x=np.arange(5))
b = xr.DataArray(a,dims=['t', 'z', 'y', 'x'],coords=coords)
b.squeeze() # Throws ValueError: dimension 't' already exists as a scalar variable

coords = dict(t=[0], z=np.arange(2), y=np.arange(2), x=np.arange(5))
c = xr.DataArray(a,dims=['t', 'z', 'y', 'x'],coords=coords)
c.squeeze()  # This works!
```
#### Problem description

Besides `squeeze`, a number of other methods like `drop` fail on `DataArray b`. Since, scalar dimensions are not dropped, it is almost impossible to do anything useful like `plot`, `concat`, etc with `b`. All these methods fail with `ValueError: dimension 't' already exists as a scalar variable`.

#### Expected Output
In my opinion, `b` and `c` should behave in the same way. It would just involve converting the scalar dimensions into lists. Is this behavior a bug or by design?

#### Output of ``xr.show_versions()``
0.9.6

Discussion:

Patch:
diff --git a/doc/api.rst b/doc/api.rst
index 0eb9e4c1..7bcb8447 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -487,6 +487,7 @@ Exceptions
    :toctree: generated/
 
    MergeError
+   SerializationWarning
 
 Advanced API
 ============
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 668e70a4..459826da 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -46,6 +46,10 @@ Bug fixes
 - Fix importing xarray when running Python with ``-OO`` (:issue:`1706`).
   By `Stephan Hoyer <https://github.com/shoyer>`_.
 
+- Saving a netCDF file with a coordinates with a spaces in its names now raises
+  an appropriate warning (:issue:`1689`).
+  By `Stephan Hoyer <https://github.com/shoyer>`_.
+
 - Fix two bugs that were preventing dask arrays from being specified as
   coordinates in the DataArray constructor (:issue:`1684`).
   By `Joe Hamman <https://github.com/jhamman>`_
@@ -54,6 +58,10 @@ Bug fixes
   (:issue:`1697`).
   By `Stephan Hoyer <https://github.com/shoyer>`_.
 
+- Validate the shape of coordinates with names matching dimensions in the
+  DataArray constructor (:issue:`1709`).
+  By `Stephan Hoyer <https://github.com/shoyer>`_.
+
 Testing
 ~~~~~~~
 
diff --git a/xarray/__init__.py b/xarray/__init__.py
index 89c6ee7c..1b344252 100644
--- a/xarray/__init__.py
+++ b/xarray/__init__.py
@@ -19,7 +19,7 @@ from .backends.api import (open_dataset, open_dataarray, open_mfdataset,
                            save_mfdataset)
 from .backends.rasterio_ import open_rasterio
 
-from .conventions import decode_cf
+from .conventions import decode_cf, SerializationWarning
 
 try:
     from .version import version as __version__
diff --git a/xarray/conventions.py b/xarray/conventions.py
index 94c963da..87fb6694 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -32,6 +32,10 @@ _NS_PER_TIME_DELTA = {'us': 1e3,
                       'D': 1e9 * 60 * 60 * 24}
 
 
+class SerializationWarning(RuntimeWarning):
+    """Warnings about encoding/decoding issues in serialization."""
+
+
 def mask_and_scale(array, fill_value=None, scale_factor=None, add_offset=None,
                    dtype=float):
     """Scale and mask array values according to CF conventions for packed and
@@ -113,7 +117,7 @@ def _decode_datetime_with_netcdf4(num_dates, units, calendar):
         warnings.warn('Unable to decode time axis into full '
                       'numpy.datetime64 objects, continuing using dummy '
                       'netCDF4.datetime objects instead, reason: dates out'
-                      ' of range', RuntimeWarning, stacklevel=3)
+                      ' of range', SerializationWarning, stacklevel=3)
     else:
         try:
             dates = nctime_to_nptime(dates)
@@ -121,7 +125,7 @@ def _decode_datetime_with_netcdf4(num_dates, units, calendar):
             warnings.warn('Unable to decode time axis into full '
                           'numpy.datetime64 objects, continuing using '
                           'dummy netCDF4.datetime objects instead, reason:'
-                          '{0}'.format(e), RuntimeWarning, stacklevel=3)
+                          '{0}'.format(e), SerializationWarning, stacklevel=3)
     return dates
 
 
@@ -773,7 +777,7 @@ def maybe_encode_nonstring_dtype(var, name=None):
                     warnings.warn('saving variable %s with floating '
                                   'point data as an integer dtype without '
                                   'any _FillValue to use for NaNs' % name,
-                                  RuntimeWarning, stacklevel=3)
+                                  SerializationWarning, stacklevel=3)
                 data = duck_array_ops.around(data)[...]
                 if encoding.get('_Unsigned', False):
                     signed_dtype = np.dtype('i%s' % dtype.itemsize)
@@ -951,7 +955,7 @@ def decode_cf_variable(name, var, concat_characters=True, mask_and_scale=True,
         else:
             warnings.warn("variable %r has _Unsigned attribute but is not "
                           "of integer type. Ignoring attribute." % name,
-                          RuntimeWarning, stacklevel=3)
+                          SerializationWarning, stacklevel=3)
 
     if mask_and_scale:
         if 'missing_value' in attributes:
@@ -975,7 +979,7 @@ def decode_cf_variable(name, var, concat_characters=True, mask_and_scale=True,
             warnings.warn("variable {!r} has multiple fill values {}, "
                           "decoding all values to NaN."
                           .format(name, fill_value),
-                          RuntimeWarning, stacklevel=3)
+                          SerializationWarning, stacklevel=3)
 
         scale_factor = pop_to(attributes, encoding, 'scale_factor')
         add_offset = pop_to(attributes, encoding, 'add_offset')
@@ -1185,6 +1189,16 @@ def cf_decoder(variables, attributes,
 def _encode_coordinates(variables, attributes, non_dim_coord_names):
     # calculate global and variable specific coordinates
     non_dim_coord_names = set(non_dim_coord_names)
+
+    for name in list(non_dim_coord_names):
+        if isinstance(name, basestring) and ' ' in name:
+            warnings.warn(
+                'coordinate {!r} has a space in its name, which means it '
+                'cannot be marked as a coordinate on disk and will be '
+                'saved as a data variable instead'.format(name),
+                SerializationWarning, stacklevel=6)
+            non_dim_coord_names.discard(name)
+
     global_coordinates = non_dim_coord_names.copy()
     variable_coordinates = defaultdict(set)
     for coord_name in non_dim_coord_names:
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index 1dac7233..0516b473 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -35,7 +35,7 @@ def _infer_coords_and_dims(shape, coords, dims):
     """All the logic for creating a new DataArray"""
 
     if (coords is not None and not utils.is_dict_like(coords) and
-        len(coords) != len(shape)):
+            len(coords) != len(shape)):
         raise ValueError('coords is not dict-like, but it has %s items, '
                          'which does not match the %s dimensions of the '
                          'data' % (len(coords), len(shape)))
@@ -50,8 +50,8 @@ def _infer_coords_and_dims(shape, coords, dims):
             if utils.is_dict_like(coords):
                 # deprecated in GH993, removed in GH1539
                 raise ValueError('inferring DataArray dimensions from '
-                                 'dictionary like ``coords`` has been '
-                                 'deprecated. Use an explicit list of '
+                                 'dictionary like ``coords`` is no longer '
+                                 'supported. Use an explicit list of '
                                  '``dims`` instead.')
             for n, (dim, coord) in enumerate(zip(dims, coords)):
                 coord = as_variable(coord,
@@ -87,6 +87,12 @@ def _infer_coords_and_dims(shape, coords, dims):
                                  'length %s on the data but length %s on '
                                  'coordinate %r' % (d, sizes[d], s, k))
 
+        if k in sizes and v.shape != (sizes[k],):
+            raise ValueError('coordinate %r is a DataArray dimension, but '
+                             'it has shape %r rather than expected shape %r '
+                             'matching the dimension size'
+                             % (k, v.shape, (sizes[k],)))
+
     assert_unique_multiindex_level_names(new_coords)
 
     return new_coords, dims
diff --git a/xarray/core/variable.py b/xarray/core/variable.py
index aa64dcab..f2bc004c 100644
--- a/xarray/core/variable.py
+++ b/xarray/core/variable.py
@@ -96,7 +96,7 @@ def as_variable(obj, name=None):
                             '{}'.format(obj))
     elif utils.is_scalar(obj):
         obj = Variable([], obj)
-    elif (isinstance(obj, (pd.Index, IndexVariable)) and obj.name is not None):
+    elif isinstance(obj, (pd.Index, IndexVariable)) and obj.name is not None:
         obj = Variable(obj.name, obj)
     elif name is not None:
         data = as_compatible_data(obj)


Test patch:
diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py
index 53cb3c6d..316c97eb 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -376,9 +376,17 @@ class DatasetIOTestCases(object):
         with self.roundtrip(original) as actual:
             self.assertDatasetIdentical(original, actual)
 
-        expected = original.drop('foo')
-        with self.roundtrip(expected) as actual:
-            self.assertDatasetIdentical(expected, actual)
+    def test_roundtrip_global_coordinates(self):
+        original = Dataset({'x': [2, 3], 'y': ('a', [42]), 'z': ('x', [4, 5])})
+        with self.roundtrip(original) as actual:
+            self.assertDatasetIdentical(original, actual)
+
+    def test_roundtrip_coordinates_with_space(self):
+        original = Dataset(coords={'x': 0, 'y z': 1})
+        expected = Dataset({'y z': 1}, {'x': 0})
+        with pytest.warns(xr.SerializationWarning):
+            with self.roundtrip(original) as actual:
+                self.assertDatasetIdentical(expected, actual)
 
     def test_roundtrip_boolean_dtype(self):
         original = create_boolean_data()
@@ -1506,8 +1514,11 @@ class DaskTest(TestCase, DatasetIOTestCases):
                   allow_cleanup_failure=False):
         yield data.chunk()
 
+    # Override methods in DatasetIOTestCases - not applicable to dask
     def test_roundtrip_string_encoded_characters(self):
-        # Override method in DatasetIOTestCases - not applicable to dask
+        pass
+
+    def test_roundtrip_coordinates_with_space(self):
         pass
 
     def test_roundtrip_datetime_data(self):
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index d39232c0..c86f706f 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -298,6 +298,9 @@ class TestDataArray(TestCase):
             DataArray(np.random.rand(4, 4),
                       [('x', self.mindex), ('level_1', range(4))])
 
+        with raises_regex(ValueError, 'matching the dimension size'):
+            DataArray(data, coords={'x': 0}, dims=['x', 'y'])
+
     def test_constructor_from_self_described(self):
         data = [[-0.1, 21], [0, 2]]
         expected = DataArray(data,